summaryrefslogtreecommitdiffstats
path: root/scripts/nickcolor_expando.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/nickcolor_expando.pl')
-rw-r--r--scripts/nickcolor_expando.pl1065
1 files changed, 1065 insertions, 0 deletions
diff --git a/scripts/nickcolor_expando.pl b/scripts/nickcolor_expando.pl
new file mode 100644
index 0000000..60d9c9f
--- /dev/null
+++ b/scripts/nickcolor_expando.pl
@@ -0,0 +1,1065 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.4.0'; # c274f630aff9967
+our %IRSSI = (
+ authors => 'Nei',
+ name => 'nickcolor_expando',
+ description => 'colourise nicks',
+ license => 'GPL v2',
+ );
+
+# inspired by bc-bd's nm.pl and mrwright's nickcolor.pl
+
+# Usage
+# =====
+# after loading the script, add the colour expando to the format
+# (themes' abstracts are not supported)
+#
+# /format pubmsg {pubmsgnick $2 {pubnick $nickcolor$0}}$1
+#
+# alternatively, use it together with nm2 script
+
+# Options
+# =======
+# /set neat_colors <list of colours>
+# * the list of colours for automatic colouring (you can edit it more
+# conveniently with /neatcolor colors)
+#
+# /set neat_ignorechars <regex>
+# * regular expression of characters to remove from nick before
+# calculating the hash function
+#
+# /set neat_color_reassign_time <time>
+# * if the user has not spoken for so long, the assigned colour is
+# forgotten and another colour may be picked next time the user
+# speaks
+#
+# /set neat_global_colors <ON|OFF>
+# * more strongly prefer one global colour per nickname regardless of
+# channel
+
+# Commands
+# ========
+# /neatcolor
+# * show the current colour distribution of nicks
+#
+# /neatcolor set [<network>/<#channel>] <nick> <colour>
+# * set a fixed colour for nick
+#
+# /neatcolor reset [<network>/<#channel>] <nick>
+# * remove a set colour of nick
+#
+# /neatcolor get [<network>/<#channel>] <nick>
+# * query the current or set colour of nick
+#
+# /neatcolor re [<network>/<#channel>] <nick>
+# * force change the colour of nick to a random other colour (to
+# manually resolve clashes)
+#
+# /neatcolor save
+# * save the colours to ~/.irssi/saved_nick_colors
+#
+# /neatcolor reset --all
+# * re-set all colours
+#
+# /neatcolor colors
+# * show currently configured colours, in colour
+#
+# /neatcolor colors add <list of colours>
+# /neatcolor colors remove <list of colours>
+# * add or remove these colours from the neat_colors setting
+
+
+sub cmd_help_neatcolor {
+ print CLIENTCRAP <<HELP
+%9Syntax:%9
+
+NEATCOLOR
+NEATCOLOR SET [<network>/<#channel>] <nick> <colour>
+NEATCOLOR RESET [<network>/<#channel>] <nick>
+NEATCOLOR GET [<network>/<#channel>] <nick>
+NEATCOLOR RE [<network>/<#channel>] <nick>
+NEATCOLOR SAVE
+NEATCOLOR RESET --all
+NEATCOLOR COLORS
+NEATCOLOR COLORS ADD <list of colours>
+NEATCOLOR COLORS REMOVE <list of colours>
+
+%9Parameters:%9
+
+ SET: set a fixed colour for nick
+ RESET: remove a set colour of nick
+ GET: query the current or set colour of nick
+ RE: force change the colour of nick to a random other
+ colour (to manually resolve clashes)
+ SAVE: save the colours to ~/.irssi/saved_nick_colors
+ RESET --all: re-set all colours
+ COLORS: show currently configured colours, in colour
+ COLORS ADD/REMOVE: add or remove these colours from the
+ neat_colors setting
+
+ If no parameters are given, the current colour distribution of
+ nicks is shown.
+
+%9Description:%9
+
+ Manages nick based colouring
+
+HELP
+}
+
+use Hash::Util qw(lock_keys);
+use Irssi;
+
+
+{ package Irssi::Nick }
+
+my @action_protos = qw(irc silc xmpp);
+my (%set_colour, %avoid_colour, %has_colour, %last_time, %netchan_hist);
+my ($expando, $iexpando, $ignore_re, $ignore_setting, $global_colours, $retain_colour_time, @colours, $exited, $session_load_time);
+($expando, $iexpando) = ('', ''); # Initialise to empty
+
+# the numbers for the scoring system, highest colour value will be chosen
+my %scores = (
+ set => 200,
+ keep => 5,
+ global => 4,
+ hash => 3,
+
+ avoid => -20,
+ hist => -10,
+ used => -2,
+ );
+lock_keys(%scores);
+
+my $history_lines = 40;
+my $global_mode = 1; # start out with global nick colour
+
+my @colour_bags = (
+ [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[11 12 23 25 24 13 14 01 15 2B 4N]], # DARK-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[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
+ );
+my %colour_bags;
+{ my $idx = 0;
+ for my $bag (@colour_bags) {
+ @colour_bags{ @$bag } = ($idx)x@$bag;
+ }
+ continue {
+ ++$idx;
+ }
+}
+my @colour_list = map { @$_ } @colour_bags;
+my @bases = split //, 'kbgcrmywKBGCRMYW04261537';
+my %base_map = map { $bases[$_] => sprintf '%02X', ($_ % 0x10) } 0..$#bases;
+my %ext_to_base_map = map { (sprintf '%02X', $_) => $bases[$_] } 0..15;
+
+sub expando_neatcolour {
+ return $expando;
+}
+
+sub expando_neatcolour_inv {
+ return $iexpando;
+}
+
+# one-at-a-time hash
+sub simple_hash {
+ use integer;
+ my $hash = 0x5065526c + length $_[0];
+ for my $ord (unpack 'U*', $_[0]) {
+ $hash += $ord;
+ $hash += $hash << 10;
+ $hash &= 0xffffffff;
+ $hash ^= $hash >> 6;
+ }
+ $hash += $hash << 3;
+ $hash &= 0xffffffff;
+ $hash ^= $hash >> 11;
+ $hash = $hash + ($hash << 15);
+ $hash &= 0xffffffff;
+}
+
+{ my %lut1;
+ my @z = (0 .. 9, 'A' .. 'Z');
+ for my $x (16..255) {
+ my $idx = $x - 16;
+ my $col = 1+int($idx / @z);
+ $lut1{ $col . @z[(($col > 6 ? 10 : 0) + $idx) % @z] } = $x;
+ }
+ for my $idx (0..15) {
+ $lut1{ (sprintf "%02X", $idx) } = ($idx&8) | ($idx&4)>>2 | ($idx&2) | ($idx&1)<<2;
+ }
+
+ sub debug_ansicolour {
+ my ($col, $bg) = @_;
+ return '' unless defined $col && exists $lut1{$col};
+ $bg = $bg ? 48 : 38;
+ "\e[$bg;5;$lut1{$col}m"
+ }
+}
+sub debug_colour {
+ my ($col, $bg) = @_;
+ defined $col ? (debug_ansicolour($col, $bg) . $col . "\e[0m") : '(none)'
+}
+sub debug_score {
+ my ($score) = @_;
+ if ($score == 0) {
+ return $score
+ }
+ my @scale = $score > 0 ? (qw(16 1C 1I 1U 2V 4X)) : (qw(20 30 40 60 67 6L));;
+ my $v = (log 1+ abs $score)*(log 20);
+ debug_ansicolour($scale[$v >= $#scale ? -1 : $v], 1) . $score . "\e[0m"
+}
+sub debug_reused {
+ my ($netchan, $nick, $col) = @_;
+ my $chc = simple_hash($netchan);
+ my $hashcolour = @colours ? $colours[ $chc % @colours ] : 0;
+}
+sub debug_scores {
+ my ($netchan, $nick, $col, $prios, $colours) = @_;
+ my $inprogress;
+ unless (ref $prios) {
+ $inprogress = $prios;
+ $prios = [ sort { $colours->{$b} <=> $colours->{$a} } grep { exists $colours->{$_} } @colour_list ];
+ }
+ my $chc = simple_hash($netchan);
+ my $hashcolour = @colours ? $colours[ $chc % @colours ] : 0;
+ unless ($inprogress) {
+ }
+ else {
+ }
+ for my $i (0..$#$prios) {
+ }
+}
+
+sub colourise_nt {
+ my ($netchan, $nick, $weak) = @_;
+ my $time = time;
+
+ my $g_or_n = $global_colours ? '' : $netchan;
+
+ my $old_colour = $has_colour{$g_or_n}{$nick} // $has_colour{$netchan}{$nick};
+ my $last_time = $last_time{$g_or_n}{$nick} // $last_time{$netchan}{$nick};
+
+ my $keep_score = $weak ? $scores{keep} + $scores{set} : $scores{keep};
+
+ unless ($weak) {
+ $last_time{$netchan}{$nick}
+ = $last_time{''}{$nick} = $time;
+ }
+ else {
+ $last_time{$netchan}{$nick} ||= 0;
+ }
+
+ my $colour;
+ if (defined $old_colour && ($weak || (defined $last_time
+ && ($last_time + $retain_colour_time > $time
+ || ($last_time > 0 && grep { $_->[0] eq $nick } @{ $netchan_hist{$netchan} // [] }))))) {
+ $colour = $old_colour;
+ }
+ else {
+ # search for a suitable colour
+ my %colours = map { $_ => 0 } @colours;
+ my $hashnick = $nick;
+ $hashnick =~ s/$ignore_re//g if (defined $ignore_re && length $ignore_re);
+ my $hash = simple_hash($global_mode ? "/$hashnick" : "$netchan/$hashnick");
+
+ if (exists $set_colour{$netchan} && exists $set_colour{$netchan}{$nick}) {
+ $colours{ $set_colour{$netchan}{$nick} } += $scores{set};
+ }
+ elsif (exists $set_colour{$netchan} && exists $set_colour{$netchan}{$hashnick}) {
+ $colours{ $set_colour{$netchan}{$hashnick} } += $scores{set};
+ }
+ elsif (exists $set_colour{''} && exists $set_colour{''}{$nick}) {
+ $colours{ $set_colour{''}{$nick} } += $scores{set};
+ }
+ elsif (exists $set_colour{''} && exists $set_colour{''}{$hashnick}) {
+ $colours{ $set_colour{''}{$hashnick} } += $scores{set};
+ }
+
+ if (exists $avoid_colour{$netchan} && exists $avoid_colour{$netchan}{$nick}) {
+ for (@{ $avoid_colour{$netchan}{$nick} }) {
+ $colours{ $_ } += $scores{avoid} if exists $colours{ $_ };
+ }
+ }
+ elsif (exists $avoid_colour{$g_or_n} && exists $avoid_colour{$g_or_n}{$nick}) {
+ for (@{ $avoid_colour{$g_or_n}{$nick} }) {
+ $colours{ $_ } += $scores{avoid} if exists $colours{ $_ };
+ }
+ }
+
+ if (defined $old_colour) {
+ $colours{$old_colour} += $keep_score
+ if exists $colours{$old_colour};
+ }
+ elsif (exists $has_colour{''}{$nick}) {
+ $colours{ $has_colour{''}{$nick} } += $scores{global}
+ if exists $colours{ $has_colour{''}{$nick} };
+ }
+
+ if (@colours) {
+ my $hashcolour = $colours[ $hash % @colours ];
+ if (!defined $old_colour || $hashcolour ne $old_colour) {
+ $colours{ $hashcolour } += $scores{hash};
+ }
+ }
+
+ { my @netchans = $global_mode ? keys %has_colour : $netchan;
+ my $total;
+ my %colour_pens;
+ for my $gnc (@netchans) {
+ for my $onick (keys %{ $has_colour{$gnc} }) {
+ next if $gnc ne $netchan && exists $has_colour{$netchan}{$onick};
+ next unless exists $last_time{$gnc}{$onick};
+ if ($last_time{$gnc}{$onick} + $retain_colour_time > $time # XXX
+ || ($last_time{$gnc}{$onick} == 0 && $session_load_time + $retain_colour_time > $time)) {
+ if (exists $colours{ $has_colour{$gnc}{$onick} }) {
+ $colour_pens{ $has_colour{$gnc}{$onick} } += $scores{used};
+ ++$total;
+ }
+ }
+ }
+ }
+ for (keys %colour_pens) {
+ $colours{ $_ } += $colour_pens{ $_ } / $total * @colours
+ if @colours;
+ }
+ }
+
+ { my $fac = 1;
+ for my $gnetchan ($netchan, '') {
+ my $idx = exp(-log($history_lines)/$scores{hist});
+ for my $hent (reverse @{ $netchan_hist{$gnetchan} // [] }) {
+ next unless defined $hent->[1];
+ if ($hent->[0] ne $nick) {
+ my $pen = 1;
+ $pen *= 3 if length $nick == length $hent->[0];
+ $pen *= 2 if (substr $nick, 0, 1) eq (substr $hent->[0], 0, 1)
+ || 1 == abs +(length $nick) - (length $hent->[0]);
+ $colours{ $hent->[1] } -= log($pen*$history_lines)/log($idx) / $fac
+ if exists $colours{ $hent->[1] };
+ }
+ ++$idx;
+ last if $idx > $history_lines;
+ }
+ ++$fac;
+ }
+ }
+
+ { my %bag_pens;
+ for my $co (keys %colours) {
+ $bag_pens{ $colour_bags{$co} } -= $colours{$co}/2 if $colours{$co} < 0;
+ }
+ for my $bag (keys %bag_pens) {
+ for my $co (@{ $colour_bags[$bag] }) {
+ $colours{$co} -= $bag_pens{$bag} / @colours
+ if @colours && exists $colours{$co};
+ }
+ }
+ }
+
+ my @prio_colours = sort { $colours{$b} <=> $colours{$a} } grep { exists $colours{$_} } @colour_list;
+ my $stop_at = 0;
+ while ($stop_at < $#prio_colours
+ && $colours{ $prio_colours[$stop_at] } <= $colours{ $prio_colours[$stop_at + 1] }) {
+ ++$stop_at;
+ }
+ $colour = $prio_colours[ $hash % ($stop_at + 1) ]
+ if @prio_colours;
+
+ }
+
+ unless ($weak) {
+ expire_hist($netchan, '');
+
+ my $ent = [$nick, $colour];
+ push @{ $netchan_hist{$netchan} }, $ent;
+ push @{ $netchan_hist{''} }, $ent;
+ }
+
+ defined $colour ? ($has_colour{$g_or_n}{$nick} = $has_colour{$netchan}{$nick} = $colour) : $colour
+}
+
+sub expire_hist {
+ for my $ch (@_) {
+ if ($netchan_hist{$ch}
+ && @{$netchan_hist{$ch}} > 2 * $history_lines) {
+ splice @{$netchan_hist{$ch}}, 0, $history_lines;
+ }
+ }
+}
+
+sub msg_line_tag {
+ my ($srv, $msg, $nick, $addr, $targ) = @_;
+ my $obj = $srv->channel_find($targ);
+ clear_ref(), return unless $obj;
+ my $nickobj = $obj->nick_find($nick);
+ $nick = $nickobj->{nick} if $nickobj;
+ my $colour = colourise_nt($srv->{tag}.'/'.$obj->{name}, $nick);
+ $expando = $colour ? format_expand('%X'.$colour) : '';
+ $iexpando = $colour ? format_expand('%x'.$colour) : '';
+}
+
+sub msg_line_tag_xmppaction {
+ clear_ref(), return unless @_;
+ my ($srv, $msg, $nick, $targ) = @_;
+ msg_line_tag($srv, $msg, $nick, undef, $targ);
+}
+
+sub msg_line_clear {
+ clear_ref();
+}
+
+sub prnt_clear_public {
+ my ($dest) = @_;
+ clear_ref() if $dest->{level} & MSGLEVEL_PUBLIC;
+}
+
+sub clear_ref {
+ $expando = '';
+ $iexpando = '';
+}
+
+sub nicklist_changed {
+ my ($chanobj, $nickobj, $old_nick) = @_;
+
+ my $netchan = $chanobj->{server}{tag}.'/'.$chanobj->{name};
+ my $nickstr = $nickobj->{nick};
+
+ if (!exists $has_colour{''}{$nickstr} && exists $has_colour{''}{$old_nick}) {
+ $has_colour{''}{$nickstr} = delete $has_colour{''}{$old_nick};
+ }
+ if (exists $has_colour{$netchan}{$old_nick}) {
+ $has_colour{$netchan}{$nickstr} = delete $has_colour{$netchan}{$old_nick};
+ }
+
+ $last_time{$netchan}{$nickstr}
+ = $last_time{''}{$nickstr} = time;
+
+ for my $old_ent (@{ $netchan_hist{$netchan} }) {
+ $old_ent->[0] = $nickstr if $old_ent->[0] eq $old_nick;
+ }
+
+}
+
+{
+ my %format2control = (
+ 'F' => "\cDa", '_' => "\cDc", '|' => "\cDe", '#' => "\cDi", "n" => "\cDg", "N" => "\cDg",
+ 'U' => "\c_", '8' => "\cV", 'I' => "\cDf",
+ );
+ my %bg_base = (
+ '0' => '0', '4' => '1', '2' => '2', '6' => '3', '1' => '4', '5' => '5', '3' => '6', '7' => '7',
+ 'x08' => '8', 'x09' => '9', 'x0a' => ':', 'x0b' => ';', 'x0c' => '<', 'x0d' => '=', 'x0e' => '>', 'x0f' => '?',
+ );
+ my %fg_base = (
+ 'k' => '0', 'b' => '1', 'g' => '2', 'c' => '3', 'r' => '4', 'm' => '5', 'p' => '5', 'y' => '6', 'w' => '7',
+ 'K' => '8', 'B' => '9', 'G' => ':', 'C' => ';', 'R' => '<', 'M' => '=', 'P' => '=', 'Y' => '>', 'W' => '?',
+ );
+ my @ext_colour_off = (
+ '.', '-', ',',
+ '+', "'", '&',
+ );
+ sub format_expand {
+ my $copy = $_[0];
+ $copy =~ s{%(Z.{6}|z.{6}|X..|x..|.)}{
+ my $c = $1;
+ if (exists $format2control{$c}) {
+ $format2control{$c}
+ }
+ elsif (exists $bg_base{$c}) {
+ "\cD/$bg_base{$c}"
+ }
+ elsif (exists $fg_base{$c}) {
+ "\cD$fg_base{$c}/"
+ }
+ elsif ($c =~ /^[{}%]$/) {
+ $c
+ }
+ elsif ($c =~ /^(z|Z)([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})$/) {
+ my $bg = $1 eq 'z';
+ my (@rgb) = map { hex $_ } $2, $3, $4;
+ my $x = $bg ? 0x1 : 0;
+ my $out = "\cD" . (chr -13 + ord '0');
+ for (my $i = 0; $i < 3; ++$i) {
+ if ($rgb[$i] > 0x20) {
+ $out .= chr $rgb[$i];
+ }
+ else {
+ $x |= 0x10 << $i; $out .= chr 0x20 + $rgb[$i];
+ }
+ }
+ $out .= chr 0x20 + $x;
+ $out
+ }
+ elsif ($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';
+ "\cD" . ($bg ? "/$chr" : "$chr/")
+ }
+ else {
+ "\cD" . $ext_colour_off[($col - 0x10) / 0x50 + $bg * 3] . chr (($col - 0x10) % 0x50 - 1 + ord '0')
+ }
+ }
+ else {
+ "%$c"
+ }
+ }ge;
+ $copy
+ }
+}
+
+sub save_colours {
+ open my $fid, '>', Irssi::get_irssi_dir() . '/saved_nick_colors'
+ or do {
+ Irssi::print("Error saving nick colours: $!", MSGLEVEL_CLIENTERROR)
+ unless $exited;
+ return;
+ };
+
+ local $\ = "\n";
+ if (%set_colour) {
+ print $fid '[set]';
+ for my $netch (sort keys %set_colour) {
+ for my $nick (sort keys %{ $set_colour{$netch} }) {
+ print $fid "$netch/$nick:".$set_colour{$netch}{$nick};
+ }
+ }
+ print $fid '';
+ }
+ my $time = time;
+ print $fid '[session]';
+ my %session_colour;
+ for my $netch (sort keys %last_time) {
+ for my $nick (sort keys %{ $last_time{$netch} }) {
+ if (exists $has_colour{$netch} && exists $has_colour{$netch}{$nick}
+ && ($last_time{$netch}{$nick} + $retain_colour_time > $time
+ || ($last_time{$netch}{$nick} == 0 && $session_load_time + $retain_colour_time > $time)
+ || grep { $_->[0] eq $nick } @{ $netchan_hist{$netch} // [] })) {
+ $session_colour{$netch}{$nick} = $has_colour{$netch}{$nick};
+ if (exists $session_colour{''}{$nick}) {
+ if (defined $session_colour{''}{$nick}
+ && $session_colour{''}{$nick} ne $session_colour{$netch}{$nick}) {
+ $session_colour{''}{$nick} = undef;
+ }
+ }
+ else {
+ $session_colour{''}{$nick} = $session_colour{$netch}{$nick};
+ }
+ }
+ }
+ }
+ for my $nick (sort keys %{ $session_colour{''} }) {
+ if (defined $session_colour{''}{$nick}) {
+ print $fid "/$nick:".$session_colour{''}{$nick};
+ }
+ else {
+ for my $netch (sort keys %session_colour) {
+ print $fid "$netch/$nick:".$session_colour{$netch}{$nick}
+ if exists $session_colour{$netch}{$nick} && defined $session_colour{$netch}{$nick};
+ }
+ }
+ }
+
+ close $fid;
+}
+
+sub load_colours {
+ $session_load_time = time;
+
+ open my $fid, '<', Irssi::get_irssi_dir() . '/saved_nick_colors'
+ or return;
+ my $mode;
+ while (my $line = <$fid>) {
+ chomp $line;
+ if ($line =~ /^\[(.*)\]$/) {
+ $mode = $1;
+ next;
+ }
+
+ my $colon = rindex $line, ':';
+ next if $colon < 0;
+ my $slash = rindex $line, '/', $colon;
+ next if $slash < 0;
+ my $col = substr $line, $colon +1;
+ next unless length $col;
+ my $netch = substr $line, 0, $slash;
+ my $nick = substr $line, $slash +1, $colon-$slash -1;
+ if ($mode eq 'set') {
+ $set_colour{$netch}{$nick} = $col;
+ }
+ elsif ($mode eq 'session') {
+ $has_colour{$netch}{$nick} = $col;
+ $last_time{$netch}{$nick} = 0;
+ }
+ }
+ close $fid;
+}
+
+sub UNLOAD {
+ return if $exited;
+ exit_save();
+}
+
+sub exit_save {
+ $exited = 1;
+ save_colours() if Irssi::settings_get_bool('settings_autosave');
+}
+
+sub get_nick_color2 {
+ my ($tag, $chan, $nick, $format) = @_;
+ my $col = colourise_nt($tag.'/'.$chan, $nick, 1);
+ $col ? $format ? format_expand('%X'.$col) : $col : ''
+}
+
+sub _cmd_colours_check {
+ my ($add, $data) = @_;
+ my @to_check = grep { defined && length } map {
+ length == 1 ? $base_map{$_}
+ : length == 3 ? substr $_, 1
+ : $_ } map { /(?|x(..)|([0-7].)|(.))/gi }
+ split ' ', $data;
+ my @valid;
+ my %scolours = map { $_ => undef } @colours;
+ for my $c (@to_check) {
+ if ((grep { $_ eq $c } @colour_list)) {
+ if ($add) { next if exists $scolours{$c} }
+ else { next if !exists $scolours{$c} }
+ push @valid, $c;
+ if ($add) { $scolours{$c} = undef; }
+ else { delete $scolours{$c}; }
+ }
+ }
+ (\@valid, \%scolours)
+}
+
+sub _cmd_colours_set {
+ my $scolours = shift;
+ Irssi::settings_set_str('neat_colors', join '', map { $ext_to_base_map{$_} // "X$_" } grep { exists $scolours->{$_} } @colour_list);
+}
+
+sub _cmd_colours_list {
+ map { "%X$_".($ext_to_base_map{$_} // "X$_").'%n' } @{+shift}
+}
+
+sub cmd_neatcolor_colors_add {
+ my ($data, $server, $witem) = @_;
+ my ($added, $scolours) = _cmd_colours_check(1, $data);
+ if (@$added) {
+ _cmd_colours_set($scolours);
+ Irssi::print("%_nce2%_: added @{[ _cmd_colours_list($added) ]} to neat_colors", MSGLEVEL_CLIENTCRAP);
+ setup_changed();
+ }
+ else {
+ Irssi::print("%_nce2%_: nothing added", MSGLEVEL_CLIENTCRAP);
+ }
+}
+sub cmd_neatcolor_colors_remove {
+ my ($data, $server, $witem) = @_;
+ my ($removed, $scolours) = _cmd_colours_check(0, $data);
+ if (@$removed) {
+ _cmd_colours_set($scolours);
+ Irssi::print("%_nce2%_: removed @{[ _cmd_colours_list($removed) ]} from neat_colors", MSGLEVEL_CLIENTCRAP);
+ setup_changed();
+ }
+ else {
+ Irssi::print("%_nce2%_: nothing removed", MSGLEVEL_CLIENTCRAP);
+ }
+}
+
+sub cmd_neatcolor_colors {
+ my ($data, $server, $witem) = @_;
+ $data =~ s/\s+$//;
+ unless (length $data) {
+ Irssi::print("%_nce2%_: current colours: @{[ @colours ? _cmd_colours_list(\@colours) : '(none)' ]}");
+ }
+ Irssi::command_runsub('neatcolor colors', $data, $server, $witem);
+}
+
+sub cmd_neatcolor {
+ my ($data, $server, $witem) = @_;
+ $data =~ s/\s+$//;
+ unless (length $data) {
+ $witem ||= Irssi::active_win;
+ my $time = time;
+ my %distribution = map { $_ => 0 } @colours;
+ for my $netch (keys %has_colour) {
+ next unless length $netch;
+ for my $nick (keys %{ $has_colour{$netch} }) {
+ if (exists $last_time{$netch}{$nick}
+ && ($last_time{$netch}{$nick} + $retain_colour_time > $time
+ || grep { $_->[0] eq $nick } @{ $netchan_hist{$netch} // [] })) {
+ $distribution{ $has_colour{$netch}{$nick} }++
+ }
+ }
+ }
+ $witem->print('%_nce2%_ Colour distribution: '.
+ (join ', ',
+ map { "%X$_$_:$distribution{$_}" }
+ sort { $distribution{$b} <=> $distribution{$a} }
+ grep { exists $distribution{$_} } @colour_list), MSGLEVEL_CLIENTCRAP);
+ }
+ Irssi::command_runsub('neatcolor', $data, $server, $witem);
+}
+
+sub _cmd_check_netchan_arg {
+ my ($cmd, $netchan, $nick) = @_;
+ my %global = map { $_ => undef } qw(set get reset);
+ unless (length $netchan) {
+ Irssi::print('%_nce2%_: no network/channel argument given for neatcolor '.$cmd
+ .(exists $global{$cmd} ? ', use / to '.$cmd.' global colours' : ''),
+ MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ elsif (-1 == index $netchan, '/') {
+ Irssi::print('%_nce2%_: missing network/ in argument given for neatcolor '.$cmd, MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ elsif ($netchan =~ m\^[^/]+/$\) {
+ Irssi::print('%_nce2%_: missing /channel in argument given for neatcolor '.$cmd, MSGLEVEL_CLIENTERROR);
+ return;
+ }
+
+ unless (length $nick) {
+ Irssi::print('%_nce2%_: no nick argument given for neatcolor '.$cmd, MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ elsif (-1 != index $nick, '/') {
+ Irssi::print('%_nce2%_: / not supported in nicks in argument given for neatcolor '.$cmd, MSGLEVEL_CLIENTERROR);
+ return;
+ }
+
+ return 1;
+}
+
+sub _cmd_check_colour {
+ my ($cmd, $colour) = @_;
+ $colour = substr $colour, 1 if length $colour == 3;
+ $colour = $base_map{$colour} if length $colour == 1;
+ unless (length $colour && grep { $_ eq $colour } @colour_list) {
+ Irssi::print('%_nce2%_: no colour or invalid colour argument given for neatcolor '.$cmd, MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ return $colour;
+}
+
+sub cmd_neatcolor_set {
+ my ($data, $server, $witem) = @_;
+ my @args = split ' ', $data;
+ if (@args < 2) {
+ Irssi::print('%_nce2%_: not enough arguments for neatcolor set', MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $netchan;
+ if (ref $witem) {
+ $netchan = $witem->{server}{tag}.'/'.$witem->{name};
+ }
+ my $nick;
+ my $colour;
+ if (@args < 3) {
+ ($nick, $colour) = @args;
+ }
+ else {
+ ($netchan, $nick, $colour) = @args;
+ }
+
+ return unless _cmd_check_netchan_arg('set', $netchan, $nick);
+ return unless defined ($colour = _cmd_check_colour('set', $colour));
+
+ $set_colour{$netchan eq '/' ? '' : $netchan}{$nick} = $colour;
+ for my $netch ($netchan eq '/' ? keys %has_colour
+ : $global_colours ? ('', $netchan)
+ : $netchan) {
+ delete $has_colour{$netch}{$nick} unless
+ exists $has_colour{$netch}{$nick} && $has_colour{$netch}{$nick} eq $colour;
+ }
+ Irssi::print("%_nce2%_: %X$colour$nick%n colour set to: %X$colour$colour%n ".($netchan eq '/' ? 'globally' : "in $netchan"), MSGLEVEL_CLIENTCRAP);
+}
+sub cmd_neatcolor_get {
+ my ($data, $server, $witem) = @_;
+ my @args = split ' ', $data;
+ if (@args < 1) {
+ Irssi::print('%_nce2%_: not enough arguments for neatcolor get', MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $netchan;
+ if (ref $witem) {
+ $netchan = $witem->{server}{tag}.'/'.$witem->{name};
+ }
+ my $nick;
+ if (@args < 2) {
+ $nick = $args[0];
+ }
+ else {
+ ($netchan, $nick) = @args;
+ }
+
+ return unless _cmd_check_netchan_arg('get', $netchan, $nick);
+
+ if ($netchan ne '/') {
+ unless (exists $has_colour{$netchan} && exists $has_colour{$netchan}{$nick}) {
+ Irssi::print("%_nce2%_: $nick is not coloured (yet) in $netchan", MSGLEVEL_CLIENTCRAP);
+ }
+ else {
+ my $colour = $has_colour{$netchan}{$nick};
+ Irssi::print("%_nce2%_: %X$colour$nick%n has colour: %X$colour$colour%n in $netchan", MSGLEVEL_CLIENTCRAP);
+ }
+ }
+ my $hashnick = $nick;
+ $hashnick =~ s/$ignore_re//g if (defined $ignore_re && length $ignore_re);
+ if (exists $set_colour{$netchan} && exists $set_colour{$netchan}{$nick}) {
+ my $colour = $set_colour{$netchan}{$nick};
+ Irssi::print("%_nce2%_: set colour for %X$colour$nick%n in $netchan: %X$colour$colour%n ", MSGLEVEL_CLIENTCRAP);
+ }
+ elsif (exists $set_colour{$netchan} && exists $set_colour{$netchan}{$hashnick}) {
+ my $colour = $set_colour{$netchan}{$hashnick};
+ Irssi::print("%_nce2%_: set colour for %X$colour$hashnick%n in $netchan: %X$colour$colour%n ", MSGLEVEL_CLIENTCRAP);
+ }
+ elsif (exists $set_colour{''} && exists $set_colour{''}{$nick}) {
+ my $colour = $set_colour{''}{$nick};
+ Irssi::print("%_nce2%_: set colour for %X$colour$nick%n (global): %X$colour$colour%n ", MSGLEVEL_CLIENTCRAP);
+ }
+ elsif (exists $set_colour{''} && exists $set_colour{''}{$hashnick}) {
+ my $colour = $set_colour{''}{$hashnick};
+ Irssi::print("%_nce2%_: set colour for %X$colour$hashnick%n (global): %X$colour$colour%n ", MSGLEVEL_CLIENTCRAP);
+ }
+ elsif ($netchan eq '/') {
+ Irssi::print("%_nce2%_: no global colour set for $nick", MSGLEVEL_CLIENTCRAP);
+ }
+}
+sub cmd_neatcolor_reset {
+ my ($data, $server, $witem) = @_;
+ my @args = split ' ', $data;
+ if (@args < 1) {
+ Irssi::print('%_nce2%_: not enough arguments for neatcolor reset', MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $netchan;
+ if (ref $witem) {
+ $netchan = $witem->{server}{tag}.'/'.$witem->{name};
+ }
+ my $nick;
+ if (@args == 1 && $args[0] eq '--all') {
+ %set_colour = %avoid_colour = %has_colour = ();
+ Irssi::print("%_nce2%_: re-set all colouring");
+ return;
+ }
+ if (@args < 2) {
+ $nick = $args[0];
+ }
+ else {
+ ($netchan, $nick) = @args;
+ }
+
+ return unless _cmd_check_netchan_arg('reset', $netchan, $nick);
+
+ $netchan = '' if $netchan eq '/';
+ unless (exists $set_colour{$netchan} && exists $set_colour{$netchan}{$nick}) {
+ Irssi::print("%_nce2%_: $nick has no colour set ". (length $netchan ? "in $netchan" : "globally"), MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $colour = delete $set_colour{$netchan}{$nick};
+ for my $netch ($netchan eq '' ? keys %has_colour
+ : $global_colours ? ('', $netchan)
+ : $netchan) {
+ delete $has_colour{$netch}{$nick} if exists $has_colour{$netch} && exists $has_colour{$netch}{$nick}
+ && $has_colour{$netch}{$nick} eq $colour;
+ }
+ Irssi::print("%_nce2%_: ".($netchan eq '' ? 'global ' : '')."colouring re-set for $nick".($netchan eq '' ? '' : " in $netchan"), MSGLEVEL_CLIENTERROR);
+}
+sub cmd_neatcolor_re {
+ my ($data, $server, $witem) = @_;
+ my @args = split ' ', $data;
+ if (@args < 1) {
+ Irssi::print('%_nce2%_: not enough arguments for neatcolor re', MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $netchan;
+ if (ref $witem) {
+ $netchan = $witem->{server}{tag}.'/'.$witem->{name};
+ }
+ my $nick;
+ if (@args < 2) {
+ $nick = $args[0];
+ }
+ else {
+ ($netchan, $nick) = @args;
+ }
+
+ return unless _cmd_check_netchan_arg('re', $netchan, $nick);
+
+ unless (exists $has_colour{$netchan} && exists $has_colour{$netchan}{$nick}) {
+ Irssi::print("%_nce2%_: could not find $nick in $netchan", MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ my $colour = delete $has_colour{$netchan}{$nick};
+ if (grep { $colour eq $_ } @{ $avoid_colour{$netchan}{$nick} || [] }) {
+ $avoid_colour{$netchan}{$nick} = [ $colour ]
+ }
+ else {
+ push @{ $avoid_colour{$netchan}{$nick} }, $colour;
+ }
+ if ($global_colours) {
+ delete $has_colour{''}{$nick} if defined $colour;
+
+ if (grep { $colour eq $_ } @{ $avoid_colour{''}{$nick} || [] }) {
+ $avoid_colour{''}{$nick} = [ $colour ]
+ }
+ else {
+ push @{ $avoid_colour{''}{$nick} }, $colour;
+ }
+ }
+ Irssi::print("%_nce2%_: re-colouring $nick in $netchan", MSGLEVEL_CLIENTERROR);
+}
+sub cmd_neatcolor_save {
+ Irssi::print("%_nce2%_: saving colours to file", MSGLEVEL_CLIENTCRAP);
+ save_colours();
+}
+
+sub setup_changed {
+ $global_colours = Irssi::settings_get_bool('neat_global_colors');
+ $retain_colour_time = int( abs( Irssi::settings_get_time('neat_color_reassign_time') ) / 1000 );
+ my $old_ignore = $ignore_setting // '';
+ $ignore_setting = Irssi::settings_get_str('neat_ignorechars');
+ if ($old_ignore ne $ignore_setting) {
+ local $@;
+ eval { $ignore_re = qr/$ignore_setting/ };
+ if ($@) {
+ $@ =~ /^(.*)/;
+ print '%_neat_ignorechars%_ did not compile: '.$1;
+ }
+ }
+ my $old_colours = "@colours";
+ my %scolours = map { ($base_map{$_} // $_) => undef } Irssi::settings_get_str('neat_colors') =~ /(?|x(..)|(.))/ig;
+ @colours = grep { exists $scolours{$_} } @colour_list;
+
+ if ($old_colours ne "@colours") {
+ my $time = time;
+ for my $netch (sort keys %last_time) {
+ for my $nick (sort keys %{ $last_time{$netch} }) {
+ if (exists $has_colour{$netch} && exists $has_colour{$netch}{$nick}) {
+ if ($last_time{$netch}{$nick} + $retain_colour_time > $time
+ || ($last_time{$netch}{$nick} == 0 && $session_load_time + $retain_colour_time > $time)) {
+ $last_time{$netch}{$nick} = 0;
+ }
+ else {
+ delete $last_time{$netch}{$nick};
+ }
+ }
+ }
+ $session_load_time = $time;
+ }
+ }
+}
+
+sub internals {
+ +{
+ set => \%set_colour,
+ avoid => \%avoid_colour,
+ has => \%has_colour,
+ time => \%last_time,
+ hist => \%netchan_hist,
+ colours => \@colours
+ }
+}
+
+sub init_nickcolour {
+ setup_changed();
+ load_colours();
+}
+
+Irssi::settings_add_str('misc', 'neat_colors', 'rRgGybBmMcCX42X3AX5EX4NX3HX3CX32');
+Irssi::settings_add_str('misc', 'neat_ignorechars', '');
+Irssi::settings_add_time('misc', 'neat_color_reassign_time', '30min');
+Irssi::settings_add_bool('misc', 'neat_global_colors', 0);
+init_nickcolour();
+
+Irssi::expando_create('nickcolor', \&expando_neatcolour, {
+ 'message public' => 'none',
+ 'message own_public' => 'none',
+ (map { ("message $_ action" => 'none',
+ "message $_ own_action" => 'none')
+ } @action_protos),
+ });
+
+Irssi::expando_create('inickcolor', \&expando_neatcolour_inv, {
+ 'message public' => 'none',
+ 'message own_public' => 'none',
+ (map { ("message $_ action" => 'none',
+ "message $_ own_action" => 'none')
+ } @action_protos),
+ });
+
+Irssi::signal_add({
+ 'message public' => 'msg_line_tag',
+ 'message own_public' => 'msg_line_clear',
+ (map { ("message $_ action" => 'msg_line_tag',
+ "message $_ own_action" => 'msg_line_clear')
+ } qw(irc silc)),
+ "message xmpp action" => 'msg_line_tag_xmppaction',
+ "message xmpp own_action" => 'msg_line_clear',
+ 'print text' => 'prnt_clear_public',
+ 'nicklist changed' => 'nicklist_changed',
+ 'gui exit' => 'exit_save',
+});
+Irssi::command_bind({
+ 'help' => sub { &cmd_help_neatcolor if $_[0] =~ /^neatcolor\s*$/i;},
+ 'neatcolor' => 'cmd_neatcolor',
+ 'neatcolor save' => 'cmd_neatcolor_save',
+ 'neatcolor set' => 'cmd_neatcolor_set',
+ 'neatcolor get' => 'cmd_neatcolor_get',
+ 'neatcolor reset' => 'cmd_neatcolor_reset',
+ 'neatcolor re' => 'cmd_neatcolor_re',
+ 'neatcolor colors' => 'cmd_neatcolor_colors',
+ 'neatcolor colors add' => 'cmd_neatcolor_colors_add',
+ 'neatcolor colors remove' => 'cmd_neatcolor_colors_remove',
+ });
+
+Irssi::signal_add_last('setup changed' => 'setup_changed');
+
+
+# Changelog
+# =========
+# 0.4.0
+# - Allow usage of the colour as a background (using $inickcolor)
+# 0.3.7
+# - fix crash if xmpp action signal is not registered (just ignore it)
+# 0.3.6
+# - also look up ignorechars in set colours
+# 0.3.5
+# - bug fix release
+# 0.3.4
+# - re/set/reset-colouring was affected by the global colour
+# - set colour score too weak
+# 0.3.3
+# - fix error with get / reported by Meicceli
+# - now possible to reset global colour
+# - check for invalid colours
+# 0.3.2
+# - add global colour option
+# - respect save settings setting
+# - add action handling
+# 0.3.1
+# - regression: reset colours after removing colour
+# 0.3.0
+# - save some more colours
+# 0.2.9
+# - fix incorrect calculation of used colours
+# - add some sanity checks to set/get command
+# - avoid random colour changes