From: Ailin Nemui Date: Mon, 8 Sep 2025 19:52:52 +0000 (+0200) Subject: [mlist] add mlist script X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=537be591bc8b64f41dff3068558df85cde557bc5;p=irssi-scripts.irssi.org.git [mlist] add mlist script --- diff --git a/scripts/mlist.pl b/scripts/mlist.pl new file mode 100644 index 0000000..91666bf --- /dev/null +++ b/scripts/mlist.pl @@ -0,0 +1,632 @@ +use strict; +use warnings; + +our $VERSION = '0.3'; # 75c8d7a9c21c683 +our %IRSSI = ( + authors => 'Nei', + contact => 'Nei @ anti@conference.jabber.teamidiot.de', + url => "http://anti.teamidiot.de/", + name => 'mlist', + description => 'Sortable /(M)LIST.', + license => 'ISC', + ); + +use experimental 'signatures'; +use List::Util qw(min max); +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use constant PFM => Irssi::UI::TextDest->can('printformat_module') ? 1 : 0; +use constant SAC => Irssi->can('settings_add_choice') ? 1 : 0; + +my %finished; + +my %pos; +use constant { + COUNT => 0, + START => 1, + CHANNEL_LEN => 2, + USERS_LEN => 3, + SORT => 4, + NAME_FILTER => 5, + TOPIC_FILTER => 6, + MIN => 7, + MAX => 8, + ORDER => 9, + TOPICLEN => 10, + REGEXP => 11, +}; + +my %in_progress; +use constant { + TIME => 0, + ARGS => 1, +}; + +my %list; +my %sorted_list; +use constant { + CHANNEL => 0, + USERS => 1, + TOPIC => 2, + CHANNEL_CLEAN => 3, +}; + +use constant { + DESC => -1, + ASC => 1, +}; + +if (Irssi->can('string_width')) { + *screen_length = sub { Irssi::string_width($_[0]) }; +} +else { + require Text::CharWidth; + *screen_length = sub { Text::CharWidth::mbswidth($_[0]) }; +} + +sub printt ($server, $level, $format, @args) { + Irssi::Server::printformat($server, '', $level, $format, @args); +} + +sub clean_name ($server, $channel) { + my $chantypes = $server->isupport('chantypes') || '#&'; + my %idchan = split /[:,]/, $server->isupport('idchan') // ''; + my $pfx = substr $channel, 0, 1; + $channel = substr $channel, 1 + $idchan{$pfx} + if exists $idchan{$pfx}; + $channel =~ s/^[\Q$chantypes\E]+//; + "\L$channel" +} + +sub sig_liststart { + my ($server, $args) = @_; + my $tag = $server->{tag}; + unless ($in_progress{$tag} && $in_progress{$tag}[+TIME]) { + $_[1] = "321 $_[1]"; + Irssi::signal_emit('default event numeric', @_); + return; + } + + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_liststart', $tag); +} + +sub sig_list { + my ($server, $args) = @_; + my $tag = $server->{tag}; + unless ($in_progress{$tag} && $in_progress{$tag}[+TIME]) { + $_[1] = "322 $_[1]"; + Irssi::signal_emit('default event numeric', @_); + return; + } + + my @args = split ' :', $args, 2; + unshift @args, split ' ', shift @args; + my ($client, $channel, $users, $info) = @args; + push $list{$tag}->@*, [ $channel, $users, $info, clean_name($server, $channel) ]; + my $count = scalar $list{$tag}->@*; + unless ($count % 123) { + my $win = $server->window_find_level(MSGLEVEL_CLIENTCRAP) || Irssi::active_win; + my $oldline = $win->view->get_bookmark('mlist_list'); + $win->view->remove_line($oldline) if $oldline; + + my $oll = $win->view->{buffer}{cur_line}; + my $duration = time - $in_progress{$tag}[+TIME]; + $win->print("[$tag] [mlist] Channel list in progress: retrieved $count channels in $duration seconds...", MSGLEVEL_NEVER); + my $ll = $win->view->{buffer}{cur_line}; + $win->view->set_bookmark_bottom('mlist_list') + if $ll->{_irssi} ne $oll->{_irssi}; + $win->view->redraw if $oldline; + } +} + +sub sig_listend { + my ($server, $args) = @_; + my $tag = $server->{tag}; + unless ($in_progress{$tag} && $in_progress{$tag}[+TIME]) { + $_[1] = "323 $_[1]"; + Irssi::signal_emit('default event numeric', @_); + return; + } + + return unless $in_progress{$tag} && $in_progress{$tag}[+TIME]; + $finished{$tag} = time; + my $delta = $finished{$tag} - $in_progress{$tag}[+TIME]; + $in_progress{$tag}[+TIME] = 0; + my $win = $server->window_find_level(MSGLEVEL_CLIENTCRAP) || Irssi::active_win; + my $oldline = $win->view->get_bookmark('mlist_list'); + $win->view->remove_line($oldline) if $oldline; + $win->view->redraw if $oldline; + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_listend', "$delta", "@{[ scalar $list{$tag}->@* ]}", $tag); +} + +sub sig_server_disconnected ($server) { + my $tag = $server->{tag}; + delete $in_progress{$tag}; +} + +sub sort_flag ($f) { + if (!defined $f) { + 'none' + } + elsif ($f == +CHANNEL || $f == +CHANNEL_CLEAN) { + 'name' + } + elsif ($f == +USERS) { + 'users' + } + elsif ($f == +TOPIC) { + 'topic' + } + else { + 'none' + } +} + +sub cmd_mlist ($data, $server, $witem) { + unless ($server) { + if (+PFM) { + Irssi::UI::Window::format_create_dest(undef, MSGLEVEL_CLIENTERROR)->printformat_module('fe-common/core', 'not_connected'); + } + else { + print CLIENTERROR 'Not connected to server'; + } + return; + } + my $tag = $server->{tag}; + + $data =~ s/^\s*//; + $data =~ s/\s*$//; + + my ($opts, $arg) = Irssi::command_parse_options('mlist', $data); + $opts //= +{}; + my $numarg = " $arg"; + if ($numarg =~ s/\s+(\d+)(?:\s+(\d+))?+$//) { + $opts->{count} = $1; + $opts->{start} = $2 + if defined $2; + $arg = $numarg; + } + + my $error = 0; + + if (exists $opts->{clear}) { + delete $opts->{clear}; + if (keys $opts->%* || $arg) { + print CLIENTERROR '[mlist] -clear cannot be used together with other arguments'; + return; + } + if ($in_progress{$tag} && $in_progress{$tag}[+TIME]) { + print CLIENTERROR '[mlist] Cannot -clear while the channel list is still being retrieved'; + return; + } + delete $pos{$tag}; + delete $in_progress{$tag}; + delete $finished{$tag}; + delete $sorted_list{$tag}; + if (!exists $list{$tag}) { + print CLIENTCRAP '[mlist] There is no channel list'; + } + else { + delete $list{$tag}; + print CLIENTCRAP '[mlist] Channel list cleared'; + } + return; + } + + my ($order, $field, $num, $sort, $show, $prev, $next); + my ($name_filter, $topic_filter); + + my $s = $opts->{sort} || ($pos{$tag} && sort_flag($pos{$tag}[+SORT])) || (+SAC ? sort_flag(Irssi::settings_get_choice('mlist_default_sort') - 1) : Irssi::settings_get_string('mlist_default_sort')); + if ($s =~ /^no(ne?)?$/i) { + $sort = 0; + } + else { + if ($s =~ /^t(o(p(ic?)?)?)?$/i) { + $field = +TOPIC; + $order = exists $opts->{desc} ? +DESC : +ASC; + } + elsif ($s =~ /^u(s(e(rs?)?)?)?/i || $s =~ /^m(e(m(b(e(rs?)?)?)?)?)?/i) { + $field = +USERS; + $num = 1; + $order = exists $opts->{asc} ? +ASC : +DESC; + } + elsif ($s =~ /^n(a(me?)?)?/i || $s =~ /^c(h(a(n(n(el?)?)?)?)?)?/i) { + $field = +CHANNEL_CLEAN; + $order = exists $opts->{desc} ? +DESC : +ASC; + } + else { + print CLIENTERROR '[mlist] -sort must be one of [topic, users, name]'; + $error++; + } + $sort = 1; + } + if (!$opts->{sort} && !exists $opts->{asc} && !exists $opts->{desc} && $pos{$tag}) { + $order = $pos{$tag}[+ORDER]; + } + + if (exists $opts->{prev}) { + $prev = 1; + } + if (exists $opts->{next}) { + $next = 1; + } + if (exists $opts->{count}) { + } + + if (exists $opts->{min} && length $opts->{min} && $opts->{min} !~ /^\d+$/) { + print CLIENTERROR '[mlist] -min is not a number'; + $error++; + } + if (exists $opts->{max} && length $opts->{max} && $opts->{max} !~ /^\d+$/) { + print CLIENTERROR '[mlist] -max is not a number'; + $error++; + } + if (exists $opts->{topiclen} && length $opts->{topiclen} && $opts->{topiclen} !~ /^\d+$/) { + print CLIENTERROR '[mlist] -topiclen is not a number'; + $error++; + } + + my $conflict = (exists $opts->{sort}) + (exists $opts->{next}) + (exists $opts->{prev}); + if ($conflict > 1) { + print CLIENTERROR '[mlist] Only one of -sort, -prev, or -next can be specified'; + $error++; + } + + $conflict = (exists $opts->{asc}) + (exists $opts->{desc}); + if ($conflict > 1) { + print CLIENTERROR '[mlist] Only one of -asc or -desc can be specified'; + $error++; + } + + $conflict = (exists $opts->{regexp}) + (exists $opts->{noregexp}); + if ($conflict > 1) { + print CLIENTERROR '[mlist] Only one of -regexp or -noregexp can be specified'; + $error++; + } + + my $regexp = exists $opts->{regexp} ? 1 : exists $opts->{noregexp} ? 0 : ($pos{$tag} && $pos{$tag}[+REGEXP]) || 0; + my $name_f = length $opts->{name} ? $opts->{name} : exists $opts->{name} ? undef : ($pos{$tag} && $pos{$tag}[+NAME_FILTER]) || undef; + my $topic_f = length $opts->{topic} ? $opts->{topic} : exists $opts->{topic} ? undef : ($pos{$tag} && $pos{$tag}[+TOPIC_FILTER]) || undef; + + if (length $name_f) { + if ($regexp) { + local $@; + eval { $name_filter = qr/$name_f/i }; + if (my $err = $@) { + $err =~ s/ at .* line .*\.\r?\n//; + print CLIENTERROR "[mlist] Error in -name regexp: $err"; + $error++; + } + } + else { + my $name = $name_f; + $name = "*$name*" unless $name =~ /\*/; + $name = "\Q$name"; + my %fn = ('*' => '.*', '?' => '.'); + $name =~ s/\\([*?])/$fn{$1}/g; + $name_filter = qr/^$name$/; + } + } + else { + $name_filter = undef; + } + + if (length $topic_f) { + if ($regexp) { + local $@; + eval { $topic_filter = qr/$topic_f/i }; + if (my $err = $@) { + $err =~ s/ at .* line .*\.\r?\n//; + print CLIENTERROR "[mlist] Error in -topic regexp: $err"; + $error++; + } + } + else { + my $topic = $topic_f; + $topic = "*$topic*" unless $topic =~ /\*/; + $topic = "\Q$topic"; + my %fn = ('*' => '.*', '?' => '.'); + $topic =~ s/\\([*?])/$fn{$1}/g; + $topic_filter = qr/^$topic$/; + } + } + else { + $topic_filter = undef; + } + + my $force = exists $opts->{force}; + delete $opts->{force}; + if ($in_progress{$tag} && $in_progress{$tag}[+TIME] && !$force) { + print CLIENTERROR '[mlist] Retrieving the channel list is still in progress, be patient...'; + $error++; + } + + return if $error; + + my $count = $opts->{count} || ($pos{$tag} && $pos{$tag}[+COUNT]) || Irssi::settings_get_int('mlist_default_count'); + my $start = $opts->{start} ? $opts->{start} - 1 : 0; + my $topiclen = length $opts->{topiclen} ? $opts->{topiclen} : exists $opts->{topiclen} ? -1 : ($pos{$tag} && $pos{$tag}[+TOPICLEN]) || -1; + my $min = length $opts->{min} ? $opts->{min} : exists $opts->{min} ? undef : ($pos{$tag} && $pos{$tag}[+MIN]) || undef; + my $max = length $opts->{max} ? $opts->{max} : exists $opts->{max} ? undef : ($pos{$tag} && $pos{$tag}[+MAX]) || undef; + + my @list; + @list = $list{$tag}->@* + if $list{$tag}; + if ($sort) { + @list = sort { + $order * ($num ? $a->[$field] <=> $b->[$field] + : $a->[$field] cmp $b->[$field]) + } @list; + } + if ($name_filter) { + @list = grep { $_->[+CHANNEL] =~ $name_filter } @list; + } + if ($topic_filter) { + @list = grep { $_->[+TOPIC] =~ $topic_filter } @list; + } + if ($min) { + @list = grep { $_->[+USERS] >= $min } @list; + } + if ($max) { + @list = grep { $_->[+USERS] <= $max } @list; + } + + if ($pos{$tag} && exists $opts->{start}) { + $pos{$tag}[+START] = $opts->{start} - 1; + } + + if ($next) { + unless ($pos{$tag}) { + print CLIENTERROR '[mlist] The first page has not been viewed'; + return; + } + if ($pos{$tag}[+START] + $pos{$tag}[+COUNT] > scalar $sorted_list{$tag}->@*) { + printt(undef, MSGLEVEL_CLIENTCRAP, 'mlist_endoflist'); + return; + } + $pos{$tag}[+START] += $pos{$tag}[+COUNT]; + } + elsif ($prev) { + unless ($pos{$tag}) { + print CLIENTERROR '[mlist] The first page has not been viewed'; + return; + } + if (!$pos{$tag}[+START]) { + printt(undef, MSGLEVEL_CLIENTCRAP, 'mlist_endoflist'); + return; + } + $pos{$tag}[+START] -= $pos{$tag}[+COUNT]; + $pos{$tag}[+START] = 0 + if $pos{$tag}[+START] < 0; + } + elsif (keys $opts->%*) { + $sorted_list{$tag} = \@list; + my $channel_len = max 7, map { screen_length($_->[+CHANNEL]) } @list; + my $users_len = max 5, map { screen_length($_->[+USERS]) } @list; + $pos{$tag} = [ $count, $start, + $channel_len, $users_len, + $field, $name_f, $topic_f, $min, $max, $order, $topiclen, $regexp ]; + } + + if ($pos{$tag} && exists $opts->{count}) { + $pos{$tag}[+COUNT] = $opts->{count}; + } + + if (keys $opts->%*) { + if (!$in_progress{$tag}) { + print CLIENTERROR '[mlist] No channel list loaded, run /MLIST without count or -sort first'; + return; + } + my @sublist; + my $i = $pos{$tag}[+START]; + my $start = $i + 1; + for my $c (1 .. $pos{$tag}[+COUNT]) { + my $e = $sorted_list{$tag}[$i]; + last unless $e; + push @sublist, $e; + $i++; + } + my $end = $i + 1; + my $channel_len = max 7, map { screen_length($_->[+CHANNEL]) } @sublist; + my $users_len = max 5, map { screen_length($_->[+USERS]) } @sublist; + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_header', $tag); + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_line', + (sprintf "%-${channel_len}s", 'Channel'), + (sprintf "%${users_len}s", 'Users'), + '(Topic info)', '', '', $tag); + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_line', + ('-' x $channel_len), + ('-' x $users_len), + ('-' x (50 - min 20, $channel_len + $users_len)), '', '', $tag); + for my $e (@sublist) { + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_line', + (sprintf "%-${channel_len}s", $e->[0]), + (sprintf "%${users_len}s", "$e->[1]"), + $topiclen && $topiclen > -1 ? substr $e->[2], 0, $topiclen : $e->[2], + "$start", "$end", $tag); + } + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_line', + ('-' x $channel_len), + ('-' x $users_len), + ('-' x (50 - min 20, $channel_len + $users_len)), '', '', $tag); + + my $sort_name; + if (!defined $pos{$tag}[+SORT]) { + $sort_name = '(none)'; + } + elsif ($pos{$tag}[+SORT] == +CHANNEL || $pos{$tag}[+SORT] == +CHANNEL_CLEAN) { + $sort_name = 'Name'; + } + elsif ($pos{$tag}[+SORT] == +USERS) { + $sort_name = 'Users'; + } + elsif ($pos{$tag}[+SORT] == +TOPIC) { + $sort_name = 'Topic'; + } + my $sort_order; + if (!defined $pos{$tag}[+ORDER]) { + $sort_order = ''; + } + elsif ($pos{$tag}[+ORDER] == +ASC) { + $sort_order = 'Asc'; + } + elsif ($pos{$tag}[+ORDER] == +DESC) { + $sort_order = 'Desc'; + } + printt($server, MSGLEVEL_CLIENTCRAP, 'mlist_list_footer', + $sort_name . ($sort_order ? "($sort_order)" : ''), + ($pos{$tag}[+NAME_FILTER] // '(none)'), + ($pos{$tag}[+TOPIC_FILTER] // '(none)'), + ($pos{$tag}[+MIN] // '-'), + ($pos{$tag}[+MAX] // '-'), + ($in_progress{$tag}[+ARGS] || '(none)'), + "@{[ $pos{$tag}[+START] + 1 ]}", "$i", "@{[ scalar $sorted_list{$tag}->@* ]}", + $tag); + } + else { + if ($finished{$tag}) { + my $td = time - $finished{$tag}; + + if ($td <= 600 && $data eq $in_progress{$tag}[+ARGS]) { + print CLIENTERROR "[mlist] There is a channel list from $td seconds ago, use -force to reload or /MLIST $count to view it."; + return; + } + } + $list{$tag} = []; + delete $finished{$tag}; + delete $pos{$tag}; + $in_progress{$tag} = [ time, $data ]; + $server->send_raw("LIST $data"); + } +} + +sub iaquote ($str) { + if ($str =~ /"/ || $str =~ /\s/) { + $str =~ s/(["\\])/\\$1/g; + $str = "\"$str\""; + } + $str +} + +sub complete_cmd_mlist ($cl, $win, $word, $start, $ws) { + my @start = split ' ', $start; + if (@start) { + if ($start[-1] =~ /^-s(o(rt?)?)?/i) { + my @args = qw(topic users name); + @args = grep /^\Q$word/i, @args; + push @$cl, @args; + } + elsif ($start[-1] =~ /^-na(me?)?/i) { + my $server = $win ? $win->{active_server} : undef; + my $tag = $server ? $server->{tag} : undef; + if (!length $word && $tag && $pos{$tag} && $pos{$tag}[+NAME_FILTER]) { + unshift @$cl, iaquote($pos{$tag}[+NAME_FILTER]); + } + } + elsif (lc $start[-1] eq '-topic') { + my $server = $win ? $win->{active_server} : undef; + my $tag = $server ? $server->{tag} : undef; + if (!length $word && $tag && $pos{$tag} && $pos{$tag}[+TOPIC_FILTER]) { + unshift @$cl, iaquote($pos{$tag}[+TOPIC_FILTER]); + } + } + } +} + +Irssi::theme_register([ + 'mlist_list_header' => ('=' x 20) . ' Channel list ' . ('=' x 20), + 'mlist_list_line' => '[$0] $1 %|$2', + 'mlist_list_footer' => 'Sort: $0 Filter: $1 Topic filter: $2 Min: $3 Max: $4 Args: $5 [${6}-$7/$8]', + 'mlist_liststart' => '[mlist] Now retrieving channel list, be patient...', + 'mlist_listend' => '[mlist] Finished retrieving the channel list with $1 channels in $0 seconds', + 'mlist_endoflist' => '[mlist] Reached end of list', + 'mlist_beginningoflist' => '[mlist] Reached beginning of list', + ]); + +Irssi::signal_add_last({ + 'event 321' => 'sig_liststart', + 'event 322' => 'sig_list', + 'event 323' => 'sig_listend', + }); + +Irssi::signal_add('server disconnected' => 'sig_server_disconnected'); + +Irssi::settings_add_int('misc', 'mlist_default_count', 20); +if (+SAC) { + Irssi::settings_add_choice('misc', 'mlist_default_sort', 0, 'none;channel;users;topic'); +} +else { + Irssi::settings_add_string('misc', 'mlist_default_sort', 'none'); +} + +Irssi::command_bind('mlist' => 'cmd_mlist'); +Irssi::command_set_options('mlist' => '+sort +name +min +max +topic regexp noregexp asc desc next prev +topiclen force'); + +Irssi::signal_register({'complete command ' => [qw[glistptr_char* Irssi::UI::Window string string intptr]]}); + +Irssi::signal_add('complete command mlist' => 'complete_cmd_mlist'); + +Irssi::command_bind_last( + 'help' => sub ($args, $server, $witem) { + if ($args =~ /^mlist *$/i) { + print CLIENTCRAP < +MLIST %|[-sort [topic|users|name]] [-name ] [-min <#>] [-max <#>] [-topic ] [-regexp|-noregexp] [-asc|-desc] [-topiclen <#>] [-force] [] [] +MLIST -prev|-next +MLIST -clear + +%9Description:%9 + + Sort and view the channel list. Without arguments, or with + , will request the channel list from the server. With -sort + or , will show the retrieved channel list. + +%9Parameters:%9 + + : Arguments to pass to the IRC server for server side filtering, see /LIST x or /SHELP LIST. + -sort: Sort by channel name, user count, or topic. + -name: Filter the channel name by this pattern. + -min: Filter by minimum user count. + -max: Filter by maximum user count. + -topic: Filter the channel topic by this pattern. + -regexp: The patterns are regular expressions. + -noregexp: The patterns are wildcard masks. + -asc: Sort the list in ascending order. + -desc: Sort the list in descending order. + -topiclen: Limit the length of the topic column to this number of characters. + -force: Force refresh the list. + : The number of channels to view on one page. + : Display the channel list starting at this entry. + -prev: View the previous page. + -next: View the next page. + -clear: Clear the retrieved channel lists. + +%9Common server side arguments:%9 + + %9<%9%Imax_users%I ; Show all channels with less than max_users. + %9>%9%Imin_users%I ; Show all channels with more than min_users. + %9C<%9%Imax_minutes%I ; Channels that exist less than max_minutes. + %9C>%9%Imin_minutes%I ; Channels that exist more than min_minutes. + %9T<%9%Imax_minutes%I ; Channels with a topic last set less than max_minutes ago. + %9T>%9%Imin_minutes%I ; Channels with a topic last set more than min_minutes ago. + %Ipattern%I ; Channels with names matching pattern. + %9!%9%Ipattern%I ; Channels with names not matching pattern. + Note: Patterns may contain * and ?. You may only give one pattern match constraint. + Example: /MLIST <3,>1,C<10,T>0,#a* ; 2 users, younger than 10 min., topic set., starts with #a + +%9Example:%9 + + /MLIST >100 + /MLIST -sort users + /MLIST 10 + /MLIST -name ubuntu + /MLIST -next + +%9See also:%9 LIST +HELP + } + });