]> git.99rst.org Git - irssi-scripts.irssi.org.git/commitdiff
[mlist] add mlist script
authorAilin Nemui <redacted>
Mon, 8 Sep 2025 19:52:52 +0000 (21:52 +0200)
committerAilin Nemui <redacted>
Mon, 8 Sep 2025 19:52:52 +0000 (21:52 +0200)
scripts/mlist.pl [new file with mode: 0644]

diff --git a/scripts/mlist.pl b/scripts/mlist.pl
new file mode 100644 (file)
index 0000000..91666bf
--- /dev/null
@@ -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 <<HELP
+%9Syntax:%9
+
+MLIST <args>
+MLIST %|[-sort [topic|users|name]] [-name <channel name filter>] [-min <#>] [-max <#>] [-topic <topic filter>] [-regexp|-noregexp] [-asc|-desc] [-topiclen <#>] [-force] [<count>] [<start>]
+MLIST -prev|-next
+MLIST -clear
+
+%9Description:%9
+
+    Sort and view the channel list.  Without arguments, or with
+    <args>, will request the channel list from the server.  With -sort
+    or <count>, will show the retrieved channel list.
+
+%9Parameters:%9
+
+    <args>:          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.
+    <count>:         The number of channels to view on one page.
+    <start>:         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
+       }
+    });
git clone https://git.99rst.org/PROJECT