diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:19:02 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:19:02 +0000 |
commit | 03929dac2a29664878d2c971648a4fe1fb698462 (patch) | |
tree | 02c5e2b3e006234aa29545f7a93a1ce01b291a8b /scripts/ctrlact.pl | |
parent | Initial commit. (diff) | |
download | irssi-scripts-upstream/20231031.tar.xz irssi-scripts-upstream/20231031.zip |
Adding upstream version 20231031.upstream/20231031upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | scripts/ctrlact.pl | 1087 |
1 files changed, 1087 insertions, 0 deletions
diff --git a/scripts/ctrlact.pl b/scripts/ctrlact.pl new file mode 100644 index 0000000..1a59bf6 --- /dev/null +++ b/scripts/ctrlact.pl @@ -0,0 +1,1087 @@ +# ctrlact.pl — Irssi script for fine-grained control of activity indication +# +# © 2017–2021 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, possibily depending on how long ago +# you yourself were active on the channel, then ctrlact might be for you. +# +# For instance, you might never want to be disturbed by activity in any +# channel, unless someone highlights you, or if you've said something yourself +# in the channel in the past hour. You also 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 * * messages 3600 +# channel * * hilights +# query efnet * messages +# query * * all +# +# These five lines would be interpreted/read as: +# "only messages or higher in a channel matching /^#myco-/ should trigger act" +# "in all other channels where I've been active in the last 3600 seconds, +# trigger on all messages" +# "in all other channels, only hilights (or higher) should trigger act" +# "queries on efnet should only trigger act for messages and higher" +# "privmsgs of all levels should trigger act in queries elsewhere" +# +# The activity level in the fourth 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. You may also use the asterisk by itself to match everything, or +# as part of a word, e.g. #debian-*. No other wildcards are supported. +# +# If you change the file, make sure to use /ctrlact reload or else it may get +# overwritten. +# +# 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. +# +### Changelog: +# +# 2021-09-20 : v1.5 +# * Introduce snoop and sleep. Snooping means ctrlact will apply rules as if +# you had just been active on the channel, and sleeping means that ctrlact +# applies rules as if you hadn't been active recently. +# * Also display the time remaining when an attention-span rule matches +# * Sanity checks on the fallback settings +# * Implement /ctrlact help +# * Fix /ctrlact show with an empty ruleset +# +# 2021-09-11 : v1.4 +# * Let rules be defined and removed with /ctrlact add/remove +# * Implement saving of map file +# * Introduce the concept of attention span +# * Wildcard matching on substrings +# * Several code refactorings and improvements +# +# 2021-09-06 : v1.3 +# * Maintenance release, minor fixups +# +# 2017-02-24 : v1.2 +# * Fix invocation of '/ctrlact query' without a -tag (#354) +# +# 2017-02-15 : v1.1 +# * Configurable inhibition of beeps +# * Re-read configuration properly +# * Provide for matching on chatnet/server tag +# +# 2017-02-12 : v1.0 +# * Initial public release +# +### To-do: +# +# - figure out interplay with activity_hide_level +# - use Irssi formats +# +use strict; +use warnings; +use utf8; +use Carp qw( croak ); +use Irssi; +use Text::ParseWords; +use version; + +our %IRSSI = ( + authors => 'martin f. krafft', + contact => 'madduck@madduck.net', + name => 'ctrlact', + description => 'allows per-channel control over activity indication', + license => 'MIT', + url => 'https://github.com/irssi/scripts.irssi.org/blob/master/scripts/ctrlact.pl', + version => '1.5', + changed => '2021-09-20' +); + +our $VERSION = $IRSSI{version}; +my $_VERSION = version->parse($VERSION); + +### DEFAULTS AND SETTINGS ###################################################### + +my @DATALEVEL_KEYWORDS = ('all', 'messages', 'hilights', 'none'); + +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; +my $autosave = 1; + +Irssi::settings_add_str('ctrlact', 'ctrlact_map_file', $map_file); +Irssi::settings_add_bool('ctrlact', 'ctrlact_debug', $debug); +Irssi::settings_add_str('ctrlact', 'ctrlact_fallback_channel_threshold', $fallback_channel_threshold); +Irssi::settings_add_str('ctrlact', 'ctrlact_fallback_query_threshold', $fallback_query_threshold); +Irssi::settings_add_str('ctrlact', 'ctrlact_fallback_window_threshold', $fallback_window_threshold); +Irssi::settings_add_bool('ctrlact', 'ctrlact_inhibit_beep', $inhibit_beep); +Irssi::settings_add_bool('ctrlact', 'ctrlact_autosave', $autosave); + +sub init_threshold_setting { + my ($type, $ref) = @_; + my $setting = 'ctrlact_fallback_'.$type.'_threshold'; + my $th = Irssi::settings_get_str($setting); + my $dl = get_data_level($th); + if ($dl) { + ${$ref} = $dl; + } + else { + Irssi::settings_set_str($setting, ${$ref}); + } +} + +sub sig_setup_changed { + $debug = Irssi::settings_get_bool('ctrlact_debug'); + $map_file = Irssi::settings_get_str('ctrlact_map_file'); + + init_threshold_setting('channel', \$fallback_channel_threshold); + init_threshold_setting('query', \$fallback_query_threshold); + init_threshold_setting('window', \$fallback_window_threshold); + + $inhibit_beep = Irssi::settings_get_bool('ctrlact_inhibit_beep'); + $autosave = Irssi::settings_get_bool('ctrlact_autosave'); +} +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 @window_thresholds; +my @channel_thresholds; +my @query_thresholds; +my %THRESHOLDARRAYS = ('window' => \@window_thresholds, + 'channel' => \@channel_thresholds, + 'query' => \@query_thresholds + ); + +my %OWN_ACTIVITY = (); + +### HELPERS #################################################################### + +use constant DEBUGEVENTFORMAT => "%7s %7.7s %-22.22s %d %s %d → %-7s (%-8s ← %s)"; +sub say { + my ($msg, $level, $inwin) = @_; + $level = $level // MSGLEVEL_CLIENTCRAP; + if ($inwin) { + Irssi::active_win->print("ctrlact: $msg", $level); + } + else { + Irssi::print("ctrlact: $msg", $level); + } +} + +sub debug { + return unless $debug; + my ($msg, $inwin) = @_; + $msg = $msg // ""; + say("DEBUG: ".$msg, MSGLEVEL_CRAP + MSGLEVEL_NO_ACT, $inwin); +} + +use Data::Dumper; +sub dumper { + debug(scalar Dumper(@_), 1); +} + +sub info { + my ($msg, $inwin) = @_; + say($msg, MSGLEVEL_CLIENTCRAP, $inwin); +} + +sub warning { + my ($msg, $inwin) = @_; + $msg = $msg // ""; + say("WARNING: ".$msg, MSGLEVEL_CLIENTERROR, $inwin); +} + +sub error { + my ($msg, $inwin) = @_; + $msg = $msg // ""; + say("ERROR: ".$msg, MSGLEVEL_CLIENTERROR, $inwin); +} + +sub match { + my ($pat, $text) = @_; + if ($pat =~ m/^(\W)(.+)\1$/) { + return ($pat, $text) if $text =~ /$2/i; + } + elsif ($pat =~ m/\*/) { + my $rpat = $pat =~ s/\*/.*/gr; + return ($pat, $text) if $text =~ /$rpat/ + } + else { + return ($pat, $text) if lc($text) eq lc($pat); + } + return (); +} + +sub to_data_level { + my ($kw) = @_; + my $ret = 0; + for my $i (0 .. $#DATALEVEL_KEYWORDS) { + if ($kw eq $DATALEVEL_KEYWORDS[$i]) { + $ret = $i + 1; + } + } + return $ret +} + +sub is_data_level { + my ($dl) = @_; + return $dl =~ /^[1-4]$/; +} + +sub from_data_level { + my ($dl) = @_; + if (is_data_level($dl)) { + return $DATALEVEL_KEYWORDS[$dl-1]; + } +} + +sub get_data_level { + my ($data) = @_; + if (is_data_level($data)) { + return $data; + } + elsif((my $dl = to_data_level($data)) > 0) { + return $dl; + } + else { + error("Invalid data level: $data"); + } +} + +sub walk_match_array { + my ($name, $net, $type, $arr) = @_; + foreach my $rule (@{$arr}) { + my ($netpat, $net) = match($rule->[0], $net); + my ($namepat, $name) = match($rule->[1], $name); + next unless $netpat and $namepat; + + my $own = $OWN_ACTIVITY{($net, $name)} // 0; + my $time = time(); + my $span = ($rule->[3] eq '∞') ? 0 : $rule->[3]; + my $remaining = $own + $span - $time; + + if ($span > 0 and $remaining <= 0) { + delete $OWN_ACTIVITY{($net, $name)}; + next; + } + + my $result = to_data_level($rule->[2]); + my $tresult = from_data_level($result); + $name = '(unnamed)' unless length $name; + my $match = sprintf('%s = net:%s name:%s span:%s', + $rule->[4], $netpat, $namepat, + ($remaining < 0) ? $rule->[3] : $remaining.'s remain'); + return ($result, $tresult, $match); + } + return -1; +} + +sub get_mappings_table { + my ($arr, $fallback) = @_; + my @ret = (); + while (my ($i, $elem) = each @{$arr}) { + push @ret, sprintf("%7d: %-16s %-32s %-9s %-5s (%s)", + $i, @{$elem}); + } + push @ret, sprintf("%7s: %-16s %-32s %-9s %-5s (%s)", + 'last', '*', '*', from_data_level($fallback), '∞', 'default'); + return join("\n", @ret); +} + +sub get_specific_threshold { + my ($type, $name, $net) = @_; + $type = lc($type); + if (exists $THRESHOLDARRAYS{$type}) { + return walk_match_array($name, $net, $type, $THRESHOLDARRAYS{$type}); + } + else { + croak "ctrlact: can't look up threshold for type: $type"; + } +} + +sub get_item_threshold { + my ($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 set_threshold { + my ($arr, $chatnet, $name, $level, $pos, $span) = @_; + + if ($level =~ /^[1-4]$/) { + $level = from_data_level($level); + } + elsif (!to_data_level($level)) { + error("Not a valid activity level: $level", 1); + return -1; + } + + my $found = 0; + my $index = 0; + for (; $index < scalar @{$arr}; ++$index) { + my $item = $arr->[$index]; + if ($item->[0] eq $chatnet and $item->[1] eq $name) { + $found = 1; + last; + } + } + + if ($found) { + splice @{$arr}, $index, 1; + $pos = $index unless defined $pos; + } + + splice @{$arr}, $pos // 0, 0, [$chatnet, $name, $level, $span, 'manual']; + $changed_since_last_save = 1; + return $found; +} + +sub unset_threshold { + my ($arr, $chatnet, $name, $pos) = @_; + my $found = 0; + if (defined $pos) { + if ($pos > $#{$arr}) { + warning("There exists no rule \@$pos"); + } + else { + splice @{$arr}, $pos, 1; + $found = 1; + } + } + else { + for (my $i = scalar @{$arr} - 1; $i >= 0; --$i) { + my $item = $arr->[$i]; + if ($item->[0] eq $chatnet and $item->[1] eq $name) { + splice @{$arr}, $i, 1; + $found = 1; + } + } + if (!$found) { + warning("No matching rule found for deletion"); + } + } + $changed_since_last_save = $found; + return $found; +} + +sub print_levels_for_all { + my ($type, @arr) = @_; + info(uc("$type mappings:")); + for my $i (@arr) { + my $name = $i->{'name'}; + my $net = $i->{'server'}->{'tag'} // ''; + my ($c, $t, $tt, $match); + if ($type eq 'window') { + ($t, $tt, $match) = get_win_threshold($name, $net); + $c = $i->{'refnum'}; + } + else { + ($t, $tt, $match) = get_item_threshold($type, $name, $net); + $c = $i->window()->{'refnum'}; + } + info(sprintf("%4d: %-40.40s → %d (%-8s) match %s", $c, $name, $t, $tt, $match)); + } +} + +sub parse_args { + # type: -window -channel -query + # tag: -* + # span: +\d + # position: @\d + # anything else: item + my ($data) = @_; + my @args = shellwords($data); + my ($type, $tag, $pos, $span); + my @rest = (); + my $max = 0; + + foreach my $arg (@args) { + if ($arg =~ m/^-(windows?|channels?|quer(?:ys?|ies))/) { + if ($type) { + error("Can't specify $arg after -$type", 1); + return 1; + } + my $m = $1; + $type = 'window' if $m =~ m/^w/; + $type = 'channel' if $m =~ m/^c/; + $type = 'query' if $m =~ m/^q/; + } + elsif ($arg =~ m/^-(\S+)/) { + if ($tag) { + error("Tag -$tag already specified, cannot accept $arg", 1); + return 1; + } + $tag = $1; + } + elsif ($arg =~ m/^@([0-9]+)/) { + if ($pos) { + error("Position $pos already given, cannot accept $arg", 1); + return 1; + } + $pos = $1; + } + elsif ($arg =~ m/^\+([0-9]+)/) { + if ($span) { + error("Span $span already given, cannot accept $arg", 1); + return 1; + } + $span = $1; + } + else { + push @rest, $arg; + $max = length $arg if length $arg > $max; + } + } + + my %args = ( + type => $type, + tag => $tag, + pos => $pos, + span => $span, + rest => \@rest, + max => $max + ); + return \%args; +} + +### 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 $witype = $witem->{'type'}; + my $winame = $witem->{'name'}; + my $witag = $witem->{'server'}->{'tag'} // ''; + my ($th, $tth, $match) = get_item_threshold($witype, $winame, $witag); + my $inhibit = $newlevel > 0 && $newlevel < $th; + debug(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_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; + debug(sprintf(DEBUGEVENTFORMAT, 'window', $wtag, + $wname?$wname:"$win->{'refnum'}(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); + +### + +sub record_own_message { + my ($server, $msg, $target) = @_; + $OWN_ACTIVITY{($server->{chatnet}, $target)} = time(); +} +for my $i ('public', 'private') { + Irssi::signal_add("message own_$i", \&record_own_message); +} + +### SAVING AND LOADING ######################################################### + +sub get_mappings_fh { + my ($filename) = @_; + my $fh; + if (! -e $filename) { + save_mappings($filename); + info("Created new/empty mappings file: $filename"); + } + open($fh, '<', $filename) || croak "Cannot open mappings file: $!"; + 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 = version->parse($1); + } + else { + croak "First line of $filename is not a ctrlact header."; + } + + my $nrcols = 5; + if ($version <= version->parse('1.0')) { + $nrcols = 3; + } + elsif ($version <= version->parse('1.3')) { + $nrcols = 4; + } + my $l = 1; + my $cnt = 0; + while (<$fh>) { + $l++; + next if m/^\s*(?:#|$)/; + my ($type, @matchers) = split; + if (scalar @matchers >= $nrcols) { + error("Cannot parse $filename:$l: $_"); + return; + } + @matchers = ['*', @matchers] if $version <= version->parse('1.0'); + + if (scalar @matchers == $nrcols - 2) { + push @matchers, '∞'; + } + + push @matchers, sprintf('line %2d', $l); + + if (exists $THRESHOLDARRAYS{$type}) { + push @{$THRESHOLDARRAYS{$type}}, [@matchers]; + $cnt += 1; + } + } + close($fh) || croak "Cannot close mappings file: $!"; + return $cnt; +} + +sub save_mappings { + my ($filename) = @_; + 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) +# +# WARNING: this file will be overwritten on /save, +# use "/set ctrlact_autosave off" to avoid. +# +# 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 +# span: "attention span", how many seconds after your own +# last message should this rule apply +# +# type server name min.level span + +EOF + foreach my $type (sort keys %THRESHOLDARRAYS) { + foreach my $arr (@{$THRESHOLDARRAYS{$type}}) { + print FH "$type\t"; + print FH join "\t", @{$arr}[0..2]; + print FH "\t" . @{$arr}[3] if @{$arr}[3] ne '∞'; + print FH "\n"; + } + } + print FH <<"EOF"; + +# 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 +# +### display messages in channels in which we were recently (3600s) active: +# channel * * messages 3600 +# +### 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 + close FH; +} + +sub cmd_load { + my $cnt = load_mappings($map_file); + if (!$cnt) { + @window_thresholds = @channel_thresholds = @query_thresholds = (); + } + else { + info("Loaded $cnt mappings from $map_file"); + $changed_since_last_save = 0; + } +} + +sub cmd_save { + my ($args) = @_; + if (!$changed_since_last_save and $args ne '-force') { + info("Not saving unchanged mappings without -force"); + return; + } + autosave(1); +} + +### OTHER COMMANDS ############################################################# + +sub cmd_add { + my ($data, $server, $witem) = @_; + my $args = parse_args($data); + my $type = $args->{type} // 'channel'; + my $tag = $args->{tag} // '*'; + my $pos = $args->{pos}; + my $span = $args->{span} // '∞'; + my ($name, $level); + + for my $item (@{$args->{rest}}) { + if (!$name) { + $name = $item; + } + elsif (!$level) { + $level = $item; + } + else { + error("Unexpected argument: $item"); + return; + } + } + + if (!$name) { + error("Must specify at least a level"); + return; + } + elsif (!length $level) { + if ($witem) { + $level = $name; + $name = $witem->{name}; + $tag = $server->{chatnet} unless $tag; + } + else { + error("No name specified, and no active window item"); + return; + } + } + + my $res = set_threshold($THRESHOLDARRAYS{$type}, $tag, $name, $level, $pos, $span); + if ($res > 0) { + info("Existing rule replaced."); + } + elsif ($res == 0) { + info("Rule added."); + } +} + +sub cmd_remove { + my ($data, $server, $witem) = @_; + my $args = parse_args($data); + my $type = $args->{type} // 'channel'; + my $tag = $args->{tag} // '*'; + my $pos = $args->{pos}; + my $name; + + for my $item (@{$args->{rest}}) { + if (!$name) { + $name = $item; + } + else { + error("Unexpected argument: $item"); + return; + } + } + if (!defined $pos) { + if (!$name) { + if ($witem) { + $name = $witem->{name}; + $tag = $server->{chatnet} unless $tag; + } + else { + error("No name specified, and no active window item"); + return; + } + } + } + + if (unset_threshold($THRESHOLDARRAYS{$type}, $tag, $name, $pos)) { + info("Rule removed."); + } +} + +sub cmd_snoop { + my ($data, $server, $witem) = @_; + my $args = parse_args($data); + my $type = $args->{type} // 'channel'; + my $tag = $args->{tag}; + my $name; + + for my $item (@{$args->{rest}}) { + if (!$name) { + $name = $item; + } + else { + error("Unexpected argument: $item"); + return; + } + } + + if (!$name) { + if ($witem) { + $name = $witem->{name}; + $tag = $server->{chatnet} unless $tag; + } + else { + error("No name specified, and no active window item"); + return; + } + } + + $OWN_ACTIVITY{($tag, $name)} = time(); + info("Snooping in on $tag/$name", 1); +} + +sub cmd_sleep { + my ($data, $server, $witem) = @_; + my $args = parse_args($data); + my $type = $args->{type} // 'channel'; + my $tag = $args->{tag}; + my $name; + + for my $item (@{$args->{rest}}) { + if (!$name) { + $name = $item; + } + else { + error("Unexpected argument: $item"); + return; + } + } + + if (!$name) { + if ($witem) { + $name = $witem->{name}; + $tag = $server->{chatnet} unless $tag; + } + else { + error("No name specified, and no active window item"); + return; + } + } + + my $was = $OWN_ACTIVITY{($tag, $name)}; + delete $OWN_ACTIVITY{($tag, $name)}; + if ($was) { + $was = time() - $was; + info("Back to sleep on $tag/$name (after $was seconds)", 1); + } +} + +sub cmd_list { + info("WINDOW MAPPINGS\n" . get_mappings_table(\@window_thresholds, $fallback_window_threshold)); + info("CHANNEL MAPPINGS\n" . get_mappings_table(\@channel_thresholds, $fallback_channel_threshold)); + info("QUERY MAPPINGS\n" . get_mappings_table(\@query_thresholds, $fallback_query_threshold)); +} + +sub cmd_query { + my ($data, $server, $witem) = @_; + my $args = parse_args($data); + my $type = $args->{type} // 'channel'; + my $tag = $args->{tag} // '*'; + my $max = $args->{max}; + my @words = @{$args->{rest}}; + + if (!@words) { + if ($witem) { + push @words, $witem->{name}; + $tag = $server->{chatnet} unless $tag ne '*'; + } + else { + error("No name specified, and no active window item"); + return; + } + } + + foreach my $name (@words) { + my ($t, $tt, $match) = get_specific_threshold($type, $name, $tag); + info(sprintf("%7s: %7s %-22s → %-8s match: %s", $type, $tag, $name, $tt, $match), 1); + } +} + +sub cmd_show { + my ($data, $server, $item) = @_; + my $args = parse_args($data); + my $type = $args->{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 { + my ($force) = @_; + return unless $force or $changed_since_last_save; + if (!$autosave) { + info("Not saving mappings due to ctrlact_autosave setting"); + return; + } + info("Saving mappings to $map_file"); + save_mappings($map_file); + $changed_since_last_save = 0; +} + +sub UNLOAD { + autosave(); +} + +sub cmd_help { + my ($data, $server, $item) = @_; + Irssi::print (<<"SCRIPTHELP_EOF", MSGLEVEL_CLIENTCRAP); +%_ctrlact $_VERSION - fine-grained control of activity indication%_ + +%U%_Synopsis%_%U + +%_CTRLACT ADD%_ [<%Umatchspec%U>] [@<%Uposition%U>] [+<%Uspan%U>] <%Ulevel%U> +%_CTRLACT REMOVE%_ [<%Umatchspec%U>] [@<%Uposition%U>] +%_CTRLACT QUERY%_ [<%Umatchspec%U>] +%_CTRLACT SNOOP%_ [<%Umatchspec%U>] +%_CTRLACT SLEEP%_ [<%Umatchspec%U>] +%_CTRLACT LIST%_ +%_CTRLACT SHOW%_ [<%Utype%U>] +%_CTRLACT SAVE%_ [-force] +%_CTRLACT [RE]LOAD%_ +%_CTRLACT HELP%_ + +<%Umatchspec%U> %| [-<%Utype%U>] [-<%Utag%U>] <%Uname%U> +%U%U %| (defaults to current window item, if available) +<%Utype%U> %| "window"|"channel"|"query" +%U%U %| (default: "channel") +<%Utag%U> %| The chat network's tag, e.g. oftc +<%Uname%U> %| Name of the channel, query, or window +%U%U %| May include '*', or be a regular expression: /.../ +<%Ulevel%U> %| Minimum activity level to match: +%U%U %| 1, all, 2, messages, 3, highlights, 4, none +<%Uposition%U> %| Integer index where to insert new rule, or of rule to remove +<%Uspan%U> %| Time in seconds during which this rule applies following own engagement + +%U%_Settings%_%U + +/set %_ctrlact_map_file%_ [$map_file] + %| Controls where the activity control map will be read from (and saved to) + +/set %_ctrlact_fallback_channel_threshold%_ [$fallback_channel_threshold] +/set %_ctrlact_fallback_query_threshold%_ [$fallback_query_threshold] +/set %_ctrlact_fallback_window_threshold%_ [$fallback_window_threshold] + %| Controls the lowest data level that will trigger activity for channels, + %| queries, and windows respectively, if no applicable mapping could be + %| found. Valid values are 1, all, 2, messages, 3, highlights, 4, none. + +/set %_ctrlact_inhibit_beep%_ [$inhibit_beep] + %| If an activity wouldn't be indicated, also inhibit the beep/bell. Turn + %| this off if you want the bell anyway. + +/set %_ctrlact_autosave%_ [$autosave] + %| Unless this is disabled, the rules will be written out to the map file + %| (and overwriting it) on /save and /ctrlact save. + +/set %_ctrlact_debug%_ [$debug] + %| Turns on debug output. Not that this may itself be buggy, so please don't + %| use it unless you really need it. + +%U%_Examples%_%U + +Set channel default level to hilights only: + %|%#/SET %_ctrlact_fallback_channel_threshold%_ hilights + +Show activity for messages in the #irssi channel on LiberaChat: + %|%#/%_CTRLACT ADD%_ -LiberaChat #irssi messages + +Show all activity for messages on my company's channels: + %|%#/%_CTRLACT ADD%_ -channel #myco-* all + +Create a rule for the current window item: + %|%#/%_CTRLACT ADD%_ all + +Insert a rule at position 3 (default is to insert at the top): + %|%#/%_CTRLACT ADD%_ @3 #mutt messages + +List all mappings: + %|%#/%_CTRLACT LIST%_ + +Remove mapping at position 3: + %|%#/%_CTRLACT REMOVE%_ @3 + +Remove mapping for current window item: + %|%#/%_CTRLACT REMOVE%_ + +Remove mapping for #irssi channel (see above) + %|%#/%_CTRLACT REMOVE%_ -LiberaChat #irssi + +Save mappings to file ($map_file), using -force to write even if nothing has changed: + %|%#/%_CTRLACT SAVE%_ -force + +Load mappings from file ($map_file): + %|%#/%_CTRLACT LOAD%_ + +Create a rule to show activity on any channel in which we've engaged in the last hour: + %|%#/%_CTRLACT ADD%_ +3600 -* * messages + +Pretend that we interacted with the #perl channel, so as to get activity as per the last rule: + %|%#/%_CTRLACT SNOOP%_ #perl + +Stop activity indication for the current channel after we engaged with it: + %|%#/%_CTRLACT SLEEP%_ + +Query which rule would apply to the current channel: + %|%#/%_CTRLACT QUERY%_ + +Show the matching rule for every query: + %|%#/%_CTRLACT SHOW%_ -query +SCRIPTHELP_EOF +} + +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 add',\&cmd_add); +Irssi::command_bind('ctrlact remove',\&cmd_remove); +Irssi::command_bind('ctrlact snoop',\&cmd_snoop); +Irssi::command_bind('ctrlact sleep',\&cmd_sleep); +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 { + my ($data, $server, $item) = @_; + my @words = split /\s+/, $data; + return unless shift @words eq 'ctrlact'; + cmd_help(); + Irssi::signal_stop(); + } +); + +cmd_load(); |