summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorailin-nemui2015-11-16 19:32:11 -0300
committerdequis2015-11-16 19:34:05 -0300
commit2bc8e3368c0b842f1fe6fad069915a4bb90b1fa2 (patch)
treecafd9a0ee1dace65772f70f6147598227c38a35b /scripts
parentb8965d1651f89ebcd395de3309c1255542ad4c78 (diff)
downloadscripts.irssi.org-2bc8e3368c0b842f1fe6fad069915a4bb90b1fa2.tar.bz2
Add all of nei's scripts from nei's website
Diffstat (limited to 'scripts')
-rw-r--r--scripts/adv_windowlist.pl2456
-rw-r--r--scripts/clearable.pl71
-rw-r--r--scripts/colorize_nicks.pl136
-rw-r--r--scripts/complete_at.pl40
-rw-r--r--scripts/dim_nicks.pl392
-rw-r--r--scripts/hideshow.pl286
-rw-r--r--scripts/linebuffer.pl278
-rw-r--r--scripts/nickcolor_expando.pl1048
-rw-r--r--scripts/nm2.pl560
-rw-r--r--scripts/sbclearmatch.pl93
10 files changed, 5360 insertions, 0 deletions
diff --git a/scripts/adv_windowlist.pl b/scripts/adv_windowlist.pl
new file mode 100644
index 0000000..19bbe70
--- /dev/null
+++ b/scripts/adv_windowlist.pl
@@ -0,0 +1,2456 @@
+use strict;
+use warnings;
+
+our $VERSION = '1.0a2'; # 185124f561a65ff
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'adv_windowlist',
+ description => 'Adds a permanent advanced window list on the right or in a status bar.',
+ license => 'GNU GPLv2 or later',
+ );
+
+# UPGRADE NOTE
+# ============
+# for users of 0.7 or earlier series, please note that appearance
+# settings have moved to /format, i.e. inside your theme!
+# the fifo (screen) has been replaced by an external viewer script
+
+# Usage
+# =====
+# copy the script to ~/.irssi/scripts/
+#
+# In irssi:
+#
+# /run adv_windowlist
+#
+# In your shell (for example a tmux split):
+#
+# perl ~/.irssi/scripts/adv_windowlist.pl
+#
+# To use sbar mode instead:
+#
+# /toggle awl_viewer
+#
+# Hint: to get rid of the old [Act:] display
+# /statusbar window remove act
+#
+# to get it back:
+# /statusbar window add -after lag -priority 10 act
+
+# Options
+# =======
+# formats can be cleared with /format -delete
+#
+# /format awl_display_(no)key(_active|_visible) <string>
+# * string : Format String for one window. The following $'s are expanded:
+# $C : Name
+# $N : Number of the Window
+# $Q : meta-Keymap
+# $H : Start hilighting
+# $S : Stop hilighting
+# /+++++++++++++++++++++++++++++++++,
+# | **** I M P O R T A N T : **** |
+# | |
+# | don't forget to use $S if you |
+# | used $H before! |
+# | |
+# '+++++++++++++++++++++++++++++++++/
+# key : a key binding that goes to this window could be detected in /bind
+# nokey : no such key binding was detected
+# active : window would receive the input you are currently typing
+# visible : window is also visible on screen but not active (a split window)
+#
+# /format awl_name_display <string>
+# * string : Format String for window names
+# $0 : name as formatted by the settings
+#
+# /format awl_display_header <string>
+# * string : Format String for this header line. The following $'s are expanded:
+# $C : network tag
+#
+# /format awl_separator(2) <string>
+# * string : Character to use between the channel entries
+# variant 2 can be used for alternating separators (only in status bar
+# without block display)
+#
+# /format awl_viewer_item_bg <string>
+# * string : Format String specifying the viewer's item background colour
+#
+# /set awl_prefer_name <ON|OFF>
+# * this setting decides whether awl will use the active_name (OFF) or the
+# window name as the name/caption in awl_display_*.
+# That way you can rename windows using /window name myownname.
+#
+# /set awl_hide_empty <num>
+# * if visible windows without items should be hidden from the window list
+# set it to 0 to show all windows
+# 1 to hide visible windows without items (negative exempt
+# active window)
+#
+# /set awl_hide_data <num>
+# * num : hide the window if its data_level is below num
+# set it to 0 to basically disable this feature,
+# 1 if you don't want windows without activity to be shown
+# 2 to show only those windows with channel text or hilight
+# 3 to show only windows with hilight (negative exempt active window)
+#
+# /set awl_hide_name_data <num>
+# * num : hide the name of the window if its data_level is below num
+# (only works in status bar without block display)
+# you will want to change your formats to add $H...$S around $Q or $N
+# if you plan to use this
+#
+# /set awl_maxlines <num>
+# * num : number of lines to use for the window list (0 to disable, negative
+# lock)
+#
+# /set awl_maxcolumns <num>
+# * num : number of columns to use for the window list when using the
+# tmux integration (0 to disable)
+#
+# /set awl_block <num>
+# * num : width of a column in viewer mode (negative values = block
+# display in status bar mode)
+# /+++++++++++++++++++++++++++++++++,
+# | ****** W A R N I N G ! ****** |
+# | |
+# | If your block display looks |
+# | DISTORTED, you need to add the |
+# | following line to your .theme |
+# | file under |
+# | abstracts = { : |
+# | |
+# | sb_act_none = "%K$*"; |
+# | |
+# '+++++++++++++++++++++++++++++++++/
+#
+# /set awl_sbar_maxlength <ON|OFF>
+# * if you enable the maxlength setting, the block width will be used as a
+# maximum length for the non-block status bar mode too.
+#
+# /set awl_height_adjust <num>
+# * num : how many lines to leave empty in viewer mode
+#
+# /set awl_sort <-data_level|-last_line|refnum>
+# * you can change the window sort order with this variable
+# -data_level : sort windows with hilight first
+# -last_line : sort windows in order of activity
+# refnum : sort windows by window number
+# active/server/tag : sort by server name
+# "-" reverses the sort order
+# typechecks are supported via ::, e.g. active::Query or active::Irc::Query
+# undefinedness can be checked with ~, e.g. ~active
+# string comparison can be done with =, e.g. name=(status)
+# to make sort case insensitive, use #i, e.g. name#i
+# any key in the window hash can be tested, e.g. active/chat_type=XMPP
+# multiple criteria can be separated with , or +, e.g. -data_level+-last_line
+#
+# /set awl_placement <top|bottom>
+# /set awl_position <num>
+# * these settings correspond to /statusbar because awl will create
+# status bars for you
+# (see /help statusbar to learn more)
+#
+# /set awl_all_disable <ON|OFF>
+# * if you set awl_all_disable to ON, awl will also remove the
+# last status bar it created if it is empty.
+# As you might guess, this only makes sense with awl_hide_data > 0 ;)
+#
+# /set awl_viewer <ON|OFF>
+# * enable the external viewer script
+#
+# /set awl_viewer_launch <ON|OFF>
+# * try to auto-launch the viewer under tmux or with a shell command
+# /awl restart is required all auto-launch related settings to take
+# effect
+#
+# /set awl_viewer_tmux_position <left|top|right|bottom|custom>
+# * try to split in this direction when using tmux for the viewer
+# custom : use custom_command setting
+#
+# /set awl_viewer_xwin_command <shell command>
+# * custom command to run in order to start the viewer when irssi is
+# running under X
+# %A - gets replaced by the command to run the viewer
+# %qA - additionally quote the command
+#
+# /set awl_viewer_custom_command <shell command>
+# * custom command to run in order to start the viewer
+#
+# /set awl_viewer_launch_env <string>
+# * specific environment settings for use on viewer auto-launch,
+# without the AWL_ prefix
+#
+# /set awl_shared_sbar <left<right|OFF>
+# * share a status bar for the first awl item, you will need to manually
+# /statusbar window add -after lag -priority 10 awl_shared
+# left : space in cells occupied on the left of status bar
+# right : space occupied on the right
+# Note: you need to replace "left" AND "right" with the appropriate numbers!
+#
+# /set awl_path <path>
+# * path to the file which the viewer script reads
+#
+# /set fancy_abbrev <no|head|strict|fancy>
+# * how to shorten too long names
+# no : shorten in the middle
+# head : always cut off the ends
+# strict : shorten repeating substrings
+# fancy : combination of no+strict
+#
+# /set awl_custom_xform <perl code>
+# * specify a custom routine to transform window names
+# example: s/^#// remove the #-mark of IRC channels
+# the special flags $CHANNEL / $TAG / $QUERY / $NAME can be
+# tested in conditionals
+#
+# /set awl_last_line_shade <timeout>
+# * set timeout to shade activity base colours, to enable
+# you also need to add +-last_line to awl_sort
+# (requires 256 colour support)
+#
+# /set awl_no_mode_hint <ON|OFF>
+# * whether to show the hint of running the viewer script in the
+# status bar
+#
+# /set awl_mouse <ON|OFF>
+# * enable the terminal mouse in irssi
+# (use the awl-patched mouse.pl for gestures and commands if you need
+# them and disable mouse_escape)
+#
+# /set awl_mouse_offset <num>
+# * specifies where on the screen is the awl status bar
+# (0 = on top/bottom, 1 = one additional line in between,
+# e.g. prompt)
+# you MUST set this correctly otherwise the mouse coordinates will
+# be off
+#
+# /set mouse_scroll <num>
+# * how many lines the mouse wheel scrolls
+#
+# /set mouse_escape <num>
+# * seconds to disable the mouse, when not clicked on the windowlist
+#
+
+# Commands
+# ========
+# /awl redraw
+# * redraws the windowlist. There may be occasions where the
+# windowlist can get destroyed so you can use this command to
+# force a redraw.
+#
+# /awl restart
+# * restart the connection to the viewer script.
+
+# Viewer script
+# =============
+# When run from the command line, adv_windowlist acts as the viewer
+# script to be used together with the irssi script to display the
+# window list in a sidebar/terminal of its own.
+#
+# One optional parameter is accepted, the awl_path
+#
+# The viewer can be configured by three environment variables:
+#
+# AWL_HI9=1
+# * interpret %9 as high-intensity toggle instead of bold. This had
+# been the default prior to version 0.9b8
+#
+# AWL_AUTOFOCUS=0
+# * disable auto-focus behaviour when activating a window
+#
+# AWL_NOTITLE=1
+# * disable the title bar
+
+# Nei =^.^= ( anti@conference.jabber.teamidiot.de )
+
+no warnings 'redefine';
+use constant IN_IRSSI => __PACKAGE__ ne 'main' || $ENV{IRSSI_MOCK};
+use constant SCRIPT_FILE => __FILE__;
+no if !IN_IRSSI, strict => (qw(subs refs));
+use if IN_IRSSI, Irssi => ();
+use if IN_IRSSI, 'Irssi::TextUI' => ();
+use v5.10;
+use Encode;
+use Storable ();
+use IO::Socket::UNIX;
+use List::Util qw(min max reduce);
+use Hash::Util qw(lock_keys);
+use Text::ParseWords qw(shellwords);
+
+unless (IN_IRSSI) {
+ local *_ = \@ARGV;
+ &AwlViewer::main;
+ exit;
+}
+
+
+use constant GLOB_QUEUE_TIMER => 100;
+
+our $BLOCK_ALL; # localized blocker
+my @actString; # status bar texts
+my @win_items;
+my $currentLines = 0;
+my %awins;
+my $globTime; # timer to limit remake calls
+
+my %CHANGED;
+my $VIEWER_MODE;
+my $MOUSE_ON;
+my %mouse_coords;
+my %statusbars;
+my %S; # settings
+my $settings_str = '';
+my $window_sort_func;
+my $custom_xform;
+my ($sb_base_width, $sb_base_width_pre, $sb_base_width_post);
+my $print_text_activity;
+my $shade_line_timer;
+my ($screenHeight, $screenWidth);
+my %viewer;
+
+my (%keymap, %nummap, %wnmap, %specialmap, %wnmap_exp, %custom_key_map);
+my %banned_channels;
+my %abbrev_cache;
+
+use constant setc => 'awl';
+
+sub set ($) {
+ setc . '_' . $_[0]
+}
+
+sub add_statusbar {
+ for (@_) {
+ # add subs
+ my $l = set $_;
+ {
+ my $close = $_;
+ no strict 'refs';
+ *{$l} = sub { awl($close, @_) };
+ }
+ Irssi::command("statusbar $l reset");
+ Irssi::command("statusbar $l enable");
+ if (lc $S{placement} eq 'top') {
+ Irssi::command("statusbar $l placement top");
+ }
+ if (my $x = $S{position}) {
+ Irssi::command("statusbar $l position $x");
+ }
+ Irssi::command("statusbar $l add -priority 100 -alignment left barstart");
+ Irssi::command("statusbar $l add $l");
+ Irssi::command("statusbar $l add -priority 100 -alignment right barend");
+ Irssi::command("statusbar $l disable");
+ Irssi::statusbar_item_register($l, '$0', $l);
+ $statusbars{$_} = 1;
+ Irssi::command("statusbar $l enable");
+ }
+}
+
+sub remove_statusbar {
+ for (@_) {
+ my $l = set $_;
+ Irssi::command("statusbar $l disable");
+ Irssi::command("statusbar $l reset");
+ Irssi::statusbar_item_unregister($l);
+ {
+ no strict 'refs';
+ undef &{$l};
+ }
+ delete $statusbars{$_};
+ }
+}
+
+my $awl_shared_empty = sub {
+ return if $BLOCK_ALL;
+ my ($item, $get_size_only) = @_;
+ $item->default_handler($get_size_only, '', '', 0);
+};
+
+sub syncLines {
+ my $maxLines = $S{maxlines};
+ my $newLines = ($maxLines > 0 and @actString > $maxLines) ?
+ $maxLines :
+ ($maxLines < 0) ?
+ -$maxLines :
+ @actString;
+ $currentLines = 1 if !$currentLines && $S{shared_sbar};
+ if ($S{shared_sbar} && !$statusbars{shared}) {
+ my $l = set 'shared';
+ {
+ no strict 'refs';
+ *{$l} = sub {
+ return if $BLOCK_ALL;
+ my ($item, $get_size_only) = @_;
+
+ my $text = $actString[0];
+ my $pat = defined $text ? '{sb '.ucfirst(setc()).': $*}' : '{sb }';
+ $text //= '';
+ $item->default_handler($get_size_only, $pat, $text, 0);
+ };
+ }
+ $statusbars{shared} = 1;
+ remove_statusbar (0) if $statusbars{0};
+ }
+ elsif ($statusbars{shared} && !$S{shared_sbar}) {
+ add_statusbar (0) if $currentLines && $newLines;
+ delete $statusbars{shared};
+ my $l = set 'shared';
+ {
+ no strict 'refs';
+ *{$l} = $awl_shared_empty;
+ }
+ }
+ if ($currentLines == $newLines) { return; }
+ elsif ($newLines > $currentLines) {
+ add_statusbar ($currentLines .. ($newLines - 1));
+ }
+ else {
+ remove_statusbar (reverse ($newLines .. ($currentLines - 1)));
+ }
+ $currentLines = $newLines;
+}
+
+sub awl {
+ return if $BLOCK_ALL;
+ my ($line, $item, $get_size_only) = @_;
+
+ my $text = $actString[$line];
+ my $pat = defined $text ? '{sb $*}' : '{sb }';
+ $text //= '';
+ $item->default_handler($get_size_only, $pat, $text, 0);
+}
+
+# remove old statusbars
+{ my %killBar;
+ sub get_old_status {
+ my ($textDest, $cont, $cont_stripped) = @_;
+ if ($textDest->{level} == 524288 and $textDest->{target} eq '' and !defined $textDest->{server}) {
+ my $name = quotemeta(set '');
+ if ($cont_stripped =~ m/^$name(\d+)\s/) { $killBar{$1} = 1; }
+ Irssi::signal_stop;
+ }
+ }
+ sub killOldStatus {
+ %killBar = ();
+ Irssi::signal_add_first('print text' => 'get_old_status');
+ Irssi::command('statusbar');
+ Irssi::signal_remove('print text' => 'get_old_status');
+ remove_statusbar(keys %killBar);
+ }
+}
+
+sub get_keymap {
+ my ($textDest, undef, $cont_stripped) = @_;
+ if ($textDest->{level} == 524288 and $textDest->{target} eq '' and !defined $textDest->{server}) {
+ my $one_meta_or_ctrl_key = qr/((?:meta-)*?)(?:(meta-|\^)(\S)|(\w+))/;
+ $cont_stripped = as_uni($cont_stripped);
+ if ($cont_stripped =~ m/((?:$one_meta_or_ctrl_key-)*$one_meta_or_ctrl_key)\s+(.*)$/) {
+ my ($combo, $command) = ($1, $10);
+ my $map = '';
+ while ($combo =~ s/(?:-|^)$one_meta_or_ctrl_key$//) {
+ my ($level, $ctl, $key, $nkey) = ($1, $2, $3, $4);
+ my $numlevel = ($level =~ y/-//);
+ $ctl = '' if !$ctl || $ctl ne '^';
+ $map = ('-' x ($numlevel%2)) . ('+' x ($numlevel/2)) .
+ $ctl . (defined $key ? $key : "\01$nkey\01") . $map;
+ }
+ for ($command) {
+ last unless length $map;
+ if (/^change_window (\d+)/i) {
+ $nummap{$1} = $map;
+ }
+ elsif (/^command window goto (\S+)/i) {
+ my $window = $1;
+ if ($window !~ /\D/) {
+ $nummap{$window} = $map;
+ }
+ elsif (lc $window eq 'active') {
+ $specialmap{_active} = $map;
+ }
+ else {
+ $wnmap{$window} = $map;
+ }
+ }
+ elsif (/^(?:active_window|command (ack))/i) {
+ $specialmap{_active} = $map;
+ $viewer{use_ack} = !!$1;
+ }
+ elsif (/^command window last/i) {
+ $specialmap{_last} = $map;
+ }
+ elsif (/^(?:upper_window|command window up)/i) {
+ $specialmap{_up} = $map;
+ }
+ elsif (/^(?:lower_window|command window down)/i) {
+ $specialmap{_down} = $map;
+ }
+ elsif (/^key\s+(\w+)/i) {
+ $custom_key_map{$1} = $map;
+ }
+ }
+ }
+ Irssi::signal_stop;
+ }
+}
+
+sub update_keymap {
+ %nummap = %wnmap = %specialmap = %custom_key_map = ();
+ Irssi::signal_remove('command bind' => 'watch_keymap');
+ Irssi::signal_add_first('print text' => 'get_keymap');
+ Irssi::command('bind');
+ Irssi::signal_remove('print text' => 'get_keymap');
+ for (keys %custom_key_map) {
+ if (exists $custom_key_map{$_} &&
+ $custom_key_map{$_} =~ s/\01(\w+)\01/exists $custom_key_map{$1} ? $custom_key_map{$1} : "\02"/ge) {
+ if ($custom_key_map{$_} =~ /\02/) {
+ delete $custom_key_map{$_};
+ }
+ else {
+ redo;
+ }
+ }
+ }
+ for my $keymap (\(%specialmap, %wnmap, %nummap)) {
+ for (keys %$keymap) {
+ if ($keymap->{$_} =~ s/\01(\w+)\01/exists $custom_key_map{$1} ? $custom_key_map{$1} : "\02"/ge) {
+ if ($keymap->{$_} =~ /\02/) {
+ delete $keymap->{$_};
+ }
+ }
+ }
+ }
+ Irssi::signal_add('command bind' => 'watch_keymap');
+ delete $viewer{client_keymap};
+ &wl_changed;
+}
+
+# watch keymap changes
+sub watch_keymap {
+ Irssi::timeout_add_once(1000, 'update_keymap', undef);
+}
+
+{ my %strip_table = (
+ # fe-common::core::formats.c:format_expand_styles
+ # delete format_backs format_fores bold_fores other stuff
+ (map { $_ => '' } (split //, '04261537' . 'kbgcrmyw' . 'KBGCRMYW' . 'U9_8I:|FnN>#[' . 'pP')),
+ # escape
+ (map { $_ => $_ } (split //, '{}%')),
+ );
+ sub ir_strip_codes { # strip %codes
+ my $o = shift;
+ $o =~ s/(%(%|Z.{6}|z.{6}|X..|x..|.))/exists $strip_table{$2} ? $strip_table{$2} :
+ $2 =~ m{x(?:0[a-f]|[1-6][0-9a-z]|7[a-x])|z[0-9a-f]{6}}i ? '' : $1/gex;
+ $o
+ }
+}
+## ir_parse_special -- wrapper around parse_special
+## $i - input format
+## $args - array ref of arguments to format
+## $win - different target window (default current window)
+## $flags - different kind of escape flags (default 4|8)
+## returns formatted str
+sub ir_parse_special {
+ my $o;
+ my $i = shift;
+ my $args = shift // [];
+ y/ /\177/ for @$args; # hack to escape spaces
+ my $win = shift || Irssi::active_win;
+ my $flags = shift // 0x4|0x8;
+ my @cmd_args = ($i, (join ' ', @$args), $flags);
+ my $server = Irssi::active_server();
+ if (ref $win and ref $win->{active}) {
+ $o = $win->{active}->parse_special(@cmd_args);
+ }
+ elsif (ref $win and ref $win->{active_server}) {
+ $o = $win->{active_server}->parse_special(@cmd_args);
+ }
+ elsif (ref $server) {
+ $o = $server->parse_special(@cmd_args);
+ }
+ else {
+ $o = &Irssi::parse_special(@cmd_args);
+ }
+ $o =~ y/\177/ /;
+ $o
+}
+
+sub sb_format_expand { # Irssi::current_theme->format_expand wrapper
+ Irssi::current_theme->format_expand(
+ $_[0],
+ (
+ Irssi::EXPAND_FLAG_IGNORE_REPLACES
+ |
+ ($_[1] ? 0 : Irssi::EXPAND_FLAG_IGNORE_EMPTY)
+ )
+ )
+}
+
+{ my $term_type = Irssi::version > 20040819 ? 'term_charset' : 'term_type';
+ local $@;
+ eval { require Text::CharWidth; };
+ unless ($@) {
+ *screen_length = sub { Text::CharWidth::mbswidth($_[0]) };
+ }
+ else {
+ my $err = $@; chomp $err; $err =~ s/\sat .* line \d+\.$//;
+ #Irssi::print("%_$IRSSI{name}: warning:%_ Text::CharWidth module failed to load. Length calculation may be off! Error was:");
+ print "%_$IRSSI{name}:%_ $err";
+ *screen_length = sub {
+ my $temp = shift;
+ if (lc Irssi::settings_get_str($term_type) eq 'utf-8') {
+ Encode::_utf8_on($temp);
+ }
+ length($temp)
+ };
+ }
+ sub as_uni {
+ no warnings 'utf8';
+ Encode::decode(Irssi::settings_get_str($term_type), $_[0], 0)
+ }
+ sub as_tc {
+ Encode::encode(Irssi::settings_get_str($term_type), $_[0], 0)
+ }
+}
+
+sub sb_length {
+ screen_length(ir_strip_codes($_[0]))
+}
+
+sub run_custom_xform {
+ local $@;
+ eval {
+ $custom_xform->()
+ };
+ if ($@) {
+ $@ =~ /^(.*)/;
+ print '%_'.(set 'custom_xform').'%_ died (disabling): '.$1;
+ $custom_xform = undef;
+ }
+}
+
+sub remove_uniform {
+ my $o = shift;
+ $o =~ s/^xmpp:(.*?[%@]).+\.[^.]+$/$1/ or
+ $o =~ s#^psyc://.+\.[^.]+/([@~].*)$#$1#;
+ if ($custom_xform) {
+ run_custom_xform() for $o;
+ }
+ $o
+}
+
+sub remove_uniform_vars {
+ my $win = shift;
+ my $name = __PACKAGE__ . '::custom_xform::' . $win->{active}{type}
+ if $win->{active} && $win->{active}{type};
+ no strict 'refs';
+ local ${$name} = 1 if $name;
+ remove_uniform(+shift);
+}
+
+sub lc1459 {
+ my $x = shift;
+ $x =~ y/][\\^/}{|~/;
+ lc $x
+}
+
+sub window_list {
+ sort $window_sort_func Irssi::windows;
+}
+
+sub _calculate_abbrev {
+ my ($wins, $abbrevList) = @_;
+ if ($S{fancy_abbrev} !~ /^(no|off|head)/i) {
+ my @nameList = map { ref $_ ? remove_uniform_vars($_, as_uni($_->get_active_name) // '') : '' } @$wins;
+ for (my $i = 0; $i < @nameList - 1; ++$i) {
+ my ($x, $y) = ($nameList[$i], $nameList[$i + 1]);
+ s/^[+#!=]// for $x, $y;
+ my $res = exists $abbrev_cache{$x}{$y} ? $abbrev_cache{$x}{$y}
+ : $abbrev_cache{$x}{$y} = string_LCSS($x, $y);
+ if (defined $res) {
+ for ($nameList[$i], $nameList[$i + 1]) {
+ $abbrevList->{$_} //= int((index $_, $res) + (length $res) / 2);
+ }
+ }
+ }
+ }
+}
+
+my %act_last_line_shades = (
+ r => [qw[ 50 40 30 20 ]],
+ g => [qw[ 1O 1I 1C 16 ]],
+ y => [qw[ 5O 4I 3C 26 ]],
+ b => [qw[ 15 14 13 12 ]],
+ m => [qw[ 54 43 32 21 ]],
+ c => [qw[ 1S 1L 1E 17 ]],
+ w => [qw[ 7W 7T 7Q 3E ]],
+ K => [qw[ 7M 7K 27 7H ]],
+ R => [qw[ 60 50 40 30 ]],
+ G => [qw[ 1U 1O 1I 1C ]],
+ Y => [qw[ 6U 5O 4I 3C ]],
+ B => [qw[ 2B 2A 29 28 ]],
+ M => [qw[ 65 54 43 32 ]],
+ C => [qw[ 1Z 1S 1L 1E ]],
+ W => [qw[ 6Z 5S 7R 7O ]],
+ );
+
+sub _format_display {
+ my (undef, $format, $cformat, $hilight, $name, $number, $key, $win) = @_;
+ if ($print_text_activity && $S{line_shade}) {
+ my @hilight_code = split /\177/, sb_format_expand("{$hilight \177}"), 2;
+ my $max_time = max(1, log($S{line_shade}) - log(1000));
+ my $time_delta = min(3, min($max_time, log(max(1, time - $win->{last_line}))) / $max_time * 3);
+ if ($hilight_code[0] =~ /%(.)/ && exists $act_last_line_shades{$1}) {
+ $hilight = 'sb_act_hilight_color %X'.$act_last_line_shades{$1}[$time_delta];
+ }
+ }
+ $cformat = '$0' unless defined $cformat && length $cformat;
+ my %map = ('$C' => $cformat, '$N' => '$1', '$Q' => '$2');
+ $format =~ s<(\$.)><$map{$1}//$1>ge;
+ $format =~ s<\$H((?:\$.|[^\$])*?)\$S><{$hilight $1%n}>g;
+ my @ret = ir_parse_special(sb_format_expand($format), [$name, $number, $key], $win);
+ @ret
+}
+
+sub _calculate_items {
+ my ($wins, $abbrevList) = @_;
+
+ my $display_header = Irssi::current_theme->get_format(__PACKAGE__, set 'display_header');
+ my $name_format = Irssi::current_theme->get_format(__PACKAGE__, set 'name_display');
+ my %displays;
+
+ my $active = Irssi::active_win;
+ @win_items = ();
+ %keymap = (%nummap, %wnmap_exp);
+
+ my ($numPad, $keyPad) = (0, 0);
+ if ($VIEWER_MODE or $S{block} < 0) {
+ $numPad = length((sort { length $b <=> length $a } keys %keymap)[0]) // 0;
+ $keyPad = length((sort { length $b <=> length $a } values %keymap)[0]) // 0;
+ }
+ my $last_net;
+ for my $win (@$wins) {
+ my $global_hack_alert_tag_header;
+
+ next unless ref $win;
+
+ my $backup_win = Storable::dclone($win);
+ delete $backup_win->{active} unless ref $backup_win->{active};
+
+ $global_hack_alert_tag_header =
+ $display_header && ($last_net // '') ne ($backup_win->{active}{server}{tag} // '');
+
+ if ($win->{data_level} < abs $S{hide_data}
+ && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_data})) {
+ next; }
+ elsif (exists $awins{$win->{refnum}} && $S{hide_empty} && !$win->items
+ && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_empty})) {
+ next; }
+
+ my $colour = $win->{hilight_color} // '';
+ my $hilight = do {
+ if ($win->{data_level} == 0) { 'sb_act_none'; }
+ elsif ($win->{data_level} == 1) { 'sb_act_text'; }
+ elsif ($win->{data_level} == 2) { 'sb_act_msg'; }
+ elsif ($colour ne '') { "sb_act_hilight_color $colour"; }
+ elsif ($win->{data_level} == 3) { 'sb_act_hilight'; }
+ else { 'sb_act_special'; }
+ };
+ my $number = $win->{refnum};
+
+ my ($name, $display, $cdisplay);
+ if ($global_hack_alert_tag_header) {
+ $display = $display_header;
+ $name = as_uni($backup_win->{active}{server}{tag}) // '';
+ if ($custom_xform) {
+ no strict 'refs';
+ local ${ __PACKAGE__ . '::custom_xform::TAG' } = 1;
+ run_custom_xform() for $name;
+ }
+ }
+ else {
+ my @display = ('display_nokey');
+ if (defined $keymap{$number} and $keymap{$number} ne '') {
+ unshift @display, map { (my $cpy = $_) =~ s/_no/_/; $cpy } @display;
+ }
+ if (exists $awins{$number}) {
+ unshift @display, map { my $cpy = $_; $cpy .= '_visible'; $cpy } @display;
+ }
+ if ($active->{refnum} == $number) {
+ unshift @display, map { my $cpy = $_; $cpy .= '_active'; $cpy }
+ grep { !/_visible$/ } @display;
+ }
+ $display = (grep { length $_ }
+ map { $displays{$_} //= Irssi::current_theme->get_format(__PACKAGE__, set $_) }
+ @display)[0];
+ $cdisplay = $name_format;
+ $name = as_uni($win->get_active_name) // '';
+ $name = '*' if $S{banned_on} and exists $banned_channels{lc1459($name)};
+ $name = remove_uniform_vars($win, $name) if $name ne '*';
+ if ($name ne '*' and $win->{name} ne '' and $S{prefer_name}) {
+ $name = as_uni($win->{name});
+ if ($custom_xform) {
+ no strict 'refs';
+ local ${ __PACKAGE__ . '::custom_xform::NAME' } = 1;
+ run_custom_xform() for $name;
+ }
+ }
+
+ if (!$VIEWER_MODE && $S{block} >= 0 && $S{hide_name}
+ && $win->{data_level} < abs $S{hide_name}
+ && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_name})) {
+ $name = '';
+ $cdisplay = '';
+ }
+ }
+
+ $display = "$display%n";
+ my $num_ent = (' 'x max(0,$numPad - length $number)) . $number;
+ my $key_ent = exists $keymap{$number} ? ((' 'x max(0,$keyPad - length $keymap{$number})) . $keymap{$number}) : ' 'x$keyPad;
+ if ($VIEWER_MODE or $S{sbar_maxlen} or $S{block} < 0) {
+ my $baseLength = sb_length(_format_display(
+ '', $display, $cdisplay, $hilight,
+ 'x', # placeholder
+ $num_ent,
+ $key_ent,
+ $win)) - 1;
+ my $diff = (abs $S{block}) - (screen_length(as_tc($name)) + $baseLength);
+ if ($diff < 0) { # too long
+ my $screen_length = screen_length(as_tc($name));
+ if ((abs $diff) >= $screen_length) { $name = '' } # forget it
+ elsif ((abs $diff) + screen_length(as_tc(substr($name, 0, 1))) >= $screen_length) { $name = substr($name, 0, 1); }
+ else {
+ my $ulen = length $name;
+ my $middle2 = exists $abbrevList->{$name} ?
+ ($S{fancy_strict}) ?
+ 2* $abbrevList->{$name} :
+ (2*($abbrevList->{$name} + $ulen) / 3) :
+ ($S{fancy_head}) ?
+ 2*$ulen :
+ $ulen;
+ my $first = 1;
+ while (length $name > 1) {
+ my $cp = $middle2 > -1 ? $middle2/2 : -1; # check position for double width
+ my $rm = 2;
+ if (screen_length(as_tc(substr $name, $cp, 1)) > 1) {
+ if ($first || $cp < 0) {
+ $rm = 1;
+ $first = undef;
+ }
+ }
+ elsif ($cp < 0) {
+ --$cp;
+ }
+ (substr $name, $cp, $rm) = '~';
+ if ($cp > -1 && $rm > 1) {
+ --$middle2;
+ }
+ my $sl = screen_length(as_tc($name));
+ if ($sl + $baseLength < abs $S{block}) {
+ (substr $name, ($middle2+1)/2, 1) = "\x{301c}";
+ last;
+ }
+ elsif ($sl + $baseLength == abs $S{block}) {
+ last;
+ }
+ }
+ }
+ }
+ elsif ($VIEWER_MODE or $S{block} < 0) {
+ $name .= (' ' x $diff);
+ }
+ }
+
+ push @win_items, _format_display(
+ '', $display, $cdisplay, $hilight,
+ as_tc($name),
+ $num_ent,
+ as_tc($key_ent),
+ $win);
+
+ if ($global_hack_alert_tag_header) {
+ $last_net = $backup_win->{active}{server}{tag};
+ redo;
+ }
+
+ $mouse_coords{refnum}{$#win_items} = $number;
+ }
+}
+
+sub _spread_items {
+ my $width = [Irssi::windows]->[0]{width} - $sb_base_width - 1;
+ my @separator = Irssi::current_theme->get_format(__PACKAGE__, set 'separator');
+ if ($S{block} >= 0) {
+ my $sep2 = Irssi::current_theme->get_format(__PACKAGE__, set 'separator2');
+ push @separator, $sep2 if length $sep2 && $sep2 ne $separator[0];
+ }
+ $separator[0] .= '%n';
+ my @sepLen = map { sb_length($_) } @separator;
+
+ @actString = ();
+ my $curLine;
+ my $curLen = 0;
+ if ($S{shared_sbar}) {
+ $curLen += $S{shared_sbar}[0] + 2 + length setc();
+ $width -= $S{shared_sbar}[2];
+ }
+ my $mouse_header_check = 0;
+ for my $it (@win_items) {
+ my $itemLen = sb_length($it);
+ if ($curLen) {
+ if ($curLen + $itemLen + $sepLen[$mouse_header_check % @sepLen] > $width) {
+ $width += $S{shared_sbar}[2]
+ if !@actString && $S{shared_sbar};
+ push @actString, $curLine;
+ $curLine = undef;
+ $curLen = 0;
+ }
+ elsif (defined $curLine) {
+ $curLine .= $separator[$mouse_header_check % @separator];
+ $curLen += $sepLen[$mouse_header_check % @sepLen];
+ }
+ }
+ $curLine .= $it;
+ if (exists $mouse_coords{refnum}{$mouse_header_check}) {
+ $mouse_coords{scalar @actString}{ $_ } = $mouse_coords{refnum}{$mouse_header_check}
+ for $curLen .. $curLen + $itemLen - 1;
+ }
+ $curLen += $itemLen;
+ }
+ continue {
+ ++$mouse_header_check;
+ }
+ $curLen -= $S{shared_sbar}[0]
+ if !@actString && $S{shared_sbar};
+ push @actString, $curLine if $curLen;
+}
+
+sub remake {
+ my %abbrevList;
+ my @wins = window_list();
+ if ($VIEWER_MODE or $S{sbar_maxlen} or $S{block} < 0) {
+ _calculate_abbrev(\@wins, \%abbrevList);
+ }
+
+ %mouse_coords = ( refnum => +{} );
+ _calculate_items(\@wins, \%abbrevList);
+
+ unless ($VIEWER_MODE) {
+ _spread_items();
+
+ push @actString, undef unless @actString || $S{all_disable};
+ }
+}
+
+sub update_wl {
+ return if $BLOCK_ALL;
+ remake();
+
+ Irssi::statusbar_items_redraw(set $_) for keys %statusbars;
+
+ unless ($VIEWER_MODE) {
+ Irssi::timeout_add_once(100, 'syncLines', undef);
+ }
+ else {
+ syncViewer();
+ }
+}
+
+sub screenFullRedraw {
+ my ($window) = @_;
+ if (!ref $window or $window->{refnum} == Irssi::active_win->{refnum}) {
+ $viewer{fullRedraw} = 1 if $viewer{client};
+ $settings_str = '';
+ &setup_changed;
+ }
+}
+
+sub restartViewerServer {
+ if ($VIEWER_MODE) {
+ stop_viewer();
+ start_viewer();
+ }
+}
+
+sub _simple_quote {
+ my @r = map {
+ my $x = $_;
+ $x =~ s/'/'"'"'/g;
+ $x = "'$x'";
+ } @_;
+ wantarray ? @r : shift @r
+}
+
+sub _viewer_command_replace_format {
+ my ($ecmd, @args) = @_;
+ my $file = _simple_quote(SCRIPT_FILE());
+ my $path = _simple_quote($viewer{path});
+ my @env;
+ for my $env (shellwords($S{viewer_launch_env})) {
+ if ($env =~ /^(\w+)(?:=(.*))$/) {
+ push @env, "AWL_$1=$2"
+ }
+ }
+ my $cmd = join ' ',
+ (@env ? ('env', _simple_quote(@env)) : ()),
+ 'perl', $file, '-1', _simple_quote(@args), $path;
+ $ecmd =~ s{%(%|\w+)}{
+ my $sub = $1;
+ if ($sub eq '%') {
+ '%'
+ }
+ elsif ($sub =~ /^(q*)A(.*)/) {
+ my $ret = $cmd;
+ for (1..length $1) {
+ $ret = _simple_quote($ret);
+ }
+ "$ret$2"
+ }
+ else {
+ "%$sub"
+ }
+ }gex;
+ $ecmd
+}
+
+sub start_viewer {
+ unlink $viewer{path} if -S $viewer{path} || -p _;
+
+ $viewer{server} = IO::Socket::UNIX->new(
+ Type => SOCK_STREAM,
+ Local => $viewer{path},
+ Listen => 1
+ );
+ unless ($viewer{server}) {
+ $viewer{msg} = "Viewer: $!";
+ $viewer{retry} = Irssi::timeout_add_once(5000, 'retry_viewer', 1);
+ return;
+ }
+ $viewer{server}->blocking(0);
+ set_viewer_mode_hint();
+ $viewer{server_tag} = Irssi::input_add($viewer{server}->fileno, INPUT_READ, 'vi_connected', undef);
+
+ if ($S{viewer_launch}) {
+ if ((defined $ENV{TMUX_PANE} && length $ENV{TMUX_PANE}) && (defined $ENV{TMUX} && length $ENV{TMUX}) && lc $S{viewer_tmux_position} ne 'custom') {
+ my $cmd = _viewer_command_replace_format('%qA', '-p', lc $S{viewer_tmux_position});
+ Irssi::command("exec - tmux neww -d $cmd 2>&1 &");
+ }
+ elsif ((defined $ENV{WINDOWID} && length $ENV{WINDOWID}) && (defined $ENV{DISPLAY} && length $ENV{DISPLAY}) && length $S{viewer_xwin_command} && $S{viewer_xwin_command} =~ /\S/) {
+ my $cmd = _viewer_command_replace_format($S{viewer_xwin_command});
+ Irssi::command("exec - $cmd 2>&1 &");
+ }
+ elsif (length $S{viewer_custom_command} && $S{viewer_custom_command} =~ /\S/) {
+ my $cmd = _viewer_command_replace_format($S{viewer_custom_command});
+ Irssi::command("exec - $cmd 2>&1 &");
+ }
+ }
+}
+
+sub set_viewer_mode_hint {
+ return unless $viewer{server};
+ if ($S{no_mode_hint}) {
+ $viewer{msg} = undef;
+ }
+ else {
+ my ($name) = __PACKAGE__ =~ /::([^:]+)$/;
+ $viewer{msg} = "Run $name from the shell or switch to sbar mode";
+ }
+}
+
+sub retry_viewer {
+ start_viewer();
+}
+
+sub vi_close_client {
+ Irssi::input_remove(delete $viewer{client_tag}) if exists $viewer{client_tag};
+ $viewer{client}->close if $viewer{client};
+ delete $viewer{client};
+ delete $viewer{client_keymap};
+ delete $viewer{client_settings};
+ delete $viewer{client_env};
+ delete $viewer{fullRedraw};
+}
+
+sub vi_connected {
+ vi_close_client();
+ $viewer{client} = $viewer{server}->accept or return;
+ $viewer{client}->blocking(0);
+ $viewer{client_tag} = Irssi::input_add($viewer{client}->fileno, INPUT_READ, 'vi_clientinput', undef);
+ syncViewer();
+}
+
+use constant VIEWER_BLOCK_SIZE => 1024;
+sub vi_clientinput {
+ if ($viewer{client}->read(my $buf, VIEWER_BLOCK_SIZE)) {
+ $viewer{rcvbuf} .= $buf;
+ if ($viewer{rcvbuf} =~ s/^(?:(active|\d+)|(last|up|down))\n//igm) {
+ if (defined $2) {
+ Irssi::command("window $2");
+ }
+ elsif (lc $1 eq 'active' && $viewer{use_ack}) {
+ Irssi::command("ack");
+ }
+ else {
+ Irssi::command("window goto $1");
+ }
+ }
+ }
+ else {
+ vi_close_client();
+ Irssi::timeout_add_once(100, 'syncViewer', undef);
+ }
+}
+
+sub stop_viewer {
+ Irssi::timeout_remove(delete $viewer{retry}) if exists $viewer{retry};
+ vi_close_client();
+ Irssi::input_remove(delete $viewer{server_tag}) if exists $viewer{server_tag};
+ return unless $viewer{server};
+ $viewer{server}->close;
+ delete $viewer{server};
+}
+sub _encode_var {
+ my $str;
+ while (@_) {
+ my ($name, $var) = splice @_, 0, 2;
+ my $type = ref $var ? $var =~ /HASH/ ? 'map' : $var =~ /ARRAY/ ? 'list' : '' : '';
+ $str .= "\n\U$name$type\_begin\n";
+ if ($type eq 'map') {
+ no warnings 'numeric';
+ $str .= " $_\n ${$var}{$_}\n" for sort { $a <=> $b || $a cmp $b } keys %$var;
+ }
+ elsif ($type eq 'list') {
+ $str .= " $_\n" for @$var;
+ }
+ else {
+ $str .= " $var\n";
+ }
+ $str .= "\U$name$type\_end\n";
+ }
+ $str
+}
+sub syncViewer {
+ if ($viewer{client}) {
+ @actString = ();
+ if ($currentLines) {
+ killOldStatus();
+ $currentLines = 0;
+ }
+ my $str;
+ unless ($viewer{client_keymap}) {
+ $str .= _encode_var('key', +{ %nummap, %specialmap });
+ $viewer{client_keymap} = 1;
+ }
+ unless ($viewer{client_settings}) {
+ $str .= _encode_var(
+ block => $S{block},
+ ha => $S{height_adjust},
+ mc => $S{maxcolumns},
+ ml => $S{maxlines},
+ );
+ $viewer{client_settings} = 1;
+ }
+ unless ($viewer{client_env}) {
+ $str .= _encode_var(irssienv => +{
+ (defined $ENV{TMUX_PANE} && length $ENV{TMUX_PANE}) && (defined $ENV{TMUX} && length $ENV{TMUX}) ?
+ (tmux_pane => $ENV{TMUX_PANE},
+ tmux_srv => $ENV{TMUX}) : (),
+ (defined $ENV{WINDOWID} && length $ENV{WINDOWID}) ?
+ (xwinid => $ENV{WINDOWID}) : (),
+ });
+ $viewer{client_env} = 1;
+ }
+ my $separator = Irssi::current_theme->get_format(__PACKAGE__, set 'separator');
+ my $sepLen = sb_length($separator);
+ my $item_bg = Irssi::current_theme->get_format(__PACKAGE__, set 'viewer_item_bg');
+ $str .= _encode_var(redraw => 1) if delete $viewer{fullRedraw};
+ $str .= _encode_var(separator => $separator,
+ seplen => $sepLen,
+ itembg => $item_bg,
+ mouse => $mouse_coords{refnum},
+ key2 => \%wnmap_exp,
+ win => \@win_items);
+
+ my $was = $viewer{client}->blocking(1);
+ $viewer{client}->print($str);
+ $viewer{client}->blocking($was);
+ }
+ elsif ($viewer{server}) {
+ if (defined $viewer{msg}) {
+ @actString = ((uc setc()).": $viewer{msg}");
+ }
+ else {
+ @actString = ();
+ }
+ }
+ elsif (defined $viewer{msg}) {
+ @actString = ((uc setc()).": $viewer{msg}");
+ }
+ if (@actString) {
+ Irssi::timeout_add_once(100, 'syncLines', undef);
+ }
+ elsif ($currentLines) {
+ killOldStatus();
+ $currentLines = 0;
+ }
+}
+
+sub reset_awl {
+ Irssi::timeout_remove($shade_line_timer) if $shade_line_timer; $shade_line_timer = undef;
+ my $was_sort = $S{sort} // '';
+ my $was_xform = $S{xform} // '';
+ my $was_shared = $S{shared_sbar};
+ my $was_no_hint = $S{no_mode_hint};
+ %S = (
+ sort => Irssi::settings_get_str( set 'sort'),
+ fancy_abbrev => Irssi::settings_get_str('fancy_abbrev'),
+ xform => Irssi::settings_get_str( set 'custom_xform'),
+ block => Irssi::settings_get_int( set 'block'),
+ banned_on => Irssi::settings_get_bool('banned_channels_on'),
+ prefer_name => Irssi::settings_get_bool(set 'prefer_name'),
+ hide_data => Irssi::settings_get_int( set 'hide_data'),
+ hide_name => Irssi::settings_get_int( set 'hide_name_data'),
+ hide_empty => Irssi::settings_get_int( set 'hide_empty'),
+ sbar_maxlen => Irssi::settings_get_bool(set 'sbar_maxlength'),
+ placement => Irssi::settings_get_str( set 'placement'),
+ position => Irssi::settings_get_int( set 'position'),
+ maxlines => Irssi::settings_get_int( set 'maxlines'),
+ maxcolumns => Irssi::settings_get_int( set 'maxcolumns'),
+ all_disable => Irssi::settings_get_bool(set 'all_disable'),
+ height_adjust => Irssi::settings_get_int( set 'height_adjust'),
+ mouse_offset => Irssi::settings_get_int( set 'mouse_offset'),
+ mouse_scroll => Irssi::settings_get_int( 'mouse_scroll'),
+ mouse_escape => Irssi::settings_get_int( 'mouse_escape'),
+ line_shade => Irssi::settings_get_time(set 'last_line_shade'),
+ no_mode_hint => Irssi::settings_get_bool(set 'no_mode_hint'),
+ viewer_launch => Irssi::settings_get_bool(set 'viewer_launch'),
+ viewer_launch_env => Irssi::settings_get_str(set 'viewer_launch_env'),
+ viewer_xwin_command => Irssi::settings_get_str(set 'viewer_xwin_command'),
+ viewer_custom_command => Irssi::settings_get_str(set 'viewer_custom_command'),
+ viewer_tmux_position => Irssi::settings_get_str(set 'viewer_tmux_position'),
+ );
+ $S{fancy_strict} = $S{fancy_abbrev} =~ /^strict/i;
+ $S{fancy_head} = $S{fancy_abbrev} =~ /^head/i;
+ my $shared = Irssi::settings_get_str(set 'shared_sbar');
+ if ($shared =~ /^(\d+)([<])(\d+)$/) {
+ $S{shared_sbar} = [$1, $2, $3];
+ }
+ else {
+ Irssi::settings_set_str(set 'shared_sbar', 'OFF');
+ $S{shared_sbar} = undef;
+ }
+ lock_keys(%S);
+ if ($was_sort ne $S{sort}) {
+ $print_text_activity = undef;
+ my @sort_order = grep { @$_ > 4 } map {
+ s/^\s*//;
+ my $reverse = s/^\W*\K[-!]//;
+ my $undef_check = s/^\W*\K~// ? 1 : undef;
+ my $equal_check = s/=(.*)\s?$// ? $1 : undef;
+ s/\s*$//;
+ my $ignore_case = s/#i$// ? 1 : undef;
+
+ $print_text_activity = 1 if $_ eq 'last_line';
+
+ my @path = split '/';
+ my $class_check = @path && $path[-1] =~ s/(::.*)$// ? $1 : undef;
+
+ [ $reverse ? -1 : 1, $undef_check, $equal_check, $class_check, $ignore_case, @path ]
+ } "$S{sort}," =~ /([^+,]*|[^+,]*=[^,]*?\s(?=\+)|[^+,]*=[^,]*)[+,]/g;
+ $window_sort_func = sub {
+ no warnings qw(numeric uninitialized);
+ for my $so (@sort_order) {
+ my @x = map {
+ my $ret = 0;
+ $_ = lc1459($_) if defined $_ && !ref $_ && $so->[4];
+ $ret = $_ eq ($so->[4] ? lc1459($so->[2]) : $so->[2]) ? 1 : -1 if defined $so->[2];
+ $ret = defined $_ ? ($ret || -3) : 3 if $so->[1];
+ $ret = ref $_ && $_->isa('Irssi'.$so->[3]) ? 2 : ($ret || -2) if $so->[3];
+ -$ret || $_
+ }
+ map {
+ reduce { return unless ref $a; $a->{$b} } $_, @{$so}[5..$#$so]
+ } $a, $b;
+ return ((($x[0] <=> $x[1] || $x[0] cmp $x[1]) * $so->[0]) || next);
+ }
+ return ($a->{refnum} <=> $b->{refnum});
+ };
+ }
+ if ($was_xform ne $S{xform}) {
+ if ($S{xform} !~ /\S/) {
+ $custom_xform = undef;
+ }
+ else {
+ my $script_pkg = __PACKAGE__ . '::custom_xform';
+ local $@;
+ $custom_xform = eval qq{
+package $script_pkg;
+use strict;
+no warnings;
+our (\$QUERY, \$CHANNEL, \$TAG, \$NAME);
+return sub {
+# line 1 @{[ set 'custom_xform' ]}\n$S{xform}\n}};
+ if ($@) {
+ $@ =~ /^(.*)/;
+ print '%_'.(set 'custom_xform').'%_ did not compile: '.$1;
+ }
+ }
+ }
+
+ my $new_settings = join "\n", $VIEWER_MODE
+ ? ("\\", $S{block}, $S{height_adjust}, $S{maxlines}, $S{maxcolumns})
+ : ("!", $S{placement}, $S{position});
+
+ if ($settings_str ne $new_settings) {
+ @actString = ();
+ %abbrev_cache = ();
+ $currentLines = 0;
+ killOldStatus();
+ delete $viewer{client_settings};
+ $settings_str = $new_settings;
+ }
+
+ my $was_mouse_mode = $MOUSE_ON;
+ if ($MOUSE_ON = Irssi::settings_get_bool(set 'mouse') and !$was_mouse_mode) {
+ install_mouse();
+ }
+ elsif ($was_mouse_mode and !$MOUSE_ON) {
+ uninstall_mouse();
+ }
+
+ my $path = Irssi::settings_get_str(set 'path');
+ my $was_viewer_mode = $VIEWER_MODE;
+ if ($was_viewer_mode &&
+ defined $viewer{path} && $viewer{path} ne $path) {
+ stop_viewer();
+ $was_viewer_mode = 0;
+ }
+ elsif ($was_viewer_mode && $S{no_mode_hint} != $was_no_hint + 0) {
+ set_viewer_mode_hint();
+ }
+ $viewer{path} = $path;
+ if ($VIEWER_MODE = Irssi::settings_get_bool(set 'viewer') and !$was_viewer_mode) {
+ start_viewer();
+ }
+ elsif ($was_viewer_mode and !$VIEWER_MODE) {
+ stop_viewer();
+ }
+
+ %banned_channels = map { lc1459(to_uni($_)) => undef }
+ split ' ', Irssi::settings_get_str('banned_channels');
+
+ my @sb_base = split /\177/, sb_format_expand("{sb \177}"), 2;
+ $sb_base_width_pre = sb_length($sb_base[0]);
+ $sb_base_width_post = sb_length($sb_base[1]);
+ $sb_base_width = $sb_base_width_pre + $sb_base_width_post;
+
+ if ($print_text_activity && $S{line_shade}) {
+ $shade_line_timer = Irssi::timeout_add(max(10 * GLOB_QUEUE_TIMER, 100*$S{line_shade}**(1/3)), 'wl_changed', undef);
+ }
+
+ $CHANGED{AWINS} = 1;
+}
+
+sub stop_mouse_tracking {
+ print STDERR "\e[?1005l\e[?1000l";
+}
+sub start_mouse_tracking {
+ print STDERR "\e[?1000h\e[?1005h";
+}
+sub install_mouse {
+ Irssi::command_bind('mouse_xterm' => 'mouse_xterm');
+ Irssi::command('^bind meta-[M command mouse_xterm');
+ Irssi::signal_add_first('gui key pressed' => 'mouse_key_hook');
+ start_mouse_tracking();
+}
+sub uninstall_mouse {
+ stop_mouse_tracking();
+ Irssi::signal_remove('gui key pressed' => 'mouse_key_hook');
+ Irssi::command('^bind -delete meta-[M');
+ Irssi::command_unbind('mouse_xterm' => 'mouse_xterm');
+}
+
+sub awl_mouse_event {
+ return if $VIEWER_MODE;
+ if ((($_[0] == 3 and $_[3] == 0)
+ || $_[0] == 64 || $_[0] == 65) and
+ $_[1] == $_[4] and $_[2] == $_[5]) {
+ my $top = lc $S{placement} eq 'top';
+ my ($pos, $line) = @_[1 .. 2];
+ unless ($top) {
+ $line -= $screenHeight;
+ $line += $currentLines;
+ $line += $S{mouse_offset};
+ }
+ else {
+ $line -= $S{mouse_offset};
+ }
+ $pos -= $sb_base_width_pre;
+ return if $line < 0 || $line >= $currentLines;
+ if ($_[0] == 64) {
+ Irssi::command('window up');
+ }
+ elsif ($_[0] == 65) {
+ Irssi::command('window down');
+ }
+ elsif (exists $mouse_coords{$line}{$pos}) {
+ my $win = $mouse_coords{$line}{$pos};
+ Irssi::command('window ' . $win);
+ }
+ Irssi::signal_stop;
+ }
+}
+
+sub mouse_scroll_event {
+ return unless $S{mouse_scroll};
+ if (($_[3] == 64 or $_[3] == 65) and
+ $_[0] == $_[3] and $_[1] == $_[4] and $_[2] == $_[5]) {
+ my $cmd = 'scrollback goto ' . ($_[3] == 64 ? '-' : '+') . $S{mouse_scroll};
+ Irssi::active_win->command($cmd);
+ Irssi::signal_stop;
+ }
+ elsif ($_[0] == 64 or $_[0] == 65) {
+ Irssi::signal_stop;
+ }
+}
+
+sub mouse_escape {
+ return unless $S{mouse_escape} > 0;
+ if ($_[0] == 3) {
+ my $tm = $S{mouse_escape};
+ $tm *= 1000 if $tm < 1000;
+ stop_mouse_tracking();
+ Irssi::timeout_add_once($tm, 'start_mouse_tracking', undef);
+ Irssi::signal_stop;
+ }
+}
+
+{ sub UNLOAD {
+ @actString = ();
+ killOldStatus();
+ stop_viewer() if $VIEWER_MODE;
+ uninstall_mouse() if $MOUSE_ON;
+ }
+}
+
+sub addPrintTextHook { # update on print text
+ return if $BLOCK_ALL;
+ return unless $print_text_activity;
+ return if $_[0]->{level} == 262144 and $_[0]->{target} eq ''
+ and !defined($_[0]->{server});
+ &wl_changed;
+}
+
+sub block_event_window_change {
+ Irssi::signal_stop;
+}
+
+sub update_awins {
+ {
+ my @wins = Irssi::windows;
+ local $BLOCK_ALL = 1;
+ Irssi::signal_add_first('window changed' => 'block_event_window_change');
+ my $bwin =
+ my $awin = Irssi::active_win;
+ my $lwin;
+ my $defer_irssi_broken_last;
+ unless ($wins[0]{refnum} == $awin->{refnum}) {
+ # special case: more than 1 last win, so /win last;
+ # /win last doesn't come back to the current window. eg. after
+ # connect & autojoin; we can't handle this situation, bail out
+ $defer_irssi_broken_last = 1;
+ }
+ else {
+ $awin->command('window last');
+ $lwin = Irssi::active_win;
+ $lwin->command('window last');
+ $defer_irssi_broken_last = $lwin->{refnum} == $bwin->{refnum};
+ }
+ my $awin_counter = 0;
+ Irssi::signal_remove('window changed' => 'block_event_window_change');
+ unless ($defer_irssi_broken_last) {
+ # we need to keep the fe-windows code running here
+ Irssi::signal_add_priority('window changed' => 'block_event_window_change', -99);
+ %awins = %wnmap_exp = ();
+ do {
+ Irssi::active_win->command('window up');
+ $awin = Irssi::active_win;
+ $awins{$awin->{refnum}} = undef;
+ ++$awin_counter;
+ } until ($awin->{refnum} == $bwin->{refnum} || $awin_counter >= @wins);
+ Irssi::signal_remove('window changed' => 'block_event_window_change');
+
+ Irssi::signal_add_first('window changed' => 'block_event_window_change');
+ for my $key (keys %wnmap) {
+ next unless Irssi::window_find_name($key) || Irssi::window_find_item($key);
+ $awin->command("window goto $key");
+ my $cwin = Irssi::active_win;
+ $wnmap_exp{ $cwin->{refnum} } = $wnmap{$key};
+ $cwin->command('window last')
+ if $cwin->{refnum} != $awin->{refnum};
+ }
+ for my $win (reverse @wins) { # restore original window order
+ Irssi::active_win->command('window '.$win->{refnum});
+ }
+ $awin->command('window '.$lwin->{refnum}); # restore last win
+ Irssi::active_win->command('window last');
+ Irssi::signal_remove('window changed' => 'block_event_window_change');
+ }
+ }
+ $CHANGED{WL} = 1;
+}
+
+sub resizeTerm {
+ if (defined (my $r = `stty size 2>/dev/null`)) {
+ ($screenHeight, $screenWidth) = split ' ', $r;
+ $CHANGED{SETUP} = 1;
+ }
+ else {
+ $CHANGED{SIZE} = 1;
+ }
+}
+
+sub awl_refresh {
+ $globTime = undef;
+ resizeTerm() if delete $CHANGED{SIZE};
+ reset_awl() if delete $CHANGED{SETUP};
+ update_awins() if delete $CHANGED{AWINS};
+ update_wl() if delete $CHANGED{WL};
+}
+
+sub termsize_changed { $CHANGED{SIZE} = 1; &queue_refresh; }
+sub setup_changed { $CHANGED{SETUP} = 1; &queue_refresh; }
+sub awins_changed { $CHANGED{AWINS} = 1; &queue_refresh; }
+sub wl_changed { $CHANGED{WL} = 1; &queue_refresh; }
+
+sub window_changed {
+ &awins_changed if $_[1];
+}
+
+sub queue_refresh {
+ return if $BLOCK_ALL;
+ Irssi::timeout_remove($globTime)
+ if defined $globTime; # delay the update further
+ $globTime = Irssi::timeout_add_once(GLOB_QUEUE_TIMER, 'awl_refresh', undef);
+}
+
+sub awl_init {
+ termsize_changed();
+ update_keymap();
+}
+
+sub runsub {
+ my $cmd = shift;
+ sub {
+ my ($data, $server, $item) = @_;
+ Irssi::command_runsub($cmd, $data, $server, $item);
+ };
+}
+
+Irssi::signal_register({
+ 'gui mouse' => [qw/int int int int int int/],
+ });
+{ my $broken_expandos = (Irssi::version >= 20081128 && Irssi::version < 20110210)
+ ? sub { my $x = shift; $x =~ s/\$\{cumode_space\}/ /; $x } : undef;
+ Irssi::theme_register([
+ map { $broken_expandos ? $broken_expandos->($_) : $_ }
+ set 'display_nokey' => '$N${cumode_space}$H$C$S',
+ set 'display_key' => '$Q${cumode_space}$H$C$S',
+ set 'display_nokey_visible' => '%2$N${cumode_space}$H$C$S',
+ set 'display_key_visible' => '%2$Q${cumode_space}$H$C$S',
+ set 'display_nokey_active' => '%1$N${cumode_space}$H$C$S',
+ set 'display_key_active' => '%1$Q${cumode_space}$H$C$S',
+ set 'display_header' => '%8$C|${N}',
+ set 'name_display' => '$0',
+ set 'separator' => ' ',
+ set 'separator2' => '',
+ set 'viewer_item_bg' => sb_format_expand('{sb_background}'),
+ ]);
+}
+Irssi::settings_add_bool(setc, set 'prefer_name', 0); #
+Irssi::settings_add_int( setc, set 'hide_empty', 0); #
+Irssi::settings_add_int( setc, set 'hide_data', 0); #
+Irssi::settings_add_int( setc, set 'hide_name_data', 0); #
+Irssi::settings_add_int( setc, set 'maxlines', 9); #
+Irssi::settings_add_int( setc, set 'maxcolumns', 4); #
+Irssi::settings_add_int( setc, set 'block', 15); #
+Irssi::settings_add_bool(setc, set 'sbar_maxlength', 1); #
+Irssi::settings_add_int( setc, set 'height_adjust', 2); #
+Irssi::settings_add_str( setc, set 'sort', 'refnum'); #
+Irssi::settings_add_str( setc, set 'placement', 'bottom'); #
+Irssi::settings_add_int( setc, set 'position', 0); #
+Irssi::settings_add_bool(setc, set 'all_disable', 1); #
+Irssi::settings_add_bool(setc, set 'viewer', 1); #
+Irssi::settings_add_str( setc, set 'shared_sbar', 'OFF'); #
+Irssi::settings_add_bool(setc, set 'mouse', 0); #
+Irssi::settings_add_str( setc, set 'path', Irssi::get_irssi_dir . '/_windowlist'); #
+Irssi::settings_add_str( setc, set 'custom_xform', ''); #
+Irssi::settings_add_time(setc, set 'last_line_shade', '0'); #
+Irssi::settings_add_int( setc, set 'mouse_offset', 1); #
+Irssi::settings_add_int( setc, 'mouse_scroll', 3); #
+Irssi::settings_add_int( setc, 'mouse_escape', 1); #
+Irssi::settings_add_str( setc, 'banned_channels', '');
+Irssi::settings_add_bool(setc, 'banned_channels_on', 1);
+Irssi::settings_add_str( setc, 'fancy_abbrev', 'fancy'); #
+Irssi::settings_add_bool(setc, set 'no_mode_hint', 0); #
+Irssi::settings_add_bool(setc, set 'viewer_launch', 1); #
+Irssi::settings_add_str( setc, set 'viewer_launch_env', ''); #
+Irssi::settings_add_str( setc, set 'viewer_tmux_position', 'left'); #
+Irssi::settings_add_str( setc, set 'viewer_xwin_command', 'xterm +sb -e %A'); #
+Irssi::settings_add_str( setc, set 'viewer_custom_command', ''); #
+
+Irssi::signal_add_last({
+ 'setup changed' => 'setup_changed',
+ 'print text' => 'addPrintTextHook',
+ 'terminal resized' => 'termsize_changed',
+ 'setup reread' => 'screenFullRedraw',
+ 'window hilight' => 'wl_changed',
+ 'command format' => 'wl_changed',
+});
+Irssi::signal_add({
+ 'window changed' => 'window_changed',
+ 'window item changed' => 'wl_changed',
+ 'window changed automatic' => 'window_changed',
+ 'window created' => 'awins_changed',
+ 'window destroyed' => 'awins_changed',
+ 'window name changed' => 'wl_changed',
+ 'window refnum changed' => 'wl_changed',
+});
+Irssi::signal_add_last('gui mouse' => 'mouse_escape');
+Irssi::signal_add_last('gui mouse' => 'mouse_scroll_event');
+Irssi::signal_add_last('gui mouse' => 'awl_mouse_event');
+Irssi::command_bind( setc() => runsub(setc()) );
+Irssi::command_bind( setc() . ' redraw' => 'screenFullRedraw' );
+Irssi::command_bind( setc() . ' restart' => 'restartViewerServer' );
+
+{
+ my $l = set 'shared';
+ {
+ no strict 'refs';
+ *{$l} = $awl_shared_empty;
+ }
+ Irssi::statusbar_item_register($l, '$0', $l);
+}
+
+awl_init();
+
+# Mouse script based on irssi mouse patch by mirage
+{ my $mouse_status = -1; # -1:off 0,1,2:filling mouse_combo
+ my @mouse_combo; # 0:button 1:x 2:y
+ my @mouse_previous; # previous contents of mouse_combo
+
+ sub mouse_xterm_off {
+ $mouse_status = -1;
+ }
+ sub mouse_xterm {
+ $mouse_status = 0;
+ Irssi::timeout_add_once(10, 'mouse_xterm_off', undef);
+ }
+
+ sub mouse_key_hook {
+ my ($key) = @_;
+ if ($mouse_status != -1) {
+ if ($mouse_status == 0) {
+ @mouse_previous = @mouse_combo;
+ #if @mouse_combo && $mouse_combo[0] < 64;
+ }
+ $mouse_combo[$mouse_status] = $key - 32;
+ $mouse_status++;
+ if ($mouse_status == 3) {
+ $mouse_status = -1;
+ # match screen coordinates
+ $mouse_combo[1]--;
+ $mouse_combo[2]--;
+ Irssi::signal_emit('gui mouse', @mouse_combo[0 .. 2], @mouse_previous[0 .. 2]);
+ }
+ Irssi::signal_stop;
+ }
+ }
+}
+
+sub string_LCSS {
+ my $str = join "\0", @_;
+ (sort { length $b <=> length $a } $str =~ /(?=(.+).*\0.*\1)/g)[0]
+}
+
+{ package Irssi::Nick }
+
+UNITCHECK
+{ package AwlViewer;
+ use strict;
+ use warnings;
+ no warnings 'redefine';
+ use Encode;
+ use IO::Socket::UNIX;
+ use IO::Select;
+ use List::Util qw(max);
+ use constant BLOCK_SIZE => 1024;
+ use constant RECONNECT_TIME => 5;
+
+ my $sockpath;
+
+ our $VERSION = '0.8';
+
+ our ($got_int, $resized, $timeout);
+
+ my %vars;
+ my (%c2w, @seqlist);
+ my %mouse_coords;
+ my (@mouse, @last_mouse);
+ my ($err, $sock, $loop);
+ my ($keybuf, $rcvbuf);
+ my @screen;
+ my ($screenHeight, $screenWidth);
+ my ($disp_update, $fs_open, $one_shot_integration, $one_shot_resize);
+ my $integration_position;
+ my $show_title_bar;
+
+ sub connect_it {
+ $sock = IO::Socket::UNIX->new(
+ Type => SOCK_STREAM,
+ Peer => $sockpath,
+ );
+ unless ($sock) {
+ $err = $!;
+ return;
+ }
+ $sock->blocking(0);
+ $loop->add($sock);
+ }
+
+ sub remove_conn {
+ my $fh = shift;
+ $loop->remove($fh);
+ $fh->close;
+ $sock = undef;
+ %vars = ();
+ @screen = ();
+ }
+
+ { package Terminfo; # xterm
+ sub civis { "\e[?25l" }
+ sub sc { "\e7" }
+ sub cup { "\e[" . ($_[0] + 1) . ';' . ($_[1] + 1) . 'H' }
+ sub el { "\e[K" }
+ sub rc { "\e8" }
+ sub cnorm { "\e[?25h" }
+ sub setab { "\e[4" . $_[0] . 'm' }
+ sub setaf { "\e[3" . $_[0] . 'm' }
+ sub setaf16 { "\e[9" . $_[0] . 'm' }
+ sub setab16 { "\e[10" . $_[0] . 'm' }
+ sub setaf256 { "\e[38;5;" . $_[0] . 'm' }
+ sub setab256 { "\e[48;5;" . $_[0] . 'm' }
+ sub sgr0 { "\e[0m" }
+ sub bold { "\e[1m" }
+ sub it { "\e[3m" }
+ sub ul { "\e[4m" }
+ sub blink { "\e[5m" }
+ sub rev { "\e[7m" }
+ sub op { "\e[39;49m" }
+ sub exit_bold { "\e[22m" }
+ sub exit_it { "\e[23m" }
+ sub exit_ul { "\e[24m" }
+ sub exit_blink { "\e[25m" }
+ sub exit_rev { "\e[27m" }
+ sub smcup { "\e[?1049h" }
+ sub rmcup { "\e[?1049l" }
+ sub smmouse { "\e[?1000h\e[?1005h" }
+ sub rmmouse { "\e[?1005l\e[?1000l" }
+ }
+
+ sub init {
+ $sockpath = shift // "$ENV{HOME}/.irssi/_windowlist";
+ STDOUT->autoflush(1);
+ printf "\r%swaiting for %s...", Terminfo::sc, $::IRSSI{name};
+
+ `stty -icanon -echo`;
+
+ $loop = IO::Select->new;
+ STDIN->blocking(0);
+ $loop->add(\*STDIN);
+
+ $SIG{INT} = sub {
+ $got_int = 1
+ };
+ $SIG{WINCH} = sub {
+ $resized = 1
+ };
+
+ $resized = 3;
+
+ $disp_update = 2;
+
+ $show_title_bar = 1;
+ }
+
+ sub enter_fs {
+ return if $fs_open;
+ safe_print(Terminfo::rc, Terminfo::smcup, Terminfo::civis, Terminfo::smmouse);
+ $fs_open = 1;
+ }
+
+ sub leave_fs {
+ return unless $fs_open;
+ safe_print(Terminfo::rmmouse, Terminfo::cnorm, Terminfo::rmcup);
+ safe_print(sprintf "\r%swaiting for %s...", Terminfo::sc, $::IRSSI{name}) if $_[0];
+
+ $fs_open = 0;
+ }
+
+ sub end_prog {
+ leave_fs();
+ STDIN->blocking(1);
+ `stty sane`;
+ printf "\r%s%sthanks for using %s\n", Terminfo::rc, Terminfo::el, $::IRSSI{name};
+ }
+
+ sub safe_print {
+ my $st = STDIN->blocking(1);
+ print @_;
+ STDIN->blocking($st);
+ }
+
+ sub safe_qx {
+ my $st = STDIN->blocking(1);
+ my $ret = `$_[0]`;
+ STDIN->blocking($st);
+ $ret
+ }
+
+ sub safe_print_sock {
+ return unless $sock;
+ my $was = $sock->blocking(1);
+ $sock->print(@_);
+ $sock->blocking($was);
+ }
+
+ sub process_recv {
+ my $need = 0;
+ while ($rcvbuf =~ s/\n(.+)_BEGIN\n((?: .*\n)*)\1_END\n//) {
+ my $var = lc $1;
+ my $data = $2;
+ my @data = split "\n ", "\n$data ", -1;
+ shift @data; pop @data;
+ my $itembg = $vars{itembg};
+ if ($var =~ s/list$//) {
+ $vars{$var} = \@data;
+ }
+ elsif ($var =~ s/map$//) {
+ $vars{$var} = +{ @data };
+ }
+ else {
+ $vars{$var} = join "\n", @data;
+ }
+ $need = 1 if $var eq 'win';
+ $need = 1 if $var eq 'redraw' && $vars{$var};
+ if (($itembg//'') ne ($vars{itembg}//'')) {
+ $need = $vars{redraw} = 1;
+ }
+ _build_keymap() if $var eq 'key2';
+ }
+ $need
+ }
+
+ { my %ansi_table;
+ my ($i, $j, $k) = (0, 0, 0);
+ my %term_state;
+ sub reset_term_state { my %old_term = %term_state; %term_state = (); %old_term }
+ sub set_term_state { my %old_term = %term_state; %term_state = @_; %old_term }
+ %ansi_table = (
+ # fe-common::core::formats.c:format_expand_styles
+ (map { my $t = $i++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setab16 : \&Terminfo::setab;
+ $n->($t) }) } (split //, '01234567' )),
+ (map { my $t = $j++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setaf16 : \&Terminfo::setaf;
+ $n->($t) }) } (split //, 'krgybmcw' )),
+ (map { my $t = $k++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setaf : \&Terminfo::setaf16;
+ $n->($t) }) } (split //, 'KRGYBMCW')),
+ # reset
+ n => sub { $term_state{hicolor} = 0; my $r = Terminfo::op;
+ for (qw(blink rev bold)) {
+ $r .= Terminfo->can("exit_$_")->() if delete $term_state{$_};
+ }
+ {
+ local $ansi_table{n} = $ansi_table{N};
+ $r .= formats_to_ansi_basic($vars{itembg});
+ }
+ $r
+ },
+ N => sub { reset_term_state(); Terminfo::sgr0 },
+ # flash/bright
+ F => sub { my $n = 'blink'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # reverse
+ 8 => sub { my $n = 'rev'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # bold
+ "_" => sub { my $n = 'bold'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # underline
+ U => sub { my $n = 'ul'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # italic
+ I => sub { my $n = 'it'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # bold, used as colour modifier if AWL_HI9 is set
+ 9 => $ENV{AWL_HI9} ? sub { $term_state{hicolor} ^= 1; '' }
+ : sub { my $n = 'bold'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
+ # delete other stuff
+ (map { $_ => sub { '' } } (split //, ':|>#[')),
+ # escape
+ (map { my $close = $_; $_ => sub { $close } } (split //, '{}%')),
+ );
+ for my $base (0 .. 15) {
+ my $close = $base;
+ my $idx = ($close&8) | ($close&4)>>2 | ($close&2) | ($close&1)<<2;
+ $ansi_table{ (sprintf "x0%x", $close) } =
+ $ansi_table{ (sprintf "x0%X", $close) } =
+ sub { Terminfo::setab256($idx) };
+ $ansi_table{ (sprintf "X0%x", $close) } =
+ $ansi_table{ (sprintf "X0%X", $close) } =
+ sub { Terminfo::setaf256($idx) };
+ }
+ for my $plane (1 .. 6) {
+ for my $coord (0 .. 35) {
+ my $close = 16 + ($plane-1) * 36 + $coord;
+ my $ch = $coord < 10 ? $coord : chr( $coord - 10 + ord 'a' );
+ $ansi_table{ "x$plane$ch" } =
+ $ansi_table{ "x$plane\U$ch" } =
+ sub { Terminfo::setab256($close) };
+ $ansi_table{ "X$plane$ch" } =
+ $ansi_table{ "X$plane\U$ch" } =
+ sub { Terminfo::setaf256($close) };
+ }
+ }
+ for my $gray (0 .. 23) {
+ my $close = 232 + $gray;
+ my $ch = chr( $gray + ord 'a' );
+ $ansi_table{ "x7$ch" } =
+ $ansi_table{ "x7\U$ch" } =
+ sub { Terminfo::setab256($close) };
+ $ansi_table{ "X7$ch" } =
+ $ansi_table{ "X7\U$ch" } =
+ sub { Terminfo::setaf256($close) };
+ }
+ sub formats_to_ansi_basic {
+ my $o = shift;
+ $o =~ s/(%(X..|x..|.))/exists $ansi_table{$2} ? $ansi_table{$2}->() : $1/gex;
+ $o
+ }
+ }
+
+ sub _header {
+ my $str = uc ::setc();
+ my $space = int( ((abs $vars{block}) - length $str) / (1 + length $str));
+ if ($space > 0) {
+ my $ss = ' ' x $space;
+ $str = join $ss, '', (split //, $str), '';
+ }
+ my $pad = (abs $vars{block}) - length $str;
+ $str = ' ' x ($pad/2) . $str . ' ' x ($pad/2 + $pad%2);
+ $str
+ }
+
+ sub _add_item {
+ my ($i, $j, $c, $wi, $screen, $mouse) = @_;
+ $screen->[$i][$j] = "%N%n$wi";
+ if (exists $vars{mouse}{$c - 1}) {
+ $mouse->[$i][$j] = $vars{mouse}{$c - 1};
+ }
+ }
+ sub update_screen {
+ $disp_update = 0;
+ unless ($sock && exists $vars{seplen} && exists $vars{block}) {
+ leave_fs(1);
+ return;
+ }
+ enter_fs();
+ @screen = () if delete $vars{redraw};
+ %mouse_coords = ();
+ my $ncols = ($vars{seplen} + abs $vars{block}) ?
+ int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
+ my $xenl = ($vars{seplen} + abs $vars{block})
+ && $ncols > int( ($screenWidth + $vars{seplen} - 1) / ($vars{seplen} + abs $vars{block}) );
+ my $nrows = $screenHeight - $vars{ha};
+ my @wi = @{$vars{win}//[]};
+ my $max_items = $ncols * $nrows;
+ my $c = $show_title_bar ? 1 : 0;
+ my $items = @wi + $c;
+ my $titems = $items > $max_items ? $max_items : $items;
+ my $i = 0;
+ my $j = 0;
+ my @new_screen;
+ my @new_mouse;
+ $new_screen[0][0] = _header() . ' ' x $vars{seplen}
+ if $show_title_bar;
+ unless ($nrows > $ncols) { # line layout
+ ++$j if $show_title_bar;
+ for my $wi (@wi) {
+ if ($j >= $ncols) {
+ $j = 0;
+ ++$i;
+ }
+ last if $i >= $nrows;
+ _add_item($i, $j, $show_title_bar ? $c : $c + 1,
+ $wi, \@new_screen, \@new_mouse);
+ if ($c + 1 < $titems && $j + 1 < $ncols) {
+ $new_screen[$i][$j] .= $vars{separator};
+ }
+ ++$j;
+ ++$c;
+ }
+ }
+ else { # column layout
+ ++$i if $show_title_bar;
+ for my $wi (@wi) {
+ if ($i >= $nrows) {
+ $i = 0;
+ ++$j;
+ }
+ last if $j >= $ncols;
+ _add_item($i, $j, $show_title_bar ? $c : $c + 1,
+ $wi, \@new_screen, \@new_mouse);
+ if ($c + $nrows < $titems) {
+ $new_screen[$i][$j] .= $vars{separator};
+ }
+ ++$i;
+ ++$c;
+ }
+ }
+ my $step = $vars{seplen} + abs $vars{block};
+ $i = 0;
+ my $str = Terminfo::sc . Terminfo::sgr0;
+ for (my $i = 0; $i < @new_screen; ++$i) {
+ for (my $j = 0; $j < @{$new_screen[$i]}; ++$j) {
+ if (defined $new_mouse[$i] && defined $new_mouse[$i][$j]) {
+ my $from = $j * $step;
+ $mouse_coords{$i}{$_} = $new_mouse[$i][$j]
+ for $from .. $from + abs $vars{block};
+ }
+ next if defined $screen[$i] && defined $screen[$i][$j]
+ && $screen[$i][$j] eq $new_screen[$i][$j];
+ $str .= Terminfo::cup($i, $j * $step)
+ . formats_to_ansi_basic($new_screen[$i][$j])
+ . Terminfo::sgr0;
+ $str .= Terminfo::el if $j == $#{$new_screen[$i]} && (!$xenl || $j + 1 != $ncols);
+ }
+ }
+ for (@new_screen .. $screenHeight - 1) {
+ if (!@screen || defined $screen[$_]) {
+ $str .= Terminfo::cup($_, 0) . Terminfo::sgr0 . Terminfo::el;
+ }
+ }
+ $str .= Terminfo::rc;
+ safe_print $str;
+ @screen = @new_screen;
+ }
+
+ sub handle_resize {
+ if (defined (my $r = safe_qx('stty size'))) {
+ ($screenHeight, $screenWidth) = split ' ', $r;
+ $resized = 0;
+ @screen = ();
+ $disp_update = 1;
+ if ($one_shot_integration == 2) {
+ $one_shot_resize--;
+ }
+ }
+ else {
+ }
+ }
+
+ sub _build_keymap {
+ %c2w = reverse( %{$vars{key}}, %{$vars{key2}} );
+ if (!grep { /^[+-]./ } keys %c2w) {
+ %c2w = (%c2w, map { ("-$_" => $c2w{$_}) } grep { !/^\^./ } keys %c2w);
+ }
+ %c2w = map {
+ my $key = $_;
+ s{^(-)?(\+)?(\^)?(.)}{
+ join '', (
+ ($1 ? "\e" : ''),
+ ($2 ? "\e\e" : ''),
+ ($3 ? "$4"^"@" : $4)
+ )
+ }e;
+ $_ => $c2w{$key}
+ } keys %c2w;
+ @seqlist = sort { length $b <=> length $a } keys %c2w;
+ }
+
+ sub _match_tmux {
+ (defined $ENV{TMUX} && length $ENV{TMUX}) && exists $vars{irssienv}{tmux_srv} && length $vars{irssienv}{tmux_pane}
+ && $ENV{TMUX} eq $vars{irssienv}{tmux_srv}
+ }
+
+ sub process_keys {
+ Encode::_utf8_on($keybuf);
+ my $win;
+ my $use_mouse;
+ my $maybe;
+ KEY: while (length $keybuf && !$maybe) {
+ $maybe = 0;
+ if ($keybuf =~ s/^\e\[M(.)(.)(.)//) {
+ @last_mouse = @mouse;# if @mouse && $mouse[0] < 64;
+ @mouse = map { -32 + ord } ($1, $2, $3);
+ $use_mouse = 1;
+ next KEY;
+ }
+ for my $s (@seqlist) {
+ if ($keybuf =~ s/^\Q$s//) {
+ $win = $c2w{$s};
+ $use_mouse = 0;
+ next KEY;
+ }
+ elsif (length $keybuf < length $s && $s =~ /^\Q$keybuf/) {
+ $maybe = 1;
+ }
+ }
+ unless ($maybe) {
+ substr $keybuf, 0, 1, '';
+ }
+ }
+ if ($use_mouse && @mouse && @last_mouse &&
+ $mouse[2] == $last_mouse[2] &&
+ $mouse[1] == $last_mouse[1] &&
+ ($mouse[0] == 3 || $mouse[0] == 64 || $mouse[0] == 65)) {
+ if ($mouse[0] == 64) {
+ $win = 'up';
+ }
+ elsif ($mouse[0] == 65) {
+ $win = 'down';
+ }
+ elsif (exists $mouse_coords{$mouse[2] - 1}{$mouse[1] - 1}) {
+ $win = $mouse_coords{$mouse[2] - 1}{$mouse[1] - 1};
+ }
+ elsif ($mouse[2] == 1 && $mouse[1] <= abs $vars{block}) {
+ $win = $last_mouse[0] != 0 ? 'last' : 'active';
+ }
+ else {
+ }
+ }
+ if (defined $win) {
+ $win =~ s/^_//;
+ safe_print_sock("$win\n");
+ if (!exists $ENV{AWL_AUTOFOCUS} || $ENV{AWL_AUTOFOCUS}) {
+ if (_match_tmux()) {
+ safe_qx("tmux selectp -t $vars{irssienv}{tmux_pane} 2>&1");
+ }
+ elsif (exists $vars{irssienv}{xwinid}) {
+ safe_qx("wmctrl -ia $vars{irssienv}{xwinid} 2>/dev/null");
+ }
+ }
+ }
+ Encode::_utf8_off($keybuf);
+ }
+
+ sub check_integration {
+ return unless $vars{irssienv};
+ return unless $sock && exists $vars{seplen} && exists $vars{block};
+ if ($one_shot_integration == 1) {
+ my $nrows = $screenHeight - $vars{ha};
+ my $ncols = ($vars{seplen} + abs $vars{block}) ? int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
+ my $items = ($show_title_bar ? 1 : 0) + @{$vars{win}//[]};
+ my $dcols_required = $nrows ? int($items/$nrows) + !!($items%$nrows) : 0;
+ my $rows_required = $ncols ? int($items/$ncols) + !!($items%$ncols) : 0;
+ $rows_required = abs $vars{ml}
+ if ($vars{ml} < 0 || ($vars{ml} > 0 && $rows_required > $vars{ml}));
+ $dcols_required = abs $vars{mc}
+ if ($vars{mc} < 0 || ($vars{mc} > 0 && $dcols_required > $vars{mc}));
+ my $rows = $rows_required + $vars{ha};
+ my $cols = ($dcols_required * ($vars{seplen} + abs $vars{block})) - $vars{seplen};
+ if (_match_tmux()) {
+ # int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) );
+ my ($pos_flag, $before);
+ if ($integration_position eq 'left') {
+ $pos_flag = 'h';
+ $before = 1;
+ }
+ elsif ($integration_position eq 'top') {
+ $pos_flag = 'v';
+ $before = 1;
+ }
+ elsif ($integration_position eq 'right') {
+ $pos_flag = 'h';
+ }
+ else {
+ $pos_flag = 'v';
+ }
+ my @cmd = "joinp -d$pos_flag -s $ENV{TMUX_PANE} -t $vars{irssienv}{tmux_pane}";
+ push @cmd, "swapp -d -t $ENV{TMUX_PANE} -s $vars{irssienv}{tmux_pane}"
+ if $before;
+ $cols = max($cols, 2);
+ $rows = max($rows, 2);
+
+ safe_qx("tmux " . (join " \\\; ", @cmd) . " 2>&1");
+ }
+ else {
+ $resized = 1;
+ #safe_qx("resize -s $screenHeight $cols 2>&1")
+ # if $cols > 0;
+ }
+ $one_shot_integration++;
+ if ($resized == 1) {
+ handle_resize();
+ resize_integration();
+ }
+ }
+ elsif ($one_shot_integration == 2) {
+ resize_integration(1);
+ }
+ }
+
+ sub resize_integration {
+ return unless $one_shot_integration;
+ return unless ($one_shot_resize//0) < 0 || shift;
+ my $nrows = $screenHeight - $vars{ha};
+ my $ncols = ($vars{seplen} + abs $vars{block}) ? int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
+ my $items = ($show_title_bar ? 1 : 0) + @{$vars{win}//[]};
+ my $dcols_required = $nrows ? (int($items/$nrows) + !!($items%$nrows)) : 0;
+ my $rows_required = $ncols ? int($items/$ncols) + !!($items%$ncols) : 0;
+ $rows_required = abs $vars{ml}
+ if ($vars{ml} < 0 || ($vars{ml} > 0 && $rows_required > $vars{ml}));
+ $dcols_required = abs $vars{mc}
+ if ($vars{mc} < 0 || ($vars{mc} > 0 && $dcols_required > $vars{mc}));
+ my $rows = $rows_required + $vars{ha};
+ my $cols = ($dcols_required * ($vars{seplen} + abs $vars{block})) - $vars{seplen};
+ if (_match_tmux()) {
+ my $pos_flag;
+ my $before = 0;
+ if ($integration_position eq 'left') {
+ $pos_flag = 'h';
+ $before = 1;
+ }
+ elsif ($integration_position eq 'top') {
+ $pos_flag = 'v';
+ $before = 1;
+ }
+ elsif ($integration_position eq 'right') {
+ $pos_flag = 'h';
+ }
+ else {
+ $pos_flag = 'v';
+ }
+ my @cmd;
+ # hard tmux limits
+ $cols = max($cols, 2);
+ $rows = max($rows, 2);
+ if ($pos_flag eq 'h' && $cols != $screenWidth) {
+ my $change = $screenWidth - $cols;
+ my $dir = ($before ^ ($change<0)) ? 'L' : 'R';
+ push @cmd, "resizep -$dir -t $ENV{TMUX_PANE} @{[abs $change]}";
+ #push @cmd, "resizep -x $cols -t $ENV{TMUX_PANE}";
+ $one_shot_resize = 1;
+ }
+ if ($pos_flag eq 'v' && $rows != $screenHeight) {
+ #push @cmd, "resizep -y $rows -t $ENV{TMUX_PANE}";
+ my $change = $screenHeight - $rows;
+ my $dir = ($before ^ ($change<0)) ? 'U' : 'D';
+ push @cmd, "resizep -$dir -t $ENV{TMUX_PANE} @{[abs $change]}";
+ $one_shot_resize = 1;
+ }
+
+ safe_qx("tmux " . (join " \\\; ", @cmd) . " 2>&1")
+ if @cmd;
+ }
+ else {
+ $cols = max($cols, 1);
+ $rows = max($rows, 1);
+ unless ($nrows > $ncols) { # line layout
+ if ($rows != $screenHeight) {
+ safe_qx("resize -s $rows $screenWidth 2>&1");
+ $one_shot_resize = 1;
+ }
+ }
+ else {
+ if ($cols != $screenWidth) {
+ safe_qx("resize -s $screenHeight $cols 2>&1");
+ $one_shot_resize = 1;
+ }
+ }
+ }
+ if ($resized == 1) {
+ handle_resize();
+ }
+ }
+
+ sub init_integration {
+ return unless $one_shot_integration;
+ if (_match_tmux()) {
+ }
+ else {
+ }
+ safe_print("\e]2;".(uc ::setc())."\e\\");
+ }
+
+ sub main {
+ require Getopt::Std;
+ my %opts;
+ Getopt::Std::getopts('1p:', \%opts);
+ my $one_shot = $opts{1};
+ $integration_position = $opts{p};
+ $one_shot_integration = 0+!!$one_shot;
+ #shift if @_ && $_[0] eq '--';
+ &init;
+ $show_title_bar = 0 if $ENV{AWL_NOTITLE};
+ init_integration();
+ until ($got_int) {
+ $timeout = undef;
+ if ($resized) {
+ if ($resized == 1) {
+ $timeout = 1;
+ $resized++;
+ }
+ else {
+ handle_resize();
+ resize_integration();
+ }
+ }
+ unless ($sock || $timeout) {
+ connect_it();
+ }
+ $timeout ||= RECONNECT_TIME unless $sock;
+ update_screen() if $disp_update;
+ SELECT: while (my @read = $loop->can_read($timeout)) {
+ for my $fh (@read) {
+ if ($fh == \*STDIN) {
+ if (read STDIN, my $buf, BLOCK_SIZE) {
+ do {
+ $keybuf .= $buf;
+ } while read STDIN, $buf, BLOCK_SIZE;
+ }
+ else {
+ $got_int = 1;
+ last SELECT;
+ }
+ }
+ else {
+ if ($fh->read(my $buf, BLOCK_SIZE)) {
+ do {
+ $rcvbuf .= $buf;
+ } while $fh->read($buf, BLOCK_SIZE);
+ }
+ else {
+ $disp_update = 1;
+ remove_conn($fh);
+ if ($one_shot) {
+ $got_int = 1;
+ last SELECT;
+ }
+ $timeout ||= RECONNECT_TIME;
+ }
+ }
+ }
+ $disp_update |= process_recv() if length $rcvbuf;
+ process_keys() if length $keybuf;
+ check_integration() if $one_shot;
+ update_screen() if $disp_update;
+ }
+ continue {
+ }
+ }
+ end_prog();
+ }
+}
+
+1;
+
+# Changelog
+# =========
+# 1.0a2
+# - new awl_viewer_launch setting and an array of related settings
+#
+# 0.9
+# - fix endless loop in awin detection code!
+# - correct colour swap in awl_viewer
+# - fix passing of alternate socket path to the viewer
+# - potential undefinedness in mouse refnum hinted at by Canopus
+# - fixed regression bug /exec -interactive
+# - add case-insensitive modifier to awl_sort
+# - run custom_xform on awl_prefer_name also
+# - avoid inconsistent active window state after awin detection
+# reported by ss
+# - revert %9-hack in the viewer prompted by discussion with pierrot
+# - fix new warning in perl 5.22
+#
+# 0.8
+# - replace fifo mode with external viewer script
+# - remove bundled cpan modules
+# - work around bogus irssi warning
+# - improve mouse support
+# - workaround for broken cumode in irssi 0.8.15
+# - fix handling of non-meta windows (uninitialized warning)
+# - add 256 colour support, strip true colour codes
+# - fix totally bogus $N padding reported by Ed S.
+# - make /window goto #name mappings work but ignore non-existant ones
+# - improve incomplete reads reported by bcode
+# - fix single % in awl_viewer reported by bcode
+# - add support for key bindings by nike and ferret
+# - coerce utf8 key binds
+# - add settings: custom_xform, last_line_shade, hide_name_data
+# - abbreviations were broken in some cases
+# - fix some misuse of / as cmdchar in mouse script reported by bcode
+# - add shared status bar mode
+# - ${type} variables for custom_xform setting
+# - crash if custom_xform had runtime error
+# - update sorting documentation
+# - fix odd case in size calculation noted by lasers
+# - add missing font styles to the viewer reported by ishanyx
+# - add italic
+#
+# 0.7g
+# - remove screen support and replace it with fifo support
+# - add double-width support to the shortener
+# - correct documentation regarding $T vs. display_header
+# - add missing refresh for window item changed (thanks vague)
+# - add visible windows
+# - add exemptions for active window
+# - workaround for hiding the window changes from trackbar
+# - hack to force 16colours in screen mode
+# - remember last window (reported by earthnative)
+# - wrong window focus on new queries (reported by emsid)
+# - dataloss bug on trying to remember last window
+#
+# 0.6d+
+# - add support for network headers
+# - fixed regression bug /exec -interactive
+#
+# 0.6ca+
+# - add screen support (from nicklist.pl)
+# - names can now have a max length and window names can be used
+# - fixed a bug with block display in screen mode and status bar mode
+# - added space handling to ir_fe and removed it again
+# - now handling formats on my own
+# - started to work on $tag display
+# - added warning about missing sb_act_none abstract leading to
+# - display*active settings
+# - added warning about the bug in awl_display_(no)key_active settings
+# - mouse hack
+#
+# 0.5d
+# - add setting to also hide the last status bar if empty (awl_all_disable)
+# - reverted to old utf8 code to also calculate broken utf8 length correctly
+# - simplified dealing with status bars in wlreset
+# - added a little tweak for the renamed term_type somewhere after Irssi 0.8.9
+# - fixed bug in handling channel #$$
+# - reset background colour at the beginning of an entry
+#
+# 0.4d
+# - fixed order of disabling status bars
+# - several attempts at special chars, without any real success
+# and much more weird new bugs caused by this
+# - setting to specify sort order
+# - reduced timeout values
+# - added awl_hide_data
+# - make it so the dynamic sub is actually deleted
+# - fix a bug with removing of the last separator
+# - take into consideration parse_special
+#
+# 0.3b
+# - automatically kill old status bars
+# - reset on /reload
+# - position/placement settings
+#
+# 0.2
+# - automated retrieval of key bindings (thanks grep.pl authors)
+# - improved removing of status bars
+# - got rid of status chop
+#
+# 0.1
+# - Based on chanact.pl which was apparently based on lightbar.c and
+# nicklist.pl with various other ideas from random scripts.
diff --git a/scripts/clearable.pl b/scripts/clearable.pl
new file mode 100644
index 0000000..79fef1a
--- /dev/null
+++ b/scripts/clearable.pl
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.1'; # 5ef9502616f1301
+our %IRSSI = (
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'clearable',
+ description => 'make some command output clearable',
+ license => 'ISC',
+ );
+
+use Irssi 20140701;
+
+sub cmd_help {
+ return unless $_[0] =~ /^clearable\s*$/i;
+ print CLIENTCRAP <<HELP
+%9Syntax:%9
+
+CLEARABLE <command>
+
+%9Description:%9
+
+ Runs command and tags each line of immediate output with the
+ lastlog-flag so it can be cleared with /LASTLOG -clear
+
+%9Example:%9
+
+ /CLEARABLE NAMES
+ /LASTLOG -clear
+
+%9See also:%9 LASTLOG, SCROLLBACK CLEAR
+HELP
+}
+
+my %refreshers;
+
+sub sig_prt {
+ my $win = $_[0]{window};
+ my $view = $win && $win->view;
+ return unless $view;
+ my $llp = $view->{buffer}{cur_line}{_irssi}//0;
+ &Irssi::signal_continue;
+ $view = $win->view;
+ my $l2 = $view->{buffer}{cur_line};
+ return unless ($l2 && $l2->{_irssi} != $llp);
+ for (my $line = $l2; $line && $line->{_irssi} != $llp; ) {
+ $win->gui_printtext_after($line->prev, $line->{info}{level} | MSGLEVEL_NEVER | MSGLEVEL_LASTLOG, $line->get_text(1)."\n", $line->{info}{time});
+ my $ll = $win->last_line_insert;
+ $view->remove_line($line);
+ $line = $ll && $ll->prev;
+ $refreshers{ $win->{refnum} } //= $view->{bottom};
+ }
+}
+
+sub cmd_clearable {
+ my ($data, $server, $item) = @_;
+ Irssi::signal_add_first('print text' => 'sig_prt');
+ Irssi::signal_emit('send command' => Irssi::parse_special('$k').$data, $server, $item);
+ Irssi::signal_remove('print text' => 'sig_prt');
+ for my $refnum (keys %refreshers) {
+ my $bottom = delete $refreshers{$refnum};
+ my $win = Irssi::window_find_refnum($refnum) // next;
+ my $view = $win->view;
+ $win->command('^scrollback end') if $bottom && !$view->{bottom};
+ $view->redraw;
+ }
+}
+
+Irssi::command_bind('clearable' => 'cmd_clearable');
+Irssi::command_bind_last('help' => 'cmd_help');
diff --git a/scripts/colorize_nicks.pl b/scripts/colorize_nicks.pl
new file mode 100644
index 0000000..4f8d26b
--- /dev/null
+++ b/scripts/colorize_nicks.pl
@@ -0,0 +1,136 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.3.6'; # e54c56e8922561d
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'colorize_nicks',
+ description => 'Colourise mention of nicks in the message body.',
+ license => 'GNU GPLv2 or later',
+ );
+
+# inspired by mrwright's nickcolor.pl and xt's colorize_nicks.pl
+#
+# you need nickcolor_expando or another nickcolor script providing the
+# get_nick_color2 function
+
+# Usage
+# =====
+# should start working once loaded
+
+# Options
+# =======
+# /set colorize_nicks_skip_formats <num>
+# * how many forms (blocks of irssi format codes or non-letters) to
+# skip at the beginning of line before starting to colourise nicks
+# (you usually want to skip the speaker's nick itself and the
+# timestamp)
+#
+# /set colorize_nicks_ignore_list <words to ignore>
+# * list of nicks (words) that should never be coloured
+#
+# /set colorize_nicks_repeat_formats <ON|OFF>
+# * repeat the format stack from the beginning of line, enable when
+# using per-line colours and colorize_nicks breaks it
+
+# Commands
+# ========
+# you can use this alias:
+#
+# /alias nocolorize set colorize_nicks_ignore_list $colorize_nicks_ignore_list
+#
+# /nocolorize <nick>
+# * quickly add nick to the bad word list of nicks that should not be
+# colourised
+
+no warnings 'redefine';
+use Irssi;
+
+my $irssi_mumbo = qr/\cD[`-i]|\cD[&-@\xff]./;
+
+my $nickchar = qr/[][[:alnum:]\\|`^{}_-]/;
+my $nick_pat = qr/($nickchar+)/;
+
+my @ignore_list;
+
+my $colourer_script;
+
+sub prt_text_issue {
+ my ( $dest,
+ $text,
+ $stripped
+ ) = @_;
+ my $colourer;
+ unless ($colourer_script
+ && ($colourer = "Irssi::Script::$colourer_script"->can('get_nick_color2'))) {
+ for my $script (sort map { s/::$//r } grep { /^nickcolor|nm/ } keys %Irssi::Script::) {
+ if ($colourer = "Irssi::Script::$script"->can('get_nick_color2')) {
+ $colourer_script = $script;
+ last;
+ }
+ }
+ }
+ return unless $colourer;
+ return unless $dest->{level} & MSGLEVEL_PUBLIC;
+ return unless defined $dest->{target};
+ my $chanref = ref $dest->{server} && $dest->{server}->channel_find($dest->{target});
+ return unless $chanref;
+ my %nicks = map { $_->[0] => $colourer->($dest->{server}{tag}, $chanref->{name}, $_->[1], 1) }
+ grep { defined }
+ map { if (my $nr = $chanref->nick_find($_)) {
+ [ $_ => $nr->{nick} ]
+ } }
+ keys %{ +{ map { $_ => undef } $stripped =~ /$nick_pat/g } };
+ delete @nicks{ @ignore_list };
+ my @forms = split /((?:$irssi_mumbo|\s|[.,*@%+&!#$()=~'";:?\/><]+(?=$irssi_mumbo|\s))+)/, $text, -1;
+ my $ret = '';
+ my $fmtstack = '';
+ my $nick_re = join '|', map { quotemeta } sort { length $b <=> length $a } grep { length $nicks{$_} } keys %nicks;
+ my $skip = Irssi::settings_get_int('colorize_nicks_skip_formats');
+ return if $skip < 0;
+ while (@forms) {
+ my ($t, $form) = splice @forms, 0, 2;
+ if ($skip > 0) {
+ --$skip;
+ $ret .= $t;
+ $ret .= $form if defined $form;
+ if (Irssi::settings_get_bool('colorize_nicks_repeat_formats')) {
+ $fmtstack .= join '', $form =~ /$irssi_mumbo/g if defined $form;
+ $fmtstack =~ s/\cDe//g;
+ }
+ }
+ elsif (length $nick_re
+ && $t =~ s/((?:^|\s)\W{0,3}?)(?<!$nickchar|')($nick_re)(?!$nickchar)/$1$nicks{$2}$2\cDg$fmtstack/g) {
+ $ret .= "$t\cDg$fmtstack";
+ $ret .= $form if defined $form;
+ $fmtstack .= join '', $form =~ /$irssi_mumbo/g if defined $form;
+ $fmtstack =~ s/\cDe//g;
+ }
+ else {
+ $ret .= $t;
+ $ret .= $form if defined $form;
+ }
+ }
+ Irssi::signal_continue($dest, $ret, $stripped);
+}
+
+sub setup_changed {
+ @ignore_list = split /\s+|,/, Irssi::settings_get_str('colorize_nicks_ignore_list');
+}
+
+sub init {
+ setup_changed();
+}
+
+Irssi::signal_add({
+ 'print text' => 'prt_text_issue',
+});
+Irssi::signal_add_last('setup changed' => 'setup_changed');
+
+Irssi::settings_add_int('colorize_nicks', 'colorize_nicks_skip_formats' => 2);
+Irssi::settings_add_str('colorize_nicks', 'colorize_nicks_ignore_list' => '');
+Irssi::settings_add_bool('colorize_nicks', 'colorize_nicks_repeat_formats' => 0);
+
+init();
diff --git a/scripts/complete_at.pl b/scripts/complete_at.pl
new file mode 100644
index 0000000..597e81e
--- /dev/null
+++ b/scripts/complete_at.pl
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.2'; # 49f841075725906
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'complete_at',
+ description => 'Complete nicks after @ (twitter-style)',
+ license => 'ISC',
+ );
+
+# Usage
+# =====
+# write @ and type on the Tab key to complete nicks
+
+{ package Irssi::Nick }
+
+my $complete_char = '@';
+
+sub complete_at {
+ my ($cl, $win, $word, $start, $ws) = @_;
+ if ($cl && !@$cl
+ && $win && $win->{active}
+ && $win->{active}->isa('Irssi::Channel')) {
+ if ((my $pos = rindex $word, $complete_char) > -1) {
+ my ($pre, $post) = ((substr $word, 0, $pos), (substr $word, $pos + 1));
+ my $pre2 = length $start ? "$start $pre" : $pre;
+ my $pre3 = length $pre2 ? "$pre2$complete_char" : "";
+ Irssi::signal_emit('complete word', $cl, $win, $post, $pre3, $ws);
+ unless (@$cl) {
+ push @$cl, grep { /^\Q$post/i } map { $_->{nick} } $win->{active}->nicks();
+ }
+ map { $_ = "$pre$complete_char$_" } @$cl;
+ }
+ }
+}
+
+Irssi::signal_add_last('complete word' => 'complete_at');
diff --git a/scripts/dim_nicks.pl b/scripts/dim_nicks.pl
new file mode 100644
index 0000000..5c19633
--- /dev/null
+++ b/scripts/dim_nicks.pl
@@ -0,0 +1,392 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.4.6'; # 373036720cc131b
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'dim_nicks',
+ description => 'Dims nicks that are not in channel anymore.',
+ license => 'GNU GPLv2 or later',
+ );
+
+# Usage
+# =====
+# Once loaded, this script will record the nicks of each new
+# message. If the user leaves the room, the messages will be rewritten
+# with the nick in another colour/style.
+#
+# Depending on your theme, tweaking the forms settings may be
+# necessary. With the default irssi theme, this script should just
+# work.
+
+# Options
+# =======
+# /set dim_nicks_color <colour>
+# * the colour code to use for dimming the nick, or a string of format
+# codes with the special token $* in place of the nick (e.g. %I$*%I
+# for italic)
+#
+# /set dim_nicks_history_lines <num>
+# * only this many lines of messages are remembered/rewritten (per
+# window)
+#
+# /set dim_nicks_forms_skip <num>
+# /set dim_nicks_forms_search_max <num>
+# * these two settings limit the range where to search for the
+# nick.
+# It sets how many forms (blocks of irssi format codes or
+# non-letters) to skip at the beginning of line before starting to
+# search for the nick, and from then on how many forms to search
+# before stopping.
+# You should set this to the appropriate values to avoid (a) dimming
+# your timestamp (b) dimming message content instead of the nick.
+# To check your settings, you can use the command
+# /script exec Irssi::Script::dim_nicks::debug_forms
+
+
+no warnings 'redefine';
+use constant IN_IRSSI => __PACKAGE__ ne 'main' || $ENV{IRSSI_MOCK};
+use Irssi 20140701;
+use Irssi::TextUI;
+use Encode;
+
+
+sub setc () {
+ $IRSSI{name}
+}
+
+sub set ($) {
+ setc . '_' . $_[0]
+}
+
+my $history_lines = 100;
+my $skip_forms = 1;
+my $search_forms_max = 5;
+my $color_letter = 'K';
+
+my (%nick_reg, %chan_reg, %history, %history_st, %lost_nicks, %lost_nicks_backup);
+
+my ($dest, $chanref, $nickref);
+
+sub clear_ref {
+ $dest = undef;
+ $chanref = undef;
+ $nickref = undef;
+}
+
+sub msg_line_tag {
+ my ($srv, $msg, $nick, $addr, $targ) = @_;
+ $chanref = $srv->channel_find($targ);
+ $nickref = ref $chanref ? $chanref->nick_find($nick) : undef;
+}
+
+sub msg_line_clear {
+ clear_ref();
+}
+
+my @color_code;
+
+sub color_to_code {
+ my $win = Irssi::active_win;
+ my $view = $win->view;
+ if (-1 == index $color_letter, '$*') {
+ $color_letter = "%$color_letter\$*";
+ }
+ $win->print_after(undef, MSGLEVEL_NEVER, "$color_letter ");
+ my $lp = $win->last_line_insert;
+ my $color_code = $lp->get_text(1);
+ $color_code =~ s/ $//;
+ $view->remove_line($lp);
+ @color_code = split /\$\*/, $color_code, 2;
+}
+
+sub setup_changed {
+ $history_lines = Irssi::settings_get_int( set 'history_lines' );
+ $skip_forms = Irssi::settings_get_int( set 'forms_skip' );
+ $search_forms_max = Irssi::settings_get_int( set 'forms_search_max' );
+ my $new_color = Irssi::settings_get_str( set 'color' );
+ if ($new_color ne $color_letter) {
+ $color_letter = $new_color;
+ color_to_code();
+ }
+}
+
+sub init_dim_nicks {
+ setup_changed();
+}
+
+sub prt_text_issue {
+ ($dest) = @_;
+ clear_ref() unless defined $dest->{target};
+ clear_ref() unless $dest->{level} & MSGLEVEL_PUBLIC;
+}
+
+sub expire_hist {
+ for my $ch (keys %history_st) {
+ if (@{$history_st{$ch}} > 2 * $history_lines) {
+ my @del = splice @{$history_st{$ch}}, 0, $history_lines;
+ delete @history{ @del };
+ }
+ }
+}
+
+sub prt_text_ref {
+ return unless $nickref;
+ my ($win) = @_;
+ my $view = $win->view;
+ my $line_id = $view->{buffer}{_irssi} .','. $view->{buffer}{cur_line}{_irssi};
+ $chan_reg{ $chanref->{_irssi} } = $chanref;
+ $nick_reg{ $nickref->{_irssi} } = $nickref;
+ if (exists $history{ $line_id }) {
+ }
+ $history{ $line_id } = [ $win->{_irssi}, $chanref->{_irssi}, $nickref->{_irssi}, $nickref->{nick} ];
+ push @{$history_st{ $chanref->{_irssi} }}, $line_id;
+ expire_hist();
+ my @lost_forever = grep { $view->{buffer}{first_line}{info}{time} > $lost_nicks{ $chanref->{_irssi} }{ $_ } }
+ keys %{$lost_nicks{ $chanref->{_irssi} }};
+ delete @{$lost_nicks{ $chanref->{_irssi} }}{ @lost_forever };
+ delete @{$lost_nicks_backup{ $chanref->{_irssi} }}{ @lost_forever };
+ clear_ref();
+}
+
+sub win_del {
+ my ($win) = @_;
+ for my $ch (keys %history_st) {
+ @{$history_st{$ch}} = grep { exists $history{ $_ } &&
+ $history{ $_ }[0] != $win->{_irssi} } @{$history_st{$ch}};
+ }
+ my @del = grep { $history{ $_ }[0] == $win->{_irssi} } keys %history;
+ delete @history{ @del };
+}
+
+sub _alter_lines {
+ my ($chan, $check_lr, $ad) = @_;
+ my $win = $chan->window;
+ return unless ref $win;
+ my $view = $win->view;
+ my $count = $history_lines;
+ my $buffer_id = $view->{buffer}{_irssi} .',';
+ my $lp = $view->{buffer}{cur_line};
+ my %check_lr = map { $_ => undef } @$check_lr;
+ my $redraw;
+ my $bottom = $view->{bottom};
+ while ($lp && $count) {
+ my $line_id = $buffer_id . $lp->{_irssi};
+ if (exists $check_lr{ $line_id }) {
+ $lp = _alter_line($buffer_id, $line_id, $win, $view, $lp, $chan->{_irssi}, $ad);
+ unless ($lp) {
+ last;
+ }
+ $redraw = 1;
+ }
+ } continue {
+ --$count;
+ $lp = $lp->prev;
+ }
+ if ($redraw) {
+ $win->command('^scrollback end') if $bottom && !$win->view->{bottom};
+ $view->redraw;
+ }
+}
+
+my $irssi_mumbo = qr/\cD[`-i]|\cD[&-@\xff]./;
+my $irssi_mumbo_no_partial = qr/(?<!\cD)(?<!\cD[&-@\xff])/;
+my $irssi_skip_form_re = qr/((?:$irssi_mumbo|[.,*@%+&!#$()=~'";:?\/><]+(?=$irssi_mumbo|\s))+|\s+)/;
+
+sub debug_forms {
+ my $win = Irssi::active_win;
+ my $view = $win->view;
+ my $lp = $view->{buffer}{cur_line};
+ my $count = $history_lines;
+ my $buffer_id = $view->{buffer}{_irssi} .',';
+ while ($lp && $count) {
+ my $line_id = $buffer_id . $lp->{_irssi};
+ # $history{ $line_id } = [ $win->{_irssi}, $chanref->{_irssi}, $nickref->{_irssi}, $nickref->{nick} ];
+ if (exists $history{ $line_id }) {
+ my $line_nick = $history{ $line_id }[3];
+ my $text = $lp->get_text(1);
+ pos $text = 0;
+ my $from = 0;
+ for (my $i = 0; $i < $skip_forms; ++$i) {
+ last unless
+ scalar $text =~ /$irssi_skip_form_re/g;
+ $from = pos $text;
+ }
+ my $to = $from;
+ for (my $i = 0; $i < $search_forms_max; ++$i) {
+ last unless
+ scalar $text =~ /$irssi_skip_form_re/g;
+ $to = pos $text;
+ }
+ my $pre = substr $text, 0, $from;
+ my $search = substr $text, $from, $to-$from;
+ my $post = substr $text, $to;
+ unless ($to > $from) {
+ } else {
+ my @nick_reg;
+ unshift @nick_reg, quotemeta substr $line_nick, 0, $_ for 1 .. length $line_nick;
+ no warnings 'uninitialized';
+ for my $nick_reg (@nick_reg) {
+ last if $search
+ =~ s/(\Q$color_code[0]\E\s*)?((?:$irssi_mumbo)+)?$irssi_mumbo_no_partial($nick_reg)((?:$irssi_mumbo)+)?(\s*\Q$color_code[0]\E)?/<match>$1$2<nick>$3<\/nick>$4$5<\/match>/;
+ last if $search
+ =~ s/(?:\Q$color_code[0]\E)?(?:(?:$irssi_mumbo)+?)?$irssi_mumbo_no_partial($nick_reg)(?:(?:$irssi_mumbo)+?)?(?:\Q$color_code[1]\E)?/<nick>$1<\/nick>/;
+ }
+ }
+ my $msg = "$pre<search>$search</search>$post";
+ #$msg =~ s/([^[:print:]])/sprintf '\\x%02x', ord $1/ge;
+ $msg =~ s/\cDe/%|/g; $msg =~ s/%/%%/g;
+ $win->print(setc." form debug: [$msg]", MSGLEVEL_CLIENTCRAP);
+ return;
+ }
+ } continue {
+ --$count;
+ $lp = $lp->prev;
+ }
+ $win->print(setc." form debug: no usable line found", MSGLEVEL_CLIENTCRAP);
+}
+
+sub _alter_line {
+ my ($buffer_id, $lrp, $win, $view, $lp, $cid, $ad) = @_;
+ my $line_nick = $history{ $lrp }[3];
+ my $text = $lp->get_text(1);
+ pos $text = 0;
+ my $from = 0;
+ for (my $i = 0; $i < $skip_forms; ++$i) {
+ last unless
+ scalar $text =~ /$irssi_skip_form_re/g;
+ $from = pos $text;
+ }
+ my $to = $from;
+ for (my $i = 0; $i < $search_forms_max; ++$i) {
+ last unless
+ scalar $text =~ /$irssi_skip_form_re/g;
+ $to = pos $text;
+ }
+ return $lp unless $to > $from;
+ my @nick_reg;
+ unshift @nick_reg, quotemeta substr $line_nick, 0, $_ for 1 .. length $line_nick;
+ { no warnings 'uninitialized';
+ if ($ad) {
+ if (exists $lost_nicks_backup{ $cid }{ $line_nick }) {
+ my ($fs, $fc, $bc, $bs) = @{$lost_nicks_backup{ $cid }{ $line_nick }};
+ my $sen = length $bs ? $color_code[0] : '';
+ for my $nick_reg (@nick_reg) {
+ last if
+ (substr $text, $from, $to-$from)
+ =~ s/(?:\Q$color_code[0]\E)?(?:(?:$irssi_mumbo)+?)?$irssi_mumbo_no_partial($nick_reg)(?:(?:$irssi_mumbo)+?)?(?:\Q$color_code[1]\E)?/$fc$1$bc$sen/;
+ }
+ }
+ }
+ else {
+ for my $nick_reg (@nick_reg) {
+ if (
+ (substr $text, $from, $to-$from)
+ =~ s/(\Q$color_code[0]\E\s*)?((?:$irssi_mumbo)+)?$irssi_mumbo_no_partial($nick_reg)((?:$irssi_mumbo)+)?(\s*\Q$color_code[0]\E)?/$1$2$color_code[0]$3$color_code[1]$4$5/) {
+ $lost_nicks_backup{ $cid }{ $line_nick } = [ $1, $2, $4, $5 ];
+ last;
+ }
+ }
+ } }
+ $win->gui_printtext_after($lp->prev, $lp->{info}{level} | MSGLEVEL_NEVER, "$text\n", $lp->{info}{time});
+ my $ll = $win->last_line_insert;
+ my $line_id = $buffer_id . $ll->{_irssi};
+ if (exists $history{ $line_id }) {
+ }
+ grep { $_ eq $lrp and $_ = $line_id } @{$history_st{ $cid }};
+ $history{ $line_id } = delete $history{ $lrp };
+ $view->remove_line($lp);
+ $ll;
+}
+
+sub nick_add {
+ my ($chan, $nick) = @_;
+ if (delete $lost_nicks{ $chan->{_irssi} }{ $nick->{nick} }) {
+ my @check_lr = grep { $history{ $_ }[1] == $chan->{_irssi} &&
+ $history{ $_ }[2] eq $nick->{nick} } keys %history;
+ if (@check_lr) {
+ $nick_reg{ $nick->{_irssi} } = $nick;
+ for my $li (@check_lr) {
+ $history{ $li }[2] = $nick->{_irssi};
+ }
+ _alter_lines($chan, \@check_lr, 1);
+ }
+ }
+ delete $lost_nicks_backup{ $chan->{_irssi} }{ $nick->{nick} };
+}
+
+sub nick_del {
+ my ($chan, $nick) = @_;
+ my @check_lr = grep { $history{ $_ }[2] eq $nick->{_irssi} } keys %history;
+ for my $li (@check_lr) {
+ $history{ $li }[2] = $nick->{nick};
+ }
+ if (@check_lr) {
+ $lost_nicks{ $chan->{_irssi} }{ $nick->{nick} } = time;
+ _alter_lines($chan, \@check_lr, 0);
+ }
+ delete $nick_reg{ $nick->{_irssi} };
+}
+
+sub nick_change {
+ my ($chan, $nick, $oldnick) = @_;
+ nick_add($chan, $nick);
+}
+
+sub chan_del {
+ my ($chan) = @_;
+ if (my $del = delete $history_st{ $chan->{_irssi} }) {
+ delete @history{ @$del };
+ }
+ delete $chan_reg{ $chan->{_irssi} };
+ delete $lost_nicks{$chan->{_irssi}};
+ delete $lost_nicks_backup{$chan->{_irssi}};
+}
+
+Irssi::settings_add_int( setc, set 'history_lines', $history_lines);
+Irssi::signal_add_last({
+ 'setup changed' => 'setup_changed',
+});
+Irssi::signal_add({
+ 'print text' => 'prt_text_issue',
+ 'gui print text finished' => 'prt_text_ref',
+ 'nicklist new' => 'nick_add',
+ 'nicklist changed' => 'nick_change',
+ 'nicklist remove' => 'nick_del',
+ 'window destroyed' => 'win_del',
+ 'message public' => 'msg_line_tag',
+ 'message own_public' => 'msg_line_clear',
+ 'channel destroyed' => 'chan_del',
+});
+
+sub dumphist {
+ my $win = Irssi::active_win;
+ my $view = $win->view;
+ my $buffer_id = $view->{buffer}{_irssi} .',';
+ for (my $lp = $view->{buffer}{first_line}; $lp; $lp = $lp->next) {
+ my $line_id = $buffer_id . $lp->{_irssi};
+ if (exists $history{ $line_id }) {
+ my $k = $history{ $line_id };
+ if (exists $chan_reg{ $k->[1] }) {
+ }
+ if (exists $nick_reg{ $k->[2] }) {
+ }
+ if (exists $lost_nicks{ $k->[1] } && exists $lost_nicks{ $k->[1] }{ $k->[2] }) {
+ }
+ }
+ }
+}
+Irssi::settings_add_str( setc, set 'color', $color_letter);
+Irssi::settings_add_int( setc, set 'forms_skip', $skip_forms);
+Irssi::settings_add_int( setc, set 'forms_search_max', $search_forms_max);
+
+init_dim_nicks();
+
+{ package Irssi::Nick }
+
+# Changelog
+# =========
+# 0.4.6
+# - fix crash on some lines reported by pierrot
diff --git a/scripts/hideshow.pl b/scripts/hideshow.pl
new file mode 100644
index 0000000..6ef0bf8
--- /dev/null
+++ b/scripts/hideshow.pl
@@ -0,0 +1,286 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.4.4';
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'hideshow',
+ description => 'Removes and re-adds lines to the Irssi buffer view.',
+ license => 'GNU GPLv2 or later',
+ );
+
+# Usage
+# =====
+# Use this script to hide and re-add lines into your Irssi view. You
+# can grab a custom-modified recentdepart.pl to hide smart-filtered
+# messages instead of ignore, if you do
+#
+# /set recdep_use_hideshow ON
+#
+# You can use trigger.pl with:
+#
+# /trigger add ... -command 'script exec $$Irssi::scripts::hideshow::hide_next = 1'
+#
+# instead of -stop
+
+# Options
+# =======
+# /set hideshow_level <levels>
+# * list of levels that should be hidden from view
+#
+# /set hideshow_hide <ON|OFF>
+# * if hiding is currently enabled or not. make a key binding to
+# conveniently toggle this setting (see below)
+
+# Commands
+# ========
+# you can use this key binding:
+#
+# /bind meta-= command ^toggle hideshow_hide
+#
+# /scrollback status hidden
+# * like /scrollback status, but for the hidden part (some statistics)
+
+no warnings 'redefine';
+use constant IN_IRSSI => __PACKAGE__ ne 'main' || $ENV{IRSSI_MOCK};
+use Irssi;
+use Irssi::TextUI;
+use Encode;
+
+
+
+sub setc () {
+ $IRSSI{name}
+}
+
+sub set ($) {
+ setc . '_' . $_[0]
+}
+
+my (%hidden);
+
+my $dest;
+
+my $HIDE;
+my $hide_level;
+my $ext_hidden_level = MSGLEVEL_LASTLOG << 1;
+
+
+sub show_win_lines {
+ my $win = shift;
+ my $view = $win->view;
+ my $vid = $view->{_irssi};
+ my $hl = delete $hidden{$vid};
+ return unless $hl && %$hl;
+ my $redraw;
+ my $bottom = $view->{bottom};
+ for (my $lp = $view->{buffer}{cur_line}; $lp; $lp = $lp->prev) {
+ my $nl = delete $hl->{ $lp->{_irssi} };
+ next unless $nl;
+ my $ll = $lp;
+ for my $i (@$nl) {
+ $win->gui_printtext_after($ll, $i->[1] | MSGLEVEL_NEVER, "${$i}[0]\n", $i->[2]);
+ $ll = $win->last_line_insert;
+ $redraw = 1;
+ }
+ }
+ if ($redraw) {
+ $win->command('^scrollback end') if $bottom && !$win->view->{bottom};
+ $view->redraw;
+ }
+ delete $hidden{$vid};
+}
+sub show_lines {
+ for my $win (Irssi::windows) {
+ show_win_lines($win);
+ }
+ %hidden=();
+}
+
+sub hide_win_lines {
+ my $win = shift;
+ my $view = $win->view;
+ my $vid = $view->{_irssi};
+ my $bottom = $view->{bottom};
+ my $redraw;
+ my $prev;
+ my $lid;
+ for (my $lp = $view->{buffer}{cur_line}; $lp; $lp = $prev) {
+ $prev = $lp->prev;
+ if ($prev && $lp->{info}{level} & ($hide_level | $ext_hidden_level)) {
+ push @{ $hidden{ $vid }
+ { $prev->{_irssi} }
+ }, [ $lp->get_text(1), $lp->{info}{level}, $lp->{info}{time} ],
+ $hidden{$vid}{ $lp->{_irssi } } ? @{ (delete $hidden{$vid}{ $lp->{_irssi } }) } : ();
+ $view->remove_line($lp);
+ $redraw = 1;
+ }
+ }
+ if ($redraw) {
+ $win->command('^scrollback end') if $bottom && !$win->view->{bottom};
+ $view->redraw;
+ }
+}
+sub hide_lines {
+ Irssi::signal_remove('gui textbuffer line removed' => 'fix_lines');
+ for my $win (Irssi::windows) {
+ hide_win_lines($win);
+ }
+ Irssi::signal_add_last('gui textbuffer line removed' => 'fix_lines');
+}
+
+my %hideshow_timed;
+sub show_one_timed {
+ my $hide = shift;
+ for my $win (Irssi::windows) {
+ next if $hideshow_timed{ $win->{_irssi} };
+ if ($hide) {
+ Irssi::signal_remove('gui textbuffer line removed' => 'fix_lines');
+ hide_win_lines($win);
+ Irssi::signal_add_last('gui textbuffer line removed' => 'fix_lines');
+ }
+ else {
+ show_win_lines($win);
+ }
+ $hideshow_timed{$win->{_irssi}} = 1;
+ $hideshow_timed{_timer} = Irssi::timeout_add_once(10 + int rand 10, 'show_one_timed', $hide);
+ return;
+ }
+ unless ($hide) {
+ show_lines();
+ }
+ %hideshow_timed = ();
+ hideshow() if !!$hide != !!$HIDE;
+ return 1;
+}
+sub hideshow {
+ if (exists $hideshow_timed{_timer}) {
+ Irssi::timeout_remove(delete $hideshow_timed{_timer});
+ }
+ %hideshow_timed = ();
+ $hideshow_timed{_timer} = Irssi::timeout_add_once(10 + int rand 10, 'show_one_timed', !!$HIDE);
+}
+
+sub setup_changed {
+ my $old_level = $hide_level;
+ $hide_level = Irssi::settings_get_level( set 'level' );
+ my $old_hidden = $HIDE;
+ $HIDE = Irssi::settings_get_bool( set 'hide' );
+ if (!defined $old_hidden || $HIDE != $old_hidden || $old_level != $hide_level) {
+ hideshow();
+ }
+}
+
+sub init_hideshow {
+ setup_changed();
+ $Irssi::scripts::hideshow::hide_next = undef;
+}
+
+sub UNLOAD {
+ show_lines();
+}
+
+my $multi_msgs_last;
+
+sub prt_text_issue {
+ $dest = $_[0];
+ my $stripd = $_[2];
+ if (ref $dest && $Irssi::scripts::hideshow::hide_next) {
+ $multi_msgs_last = undef;
+ $dest->{hide} = 1;
+ if ($dest->{level} & (MSGLEVEL_QUITS|MSGLEVEL_NICKS)) {
+ $multi_msgs_last = $stripd;
+ }
+ }
+ elsif (ref $dest && $dest->{level} & (MSGLEVEL_QUITS|MSGLEVEL_NICKS)
+ && defined $multi_msgs_last && $multi_msgs_last eq $stripd) {
+ $dest->{hide} = 1;
+ }
+ else {
+ $multi_msgs_last = undef;
+ }
+ $Irssi::scripts::hideshow::hide_next = undef;
+}
+
+sub prt_text_ref {
+ return unless ref $dest;
+ my ($win) = @_;
+ if ($HIDE) {
+ my $view = $win->view;
+ my $vid = $view->{_irssi};
+ my $lp = $view->{buffer}{cur_line};
+ my $prev = $lp->prev;
+ if ($prev && ($dest->{hide} || $lp->{info}{level} & $hide_level)) {
+ my $level = $lp->{info}{level};
+ $level |= $ext_hidden_level if $dest->{hide};
+ push @{ $hidden{ $vid }
+ { $prev->{_irssi} }
+ }, [ $lp->get_text(1), $level, $lp->{info}{time} ];
+ $view->remove_line($lp);
+ delete @{ $hidden{ $vid } }
+ { (grep {
+ $view->{buffer}{first_line}{info}{time} > $hidden{$vid}{$_}[-1][2]
+ } keys %{$hidden{$vid}}) };
+ $view->redraw;
+ }
+ }
+ $dest = undef;
+}
+
+sub fix_lines {
+ my ($view, $rem_line, $prev_line) = @_;
+ my $vid = $view->{_irssi};
+ my $nl = delete $hidden{$vid}{ $rem_line->{_irssi} };
+ if ($nl && $prev_line) {
+ push @{ $hidden{$vid} { $prev_line->{_irssi} } }, @$nl
+ }
+}
+
+sub win_del {
+ my ($win) = @_;
+ delete $hidden{ $win->view->{_irssi} };
+}
+Irssi::signal_register({
+ 'gui textbuffer line removed' => [ qw/Irssi::TextUI::TextBufferView Irssi::TextUI::Line Irssi::TextUI::Line/ ]
+});
+
+Irssi::signal_add_last({
+ 'setup changed' => 'setup_changed',
+ 'gui print text finished' => 'prt_text_ref',
+ 'gui textbuffer line removed' => 'fix_lines',
+});
+Irssi::signal_add({
+ 'print text' => 'prt_text_issue',
+ 'window destroyed' => 'win_del',
+});
+Irssi::settings_add_level( setc, set 'level', '' );
+Irssi::settings_add_bool( setc, set 'hide', 1 );
+Irssi::command_bind({
+ 'scrollback status' => sub {
+ if ($_[0] =~ /\S/) {
+ &Irssi::command_runsub('scrollback status', @_);
+ Irssi::signal_stop;
+ }
+ },
+ 'scrollback status hidden' => sub {
+ my %vw = map { ($_->view->{_irssi}, $_->{refnum}) } Irssi::windows;
+ my ($tl, $ta, $td) = (0, 0, 0);
+ for my $v (keys %hidden) {
+ my $hl = $hidden{$v};
+ my ($lc, $dc, $ac) = (0, 0, scalar keys %$hl);
+ for my $k (keys %$hl) {
+ my $ls = $hl->{$k};
+ $lc += @$ls;
+ $dc += 16 + length $_->[0] for @$ls;
+ }
+ $tl += $lc; $ta += $ac; $td += $dc;
+ print CLIENTCRAP sprintf "Window %d: %d lines hidden, %d anchors, %dkB of data", ($vw{$v}//"??"), $lc, $ac, int($dc/1024);
+ }
+ print CLIENTCRAP sprintf "Total: %d lines hidden, %d anchors, %dkB of data", $tl, $ta, int($td/1024);
+ }
+});
+init_hideshow();
+
+{ package Irssi::Nick }
diff --git a/scripts/linebuffer.pl b/scripts/linebuffer.pl
new file mode 100644
index 0000000..9e002bb
--- /dev/null
+++ b/scripts/linebuffer.pl
@@ -0,0 +1,278 @@
+use strict;
+use warnings;
+use Irssi;
+use Irssi::TextUI;
+use Hash::Util qw();
+our $VERSION = '0.2'; # c1eddc6a0d6385a
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'linebuffer',
+ description => 'dump the linebuffer content',
+ license => 'GNU GPLv2 or later',
+ );
+
+sub cmd_help {
+ my ($args) = @_;
+ if ($args =~ /^dumplines *$/i) {
+ print CLIENTCRAP <<HELP
+
+DUMPLINES [-file <filename>] [-format] [-ids] [-levels[-prepend|-hex]] [-time] [<count> [<refnum>]]
+
+ Dump the content of the line buffer to a window or file.
+
+ -file: Output to this file.
+ -format: Format the text output.
+ -ids: Print line IDs.
+ -levels: Print levels. -prepend: before text, -hex: as hex value
+ -time: Print time stamp.
+ count: Number of lines to reproduce.
+ refnum: Specifies the window to dump.
+HELP
+
+ }
+}
+
+{
+ my %control2format_d = (
+ 'a' => 'F',
+ 'c' => '_',
+ 'e' => '|',
+ 'i' => '#',
+ 'f' => 'I',
+ 'g' => 'n',
+ );
+ my %control2format_c = (
+ "\c_" => 'U',
+ "\cV" => '8',
+ );
+ my %base_bg = (
+ '0' => '0',
+ '1' => '4',
+ '2' => '2',
+ '3' => '6',
+ '4' => '1',
+ '5' => '5',
+ '6' => '3',
+ '7' => '7',
+ '8' => 'x08',
+ '9' => 'x09',
+ ':' => 'x0a',
+ ';' => 'x0b',
+ '<' => 'x0c',
+ '=' => 'x0d',
+ '>' => 'x0e',
+ '?' => 'x0f',
+ );
+ my %base_fg = (
+ '0' => 'k',
+ '1' => 'b',
+ '2' => 'g',
+ '3' => 'c',
+ '4' => 'r',
+ '5' => 'm', # p
+ '6' => 'y',
+ '7' => 'w',
+ '8' => 'K',
+ '9' => 'B',
+ ':' => 'G',
+ ';' => 'C',
+ '<' => 'R',
+ '=' => 'M', # P
+ '>' => 'Y',
+ '?' => 'W',
+ );
+
+ my $to_true_color = sub {
+ my (@rgbx) = map { ord } @_;
+ $rgbx[3] -= 0x20;
+ for (my $i = 0; $i < 3; ++$i) {
+ if ($rgbx[3] & (0x10 << $i)) {
+ $rgbx[$i] -= 0x20;
+ }
+ }
+ my $color = $rgbx[0] << 16 | $rgbx[1] << 8 | $rgbx[2];
+ ($rgbx[3] & 0x1 ? 'z' : 'Z') . sprintf '%06X', $color;
+ };
+
+ my %ext_color_off = (
+ '.' => [0, 0x10],
+ '-' => [0, 0x60],
+ ',' => [0, 0xb0],
+ '+' => [1, 0x10],
+ "'" => [1, 0x60],
+ '&' => [1, 0xb0],
+ );
+ my @ext_color_al = (0..9, 'A' .. 'Z');
+ my $to_ext_color = sub {
+ my ($sig, $chr) = @_;
+ my ($bg, $off) = @{ $ext_color_off{$sig} };
+ my $color = $off - 0x3f + ord $chr;
+ $color += 10 if $color > 213;
+ ($bg ? 'x' : 'X') . (1+int($color / 36)) . $ext_color_al[$color % 36];
+ };
+ sub control2format {
+ my $line = shift;
+ $line =~ s/%/%%/g;
+ $line =~ s{( \c_ | \cV )
+ |(?:\cD(?:
+ ([aceigf])
+ |(?:\#(.)(.)(.)(.))
+ |(?:([-.,+'&])(.))
+ |(?:(?:/|([0-?]))(?:/|([/0-?])))
+ |\xff/|(/\xff)
+ ))
+ }{
+ '%'.(defined $1 ? $control2format_c{$1} :
+ defined $2 ? $control2format_d{$2} :
+ defined $6 ? $to_true_color->($3,$4,$5,$6) :
+ defined $8 ? $to_ext_color->($7,$8) :
+ defined $10 ? ($base_bg{$10} . (defined $9 ? '%'.$base_fg{$9} : '')) :
+ defined $9 ? $base_fg{$9} :
+ defined $11 ? 'o' : 'n')
+ }gex;
+ $line
+ }
+}
+
+sub simpletime {
+ my ($sec, $min, $hour, $mday, $mon, $year) = localtime $_[0];
+ sprintf "%04d"."%02d"x5, 1900+$year, 1+$mon, $mday, $hour, $min, $sec;
+}
+
+sub prt_report {
+ my $fh = shift;
+ if ($fh->isa('Irssi::UI::Window')) {
+ for (split "\n", (join $,//'', @_)) {
+ my $line;
+ for (split "\t") {
+ if (defined $line) {
+ $line .= ' ' x (5 - (length $line) % 6);
+ $line .= ' ';
+ }
+ $line .= $_;
+ }
+ $line .= '';
+ $fh->print($line, MSGLEVEL_NEVER);
+ }
+ }
+ else {
+ $fh->print(@_);
+ }
+}
+
+sub dump_lines {
+ my ($data, $server, $item) = @_;
+ my ($args, $rest) = Irssi::command_parse_options('dumplines', $data);
+ ref $args or return;
+ my $win = Irssi::active_win;
+ my ($count, $winnum) = $data =~ /(-?\d+)/g;
+ if (defined $winnum) {
+ $win = Irssi::window_find_refnum($winnum) // do {
+ print CLIENTERROR "Window #$winnum not found";
+ return;
+ };
+ }
+ my $fh;
+ my $is_file;
+ if (defined $args->{file}) {
+ unless (length $args->{file}) {
+ print CLIENTERROR "Missing argument to option: file";
+ return;
+ }
+ open $fh, '>', $args->{file} or do {
+ print CLIENTERROR "Error opening ".$args->{file}.": $!";
+ return;
+ };
+ $is_file = 1;
+ }
+ else {
+ $fh = Irssi::Windowitem::window_create(undef, 0);
+ $fh->command('^scrollback home');
+ $fh->command('^scrollback clear');
+ $fh->command('^window scroll off');
+ }
+ prt_report($fh, "\n==========\nwindow: ", $win->{refnum}, "\n");
+ my $view = $win->view;
+ my $lclength = length $view->{buffer}{lines_count};
+ $lclength = 3 if $lclength < 3;
+ my $padlen = $lclength;
+ my $hdr = sprintf "%${lclength}s", " # ";
+ my $hllen = length sprintf '%x', MSGLEVEL_LASTLOG << 1;
+ #123456789012345
+ if (defined $args->{ids}) { $padlen += 10; $hdr .= '| ID ' }
+ if (defined $args->{time}) { $padlen += 15; $hdr .= '| date & time ' }
+ if (defined $args->{'levels-hex'}) { $padlen += $hllen + 1; $hdr .= sprintf "|%${hllen}s", ' levels ' }
+
+ prt_report($fh,
+ " "x$padlen,"\t/buffer first line\n",
+ " "x$padlen,"\t|/buffer cur line\n",
+ " "x$padlen,"\t||/bottom start line\n",
+ $hdr,"\t|||/start line\n");
+ my $j = 1;
+ $count = $view->{height} unless $count;
+ my $start_line;
+ if ($count < 0) {
+ $start_line = $view->get_lines;
+ }
+ else {
+ $j = $view->{buffer}{lines_count} - $count + 1;
+ $j = 1 if $j < 1;
+ $start_line = $view->{buffer}{cur_line};
+ for (my $line = $start_line;
+ $line && $count--;
+ ($start_line, $line) = ($line, $line->prev))
+ {}
+ }
+ for (my $line = $start_line; $line; $line = $line->next) {
+ my $i = 0;
+ my $t = sprintf "%${lclength}d", $j++;
+ $t .= sprintf " %9d", $line->{_irssi} if defined $args->{ids};
+ $t .= ' '.simpletime($line->{info}{time}) if defined $args->{time};
+ $t .= sprintf " %${hllen}x", $line->{info}{level} if defined $args->{'levels-hex'};
+ $t .= "\t" . (join '', map {;++$i; $_->{_irssi} == $line->{_irssi} ? $i : ' ' }
+ $view->{buffer}{first_line}, $view->{buffer}{cur_line},
+ $view->{bottom_startline}, $view->{startline});
+ $t .= "\t";
+ my $text = $line->get_text(1);
+ if (defined $args->{format}) {
+ if (!$is_file) {
+ $text = control2format($text);
+ $text =~ s{(%.)}{ $1 eq "%o" ? "\cD/\xff" : $1 }ge;
+ }
+ }
+ else {
+ $text = control2format($text);
+ if (!$is_file) {
+ $text =~ s/%/%%/g;
+ }
+ }
+ my $lst;
+ if (defined $args->{'levels-prepend'} || defined $args->{levels}) {
+ my $levels = Irssi::bits2level($line->{info}{level});
+ if (!$is_file) {
+ $lst = "%n%r[%n$levels%r]%n";
+ }
+ else {
+ $lst = "[$levels]";
+ }
+ }
+ $t .= "$lst\t" if defined $args->{'levels-prepend'};
+ $t .= $text;
+ $t .= "\t$lst" if defined $args->{levels};
+ $t .= "\n";
+ prt_report($fh, $t);
+ }
+ prt_report($fh, "----------\n", map { $_ // 'NULL' }
+ "view w", $view->{width}, " h", $view->{height}, " scroll ", $view->{scroll}, "\n",
+ " ypos ", $view->{ypos}, "\n",
+ " bottom subline ", $view->{bottom_subline}, " subline ", $view->{subline}, ", is bottom: ", $view->{bottom}, "\n",
+ "buffer: lines count ", $view->{buffer}{lines_count}, ", was last eol: ", $view->{buffer}{last_eol}, "\n",
+ "win: last line ", simpletime($win->{last_line}),"\n\n");
+}
+
+
+Irssi::command_bind('dumplines' => 'dump_lines');
+Irssi::command_set_options('dumplines' => 'format ids time levels levels-prepend levels-hex 1 -file');
+Irssi::command_bind_last('help' => 'cmd_help');
diff --git a/scripts/nickcolor_expando.pl b/scripts/nickcolor_expando.pl
new file mode 100644
index 0000000..ef9b084
--- /dev/null
+++ b/scripts/nickcolor_expando.pl
@@ -0,0 +1,1048 @@
+use strict;
+use warnings;
+
+our $VERSION = '0.3.7'; # 6edfe656246780e
+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 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, $ignore_re, $ignore_setting, $global_colours, $retain_colour_time, @colours, $exited, $session_load_time);
+
+# 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;
+}
+
+# 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) : '';
+}
+
+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 = '';
+}
+
+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::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.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
diff --git a/scripts/nm2.pl b/scripts/nm2.pl
new file mode 100644
index 0000000..08ec53d
--- /dev/null
+++ b/scripts/nm2.pl
@@ -0,0 +1,560 @@
+use Irssi;
+use strict;
+use v5.14;
+use List::Util qw(min max);
+use Hash::Util qw(lock_keys);
+
+our $VERSION = '2.0-dev'; # cb10e88bcd58d0c
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'nm2',
+ description => 'right aligned nicks depending on longest nick',
+ license => 'GPL v2',
+);
+
+# based on bc-bd's original nm.pl
+#
+# use a ** nickcolor_expando ** script for nick colors!
+#
+# why is there no right_mode? you can do that in your theme!
+
+# Options
+# =======
+# /set neat_dynamic <ON|OFF>
+# * whether the width should be dynamically chosen on each incoming
+# message
+#
+# /set neat_shrink <ON|OFF>
+# * whether shrinking of the width is allowed, or only growing
+#
+# /set neat_staircase_shrink <ON|OFF>
+# * whether shrinking should be done one character at a time
+#
+# The following styles decide if the nick is left/right aligned and
+# where the colour/mode goes, they're a bit complex...
+# put the desired indicator(s) between the appropriate "," and the
+# default format of the public messages or actions will be rewritten
+# appropriately.
+# This can be used to align the nick left or right, before or after
+# the nick brackets and before or between the nickmode (by using the
+# pad on the correct place). To change the mode from left of the nick
+# to right of the nick, you need to modify the abstracts in your theme
+# however.
+# By placing the colour at the end, you can even colour the message
+# text in the nick colour, however it might be broken if there are
+# other colour codes used inside the message or by scripts.
+#
+# /format neat_style , , , , , , , ,
+# î î î î î î î î î
+# p: pad | | | | | | | | `before message
+# c: colour | | | | | | | `-after msgchannel
+# t: truncate indicator | | | | | | `-before msgchannel
+# | | | | | `-after nick
+# | | | | `-before nick
+# | | | `-after mode
+# | | `-before mode
+# | `-before msgnick
+# `-none
+#
+# /format neat_action_style , , , ,
+# î î î î î
+# p: pad | | | | `-before message
+# c: colour | | | `-after nick
+# t: truncate indicator | | `-before nick
+# | `-before action
+# `-none
+#
+# /format neat_pad_char <char>
+# * the character(s) used for padding
+#
+# /format neat_truncate_char
+# * the format or character to indicate that nick was truncated
+#
+# /format neat_notruncate_char
+# * the format or character to indicate that nick NOT was truncated
+#
+# /format neat_customize_modes @@ | ++ | ?
+# * a |-separated mapping of mode prefixes and their rendition, can be
+# used to replace or colourise them
+#
+# /set neat_color_hinick <ON|OFF>
+# * whether to use colours in hilighted messages
+#
+# /set neat_color_menick <ON|OFF>
+# * whether to use colours in hilight_nick_matches
+#
+# /set neat_truncate_nick <ON|OFF>
+# * whether to truncate overlong nicks
+#
+# /set neat_custom_modes <ON|OFF>
+# * whether to enable the use of neat_customize_modes format
+#
+# /set neat_maxlength <number>
+# * number : (maximum) length to use for nick padding
+#
+# /set neat_melength <number>
+# * number : width to substract from maxlength for /me padding
+#
+# /set neat_history <number>
+# * number : number of formatted lines to remember for dynamic mode
+#
+
+my @action_protos = qw(irc silc xmpp);
+my (%histories, %S, @style, @astyle, %format_ok, %cmmap);
+
+my $align_expando = '';
+my $trunc_expando = '';
+my $cumode_expando = '';
+
+my $format_re = qr/ %(?=[}%{])
+ | %[04261537kbgcrmywKBGCRMYWU9_8I:|FnN>#pP[]
+ | %[Zz][[:xdigit:]]{6}
+ | %[Xx](?i:0[a-f]|[1-6][0-9a-z]|7[a-x]) /x;
+
+sub update_expando {
+ my ($mode, $server, $target, $nick, $space) = @_;
+ my $t_add;
+ my $nl = length $nick;
+ my $pad_len = max(0, $space - $nl);
+ if ($S{truncate_nick}) {
+ if (($mode >= 4 && $S{trunc_in_anick})
+ || ($mode < 4 && $S{trunc_in_nick})) {
+ $t_add = $S{tnolen};
+ }
+ if ($nl + $t_add > $space) {
+ $trunc_expando = format_expand($S{tyes_char});
+ $t_add = $S{tyeslen} if defined $t_add;
+ }
+ else {
+ $trunc_expando = format_expand($S{tno_char});
+ }
+ $pad_len = max(0, $pad_len - $t_add) if $t_add;
+ }
+ else {
+ $trunc_expando = '';
+ }
+ if ($pad_len) {
+ my @subs = split /($format_re)/, $S{pad_char} x $pad_len;
+ $align_expando = '';
+ my $clen = 0;
+ while (@subs) {
+ my ($tx, $fmt) = splice @subs, 0, 2;
+ my $txlen = length $tx // 0;
+ $align_expando .= substr $tx, 0, ($pad_len - $clen) if defined $tx;
+ $clen += $txlen;
+ $align_expando .= $fmt if defined $fmt;
+ last if $clen >= $pad_len;
+ }
+ $align_expando = format_expand($align_expando.'%n');
+ }
+ else {
+ $align_expando = '';
+ }
+ return $t_add;
+}
+
+sub prnt_clear_levels {
+ my ($dest) = @_;
+ clear_ref() if $dest->{level}
+ & (MSGLEVEL_PUBLIC|MSGLEVEL_MSGS|MSGLEVEL_ACTIONS|MSGLEVEL_DCCMSGS|MSGLEVEL_NOTICES);
+}
+
+sub clear_ref {
+ $trunc_expando = $align_expando = $cumode_expando = '';
+}
+
+sub expando_nickalign { $align_expando }
+sub expando_nicktrunc { $trunc_expando }
+sub expando_nickcumode { $cumode_expando }
+
+Irssi::expando_create('nickalign', \&expando_nickalign, {
+ 'message public' => 'none',
+ 'message own_public' => 'none',
+ 'message private' => 'none',
+ 'message own_private' => 'none',
+ (map { ("message $_ action" => 'none',
+ "message $_ own_action" => 'none')
+ } @action_protos),
+ });
+Irssi::expando_create('nicktrunc', \&expando_nicktrunc, {
+ 'message public' => 'none',
+ 'message own_public' => 'none',
+ 'message private' => 'none',
+ 'message own_private' => 'none',
+ (map { ("message $_ action" => 'none',
+ "message $_ own_action" => 'none')
+ } @action_protos),
+ });
+Irssi::expando_create('nickcumode', \&expando_nickcumode, {
+ 'message public' => 'none',
+ 'message own_public' => 'none',
+ 'message private' => 'none',
+ 'message own_private' => 'none',
+ (map { ("message $_ action" => 'none',
+ "message $_ own_action" => 'none')
+ } @action_protos),
+ });
+
+sub init_hist {
+ my ($server, $target) = @_;
+ if (my $ch = $server->channel_find($target)) {
+ [ max map { length } map { $_->{nick} } $ch->nicks ]
+ }
+ else {
+ [ max map { length } $server->{nick}, $target ]
+ }
+}
+
+my %em = (
+ p => '$nickalign',
+ c => '$nickcolor',
+ t => '$nicktrunc',
+ m => '$nickcumode',
+ );
+
+my %formats = (
+ own_action => [5, '{ownaction ', '$0','}','$1' ],
+ action_public => [4, '{pubaction ', '$0','}','$1' ],
+ action_private => [4, '{pvtaction ', '$0','}','$2' ],
+ action_private_query => [4, '{pvtaction_query ','$0','}','$2' ],
+ # * * * # * *
+
+ own_msg_private_query => [3, '{ownprivmsgnick ', '' ,'{ownprivnick ','$2','}','' ,'}','$1' ],
+ msg_private_query => [2, '{privmsgnick ' ,'' ,'' ,'$0','' ,'' ,'}','$2' ],
+ own_msg => [1, '{ownmsgnick ' ,'$2',' {ownnick ' ,'$0','}','' ,'}','$1' ],
+ own_msg_channel => [1, '{ownmsgnick ' ,'$3',' {ownnick ' ,'$0','}','{msgchannel $1}','}','$2' ],
+ pubmsg_me => [0, '{pubmsgmenick ' ,'$2',' {menick ' ,'$0','}','' ,'}','$1' ],
+ pubmsg_me_channel => [0, '{pubmsgmenick ' ,'$3',' {menick ' ,'$0','}','{msgchannel $1}','}','$2' ],
+ pubmsg_hilight => [0, '{pubmsghinick $0 ','$3',' ' ,'$1', '','', ,'}','$2' ],
+ pubmsg_hilight_channel => [0, '{pubmsghinick $0 ','$4',' ' ,'$1', '','{msgchannel $2}','}','$3' ],
+ pubmsg => [0, '{pubmsgnick ' ,'$2',' {pubnick ' ,'$0','}','' ,'}','$1' ],
+ pubmsg_channel => [0, '{pubmsgnick ' ,'$3',' {pubnick ' ,'$0','}','{msgchannel $1}','}','$2' ],
+ # * * * * * # * * * *
+ );
+
+sub reformat_format {
+ Irssi::signal_remove('command format', 'update_formats');
+ Irssi::signal_remove('theme changed' => 'update_formats');
+ %format_ok = () unless @_;
+ my ($mode, $server, $target, $nick, $size) = @_;
+ for my $fmt (keys %formats) {
+ next if defined $mode && $formats{$fmt}[0] != $mode;
+
+ my @fs = @{ $formats{$fmt} };
+
+ my $ls;
+ if (defined $mode) {
+ $ls = $size;
+ }
+ else {
+ $ls = $fs[0] < 4 ? $S{max} : max(0, $S{max} - $S{melength});
+ }
+ next if exists $format_ok{$fmt} && $format_ok{$fmt} == $ls;
+
+ if ($S{truncate_nick} && $ls) {
+ $fs[ $fs[0] < 4 ? 4 : 2 ] =~ s/\$/\$[.$ls]/;
+ }
+ if ($S{custom_modes} && $fs[0] < 4) {
+ $fs[2] =~ s/\$\K\d/nickcumode/;
+ }
+ my $s;
+ local $em{c} = ''
+ if ($fs[1] =~ /menick/ && !$S{color_menick})
+ || ($fs[1] =~ /hinick/ && !$S{color_hinick});
+ my $sr = $fs[0] >= 4 ? \@astyle : \@style;
+ for my $i (1..$#fs) {
+ $s .= ($sr->[$i] =~ s/(.)/$em{$1}/gr) if defined $sr->[$i];
+ $s .= $fs[$i];
+ }
+ Irssi::command("^format $fmt $s");
+ $format_ok{$fmt} = $ls;
+ }
+ Irssi::signal_add_last({
+ 'theme changed' => 'update_formats',
+ 'command format' => 'update_formats',
+ });
+}
+
+sub update_nm {
+ my ($mode, $server, $target, $nick) = @_;
+ my $tg = $server->{tag};
+ if (my $ch = $server->channel_find($target)) {
+ $target = $ch->{name};
+ my $nickobj = $ch->nick_find($nick);
+ if ($nickobj) {
+ $nick = $nickobj->{nick};
+ my $mode = substr $nickobj->{prefixes}.' ', 0, 1;
+ $cumode_expando = exists $cmmap{$mode} ? format_expand($cmmap{$mode}) : $mode;
+ }
+ else {
+ $cumode_expando = '';
+ }
+ }
+ elsif (my $q = $server->query_find($target)) {
+ $target = $q->{name};
+ }
+
+ my $longest;
+ if ($S{dynamic}) {
+ my $hist = $histories{"$tg/$target"} ||= init_hist($server, $target);
+ my $last = $histories{"$tg/$target/last"} || 1;
+ unshift @$hist, length $nick;
+ if (@$hist > 2*$S{history}) {
+ splice @$hist, $S{history};
+ }
+ my @add;
+ unless ($S{shrink}) {
+ push @add, $last;
+ }
+ if ($S{staircase}) {
+ push @add, $last - 1
+ }
+ $longest = $histories{"$tg/$target/last"} = max(@$hist, @add);
+
+ if ($S{max} && ($S{max} < $longest || !$S{shrink})) {
+ $longest = $S{max};
+ }
+ }
+ else {
+ $longest = $S{max};
+ }
+
+ my $size = $mode < 4 ? $longest : max(0, $longest - $S{melength});
+ my $t_add = update_expando($mode, $server, $target, $nick, $size);
+ $size = max(0, $size - $t_add) if defined $t_add;
+ if ($S{dynamic}) {
+ reformat_format($mode, $server, $target, $nick, $size);
+ }
+}
+
+sub sig_setup {
+ my %old_S = %S;
+ $S{history} = Irssi::settings_get_int('neat_history');
+ $S{max} = Irssi::settings_get_int('neat_maxlength');
+ $S{melength} = Irssi::settings_get_int('neat_melength');
+
+ $S{dynamic} = Irssi::settings_get_bool('neat_dynamic');
+ $S{shrink} = Irssi::settings_get_bool('neat_shrink');
+ $S{staircase} = Irssi::settings_get_bool('neat_staircase_shrink');
+
+ $S{color_hinick} = Irssi::settings_get_bool('neat_color_hinick');
+ $S{color_menick} = Irssi::settings_get_bool('neat_color_menick');
+ $S{truncate_nick} = Irssi::settings_get_bool('neat_truncate_nick');
+ $S{custom_modes} = Irssi::settings_get_bool('neat_custom_modes');
+
+ if (!defined $old_S{dynamic} || $old_S{dynamic} != $S{dynamic}) {
+ %histories = ();
+ reformat_format();
+ }
+ elsif ($old_S{max} != $S{max} || $old_S{melength} != $S{melength}
+ || $old_S{color_hinick} != $S{color_hinick} || $old_S{color_menick} != $S{color_menick}
+ || $old_S{truncate_nick} != $S{truncate_nick} || $old_S{custom_modes} != $S{custom_modes}) {
+ reformat_format();
+ }
+}
+
+sub update_formats {
+ my $was_style = "@style";
+ $S{style} = Irssi::current_theme->get_format(__PACKAGE__, 'neat_style');
+ my $was_action_style = "@astyle";
+ $S{action_style} = Irssi::current_theme->get_format(__PACKAGE__, 'neat_action_style');
+ $S{pad_char} = Irssi::current_theme->get_format(__PACKAGE__, 'neat_pad_char');
+ $S{tno_char} = Irssi::current_theme->get_format(__PACKAGE__, 'neat_notruncate_char');
+ $S{tnolen} = length($S{tno_char} =~ s/$format_re//gr);
+ $S{tyeslen} = length($S{tyes_char} =~ s/$format_re//gr);
+ $S{tyes_char} = Irssi::current_theme->get_format(__PACKAGE__, 'neat_truncate_char');
+ @style = map { y/pct//cd; $_ } split /,/, $S{style};
+ @astyle = map { y/pctm//cd; $_ } split /,/, $S{action_style};
+ $S{trunc_in_nick} = grep { /t/ } @style[2..min($#style, 6)];
+ $S{trunc_in_anick} = grep { /t/ } @astyle[2..min($#astyle, 3)];
+ my $custom_modes = Irssi::current_theme->get_format(__PACKAGE__, 'neat_custom_modes');
+ %cmmap = map { (substr $_, 0, 1), (substr $_, 1) } $custom_modes =~ /(?:^\s?|\G\s?\|\s?)((?!\s\|)(?:[^\\|[:space:]]|\\.|\s(?!\||$))*)/sg;
+ if ($was_style ne "@style" || $was_action_style ne "@astyle") {
+ reformat_format();
+ }
+}
+
+{
+ 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 {
+ $_[0] =~ 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"
+ }
+ }ger;
+ }
+}
+
+sub init {
+ update_formats();
+ sig_setup();
+ lock_keys(%S);
+ print "nm2 experimental version, please report issues. thanks!"
+}
+
+Irssi::settings_add_bool('misc', 'neat_dynamic', 1);
+Irssi::settings_add_bool('misc', 'neat_shrink', 1);
+Irssi::settings_add_bool('misc', 'neat_staircase_shrink', 0);
+
+Irssi::settings_add_bool('misc', 'neat_color_hinick', 0);
+Irssi::settings_add_bool('misc', 'neat_color_menick', 0);
+Irssi::settings_add_bool('misc', 'neat_truncate_nick', 1);
+Irssi::settings_add_bool('misc', 'neat_custom_modes', 0);
+
+Irssi::settings_add_int('misc', 'neat_maxlength', 0);
+Irssi::settings_add_int('misc', 'neat_melength', 2);
+Irssi::settings_add_int('misc', 'neat_history', 50);
+
+Irssi::signal_add('setup changed' => 'sig_setup');
+Irssi::signal_add_last({
+ 'setup reread' => 'sig_setup',
+ 'theme changed' => 'update_formats',
+ 'command format' => 'update_formats',
+ });
+
+Irssi::theme_register([
+ 'neat_style' => ' , , p , , c , t , , , ',
+ 'neat_action_style' => ' , p , , t , ',
+ 'neat_pad_char' => '%K.',
+ 'neat_truncate_char' => '%m+',
+ 'neat_notruncate_char' => '',
+ 'neat_custom_modes' => '&%B&%n | @%g@%n | +%y+%n',
+ ]);
+
+Irssi::signal_add_first({
+ 'message public' => sub {
+ my ($server, $msg, $nick, $address, $target) = @_;
+ update_nm(0, $server, $target, $nick);
+ },
+ 'message private' => sub {
+ my ($server, $msg, $nick, $address) = @_;
+ update_nm(2, $server, $nick, $nick);
+ },
+ (map { ("message $_ action" => sub {
+ my ($server, $msg, $nick, $address, $target) = @_;
+ update_nm(4, $server, $target, $nick);
+ }) } qw(irc silc)),
+ 'message xmpp action' => sub {
+ return unless @_;
+ my ($server, $msg, $nick, $target) = @_;
+ update_nm(4, $server, $target, $nick);
+ },
+ });
+
+sub channel_nick {
+ my ($server, $target) = @_;
+ ($server->channel_find($target)||+{ownnick=>$server})->{ownnick}{nick}
+}
+
+Irssi::signal_add_first({
+ 'message own_public' => sub {
+ my ($server, $msg, $target) = @_;
+ update_nm(1, $server, $target, channel_nick($server, $target));
+ },
+ 'message own_private' => sub {
+ my ($server, $msg, $target) = @_;
+ update_nm(3, $server, $target, $server->{nick});
+ },
+ (map { ("message $_ own_action" => sub {
+ my ($server, $msg, $target) = @_;
+ update_nm(5, $server, $target, $server->{nick});
+ }) } qw(irc silc)),
+ 'message xmpp own_action' => sub {
+ return unless @_;
+ my ($server, $msg, $target) = @_;
+ update_nm(5, $server, $target, channel_nick($server, $target));
+ },
+ });
+Irssi::signal_add_last({
+ 'channel destroyed' => sub {
+ my ($channel) = @_;
+ delete $histories{ $channel->{server}{tag} . '/' . $channel->{name} };
+ delete $histories{ $channel->{server}{tag} . '/' . $channel->{name} . '/last' };
+ },
+ 'query destroyed' => sub {
+ my ($query) = @_;
+ delete $histories{ $query->{server}{tag} . '/' . $query->{name} };
+ delete $histories{ $query->{server}{tag} . '/' . $query->{name} . '/last' };
+ },
+ 'query nick changed' => sub {
+ my ($query, $old_nick) = @_;
+ delete $histories{ $query->{server}{tag} . '/' . $old_nick };
+ delete $histories{ $query->{server}{tag} . '/' . $old_nick . '/last' };
+ },
+ 'query server changed' => sub {
+ my ($query, $old_server) = @_;
+ delete $histories{ $old_server->{tag} . '/' . $query->{name} };
+ delete $histories{ $old_server->{tag} . '/' . $query->{name} . '/last' };
+ }
+ });
+Irssi::signal_add({
+ 'print text' => 'prnt_clear_levels',
+});
+
+init();
+
+# Changelog
+# =========
+# 2.0-dev
+# - fix crash if xmpp action signal is not registered (just ignore it)
+# - do not grow either when using no-shrink with maxlength
+# - hopefully fix alignment in xmpp muc
diff --git a/scripts/sbclearmatch.pl b/scripts/sbclearmatch.pl
new file mode 100644
index 0000000..c2fb87f
--- /dev/null
+++ b/scripts/sbclearmatch.pl
@@ -0,0 +1,93 @@
+use strict;
+use warnings;
+use Irssi;
+use Irssi::TextUI;
+
+our $VERSION = '0.2'; # 6c39400282189a0
+our %IRSSI = (
+ authors => 'Nei',
+ contact => 'Nei @ anti@conference.jabber.teamidiot.de',
+ url => "http://anti.teamidiot.de/",
+ name => 'sbclearmatch',
+ description => 'clear matching lines in scrollback',
+ license => 'GPLv2 or later',
+);
+
+sub cmd_help {
+ my ($args) = @_;
+ if ($args =~ /^scrollback *$/i) {
+ print CLIENTCRAP <<HELP
+
+SCROLLBACK CLEARMATCH [-level <level>] [-regexp] [-case] [-word] [-all] [<pattern>]
+
+ CLEARMATCH: Clears the screen and the buffer of matching text.
+
+ -regexp: The given text pattern is a regular expression.
+ -case: Performs a case-sensitive matching.
+ -word: The text must match full words.
+HELP
+
+ }
+}
+
+
+sub cmd_sb_clearmatch {
+ my ($args, $server, $witem) = @_;
+ my ($options, $pattern) = Irssi::command_parse_options('scrollback clearmatch', $args);
+
+ my $level;
+ if (defined $options->{level}) {
+ $level = $options->{level};
+ $level =~ y/,/ /;
+ $level = Irssi::combine_level(0, $level);
+ }
+ else {
+ return unless length $pattern;
+ $level = MSGLEVEL_ALL;
+ }
+
+ my $regex;
+ if (length $pattern) {
+ my $flags = defined $options->{case} ? '' : '(?i)';
+ my $b = defined $options->{word} ? '\b' : '';
+ if (defined $options->{regexp}) {
+ local $@;
+ eval { $regex = qr/$flags$b$pattern$b/; 1 }
+ or do {
+ print CLIENTERROR "Pattern did not compile: " . do { $@ =~ /(.*) at / && $1 };
+ return;
+ };
+ }
+ else {
+ $regex = qr/$flags$b\Q$pattern\E$b/;
+ }
+ }
+
+ my $current_win = ref $witem ? $witem->window : Irssi::active_win;
+
+ for my $win (defined $options->{all} ? Irssi::windows : $current_win) {
+ my $view = $win->view;
+ my $line = $view->get_lines;
+ my $need_redraw;
+ my $bottom = $view->{bottom};
+
+ while ($line) {
+ my $line_level = $line->{info}{level};
+ my $next = $line->next;
+ if ($line_level & $level && $line->get_text(0) =~ $regex) {
+ $view->remove_line($line);
+ $need_redraw = 1;
+ }
+ $line = $next;
+ }
+
+ if ($need_redraw) {
+ $win->command('^scrollback end') if $bottom && !$win->view->{bottom};
+ $view->redraw;
+ }
+ }
+}
+
+Irssi::command_bind 'scrollback clearmatch' => 'cmd_sb_clearmatch';
+Irssi::command_set_options 'scrollback clearmatch' => '-level regexp case word all';
+Irssi::command_bind_last 'help' => 'cmd_help';