]> git.99rst.org Git - irssi-scripts.irssi.org.git/commitdiff
[matterircd-complete] Update to v2.0
authorHaw Loeung <redacted>
Mon, 5 Dec 2022 01:25:46 +0000 (12:25 +1100)
committerHaw Loeung <redacted>
Mon, 12 Dec 2022 10:40:41 +0000 (21:40 +1100)
scripts/matterircd_complete.pl

index f8b55d3a0931c291d276fcef177747519926a3a4..9e8e3ee929edfedbb97a135de03390aa86bea12c 100644 (file)
@@ -56,28 +56,13 @@ use strict;
 use warnings;
 use experimental 'smartmatch';
 
-use Irssi::TextUI;
-use Irssi qw(
-    command_bind
-    gui_input_get_pos
-    gui_input_set
-    gui_input_set_pos
-    parse_special
-    settings_add_bool
-    settings_add_int
-    settings_add_str
-    settings_get_bool
-    settings_get_int
-    settings_get_str
-    signal_add
-    signal_add_last
-    signal_continue
-);
+require Irssi::TextUI;
+require Irssi;
 
 # Enable for debugging purposes only.
 # use Data::Dumper;
 
-our $VERSION = '1.00';
+our $VERSION = '2.00';  # e2973ff
 our %IRSSI = (
     name        => 'Matterircd Tab Auto Complete',
     description => 'Adds tab completion for Matterircd message threads',
@@ -94,58 +79,220 @@ my $KEY_SPC    = 32;
 my $KEY_B      = 66;
 my $KEY_O      = 79;
 
-settings_add_str('matterircd_complete', 'matterircd_complete_networks', '');
-settings_add_str('matterircd_complete', 'matterircd_complete_nick_ignore', '');
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_networks', '');
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_nick_ignore', '');
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_channel_dont_ignore', '');
+
 
+sub _wi_print {
+    my ($wi, $msg) = @_;
+
+    if ($wi) {
+        $wi->print($msg);
+    } else {
+        Irssi::print($msg);
+    }
+}
 
 #==============================================================================
+my %color_config;
+
+# Taken from nickcolor_expando irssi script
+# These are all the colors, sorted by main color class
+# To display and select colors you want/want to avoid based on your background, use /cubes_text from cubes.pl
+my @all_colors = (
+    qw[20 30 40 50 04 66 0C 61 60 67 6L], # RED
+    qw[37 3D 36 4C 46 5C 56 6C 6J 47 5D 6K 6D 57 6E 5E 4E 4K 4J 5J 4D 5K 6R], # ORANGE
+    qw[3C 4I 5I 6O 6I 06 4O 5O 3U 0E 5U 6U 6V 6P 6Q 6W 5P 4P 4V 4W 5W 4Q 5Q 5R 6Y 6X], # YELLOW
+    qw[26 2D 2C 3I 3O 4U 5V 2J 3V 3P 3J 5X], # YELLOW-GREEN
+    qw[16 1C 2I 2U 2O 1I 1O 1V 1P 02 0A 1U 2V 4X], # GREEN
+    qw[1D 1J 1Q 1W 1X 2Y 2S 2R 3Y 3Z 3S 3R 2K 3K 4S 5Z 5Y 4R 3Q 2Q 2X 2W 3X 3W 2P 4Y], # GREEN-TURQUOIS
+    qw[17 1E 1L 1K 1R 1S 03 1M 1N 1T 0B 1Y 1Z 2Z 4Z], # TURQUOIS
+    qw[28 2E 18 1F 19 1G 1A 1B 1H 2N 2H 09 3H 3N 2T 3T 2M 2G 2A 2F 2L 3L 3F 4M 3M 3G 29 4T 5T], # LIGHT-BLUE
+    qw[22 33 44 0D 45 5B 6A 5A 5H 3B 4H 3A 4G 39 4F 6S 6T 5L 5N], # VIOLET
+    qw[21 32 42 53 63 52 43 34 35 55 65 6B 4B 4A 48 5G 6H 5M 6M 6N], # PINK
+    qw[38 31 05 64 54 41 51 62 69 68 59 5F 6F 58 49 6G], # ROSE
+    qw[11 12 23 25 24 13 14 01 15 2B 4N], # DARK-BLUE
+    qw[7A 00 10 7B 7C 7D 7E 7G 7F], # DARK-GRAY
+    qw[7H 7I 27 7K 7J 08 7L 3E 7O 7Q 7N 7M 7P], # GRAY
+    qw[7S 7T 7R 4L 7W 7U 7V 5S 07 7X 6Z 0F], # LIGHT-GRAY
+);
+# These are the colors unwanted with a dark theme
+my @dark_theme_unwanted = (
+    qw[11 12 23 25 24 13 14 01 15 2B 4N], # DARK-BLUE
+    qw[7A 00 10 7B 7C 7D 7E 7G 7F], # DARK-GRAY
+    qw[7H 7I 27 7K 7J 08 7L 3E 7O 7Q 7N 7M 7P], # GRAY
+    qw[7S 7T 7R 4L 7W 7U 7V 5S 07 7X 6Z 0F], # LIGHT-GRAY
+);
+# These are the colors unwanted with a light theme
+my @solarized_light_theme_unwanted = (
+    qw[4U 4V 4W 4X 4Y 4Z 5U 5V 5W 5X 5Y 5Z 6U 6V 6W 6X 6Y 6Z 6O 6P 6Q 6R 6S 6T], # too light flashy colors
+    qw[7T 7U 7V 7W 7X 07 7R 7S 7T 7U 7V 7W 7X 7B 7C 7D 7E 7F 7H 7I 7J 7K 7M 7N 7O 7P], # too light grayscales
+    qw[5S 4L 7Q 5T 4T 4M 5M 6M 6N 1Z 2Z 1Y 2X 2W 3X 3W 1U 2V 1X 2Y 3V 3Z 3Y 2U], # hand picked too light + redundant
+);
+
+Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_thread_id_color', -1);
+# Default color theme to none, so we use all available colors.
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_color_theme', '');
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_bold', 0);
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_italic', 0);
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_thread_id_allow_underline', 0);
+# Allowed colors will be applied first
+# These can be a list of 20 30 40 50 5F colors, or without spaces 203040505F
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_allowed_colors', '');
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_thread_id_unwanted_colors', '');
+$color_config{'color_theme'} = '';
+$color_config{'allowed_colors'} = '';
+$color_config{'unwanted_colors'} = '';
+# Initialize
+my @thread_id_selected_colors = ();
 
 # Rely on message/thread IDs stored in message cache so we can shorten
 # to save on screen real-estate.
-settings_add_int('matterircd_complete',  'matterircd_complete_shorten_message_thread_id', 5);
-settings_add_bool('matterircd_complete', 'matterircd_complete_shorten_message_thread_id_hide_prefix', 1);
+Irssi::settings_add_int('matterircd_complete',  'matterircd_complete_shorten_message_thread_id', 5);
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_shorten_message_thread_id_hide_prefix', 1);
+Irssi::settings_add_str('matterircd_complete', 'matterircd_complete_override_reply_prefix', '↪');
+
+# Taken from nickcolor_expando irssi script and adapted for our use
+sub xcolor_to_irssi {
+    # Set to foreground xcolor
+    my $c = "X".$_[0];
+    my @ext_colour_off = (
+    '.', '-', ',',
+    '+', "'", '&',
+    );
+    if ($c =~ /^(X)(?:0([[:xdigit:]])|([1-6])(?:([0-9])|([a-z]))|7([a-x]))$/i) {
+        my $bg = $1 eq 'x';
+        my $col = defined $2 ? hex $2
+            : defined $6 ? 232 + (ord lc $6) - (ord 'a')
+            : 16 + 36 * ($3 - 1) + (defined $4 ? $4 : 10 + (ord lc $5) - (ord 'a'));
+        if ($col < 0x10) {
+            my $chr = chr $col + ord '0';
+            return "\cD" . ($bg ? "/$chr" : "$chr/");
+        }
+        else {
+            return "\cD" . $ext_colour_off[($col - 0x10) / 0x50 + $bg * 3] . chr (($col - 0x10) % 0x50 - 1 + ord '0');
+        }
+    } else {
+        return $c;
+    }
+}
+
+sub get_thread_format {
+    my ($str) = @_;
+    my @nums = (0..9,'a'..'z');
+    my $chr=join('',@nums);
+    my %nums = map { $nums[$_] => $_ } 0..$#nums;
+    my $n = 0;
+    $str = lc $str;
+    foreach ($str =~ /[$chr]/g) {
+        $n += $nums{$_} * 36;
+    }
+    my @colors = @thread_id_selected_colors;
+    my $color_count = @colors;
+
+    # We have normal, bold, italic, underline
+    my $allow_bold = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_bold');
+    my $allow_italic = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_italic');
+    my $allow_underline = Irssi::settings_get_bool('matterircd_complete_thread_id_allow_underline');
+    my @classes_prepend;
+    push @classes_prepend, "\x02" if $allow_bold;
+    push @classes_prepend, "\x1d" if $allow_italic;
+    push @classes_prepend, "\x1f" if $allow_underline;
+    my $classes = 1 + @classes_prepend;
+    $n = $n % $color_count*$classes;
+    my $random = $n;
+    my $prepend = "";
+    if ($classes == 4 and $n >= $color_count*3) {
+        $n -= $color_count*3;
+        $prepend = $classes_prepend[2];
+    } elsif ($classes ge 3 and $n >= $color_count*2) {
+        $n -= $color_count*2;
+        $prepend = $classes_prepend[1];
+    } elsif ($classes ge 2 and $n >= $color_count) {
+        $n -= $color_count;
+        $prepend = $classes_prepend[0];
+    }
+    $n = $colors[$n-1];
+    return $n, $prepend;
+}
+
+sub thread_color {
+    my ($str) = @_;
+    my ($n, $prepend) = get_thread_format($str);
+    # Pick the color in the allowed_color list.
+    # n should be comprised between 1 and the array length.
+    $n = xcolor_to_irssi($n);
+    $n = "$prepend\x03$n";
+    return $n;
+}
+sub cmd_matterircd_complete_thread_id_get_color {
+    my ($data, $server, $wi) = @_;
+    my ($color, $prepend) = get_thread_format($_[0]);
+    my $n = xcolor_to_irssi($color);
+    _wi_print($wi, "Thread color for $prepend\x03$n$_[0]\x0f is $color");
+}
+Irssi::command_bind('matterircd_complete_thread_id_get_color', 'cmd_matterircd_complete_thread_id_get_color');
+
 sub update_msgthreadid {
     my($server, $msg, $nick, $address, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_shorten_message_thread_id');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     my $prefix = '';
     my $msgthreadid = '';
+    my $msgpostid = '';
+    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');
 
-    if ($msg =~ s/\[(->|↪)?\@\@([0-9a-z]{26})\]/\@\@PLACEHOLDER\@\@/) {
-        $prefix = $1 ? $1 : '';
+    if ($msg =~ s/\[(->|↪)?\@\@([0-9a-z]{26})(?:,\@\@([0-9a-z]{26}))?\]/\@\@PLACEHOLDER\@\@/) {
+        $prefix = $reply_prefix ? $reply_prefix : $1 if $1;
         $msgthreadid = $2;
+        $msgpostid = $3 ? $3 : '';
     }
     return unless $msgthreadid;
 
+    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
+    if ($thread_color == -1) {
+        $thread_color = thread_color($msgthreadid);
+    } else {
+        $thread_color = "\x03${thread_color}";
+    }
+
     # Show that message is reply to a thread. (backwards compatibility
     # when matterircd doesn't show reply)
     if ((not $prefix) && ($msg =~ /\(re \@.*\)/)) {
-        $prefix = '↪';
+        $prefix = $reply_prefix;
     }
 
-    if (not settings_get_bool('matterircd_complete_shorten_message_thread_id_hide_prefix')) {
+    if (not Irssi::settings_get_bool('matterircd_complete_shorten_message_thread_id_hide_prefix')) {
         $prefix = "${prefix}\@\@";
     }
 
-    my $len = settings_get_int('matterircd_complete_shorten_message_thread_id');
+    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
     if ($len < 25) {
         # Shorten to length configured. We use unicode ellipsis (...)
         # here to both allow word selection to just select parts of
         # the message/thread ID when copying & pasting and save on
         # screen real estate.
         $msgthreadid = substr($msgthreadid, 0, $len) . '…';
+        if ($msgpostid ne '') {
+            $msgpostid = substr($msgpostid, 0, $len) . '…';
+        }
+    }
+    if ($msgpostid eq '') {
+        $msg =~ s/\@\@PLACEHOLDER\@\@/${thread_color}[${prefix}${msgthreadid}]\x0f/;
+    } else {
+        $msg =~ s/\@\@PLACEHOLDER\@\@/${thread_color}[${prefix}${msgthreadid},${msgpostid}]\x0f/;
     }
-    $msg =~ s/\@\@PLACEHOLDER\@\@/\x0314[${prefix}${msgthreadid}]\x0f/;
 
-    signal_continue($server, $msg, $nick, $address, $target);
+    Irssi::signal_continue($server, $msg, $nick, $address, $target);
 }
-signal_add_last('message irc action', 'update_msgthreadid');
-signal_add_last('message irc notice', 'update_msgthreadid');
-signal_add_last('message private', 'update_msgthreadid');
-signal_add_last('message public', 'update_msgthreadid');
+Irssi::signal_add_last('message irc action', 'update_msgthreadid');
+Irssi::signal_add_last('message irc notice', 'update_msgthreadid');
+Irssi::signal_add_last('message private', 'update_msgthreadid');
+Irssi::signal_add_last('message public', 'update_msgthreadid');
 
 sub cache_store {
     my ($cache_ref, $item, $cache_size) = @_;
@@ -153,7 +300,7 @@ sub cache_store {
     return unless $item ne '';
 
     my $changed = 0;
-    if (@$cache_ref[0] && @$cache_ref[0] eq $item) {
+    if ((@$cache_ref[0]) && (@$cache_ref[0] eq $item)) {
         return $changed;
     }
     $changed = 1;
@@ -162,7 +309,7 @@ sub cache_store {
     # per-channel cache. But as a trade off in favor of
     # speed/performance, rather than traverse the entire per-channel
     # cache, we cap/limit it.
-    my $limit = 8;
+    my $limit = 16;
     my $max = ($#$cache_ref < $limit)? $#$cache_ref : $limit;
     for my $i (0 .. $max) {
         if ((@$cache_ref[$i]) && (@$cache_ref[$i] eq $item)) {
@@ -187,44 +334,45 @@ sub cache_store {
 
 
 my %MSGTHREADID_CACHE;
-settings_add_int('matterircd_complete', 'matterircd_complete_message_thread_id_cache_size', 50);
-command_bind 'matterircd_complete_msgthreadid_cache_dump' => sub {
+Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_message_thread_id_cache_size', 50);
+sub cmd_matterircd_complete_msgthreadid_cache_dump {
     my ($data, $server, $wi) = @_;
 
     if (not $data) {
         return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
     }
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     my $channel = $data ? $data : $wi->{name};
     # Remove leading and trailing whitespace.
     $channel =~ tr/    //d;
 
-    Irssi::print("${channel}: Message/Thread ID cache");
+    _wi_print($wi, "${channel}: Message/Thread ID cache");
 
     if ((not exists($MSGTHREADID_CACHE{$channel})) || (scalar @{$MSGTHREADID_CACHE{$channel}} == 0)) {
-        Irssi::print("${channel}: Empty");
+        _wi_print($wi, "${channel}: Empty");
         return;
     }
 
     foreach my $msgthread_id (@{$MSGTHREADID_CACHE{$channel}}) {
-        Irssi::print("${channel}: ${msgthread_id}");
+        _wi_print($wi, "${channel}: ${msgthread_id}");
     }
-    Irssi::print("${channel}: Total: " . scalar @{$MSGTHREADID_CACHE{$channel}});
+    _wi_print($wi, "${channel}: Total: " . scalar @{$MSGTHREADID_CACHE{$channel}});
 };
+Irssi::command_bind('matterircd_complete_msgthreadid_cache_dump', 'cmd_matterircd_complete_msgthreadid_cache_dump');
 
 my $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;
 my $MSGTHREADID_CACHE_INDEX = 0;
-command_bind 'message_thread_id_search' => sub {
+sub cmd_message_thread_id_search {
     my ($data, $server, $wi) = @_;
 
-    return unless settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
     return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
     return unless exists($MSGTHREADID_CACHE{$wi->{name}});
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     $MSGTHREADID_CACHE_SEARCH_ENABLED = 1;
@@ -237,24 +385,25 @@ command_bind 'message_thread_id_search' => sub {
 
     if ($msgthreadid) {
         # Save input text.
-        my $input = parse_special('$L');
+        my $input = Irssi::parse_special('$L');
         # Remove existing thread.
         $input =~ s/^@@(?:[0-9a-z]{26}|[0-9a-f]{3}) //;
         # Insert message/thread ID from cache.
-        gui_input_set_pos(0);
-        gui_input_set("\@\@${msgthreadid} ${input}");
+        Irssi::gui_input_set_pos(0);
+        Irssi::gui_input_set("\@\@${msgthreadid} ${input}");
     }
 };
+Irssi::command_bind('message_thread_id_search', 'cmd_message_thread_id_search');
 
 my $ESC_PRESSED = 0;
 my $O_PRESSED   = 0;
-signal_add_last 'gui key pressed' => sub {
+sub signal_gui_key_pressed_msgthreadid {
     my ($key) = @_;
 
     return unless $MSGTHREADID_CACHE_SEARCH_ENABLED;
 
     my $server = Irssi::active_server();
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if (($key == $KEY_RET) || ($key == $KEY_CTRL_U)) {
@@ -267,14 +416,14 @@ signal_add_last 'gui key pressed' => sub {
 
     # Cancel/abort, so remove thread stuff.
     elsif ($key == $KEY_CTRL_C) {
-        my $input = parse_special('$L');
+        my $input = Irssi::parse_special('$L');
 
         # Remove the Ctrl+C character.
         $input =~ tr/\ 3//d;
 
         my $pos = 0;
         if ($input =~ s/^(@@(?:[0-9a-z]{26}|[0-9a-f]{3}) )//) {
-            $pos = gui_input_get_pos() - length($1);
+            $pos = Irssi::gui_input_get_pos() - length($1);
         }
 
         # We also want to move the input position back one for Ctrl+C
@@ -284,8 +433,8 @@ signal_add_last 'gui key pressed' => sub {
         # Replace the text in the input box with our modified version,
         # then move cursor positon to where it was without the
         # message/thread ID.
-        gui_input_set($input);
-        gui_input_set_pos($pos);
+        Irssi::gui_input_set($input);
+        Irssi::gui_input_set_pos($pos);
 
         $MSGTHREADID_CACHE_INDEX = 0;
         $MSGTHREADID_CACHE_SEARCH_ENABLED = 0;
@@ -314,19 +463,20 @@ signal_add_last 'gui key pressed' => sub {
         $O_PRESSED = 0;
     }
 };
+Irssi::signal_add_last('gui key pressed', 'signal_gui_key_pressed_msgthreadid');
 
-signal_add_last 'complete word' => sub {
+sub signal_complete_word_msgthread_id {
     my ($complist, $window, $word, $linestart, $want_space) = @_;
 
     # We only want to tab-complete message/thread if this is the first
     # word on the line.
     return if $linestart;
-    return unless settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
     return if (substr($word, 0, 1) eq '@' and substr($word, 0, 2) ne '@@');
     return unless $window->{active} and ($window->{active}->{type} eq 'CHANNEL' || $window->{active}->{type} eq 'QUERY');
     return unless exists($MSGTHREADID_CACHE{$window->{active}->{name}});
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$window->{active_server}->{chatnet}};
 
     if (substr($word, 0, 2) eq '@@') {
@@ -339,20 +489,26 @@ signal_add_last 'complete word' => sub {
         }
     }
 };
+Irssi::signal_add_last('complete word', 'signal_complete_word_msgthread_id');
 
+my $MSGTHREADID_CACHE_STATS = 0;
 sub cache_msgthreadid {
     my($server, $msg, $nick, $address, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_message_thread_id_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
-    my $msgid = '';
+    my @msgids = ();
 
-    my @ignore_nicks = split(/\s+/, settings_get_str('matterircd_complete_nick_ignore'));
+    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
     # Ignore nicks configured to be ignored such as bots.
     if ($nick ~~ @ignore_nicks) {
-        return;
+        # But not if the channel is in matterircd_complete_channel_dont_ignore.
+        my @channel_dont_ignore = split(/\s+/, Irssi::settings_get_str('matterircd_complete_channel_dont_ignore'));
+        if ($target !~ @channel_dont_ignore) {
+            return;
+        }
     }
 
     # We also want to ignore reactions as we can't reply to those
@@ -362,16 +518,22 @@ sub cache_msgthreadid {
     }
 
     # Mattermost message/thread IDs.
-    if ($msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})\]/) {
-        $msgid = $1;
+    if ($msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})(?:,\@\@([0-9a-z]{26}))?\]/) {
+        my $msgthreadid = $1;
+        my $msgpostid = $2 ? $2 : '';
+
+        if ($msgpostid ne '') {
+            push(@msgids, $msgpostid);
+        }
+        push(@msgids, $msgthreadid);
     }
     # matterircd generated 3-letter hexadecimal.
     elsif ($msg =~ /(?:^\[([0-9a-f]{3})\])|(?:\[([0-9a-f]{3})\]\s*$)/) {
-        $msgid = $1 ? $1 : $2;
+        push(@msgids, $1 ? $1 : $2);
     }
     # matterircd generated 3-letter hexadecimal replying to threads.
     elsif ($msg =~ /(?:^\[[0-9a-f]{3}->([0-9a-f]{3})\])|(?:\[[0-9a-f]{3}->([0-9a-f]{3})\]\s*$)/) {
-        $msgid = $1 ? $1 : $2;
+        push(@msgids, $1 ? $1 : $2);
     }
     else {
         return;
@@ -386,23 +548,26 @@ sub cache_msgthreadid {
         $key = $nick
     }
 
-    my $cache_size = settings_get_int('matterircd_complete_message_thread_id_cache_size');
-    if (cache_store(\@{$MSGTHREADID_CACHE{$key}}, $msgid, $cache_size)) {
-        $MSGTHREADID_CACHE_INDEX = 0;
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    for my $msgid (@msgids) {
+        if (cache_store(\@{$MSGTHREADID_CACHE{$key}}, $msgid, $cache_size)) {
+            $MSGTHREADID_CACHE_INDEX = 0;
+            stats_increment(\$MSGTHREADID_CACHE_STATS);
+        }
     }
 }
-signal_add('message irc action', 'cache_msgthreadid');
-signal_add('message irc notice', 'cache_msgthreadid');
-signal_add('message private', 'cache_msgthreadid');
-signal_add('message public', 'cache_msgthreadid');
+Irssi::signal_add('message irc action', 'cache_msgthreadid');
+Irssi::signal_add('message irc notice', 'cache_msgthreadid');
+Irssi::signal_add('message private', 'cache_msgthreadid');
+Irssi::signal_add('message public', 'cache_msgthreadid');
 
-settings_add_bool('matterircd_complete', 'matterircd_complete_reply_msg_thread_id_at_start', 1);
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_reply_msg_thread_id_at_start', 1);
 
-signal_add_last 'message own_public' => sub {
+sub signal_message_own_public_msgthreadid {
     my($server, $msg, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_message_thread_id_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
@@ -410,14 +575,22 @@ signal_add_last 'message own_public' => sub {
     }
     my $msgid = $1;
 
-    my $cache_size = settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
     if (cache_store(\@{$MSGTHREADID_CACHE{$target}}, $msgid, $cache_size)) {
         $MSGTHREADID_CACHE_INDEX = 0;
+        stats_increment(\$MSGTHREADID_CACHE_STATS);
     }
 
     my $msgthreadid = $1;
 
-    my $len = settings_get_int('matterircd_complete_shorten_message_thread_id');
+    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
+    if ($thread_color == -1) {
+        $thread_color = thread_color($msgthreadid);
+    } else {
+        $thread_color = "\x03${thread_color}";
+    }
+
+    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
     if ($len < 25) {
         # Shorten to length configured. We use unicode ellipsis (...)
         # here to both allow word selection to just select parts of
@@ -426,21 +599,23 @@ signal_add_last 'message own_public' => sub {
         $msgthreadid = substr($msgid, 0, $len) . "…";
     }
 
-    if (settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
-        $msg =~ s/^@@[0-9a-z]{26} /\x0314[↪${msgthreadid}]\x0f /;
+    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');
+    if (Irssi::settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
+        $msg =~ s/^@@[0-9a-z]{26} /${thread_color}[${reply_prefix}${msgthreadid}]\x0f /;
     } else {
         $msg =~ s/^@@[0-9a-z]{26} //;
-        $msg =~ s/$/ \x0314[↪${msgthreadid}]\x0f/;
+        $msg =~ s/$/ ${thread_color}[${reply_prefix}${msgthreadid}]\x0f/;
     }
 
-    signal_continue($server, $msg, $target);
+    Irssi::signal_continue($server, $msg, $target);
 };
+Irssi::signal_add_last('message own_public', 'signal_message_own_public_msgthreadid');
 
-signal_add_last 'message own_private' => sub {
+sub signal_message_own_private {
     my($server, $msg, $target, $orig_target) = @_;
 
-    return unless settings_get_int('matterircd_complete_message_thread_id_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
@@ -448,14 +623,22 @@ signal_add_last 'message own_private' => sub {
     }
     my $msgid = $1;
 
-    my $cache_size = settings_get_int('matterircd_complete_message_thread_id_cache_size');
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_message_thread_id_cache_size');
     if (cache_store(\@{$MSGTHREADID_CACHE{$target}}, $msgid, $cache_size)) {
         $MSGTHREADID_CACHE_INDEX = 0;
+        stats_increment(\$MSGTHREADID_CACHE_STATS);
     }
 
     my $msgthreadid = $1;
 
-    my $len = settings_get_int('matterircd_complete_shorten_message_thread_id');
+    my $thread_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
+    if ($thread_color == -1) {
+        $thread_color = thread_color($msgthreadid);
+    } else {
+        $thread_color = "\x03${thread_color}";
+    }
+
+    my $len = Irssi::settings_get_int('matterircd_complete_shorten_message_thread_id');
     if ($len < 25) {
         # Shorten to length configured. We use unicode ellipsis (...)
         # here to both allow word selection to just select parts of
@@ -464,15 +647,17 @@ signal_add_last 'message own_private' => sub {
         $msgthreadid = substr($msgid, 0, $len) . "…";
     }
 
-    if (settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
-        $msg =~ s/^@@[0-9a-z]{26} /\x0314[↪${msgthreadid}]\x0f /;
+    my $reply_prefix = Irssi::settings_get_str('matterircd_complete_override_reply_prefix');
+    if (Irssi::settings_get_bool('matterircd_complete_reply_msg_thread_id_at_start')) {
+        $msg =~ s/^@@[0-9a-z]{26} /${thread_color}[${reply_prefix}${msgthreadid}]\x0f /;
     } else {
         $msg =~ s/^@@[0-9a-z]{26} //;
-        $msg =~ s/$/ \x0314[↪${msgthreadid}]\x0f/;
+        $msg =~ s/$/ ${thread_color}[${reply_prefix}${msgthreadid}]\x0f/;
     }
 
-    signal_continue($server, $msg, $target, $orig_target);
+    Irssi::signal_continue($server, $msg, $target, $orig_target);
 };
+Irssi::signal_add_last('message own_private', 'signal_message_own_private');
 
 
 #==============================================================================
@@ -483,49 +668,50 @@ signal_add_last 'message own_private' => sub {
 
 
 my %NICKNAMES_CACHE;
-settings_add_int('matterircd_complete', 'matterircd_complete_nick_cache_size', 20);
-command_bind 'matterircd_complete_nick_cache_dump' => sub {
+Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_nick_cache_size', 20);
+sub cmd_matterircd_complete_nick_cache_dump {
     my ($data, $server, $wi) = @_;
 
     if (not $data) {
-        return unless ref $wi and $wi->{type} eq 'CHANNEL';
+        return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
     }
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     my $channel = $data ? $data : $wi->{name};
     # Remove leading and trailing whitespace.
     $channel =~ tr/    //d;
 
-    Irssi::print("${channel}: Nicknames cache");
+    _wi_print($wi, "${channel}: Nicknames cache");
 
     if ((not exists($NICKNAMES_CACHE{$channel})) || (scalar @{$NICKNAMES_CACHE{$channel}} == 0)) {
-        Irssi::print("${channel}: Empty");
+        _wi_print($wi,"${channel}: Empty");
         return;
     }
 
     foreach my $nick (@{$NICKNAMES_CACHE{$channel}}) {
-        Irssi::print("${channel}: ${nick}");
+        _wi_print($wi, "${channel}: ${nick}");
     }
-    Irssi::print("${channel}: Total: " . scalar @{$NICKNAMES_CACHE{$channel}});
+    _wi_print($wi, "${channel}: Total: " . scalar @{$NICKNAMES_CACHE{$channel}});
 };
+Irssi::command_bind('matterircd_complete_nick_cache_dump', 'cmd_matterircd_complete_nick_cache_dump');
 
-signal_add 'complete word' => sub {
+sub signal_complete_word_nicks {
     my ($complist, $window, $word, $linestart, $want_space) = @_;
 
     return if substr($word, 0, 2) eq '@@';
     return unless $window->{active} and $window->{active}->{type} eq 'CHANNEL';
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$window->{active_server}->{chatnet}};
 
     if (substr($word, 0, 1) eq '@') {
         $word = substr($word, 1);
     }
-    my $compl_char = settings_get_str('completion_char');
+    my $compl_char = Irssi::settings_get_str('completion_char');
     my $own_nick = $window->{active}->{ownnick}->{nick};
-    my @ignore_nicks = split(/\s+/, settings_get_str('matterircd_complete_nick_ignore'));
+    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
 
     # We need to store the results in a temporary array so we can
     # sort.
@@ -585,30 +771,34 @@ signal_add 'complete word' => sub {
         }
     }
 };
+Irssi::signal_add('complete word', 'signal_complete_word_nicks');
 
+my $NICKNAMES_CACHE_STATS = 0;
 sub cache_ircnick {
     my($server, $msg, $nick, $address, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_nick_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_nick_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
-    my $cache_size = settings_get_int('matterircd_complete_nick_cache_size');
-    my @ignore_nicks = split(/\s+/, settings_get_str('matterircd_complete_nick_ignore'));
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_nick_cache_size');
+    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
     # Ignore nicks configured to be ignored such as bots.
     if ($nick !~ @ignore_nicks) {
-        cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size);
+        if (cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size)) {
+            stats_increment(\$NICKNAMES_CACHE_STATS);
+        }
     }
 }
-signal_add('message irc action', 'cache_ircnick');
-signal_add('message irc notice', 'cache_ircnick');
-signal_add('message public', 'cache_ircnick');
+Irssi::signal_add('message irc action', 'cache_ircnick');
+Irssi::signal_add('message irc notice', 'cache_ircnick');
+Irssi::signal_add('message public', 'cache_ircnick');
 
-signal_add_last 'message own_public' => sub {
+sub signal_message_own_public_nicks {
     my($server, $msg, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_nick_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_nick_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if ($msg !~ /^@([^@ \t:,\)]+)/) {
@@ -616,34 +806,37 @@ signal_add_last 'message own_public' => sub {
     }
     my $nick = $1;
 
-    my $cache_size = settings_get_int('matterircd_complete_nick_cache_size');
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_nick_cache_size');
     # We want to make sure that the nick or user is still online and
     # in the channel.
-    my $wi = Irssi::active_win()->{active};
+    my $wi = $server->window_item_find($target);
     if (not defined $wi) {
         return;
     }
     foreach my $cur ($wi->nicks()) {
         if ($nick eq $cur->{nick}) {
-            cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size);
+            if (cache_store(\@{$NICKNAMES_CACHE{$target}}, $nick, $cache_size, 1)) {
+                stats_increment(\$NICKNAMES_CACHE_STATS);
+            }
             last;
         }
     }
 };
+Irssi::signal_add_last('message own_public', 'signal_message_own_public_nicks');
 
 my @NICKNAMES_CACHE_SEARCH;
 my $NICKNAMES_CACHE_SEARCH_ENABLED = 0;
 my $NICKNAMES_CACHE_INDEX = 0;
-command_bind 'nicknames_search' => sub {
+sub cmd_nicknames_search {
     my ($data, $server, $wi) = @_;
 
     return unless ref $wi and $wi->{type} eq 'CHANNEL';
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     my $own_nick = $wi->{ownnick}->{nick};
-    my @ignore_nicks = split(/\s+/, settings_get_str('matterircd_complete_nick_ignore'));
+    my @ignore_nicks = split(/\s+/, Irssi::settings_get_str('matterircd_complete_nick_ignore'));
 
     @NICKNAMES_CACHE_SEARCH = ();
     foreach my $cur ($wi->nicks()) {
@@ -689,26 +882,27 @@ command_bind 'nicknames_search' => sub {
 
     if ($nickname) {
         # Save input text.
-        my $input = parse_special('$L');
-        my $compl_char = settings_get_str('completion_char');
+        my $input = Irssi::parse_special('$L');
+        my $compl_char = Irssi::settings_get_str('completion_char');
         # Remove any existing nickname and insert one from the cache.
         my $msgid = "";
         if ($input =~ s/^(\@\@(?:[0-9a-z]{26}|[0-9a-f]{3}) )//) {
             $msgid = $1;
         }
         $input =~ s/^\@[^${compl_char}]+$compl_char //;
-        gui_input_set_pos(0);
-        gui_input_set("${msgid}\@${nickname}${compl_char} ${input}");
+        Irssi::gui_input_set_pos(0);
+        Irssi::gui_input_set("${msgid}\@${nickname}${compl_char} ${input}");
     }
 };
+Irssi::command_bind('nicknames_search', 'cmd_nicknames_search');
 
-signal_add_last 'gui key pressed' => sub {
+sub signal_gui_key_pressed_nicks {
     my ($key) = @_;
 
     return unless $NICKNAMES_CACHE_SEARCH_ENABLED;
 
     my $server = Irssi::active_server();
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if (($key == $KEY_RET) || ($key == $KEY_CTRL_U)) {
@@ -719,15 +913,15 @@ signal_add_last 'gui key pressed' => sub {
 
     # Cancel/abort, so remove current nickname.
     elsif ($key == $KEY_CTRL_C) {
-        my $input = parse_special('$L');
+        my $input = Irssi::parse_special('$L');
 
         # Remove the Ctrl+C character.
         $input =~ tr/\ 3//d;
 
-        my $compl_char = settings_get_str('completion_char');
+        my $compl_char = Irssi::settings_get_str('completion_char');
         my $pos = 0;
         if ($input =~ s/^(\@[^${compl_char}]+$compl_char )//) {
-            $pos = gui_input_get_pos() - length($1);
+            $pos = Irssi::gui_input_get_pos() - length($1);
         }
 
         # We also want to move the input position back one for Ctrl+C
@@ -737,14 +931,15 @@ signal_add_last 'gui key pressed' => sub {
         # Replace the text in the input box with our modified version,
         # then move cursor positon to where it was without the
         # current nickname.
-        gui_input_set($input);
-        gui_input_set_pos($pos);
+        Irssi::gui_input_set($input);
+        Irssi::gui_input_set_pos($pos);
 
         $NICKNAMES_CACHE_INDEX = 0;
         $NICKNAMES_CACHE_SEARCH_ENABLED = 0;
         @NICKNAMES_CACHE_SEARCH = ();
     }
 };
+Irssi::signal_add_last('gui key pressed', 'signal_gui_key_pressed_nicks');
 
 
 #==============================================================================
@@ -755,39 +950,115 @@ signal_add_last 'gui key pressed' => sub {
 
 
 my %REPLIED_CACHE;
-settings_add_int('matterircd_complete', 'matterircd_complete_replied_cache_size', 50);
-command_bind 'matterircd_complete_replied_cache_dump' => sub {
+Irssi::settings_add_int('matterircd_complete', 'matterircd_complete_replied_cache_size', 50);
+sub cmd_matterircd_complete_replied_cache_dump {
     my ($data, $server, $wi) = @_;
 
     if (not $data) {
-        return unless ref $wi and $wi->{type} eq 'CHANNEL';
+        return unless ref $wi and ($wi->{type} eq 'CHANNEL' or $wi->{type} eq 'QUERY');
     }
 
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     my $channel = $data ? $data : $wi->{name};
     # Remove leading and trailing whitespace.
     $channel =~ tr/    //d;
 
-    Irssi::print("${channel}: Replied cache");
+    _wi_print($wi, "${channel}: Replied cache");
 
     if ((not exists($REPLIED_CACHE{$channel})) || (scalar @{$REPLIED_CACHE{$channel}} == 0)) {
-        Irssi::print("${channel}: Empty");
+        _wi_print($wi, "${channel}: Empty");
         return;
     }
 
     foreach my $threadid (@{$REPLIED_CACHE{$channel}}) {
-        Irssi::print("${channel}: ${threadid}");
+        _wi_print($wi, "${channel}: ${threadid}");
+    }
+    _wi_print($wi, "${channel}: Total: " . scalar @{$REPLIED_CACHE{$channel}});
+};
+Irssi::command_bind('matterircd_complete_replied_cache_dump', 'cmd_matterircd_complete_replied_cache_dump');
+
+my $REPLIED_CACHE_STATS = 0;
+sub cmd_matterircd_complete_replied_cache_clear {
+    my ($data, $server, $wi) = @_;
+
+    my $channel;
+    my @msgids = ();
+    my @args = ();
+    if ($data) {
+        @args = split(/\s+/, $data);
+    }
+
+    if (scalar(@args) == 0 || $args[0] eq '*') {
+        stats_increment(\$REPLIED_CACHE_STATS);
+        _wi_print($wi, "matterircd_complete replied cache cleared");
+        return;
+    }
+
+    if (exists($REPLIED_CACHE{$args[0]}) || exists($REPLIED_CACHE{"#${args[0]}"})) {
+        $channel = shift(@args);
+        if (rindex($channel, "#", 0) == -1) {
+            $channel = "#${channel}";
+        }
+    } elsif ($wi->{name}) {
+        $channel = $wi->{name};
+    } else {
+        return;
+    }
+    @msgids = @args;
+
+    if (scalar(@msgids) > 0) {
+        foreach my $id (@msgids) {
+            my $i = 0;
+            if (rindex($id, "@@", 0) == 0) {
+                $id = substr($id, 2);
+            }
+            foreach my $msgid (@{$REPLIED_CACHE{$channel}}) {
+                if ($id eq $msgid) {
+                    splice(@{$REPLIED_CACHE{$channel}}, $i, 1);
+                    stats_increment(\$REPLIED_CACHE_STATS);
+                    _wi_print($wi, "matterircd_complete replied cache removed ${id} from ${channel} cache");
+                    last;
+                }
+                $i += 1;
+            }
+        }
+    } else {
+        @{$REPLIED_CACHE{$channel}} = ();
+        stats_increment(\$REPLIED_CACHE_STATS);
+        _wi_print($wi, "matterircd_complete replied cache cleared for channel ${channel}");
+    }
+};
+Irssi::command_bind('matterircd_complete_replied_cache_clear', 'cmd_matterircd_complete_replied_cache_clear');
+
+my $REPLIED_CACHE_CLEARED = 0;
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_clear_replied_cache_on_away', 0);
+sub signal_away_mode_changed {
+    my ($server) = @_;
+
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
+    return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
+
+    # When you visit the web UI when marked away, it retriggers this
+    # event. Let's avoid that.
+    if (! $server->{usermode_away}) {
+        $REPLIED_CACHE_CLEARED = 0;
+    }
+
+    if (Irssi::settings_get_bool('matterircd_complete_clear_replied_cache_on_away') && $server->{usermode_away} && (! $REPLIED_CACHE_CLEARED)) {
+        %REPLIED_CACHE = ();
+        $REPLIED_CACHE_CLEARED = 1;
+        Irssi::print("matterircd_complete replied cache cleared");
     }
-    Irssi::print("${channel}: Total: " . scalar @{$REPLIED_CACHE{$channel}});
 };
+Irssi::signal_add('away mode changed', 'signal_away_mode_changed');
 
-signal_add 'message own_public' => sub {
+sub signal_message_own_public_replied {
     my($server, $msg, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_replied_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_replied_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     if ($msg !~ /^@@((?:[0-9a-z]{26})|(?:[0-9a-f]{3}))/) {
@@ -795,20 +1066,23 @@ signal_add 'message own_public' => sub {
     }
     my $msgid = $1;
 
-    my $cache_size = settings_get_int('matterircd_complete_replied_cache_size');
-    cache_store(\@{$REPLIED_CACHE{$target}}, $msgid, $cache_size);
+    my $cache_size = Irssi::settings_get_int('matterircd_complete_replied_cache_size');
+    if (cache_store(\@{$REPLIED_CACHE{$target}}, $msgid, $cache_size)) {
+        stats_increment(\$REPLIED_CACHE_STATS);
+    }
 };
+Irssi::signal_add('message own_public', 'signal_message_own_public_replied');
 
-signal_add 'message public' => sub {
+sub signal_message_public {
     my($server, $msg, $nick, $address, $target) = @_;
 
-    return unless settings_get_int('matterircd_complete_replied_cache_size');
-    my %chatnets = map { $_ => 1 } split(/\s+/, settings_get_str('matterircd_complete_networks'));
+    return unless Irssi::settings_get_int('matterircd_complete_replied_cache_size');
+    my %chatnets = map { $_ => 1 } split(/\s+/, Irssi::settings_get_str('matterircd_complete_networks'));
     return unless exists $chatnets{'*'} || exists $chatnets{$server->{chatnet}};
 
     # For '/me' actions, it has trailing space so we need to use
     # \s* here.
-    $msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})\]/;
+    $msg =~ /\[(?:->|↪)?\@\@([0-9a-z]{26})[\],]/;
     my $msgthreadid = $1;
     return unless $msgthreadid;
 
@@ -820,5 +1094,246 @@ signal_add 'message public' => sub {
         }
     }
 
-    signal_continue($server, $msg, $nick, $address, $target);
+    Irssi::signal_continue($server, $msg, $nick, $address, $target);
 };
+Irssi::signal_add('message public', 'signal_message_public');
+
+# Remove an array's elements per their values
+sub array_splice_values {
+    my ($ar_ref, $uw_ref) = @_;
+    my @array = @{$ar_ref};
+    my @unwanted = @{$uw_ref};
+    my %removals = map { $_ => 1 } @unwanted;
+    my @keys = keys %removals;
+    my @indices = grep { exists($removals{$array[$_]}) } 0..$#array;
+    # Each time we remove index from @arr, the next correct index to delete will be reduced of $o
+    my $o = 0;
+    for (@indices) {
+        splice(@array, $_-$o, 1);
+        $o++;
+    }
+    return @array;
+}
+
+sub setup_colors {
+    # Skip colors setup if we're using a fixed color
+    my $fixed_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
+    return if $fixed_color ne -1;
+
+    my $allowed_colors = Irssi::settings_get_str('matterircd_complete_thread_id_allowed_colors');
+    $allowed_colors = uc $allowed_colors;
+    my $unwanted_colors = Irssi::settings_get_str('matterircd_complete_thread_id_unwanted_colors');
+    $unwanted_colors = uc $unwanted_colors;
+    my $color_theme = Irssi::settings_get_str('matterircd_complete_thread_id_color_theme');
+    $color_theme = lc $color_theme;
+    my @colors;
+
+    if ($allowed_colors =~ /^[0-9A-Z]{2}( [0-9A-Z]{2})*$/) {
+        @colors = split(' ', $allowed_colors);
+        Irssi::print("[matterircd_complete] Setting allowed colors: @colors");
+    } elsif ($allowed_colors =~ /^[0-9A-Z]{2}([0-9A-Z]{2})*$/ and length($allowed_colors) % 2 == 0) {
+        @colors = ( $allowed_colors =~ m/../g );
+        Irssi::print("[matterircd_complete] Setting allowed colors: @colors");
+    } elsif (length($allowed_colors) != 0) {
+        Irssi::print("[matterircd_complete] Ignoring matterircd_complete_thread_id_allowed_colors: invalid format ($allowed_colors)");
+    } else {
+        Irssi::print("[matterircd_complete] Setting allowed colors to all colors");
+        @colors = @all_colors;
+    }
+
+    if (length($color_theme) != 0) {
+        if ($color_theme eq "dark") {
+            Irssi::print("[matterircd_complete] Removing colors incompatible with dark theme");
+            @colors = array_splice_values(\@colors, \@dark_theme_unwanted);
+        } elsif ($color_theme eq 'solarized-light') {
+            Irssi::print("[matterircd_complete] Removing colors incompatible with solarized-light theme");
+            @colors = array_splice_values(\@colors, \@solarized_light_theme_unwanted);
+        } else {
+            Irssi::print("[matterircd_complete] Ignoring unknown color theme $color_theme");
+            Irssi::print("[matterircd_complete] Valid themes are dark, solarized-light");
+        }
+    }
+
+    if ($unwanted_colors =~ /^[0-9A-Z]{2}( [0-9A-Z]{2})*$/) {
+        my @unwanted = split(' ', $unwanted_colors);
+        Irssi::print("[matterircd_complete] Removing unwanted colors");
+        @colors = array_splice_values(\@colors, \@unwanted);
+    } elsif ($unwanted_colors =~ /^[0-9A-Z]{2}([0-9A-Z]{2})*$/ and length($unwanted_colors) % 2 == 0) {
+        my @unwanted = ($unwanted_colors =~ m/../g);
+        Irssi::print("[matterircd_complete] Removing unwanted colors");
+        @colors = array_splice_values(\@colors, \@unwanted);
+    } elsif (length($unwanted_colors)) {
+        Irssi::print("[matterircd_complete] Ignoring matterircd_complete_thread_id_unwanted_colors: invalid format ($unwanted_colors)");
+    }
+
+    if (@thread_id_selected_colors) {
+        Irssi::print("[matterircd_complete] Config changed, existing threads might change colors!")
+            if $allowed_colors ne $color_config{"allowed_colors"}
+                    or $unwanted_colors ne $color_config{"unwanted_colors"}
+                    or $color_theme ne $color_config{"color_theme"};
+        $color_config{"allowed_colors"} = $allowed_colors;
+        $color_config{"unwanted_colors"} = $unwanted_colors;
+        $color_config{"color_theme"} = $color_theme;
+    } else {
+        Irssi::print("[matterircd_complete] Thread colors have been set per your config");
+    }
+    Irssi::print("[matterircd_complete] You can check colors in use with /matterircd_complete_thread_id_get_colors");
+    @thread_id_selected_colors = @colors;
+}
+Irssi::signal_add('setup changed', 'setup_colors');
+Irssi::signal_add('setup reread', 'setup_colors');
+
+sub cmd_matterircd_complete_thread_id_get_colors {
+    my ($data, $server, $wi) = @_;
+
+    # Display a warning if we're using a fixed color
+    my $fixed_color = Irssi::settings_get_int('matterircd_complete_thread_id_color');
+    if ($fixed_color ne -1) {
+        _wi_print($wi, "Thread_id_color is not set to -1");
+        _wi_print($wi, "Threads will always take \x03${fixed_color}this color\x0f");
+        return;
+    }
+
+    my $colors_text = "Selected colors: ";
+    foreach (@thread_id_selected_colors) {
+        my $n = xcolor_to_irssi($_);
+        $colors_text .= "\x03$n$_";
+    }
+    $colors_text .= "\x0f";
+    _wi_print($wi, $colors_text);
+}
+Irssi::command_bind('matterircd_complete_thread_id_get_colors', 'cmd_matterircd_complete_thread_id_get_colors');
+
+Irssi::settings_add_bool('matterircd_complete', 'matterircd_complete_stats_output', 0);
+sub stats_increment {
+    my ($stats_ref) = @_;
+
+    $$stats_ref += 1;
+
+    # autosave.
+    if (($$stats_ref % 100) == 0) {
+        my $output = Irssi::settings_get_bool('matterircd_complete_stats_output');
+        save_cache($output);
+    }
+}
+
+my $STARTUP_DATE = localtime();
+sub stats_show {
+    Irssi::print("[matterircd_complete] Started / loaded since ${STARTUP_DATE}");
+
+    my $total = 0;
+    my $entries;
+    my $channels;
+
+    my %cache = (
+        'MSGTHREADID' => \%MSGTHREADID_CACHE,
+        'NICKNAMES' => \%NICKNAMES_CACHE,
+        'REPLIED' => \%REPLIED_CACHE,
+        );
+
+    my %stats = (
+        'MSGTHREADID' => 0,
+        'NICKNAMES' => 0,
+        'REPLIED' => 0,
+        );
+    foreach my $key (sort keys %cache) {
+        foreach my $channel (sort keys %{$cache{$key}}) {
+            my $d = $cache{$key}->{$channel};
+            if (scalar(@{$d}) == 0) {
+                next;
+            }
+            $stats{$key} += scalar(@{$d});
+        }
+        $total += $stats{$key};
+    }
+
+    $entries = $stats{'MSGTHREADID'};
+    $channels = keys %{$cache{'MSGTHREADID'}};
+    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for msg/thread IDs cache (${MSGTHREADID_CACHE_STATS} updates)");
+
+    $entries = $stats{'NICKNAMES'};
+    $channels = keys %{$cache{'NICKNAMES'}};
+    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for nicknames cache (${NICKNAMES_CACHE_STATS} updates)");
+
+    $entries = $stats{'REPLIED'};
+    $channels = keys %{$cache{'REPLIED'}};
+    Irssi::print("[matterircd_complete] ${entries} entries across ${channels} channels for threads replied to cache (${REPLIED_CACHE_STATS} updates)");
+
+    my $total_updates = $MSGTHREADID_CACHE_STATS + $NICKNAMES_CACHE_STATS + $REPLIED_CACHE_STATS;
+    Irssi::print("[matterircd_complete] \x03%GSaved total of ${total} entries in the cache (${total_updates} total updates)…");
+}
+Irssi::command_bind('matterircd_complete_stats', 'stats_show');
+
+my $CACHE_FILE = Irssi::get_irssi_dir() . '/matterircd_complete.cache';
+my $exited;
+sub save_cache {
+    my ($output_stats) = @_;
+
+    open(FH, '>', $CACHE_FILE) or do {
+        Irssi::print("[matterircd_complete] \x03%RError saving matterircd_complete cache: $!")
+            unless $exited;
+        return;
+    };
+
+    my %cache = (
+        'MSGTHREADID' => \%MSGTHREADID_CACHE,
+        'NICKNAMES' => \%NICKNAMES_CACHE,
+        'REPLIED' => \%REPLIED_CACHE,
+        );
+
+    foreach my $key (sort keys %cache) {
+        foreach my $channel (sort keys %{$cache{$key}}) {
+            my $d = $cache{$key}->{$channel};
+            my $entries = join(',', @{$d});
+            if (scalar(@{$d}) == 0) {
+                next;
+            }
+            print(FH "${key} ${channel} ${entries}\n");
+        }
+    }
+    close(FH);
+
+    if ($output_stats == 0) {
+        return;
+    }
+
+    stats_show();
+}
+Irssi::command_bind('matterircd_complete_cache_save', 'save_cache');
+
+sub load_cache {
+    open(FH, '<', $CACHE_FILE) or return;
+
+    my %cache = (
+        'MSGTHREADID' => \%MSGTHREADID_CACHE,
+        'NICKNAMES' => \%NICKNAMES_CACHE,
+        'REPLIED' => \%REPLIED_CACHE,
+        );
+
+    my $total = 0;
+    while(<FH>) {
+        chomp;
+        my ($key, $channel, $entries) = split;
+        my @d = split(',', $entries);
+        $cache{$key}->{$channel} = \@d;
+        $total += scalar(@d);
+    }
+    close(FH);
+
+    Irssi::print("[matterircd_complete] \x03%GLoaded total of ${total} entries from disk cache…");
+}
+
+sub UNLOAD {
+    return if $exited;
+    exit_save();
+}
+
+sub exit_save {
+    $exited = 1;
+    save_cache(1)
+}
+Irssi::signal_add('gui exit', 'exit_save');
+
+# Set up on load!
+setup_colors();
+load_cache();
git clone https://git.99rst.org/PROJECT