diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/adv_windowlist.pl | 2456 | ||||
| -rw-r--r-- | scripts/clearable.pl | 71 | ||||
| -rw-r--r-- | scripts/colorize_nicks.pl | 136 | ||||
| -rw-r--r-- | scripts/complete_at.pl | 40 | ||||
| -rw-r--r-- | scripts/dim_nicks.pl | 392 | ||||
| -rw-r--r-- | scripts/hideshow.pl | 286 | ||||
| -rw-r--r-- | scripts/linebuffer.pl | 278 | ||||
| -rw-r--r-- | scripts/nickcolor_expando.pl | 1048 | ||||
| -rw-r--r-- | scripts/nm2.pl | 560 | ||||
| -rw-r--r-- | scripts/sbclearmatch.pl | 93 | 
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'; | 
