summaryrefslogtreecommitdiffstats
path: root/scripts/ctrlact.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/ctrlact.pl')
-rw-r--r--scripts/ctrlact.pl574
1 files changed, 574 insertions, 0 deletions
diff --git a/scripts/ctrlact.pl b/scripts/ctrlact.pl
new file mode 100644
index 0000000..da39804
--- /dev/null
+++ b/scripts/ctrlact.pl
@@ -0,0 +1,574 @@
+# ctrlact.pl — Irssi script for fine-grained control of activity indication
+#
+# © 2017 martin f. krafft <madduck@madduck.net>
+# Released under the MIT licence.
+#
+### Usage:
+#
+# /script load ctrlact
+#
+# If you like a busy activity statusbar, this script is not for you.
+#
+# If, on the other hand, you don't care about most activity, but you do want
+# the ability to define per-item and per-window, what level of activity should
+# trigger a change in the statusbar, then ctrlact might be for you.
+#
+# For instance, you might never want to be disturbed by activity in any
+# channel, unless someone highlights you. However, you do want all activity
+# in queries (except on efnet, as well as an indication about any chatter in
+# your company channels. The following ctrlact map would do this for you:
+#
+# channel /^#myco-/ messages
+# channel * hilights
+# query efnet * messages
+# query * all
+#
+# These three lines would be interpreted/read as:
+# "only messages or higher in a channel matching /^#myco-/ should trigger act"
+# "in all other channels, only hilights (or higher) should trigger act"
+# "queries on efnet should only trigger act for messages and higher"
+# "messages of all levels should trigger act in queries elsewhere"
+#
+# The activity level in the third column is thus to be interpreted as
+# "the minimum level of activity that will trigger an indication"
+#
+# Loading this script per-se should not change anything, except it will create
+# ~/.irssi/ctrlact with some informational content, including the defaults and
+# some examples.
+#
+# The four activity levels are, and you can use either the words, or the
+# integers in the map.
+#
+# all (data_level: 1)
+# messages (data_level: 2)
+# hilights (data_level: 3)
+# none (data_level: 4)
+#
+# Note that the name is either matched in full and verbatim, or treated like
+# a regular expression, if it starts and ends with the same punctuation
+# character. The asterisk ('*') is special and simply gets translated to /.*/
+# internally. No other wildcards are supported.
+#
+# Once you defined your mappings, please don't forget to /ctrlact reload them.
+# You can then use the following commands from Irssi to check out the result:
+#
+# # list all mappings
+# /ctrlact list
+#
+# # query the applicable activity levels, possibly limited to
+# # windows/channels/queries
+# /ctrlact query name [name, …] [-window|-channel|-query]
+#
+# # display the applicable level for each window/channel/query
+# /ctrlact show [-window|-channel|-query]
+#
+# There's an interplay between window items and windows here, and you can
+# specify mininum activity levels for each. Here are the rules:
+#
+# 1. if the minimum activity level of a window item (channel or query) is not
+# reached, then the window is prevented from indicating activity.
+# 2. if traffic in a window item does reach minimum activity level, then the
+# minimum activity level of the window is considered, and activity is only
+# indicated if the window's minimum activity level is lower.
+#
+# In general, this means you'd have windows defaulting to 'all', but it might
+# come in handy to move window items to windows with min.levels of 'hilights'
+# or even 'none' in certain cases, to further limit activity indication for
+# them.
+#
+# You can use the Irssi settings activity_msg_level and activity_hilight_level
+# to specify which IRC levels will be considered messages and hilights. Note
+# that if an activity indication is inhibited, then there also won't be
+# a beep (cf. beep_msg_level), unless you toggle ctrlmap_inhibit_beep.
+#
+### Settings:
+#
+# /set ctrlact_map_file [~/.irssi/ctrlact]
+# Controls where the activity control map will be read from (and saved to)
+#
+# /set ctrlact_fallback_(channel|query|window)_threshold [1]
+# Controls the lowest data level that will trigger activity for channels,
+# queries, and windows respectively, if no applicable mapping could be
+# found.
+#
+# /set ctrlact_inhibit_beep [on]
+# If an activity wouldn't be indicated, also inhibit the beep/bell. Turn
+# this off if you want the bell anyway.
+#
+# /set ctrlact_debug [off]
+# Turns on debug output. Not that this may itself be buggy, so please don't
+# use it unless you really need it.
+#
+### To-do:
+#
+# - figure out interplay with activity_hide_level
+# - /ctrlact add/delete/move and /ctrlact save, maybe
+# - completion for commands
+#
+use strict;
+use warnings;
+use Carp qw( croak );
+use Irssi;
+use Text::ParseWords;
+
+our $VERSION = '1.2';
+
+our %IRSSI = (
+ authors => 'martin f. krafft',
+ contact => 'madduck@madduck.net',
+ name => 'ctrlact',
+ description => 'allows per-channel control over activity indication',
+ license => 'MIT',
+ changed => '2017-02-24'
+);
+
+### DEFAULTS AND SETTINGS ######################################################
+
+my $debug = 0;
+my $map_file = Irssi::get_irssi_dir()."/ctrlact";
+my $fallback_channel_threshold = 1;
+my $fallback_query_threshold = 1;
+my $fallback_window_threshold = 1;
+my $inhibit_beep = 1;
+
+Irssi::settings_add_str('ctrlact', 'ctrlact_map_file', $map_file);
+Irssi::settings_add_bool('ctrlact', 'ctrlact_debug', $debug);
+Irssi::settings_add_int('ctrlact', 'ctrlact_fallback_channel_threshold', $fallback_channel_threshold);
+Irssi::settings_add_int('ctrlact', 'ctrlact_fallback_query_threshold', $fallback_query_threshold);
+Irssi::settings_add_int('ctrlact', 'ctrlact_fallback_window_threshold', $fallback_window_threshold);
+Irssi::settings_add_bool('ctrlact', 'ctrlact_inhibit_beep', $inhibit_beep);
+
+sub sig_setup_changed {
+ $debug = Irssi::settings_get_bool('ctrlact_debug');
+ $map_file = Irssi::settings_get_str('ctrlact_map_file');
+ $fallback_channel_threshold = Irssi::settings_get_int('ctrlact_fallback_channel_threshold');
+ $fallback_query_threshold = Irssi::settings_get_int('ctrlact_fallback_query_threshold');
+ $fallback_window_threshold = Irssi::settings_get_int('ctrlact_fallback_window_threshold');
+ $inhibit_beep = Irssi::settings_get_bool('ctrlact_inhibit_beep');
+}
+Irssi::signal_add('setup changed', \&sig_setup_changed);
+Irssi::signal_add('setup reread', \&sig_setup_changed);
+sig_setup_changed();
+
+my $changed_since_last_save = 0;
+
+my @DATALEVEL_KEYWORDS = ('all', 'messages', 'hilights', 'none');
+
+### HELPERS ####################################################################
+
+my $_inhibit_debug_activity = 0;
+use constant DEBUGEVENTFORMAT => "%7s %7.7s %-22.22s %d %s %d → %-7s (%-8s ← %s)";
+sub debugprint {
+ return unless $debug;
+ my ($msg, @rest) = @_;
+ $_inhibit_debug_activity = 1;
+ Irssi::print("ctrlact debug: ".$msg, MSGLEVEL_CRAP);
+ $_inhibit_debug_activity = 0;
+}
+
+sub error {
+ my ($msg) = @_;
+ Irssi::print("ctrlact: ERROR: $msg", MSGLEVEL_CLIENTERROR);
+}
+
+my @window_thresholds;
+my @channel_thresholds;
+my @query_thresholds;
+
+sub match {
+ my ($pat, $text) = @_;
+ my $npat = ($pat eq '*') ? '/.*/' : $pat;
+ if ($npat =~ m/^(\W)(.+)\1$/) {
+ my $re = qr/$2/;
+ $pat = $2 unless $pat eq '*';
+ return $pat if $text =~ /$re/i;
+ }
+ else {
+ return $pat if lc($text) eq lc($npat);
+ }
+ return 0;
+}
+
+sub to_data_level {
+ my ($kw) = @_;
+ return $1 if $kw =~ m/^(\d+)$/;
+ foreach my $i (2..4) {
+ my $matcher = qr/^$DATALEVEL_KEYWORDS[5-$i]$/;
+ return 6-$i if $kw =~ m/$matcher/i;
+ }
+ return 1;
+}
+
+sub from_data_level {
+ my ($dl) = @_;
+ croak "Invalid numeric data level: $dl" unless $dl =~ m/^([1-4])$/;
+ return $DATALEVEL_KEYWORDS[$dl-1];
+}
+
+sub walk_match_array {
+ my ($name, $net, $type, @arr) = @_;
+ foreach my $quadruplet (@arr) {
+ my $netmatch = $net eq '*' ? '(ignored)'
+ : match($quadruplet->[0], $net);
+ my $match = match($quadruplet->[1], $name);
+ next unless $netmatch and $match;
+
+ my $result = to_data_level($quadruplet->[2]);
+ my $tresult = from_data_level($result);
+ $name = '(unnamed)' unless length $name;
+ $match = sprintf('line %3d = net:%s name:%s',
+ $quadruplet->[3], $netmatch, $match);
+ return ($result, $tresult, $match)
+ }
+ return -1;
+}
+
+sub get_mappings_table {
+ my (@arr) = @_;
+ my @ret = ();
+ for (my $i = 0; $i < @arr; $i++) {
+ push @ret, sprintf("%4d: %-10s %-40s %-10s (line: %3d)",
+ $i, $arr[$i]->[0], $arr[$i]->[1], $arr[$i]->[2], $arr[$i]->[3]);
+ }
+ return join("\n", @ret);
+}
+
+sub get_specific_threshold {
+ my ($type, $name, $net) = @_;
+ $type = lc($type);
+ if ($type eq 'window') {
+ return walk_match_array($name, $net, $type, @window_thresholds);
+ }
+ elsif ($type eq 'channel') {
+ return walk_match_array($name, $net, $type, @channel_thresholds);
+ }
+ elsif ($type eq 'query') {
+ return walk_match_array($name, $net, $type, @query_thresholds);
+ }
+ else {
+ croak "ctrlact: can't look up threshold for type: $type";
+ }
+}
+
+sub get_item_threshold {
+ my ($chattype, $type, $name, $net) = @_;
+ my ($ret, $tret, $match) = get_specific_threshold($type, $name, $net);
+ return ($ret, $tret, $match) if $ret > 0;
+ if ($type eq 'CHANNEL') {
+ return ($fallback_channel_threshold, from_data_level($fallback_channel_threshold), '[default]');
+ }
+ else {
+ return ($fallback_query_threshold, from_data_level($fallback_query_threshold), '[default]');
+ }
+}
+
+sub get_win_threshold {
+ my ($name, $net) = @_;
+ my ($ret, $tret, $match) = get_specific_threshold('window', $name, $net);
+ if ($ret > 0) {
+ return ($ret, $tret, $match);
+ }
+ else {
+ return ($fallback_window_threshold, from_data_level($fallback_window_threshold), '[default]');
+ }
+}
+
+sub print_levels_for_all {
+ my ($type, @arr) = @_;
+ Irssi::print("ctrlact: $type mappings:");
+ for (my $i = 0; $i < @arr; $i++) {
+ my $name = $arr[$i]->{'name'};
+ my $net = $arr[$i]->{'server'}->{'tag'} // '';
+ my ($t, $tt, $match) = get_specific_threshold($type, $name, $net);
+ my $c = ($type eq 'window') ? $arr[$i]->{'refnum'} : $arr[$i]->window()->{'refnum'};
+ Irssi::print(sprintf("%4d: %-40.40s → %d (%-8s) match %s", $c, $name, $t, $tt, $match), MSGLEVEL_CRAP);
+ }
+}
+
+### HILIGHT SIGNAL HANDLERS ####################################################
+
+my $_inhibit_beep = 0;
+my $_inhibit_window = 0;
+
+sub maybe_inhibit_witem_hilight {
+ my ($witem, $oldlevel) = @_;
+ return unless $witem;
+ $oldlevel = 0 unless $oldlevel;
+ my $newlevel = $witem->{'data_level'};
+ return if ($newlevel <= $oldlevel);
+
+ $_inhibit_window = 0;
+ $_inhibit_beep = 0;
+ my $wichattype = $witem->{'chat_type'};
+ my $witype = $witem->{'type'};
+ my $winame = $witem->{'name'};
+ my $witag = $witem->{'server'}->{'tag'} // '';
+ my ($th, $tth, $match) = get_item_threshold($wichattype, $witype, $winame, $witag);
+ my $inhibit = $newlevel > 0 && $newlevel < $th;
+ debugprint(sprintf(DEBUGEVENTFORMAT, lc($witype), $witag, $winame, $newlevel,
+ $inhibit ? ('<',$th,'inhibit'):('≥',$th,'pass'),
+ $tth, $match));
+ if ($inhibit) {
+ Irssi::signal_stop();
+ # the rhval comes from config, so if the user doesn't want the
+ # bell inhibited, this is effectively a noop.
+ $_inhibit_beep = $inhibit_beep;
+ $_inhibit_window = $witem->window();
+ }
+}
+Irssi::signal_add_first('window item hilight', \&maybe_inhibit_witem_hilight);
+
+sub inhibit_win_hilight {
+ my ($win) = @_;
+ Irssi::signal_stop();
+ Irssi::signal_emit('window dehilight', $win);
+}
+
+sub maybe_inhibit_win_hilight {
+ my ($win, $oldlevel) = @_;
+ return unless $win;
+ if ($_inhibit_debug_activity) {
+ inhibit_win_hilight($win);
+ }
+ elsif ($_inhibit_window && $win->{'refnum'} == $_inhibit_window->{'refnum'}) {
+ inhibit_win_hilight($win);
+ }
+ else {
+ $oldlevel = 0 unless $oldlevel;
+ my $newlevel = $win->{'data_level'};
+ return if ($newlevel <= $oldlevel);
+
+ my $wname = $win->{'name'};
+ my $wtag = $win->{'server'}->{'tag'} // '';
+ my ($th, $tth, $match) = get_win_threshold($wname, $wtag);
+ my $inhibit = $newlevel > 0 && $newlevel < $th;
+ debugprint(sprintf(DEBUGEVENTFORMAT, 'window', $wtag,
+ $wname?$wname:'(unnamed)', $newlevel,
+ $inhibit ? ('<',$th,'inhibit'):('≥',$th,'pass'),
+ $tth, $match));
+ inhibit_win_hilight($win) if $inhibit;
+ }
+}
+Irssi::signal_add_first('window hilight', \&maybe_inhibit_win_hilight);
+
+sub maybe_inhibit_beep {
+ Irssi::signal_stop() if $_inhibit_beep;
+}
+Irssi::signal_add_first('beep', \&maybe_inhibit_beep);
+
+### SAVING AND LOADING #########################################################
+
+sub get_mappings_fh {
+ my ($filename) = @_;
+ my $fh;
+ if (-e $filename) {
+ open($fh, '<', $filename) || croak "Cannot open mappings file: $!";
+ }
+ else {
+ open($fh, '+>', $filename) || croak "Cannot create mappings file: $!";
+
+ my $ftw = from_data_level($fallback_window_threshold);
+ my $ftc = from_data_level($fallback_channel_threshold);
+ my $ftq = from_data_level($fallback_query_threshold);
+ print $fh <<"EOF";
+# ctrlact mappings file (version: $VERSION)
+#
+# type: window, channel, query
+# server: the server tag (chatnet)
+# name: full name to match, /regexp/, or * (for all)
+# min.level: none, messages, hilights, all, or 1,2,3,4
+#
+# type server name min.level
+
+
+# EXAMPLES
+#
+### only indicate activity in the status window if messages were displayed:
+# window * (status) messages
+#
+### never ever indicate activity for any item bound to this window:
+# window * oubliette none
+#
+### indicate activity on all messages in debian-related channels on OFTC:
+# channel oftc /^#debian/ messages
+#
+### display any text (incl. joins etc.) for the '#madduck' channel:
+# channel * #madduck all
+#
+### otherwise ignore everything in channels, unless a hilight is triggered:
+# channel * * hilights
+#
+### make somebot only get your attention if they hilight you:
+# query efnet somebot hilights
+#
+### otherwise we want to see everything in queries:
+# query * * all
+
+# DEFAULTS:
+# window * * $ftw
+# channel * * $ftc
+# query * * $ftq
+
+# vim:noet:tw=0:ts=16
+EOF
+ Irssi::print("ctrlact: created new/empty mappings file: $filename");
+ seek($fh, 0, 0) || croak "Cannot rewind $filename.";
+ }
+ return $fh;
+}
+
+sub load_mappings {
+ my ($filename) = @_;
+ @window_thresholds = @channel_thresholds = @query_thresholds = ();
+ my $fh = get_mappings_fh($filename);
+ my $firstline = <$fh> || croak "Cannot read from $filename.";;
+ my $version;
+ if ($firstline =~ m/^#+\s+ctrlact mappings file \(version: *([\d.]+)\)/) {
+ $version = $1;
+ }
+ else {
+ croak "First line of $filename is not a ctrlact header.";
+ }
+
+ my $nrcols = 4;
+ if ($version eq $VERSION) {
+ # current version, i.e. no special handling is required. If
+ # previous versions require special handling, then massage the
+ # data or do whatever is required in the following
+ # elsif-clauses:
+ }
+ elsif ($version eq "1.0") {
+ $nrcols = 3;
+ }
+ my $linesplitter = '^\s*'.join('\s+', ('(\S+)') x $nrcols).'\s*$';
+ my $l = 1;
+ while (<$fh>) {
+ $l++;
+ next if m/^\s*(?:#|$)/;
+ my ($type, @matchers) = m/$linesplitter/;
+ @matchers = ['*', @matchers] if ($version eq "1.0");
+ push @matchers, $l;
+ push @window_thresholds, [@matchers] if match($type, 'window');
+ push @channel_thresholds, [@matchers] if match($type, 'channel');
+ push @query_thresholds, [@matchers] if match($type, 'query');
+ }
+ close($fh) || croak "Cannot close mappings file: $!";
+}
+
+sub cmd_load {
+ Irssi::print("ctrlact: loading mappings from $map_file");
+ load_mappings($map_file);
+ $changed_since_last_save = 0;
+}
+
+sub cmd_save {
+ error("saving not yet implemented");
+ return 1;
+}
+
+sub cmd_list {
+ Irssi::print("ctrlact: window mappings");
+ Irssi::print(get_mappings_table(@window_thresholds), MSGLEVEL_CRAP);
+ Irssi::print("ctrlact: channel mappings");
+ Irssi::print(get_mappings_table(@channel_thresholds), MSGLEVEL_CRAP);
+ Irssi::print("ctrlact: query mappings");
+ Irssi::print(get_mappings_table(@query_thresholds), MSGLEVEL_CRAP);
+}
+
+sub parse_args {
+ my (@args) = @_;
+ my @words = ();
+ my $typewasset = 0;
+ my $tag;
+ my $max = 0;
+ my $type = undef;
+ foreach my $arg (@args) {
+ if ($arg =~ m/^-(windows?|channels?|quer(?:ys?|ies))/) {
+ if ($typewasset) {
+ error("can't specify -$1 after -$type");
+ return 1;
+ }
+ $type = 'window' if $1 =~ m/^w/;
+ $type = 'channel' if $1 =~ m/^c/;
+ $type = 'query' if $1 =~ m/^q/;
+ $typewasset = 1
+ }
+ elsif ($arg =~ m/-(\S+)/) {
+ $tag = $1;
+ }
+ else {
+ push @words, $arg;
+ $max = length $arg if length $arg > $max;
+ }
+ }
+ return ($type, $tag, $max, @words);
+}
+
+sub cmd_query {
+ my ($data, $server, $item) = @_;
+ my @args = shellwords($data);
+ my ($type, $tag, $max, @words) = parse_args(@args);
+ $type = $type // 'channel';
+ $tag = $tag // '*';
+ foreach my $word (@words) {
+ my ($t, $tt, $match) = get_specific_threshold($type, $word, $tag);
+ printf CLIENTCRAP "ctrlact $type map: %s %*s → %d (%s, match:%s)", $tag, $max, $word, $t, $tt, $match;
+ }
+}
+
+sub cmd_show {
+ my ($data, $server, $item) = @_;
+ my @args = shellwords($data);
+ my ($type, $max, @words) = parse_args(@args);
+ $type = $type // 'all';
+
+ if ($type eq 'channel' or $type eq 'all') {
+ print_levels_for_all('channel', Irssi::channels());
+ }
+ if ($type eq 'query' or $type eq 'all') {
+ print_levels_for_all('query', Irssi::queries());
+ }
+ if ($type eq 'window' or $type eq 'all') {
+ print_levels_for_all('window', Irssi::windows());
+ }
+}
+
+sub autosave {
+ cmd_save() if ($changed_since_last_save);
+}
+
+sub UNLOAD {
+ autosave();
+}
+
+Irssi::signal_add('setup saved', \&autosave);
+Irssi::signal_add('setup reread', \&cmd_load);
+
+Irssi::command_bind('ctrlact help',\&cmd_help);
+Irssi::command_bind('ctrlact reload',\&cmd_load);
+Irssi::command_bind('ctrlact load',\&cmd_load);
+Irssi::command_bind('ctrlact save',\&cmd_save);
+Irssi::command_bind('ctrlact list',\&cmd_list);
+Irssi::command_bind('ctrlact query',\&cmd_query);
+Irssi::command_bind('ctrlact show',\&cmd_show);
+
+Irssi::command_bind('ctrlact' => sub {
+ my ( $data, $server, $item ) = @_;
+ $data =~ s/\s+$//g;
+ if ($data) {
+ Irssi::command_runsub('ctrlact', $data, $server, $item);
+ }
+ else {
+ cmd_help();
+ }
+ }
+);
+Irssi::command_bind('help', sub {
+ $_[0] =~ s/\s+$//g;
+ return unless $_[0] eq 'ctrlact';
+ cmd_help();
+ Irssi::signal_stop();
+ }
+);
+
+cmd_load();