diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/adv_windowlist.pl | 2456 | ||||
| -rw-r--r-- | scripts/aspell.pl | 725 | ||||
| -rw-r--r-- | scripts/clearable.pl | 71 | ||||
| -rw-r--r-- | scripts/cmpchans.pl | 64 | ||||
| -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/hlscroll.pl | 83 | ||||
| -rw-r--r-- | scripts/ido_switcher.pl | 1166 | ||||
| -rw-r--r-- | scripts/ircuwhois.pl | 84 | ||||
| -rw-r--r-- | scripts/linebuffer.pl | 278 | ||||
| -rw-r--r-- | scripts/messages_bottom.pl | 29 | ||||
| -rw-r--r-- | scripts/mouse-awl.pl | 144 | ||||
| -rw-r--r-- | scripts/mouse_soliton.pl | 146 | ||||
| -rw-r--r-- | scripts/nickcolor_expando.pl | 1048 | ||||
| -rw-r--r-- | scripts/nickcolor_gay.pl | 83 | ||||
| -rw-r--r-- | scripts/nm2.pl | 560 | ||||
| -rw-r--r-- | scripts/recentdepart.pl | 332 | ||||
| -rw-r--r-- | scripts/sb_position.pl | 112 | ||||
| -rw-r--r-- | scripts/sbclearmatch.pl | 93 | ||||
| -rw-r--r-- | scripts/tmux-nicklist-portable.pl | 390 | ||||
| -rw-r--r-- | scripts/trackbar22.pl | 500 | ||||
| -rw-r--r-- | scripts/typofix.pl | 165 | ||||
| -rw-r--r-- | scripts/uberprompt.pl | 787 |
25 files changed, 10170 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/aspell.pl b/scripts/aspell.pl new file mode 100644 index 0000000..b6a254e --- /dev/null +++ b/scripts/aspell.pl @@ -0,0 +1,725 @@ +=pod + +=head1 NAME + +aspell.pl + +=head1 DESCRIPTION + +A spellchecker based on GNU ASpell which allows you to interactively +select the correct spellings for misspelled words in your input field. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +=head1 SETUP + +Settings: + + aspell_debug 0 + aspell_ignore_chan_nicks 1 + aspell_suggest_colour '%g' + aspell_language 'en_GB' + aspell_irssi_dict '~/.irssi/irssi.dict' + +B<Note:> Americans may wish to change the language to en_US. This can be done +with the command C</SET aspell_language en_US> once the script is loaded. + +=head1 USAGE + +Bind a key to /spellcheck, and then invoke it when you have +an input-line that you wish to check. + +If it is entirely correct, nothing will appear to happen. This is a good thing. +Otherwise, a small split window will appear at the top of the Irssi session +showing you the misspelled word, and a selection of 10 possible candidates. + +You may select one of the by pressing the appropriate number from C<0-9>, or +skip the word entirely by hitting the C<Space> bar. + +If there are more than 10 possible candidates for a word, you can cycle through +the 10-word "pages" with the C<n> (next) and C<p> (prev) keys. + +Pressing Escape, or any other key, will exit the spellcheck altogether, although +it can be later restarted. + +=head1 AUTHORS + +Copyright E<copy> 2011 Isaac Good C<E<lt>irssi@isaacgood.comE<gt>> + +Copyright E<copy> 2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> + +=head1 LICENCE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=head1 BUGS + +See README file. + +=head1 TODO + +See README file. + +=cut + + +use warnings; +use strict; +use Data::Dumper; +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use File::Spec; + +# Magic. Somehow remedies: +# "Can't locate object method "nicks" via package "Irssi::Irc::Query" Bug +# Actually, that's a bunch of lies, but I'm pretty sure there is something +# it fixes. Otherwise, a bit of cargo-culting can't hurt. + +{ package Irssi::Nick } + +eval { + use Text::Aspell; +}; + +if ($@ && $@ =~ m/Can't locate/) { + print '%_Bugger, please insteall Text::Aspell%_' +} + + +our $VERSION = '1.6.1'; +our %IRSSI = ( + authors => 'Isaac Good (yitz_), Tom Feist (shabble)', + contact => 'irssi@isaacgood.com, shabble+irssi@metavore.org', + name => 'aspell', + description => 'ASpell spellchecking system for Irssi', + license => 'MIT', + updated => "2011-10-27", + ); + +# --------------------------- +# Globals +# --------------------------- + +# CONFIG SETTINGS +# =============== + +# Settings cached vars +my $DEBUG; + +# The colour that the suggestions are rendered in in the split windowpane. +my $suggestion_colour; + +# Whether to bother spellchecking strings that match nicks in the current channel. +my $ignore_chan_nicks; + +# path to local aspell irssi dictionary file. +my $irssi_dict_filepath; + +# Language to use. It follows the same format of the LANG environment variable +# on most systems. It consists of the two letter ISO 639 language code and an +# optional two letter ISO 3166 country code after a dash or underscore. The +# default value is based on the value of the LC_MESSAGES locale. +my $aspell_language; + + +# OTHER GLOBALS +# ============= + +# current line, broken into hashref 'objects' storing word and positional data. +my @word_pos_array; +# index of word we're currently processing. +my $index; +my $active_word_obj; + +# list of all possible suggestions for current misspelled word +my @suggestions; +# page number - we only show 10 results per page so we can select with 0-9 +my $suggestion_page; + +# the spellchecker object. +my $aspell; + +# some window references to manage the window splitting and restoration +my $split_win_ref; +my $original_win_ref; + +# keypress handling flag. +my $corrections_active; + + +#my $bacon = 1; + +# --------------------------- +# key constants +# --------------------------- + +sub K_ESC () { 27 } +sub K_RET () { 10 } +sub K_SPC () { 32 } +sub K_0 () { 48 } +sub K_9 () { 57 } +sub K_N () { 110 } +sub K_P () { 112 } +sub K_I () { 105 } + +# used for printing stuff to the split window we don't want logged. +sub PRN_LEVEL () { MSGLEVEL_CLIENTCRAP | MSGLEVEL_NEVER } +sub AS_CFG () { "aspellchecker" } + +# --------------------------- +# Teh Codez +# --------------------------- + +sub check_line { + my ($line) = @_; + + # reset everything + $suggestion_page = 0; + $corrections_active = 0; + $index = 0; + @word_pos_array = (); + @suggestions = (); + close_temp_split(); + + # split into an array of words on whitespace, keeping track of + # positions of each, as well as the size of whitespace. + + my $pos = 0; + + _debug('check_line processing "%s"', $line); + + while ($line =~ m/\G(\S+)(\s*)/g) { + my ($word, $ws) = ($1, $2); # word, whitespace + + my $prefix_punct = ''; + my $suffix_punct = ''; + + if ($word =~ m/^([^a-zA-Z0-9]+)/) { + $prefix_punct = $1; + } + if ($word =~ m/([^a-zA-Z0-9]+)$/) { + $suffix_punct = $1; + } + + my $pp_len = length($prefix_punct); + my $sp_len = length($suffix_punct); + + my $actual_len = length($word) - ($pp_len + $sp_len); + my $actual_word = substr($word, $pp_len, $actual_len); + + if($DEBUG and ($pp_len or $sp_len)) { + _debug("prefix punc: %s, suffix punc: %s, actual word: %s", + $prefix_punct, $suffix_punct, $actual_word); + } + + + my $actual_pos = $pos + $pp_len; + + my $obj = { + word => $actual_word, + pos => $actual_pos, + len => $actual_len, + prefix_punct => $prefix_punct, + suffix_punct => $suffix_punct, + }; + + push @word_pos_array, $obj; + $pos += length ($word . $ws); + } + + return unless @word_pos_array > 0; + + process_word($word_pos_array[0]); +} + +sub process_word { + my ($word_obj) = @_; + + my $word = $word_obj->{word}; + + # That's a whole lotta tryin'! + my $channel = $original_win_ref->{active}; + if (not defined $channel) { + if (exists Irssi::active_win()->{active}) { + $channel = Irssi::active_win()->{active}; + } elsif (defined Irssi::active_win()) { + my @items = Irssi::active_win()->items; + $channel = $items[0] if @items; + } else { + $channel = Irssi::parse_special('$C'); + } + } + + if ($word =~ m/^\d+$/) { + + _debug("Skipping $word that is entirely numeric"); + spellcheck_next_word(); # aspell thinks numbers are wrong. + + } elsif (word_matches_chan_nick($channel, $word_obj)) { + # skip to next word if it's actually a nick + # (and the option is set) - checked for in the matches() func. + _debug("Skipping $word that matches nick in channel"); + spellcheck_next_word(); + + } elsif (not $aspell->check($word)) { + + _debug("Word '%s' is incorrect", $word); + + my $sugg_ref = get_suggestions($word); + + if (defined $sugg_ref && ref($sugg_ref) eq 'ARRAY') { + @suggestions = @$sugg_ref; + } + + if (scalar(@suggestions) == 0) { + + spellcheck_next_word(); + + } elsif (not temp_split_active()) { + + $corrections_active = 1; + highlight_incorrect_word($word_obj); + _debug("Creating temp split to show candidates"); + create_temp_split(); + + } else { + + print_suggestions(); + } + } else { + + spellcheck_next_word(); + } +} + +sub get_suggestions { + my ($word) = @_; + my @candidates = $aspell->suggest($word); + _debug("Candidates for '$word' are %s", join(", ", @candidates)); + # if ($bacon) { + return \@candidates; + # } else { + # return undef; + # } +} + +sub word_matches_chan_nick { + my ($channel, $word_obj) = @_; + + return 0 unless $ignore_chan_nicks; + return 0 unless defined $channel and ref $channel; + + my @nicks; + if (not exists ($channel->{type})) { + return 0; + } elsif ($channel->{type} eq 'QUERY') { + + # TODO: Maybe we need to parse ->{address} instead, but + # it appears empty on test dumps. + + exists $channel->{name} + and push @nicks, { nick => $channel->{name} }; + + exists $channel->{visible_name} + and push @nicks, { nick => $channel->{visible_name} }; + + } elsif($channel->{type} eq 'CHANNEL') { + @nicks = $channel->nicks(); + } + + my $nick_hash; + + $nick_hash->{$_}++ for (map { $_->{nick} } @nicks); + + _debug("Nicks: %s", Dumper($nick_hash)); + + # try various combinations of the word with its surrounding + # punctuation. + my $plain_word = $word_obj->{word}; + return 1 if exists $nick_hash->{$plain_word}; + my $pp_word = $word_obj->{prefix_punct} . $word_obj->{word}; + return 1 if exists $nick_hash->{$pp_word}; + my $sp_word = $word_obj->{word} . $word_obj->{suffix_punct}; + return 1 if exists $nick_hash->{$pp_word}; + my $full_word = + $word_obj->{prefix_punct} + . $word_obj->{word} + . $word_obj->{suffix_punct}; + return 1 if exists $nick_hash->{$full_word}; + + return 0; +} + +# Read from the input line +sub cmd_spellcheck_line { + my ($args, $server, $witem) = @_; + + if (defined $witem) { + $original_win_ref = $witem->window; + } else { + $original_win_ref = Irssi::active_win; + } + + my $inputline = _input(); + check_line($inputline); +} + +sub spellcheck_finish { + $corrections_active = 0; + close_temp_split(); + + # stick the cursor at the end of the input line? + my $input = _input(); + my $end = length($input); + Irssi::gui_input_set_pos($end); +} + +sub sig_gui_key_pressed { + my ($key) = @_; + return unless $corrections_active; + + my $char = chr($key); + + if ($key == K_ESC) { + spellcheck_finish(); + + } elsif ($key >= K_0 && $key <= K_9) { + _debug("Selecting word: $char of page: $suggestion_page"); + spellcheck_select_word($char + ($suggestion_page * 10)); + + } elsif ($key == K_SPC) { + _debug("skipping word"); + spellcheck_next_word(); + } elsif ($key == K_I) { + + my $current_word = $word_pos_array[$index]; + $aspell->add_to_personal($current_word->{word}); + $aspell->save_all_word_lists(); + + _print('Saved %s to personal dictionary', $current_word->{word}); + + spellcheck_next_word(); + + } elsif ($key == K_N) { # next 10 results + + if ((scalar @suggestions) > (10 * ($suggestion_page + 1))) { + $suggestion_page++; + } else { + $suggestion_page = 0; + } + print_suggestions(); + + } elsif ($key == K_P) { # prev 10 results + if ($suggestion_page > 0) { + $suggestion_page--; + } + print_suggestions(); + + } else { + spellcheck_finish(); + } + + Irssi::signal_stop(); +} + +sub spellcheck_next_word { + $index++; + $suggestion_page = 0; + + if ($index >= @word_pos_array) { + _debug("End of words"); + spellcheck_finish(); + return; + } + + _debug("moving onto the next word: $index"); + process_word($word_pos_array[$index]); + +} +sub spellcheck_select_word { + my ($num) = @_; + + if ($num > $#suggestions) { + _debug("$num past end of suggestions list."); + return 0; + } + + my $word = $suggestions[$num]; + _debug("Selected word $num: $word as correction"); + correct_input_line_word($word_pos_array[$index], $word); + return 1; +} + +sub _debug { + my ($fmt, @args) = @_; + return unless $DEBUG; + + $fmt = '%%RDEBUG:%%n ' . $fmt; + my $str = sprintf($fmt, @args); + Irssi::window_find_refnum(1)->print($str); +} + +sub _print { + my ($fmt, @args) = @_; + my $str = sprintf($fmt, @args); + Irssi::active_win->print('%g' . $str . '%n'); +} + +sub temp_split_active () { + return defined $split_win_ref; +} + +sub create_temp_split { + #$original_win_ref = Irssi::active_win(); + Irssi::signal_add_first('window created', 'sig_win_created'); + Irssi::command('window new split'); + Irssi::signal_remove('window created', 'sig_win_created'); +} + +sub UNLOAD { + _print("%%RASpell spellchecker Version %s unloading...%%n", $VERSION); + close_temp_split(); +} + +sub close_temp_split { + + my $original_refnum = -1; + my $active_refnum = -2; + + my $active_win = Irssi::active_win(); + + if (defined $active_win && ref($active_win) =~ m/^Irssi::/) { + if (exists $active_win->{refnum}) { + $active_refnum = $active_win->{refnum}; + } + } + + if (defined $original_win_ref && ref($original_win_ref) =~ m/^Irssi::/) { + if (exists $original_win_ref->{refnum}) { + $original_refnum = $original_win_ref->{refnum}; + } + } + + if ($original_refnum != $active_refnum && $original_refnum > 0) { + Irssi::command("window goto $original_refnum"); + } + + if (defined($split_win_ref) && ref($split_win_ref) =~ m/^Irssi::/) { + if (exists $split_win_ref->{refnum}) { + my $split_refnum = $split_win_ref->{refnum}; + _debug("split_refnum is %d", $split_refnum); + _debug("splitwin has: %s", join(", ", map { $_->{name} } + $split_win_ref->items())); + Irssi::command("window close $split_refnum"); + undef $split_win_ref; + } else { + _debug("refnum isn't in the split_win_ref"); + } + } else { + _debug("winref is undef or broken"); + } +} + +sub sig_win_created { + my ($win) = @_; + $split_win_ref = $win; + # printing directly from this handler causes irssi to segfault. + Irssi::timeout_add_once(10, \&configure_split_win, {}); +} + +sub configure_split_win { + $split_win_ref->command('window size 3'); + $split_win_ref->command('window name ASpell Suggestions'); + + print_suggestions(); +} + +sub correct_input_line_word { + my ($word_obj, $correction) = @_; + my $input = _input(); + + my $word = $word_obj->{word}; + my $pos = $word_obj->{pos}; + my $len = $word_obj->{len}; + + # handle punctuation. + # - Internal punctuation: "they're" "Bob's" should be replaced if necessary + # - external punctuation: "eg:" should not. + # this will also have impact on the position adjustments. + + _debug("Index of incorrect word is %d", $index); + _debug("Correcting word %s (%d) with %s", $word, $pos, $correction); + + + #my $corrected_word = $prefix_punct . $correction . $suffix_punct; + + my $new_length = length $correction; + + my $diff = $new_length - $len; + _debug("diff between $word and $correction is $diff"); + + # record the fix in the array. + $word_pos_array[$index] = { word => $correction, pos => $pos + $diff }; + # do the actual fixing of the input string + substr($input, $pos, $len) = $correction; + + + # now we have to go through and fix up all teh positions since + # the correction might be a different length. + + foreach my $new_obj (@word_pos_array[$index..$#word_pos_array]) { + #starting at $index, add the diff to each position. + $new_obj->{pos} += $diff; + } + + _debug("Setting input to new value: '%s'", $input); + + # put the corrected string back into the input field. + Irssi::gui_input_set($input); + + _debug("-------------------------------------------------"); + spellcheck_next_word(); +} + +# move the cursor to the beginning of the word in question. +sub highlight_incorrect_word { + my ($word_obj) = @_; + Irssi::gui_input_set_pos($word_obj->{pos}); +} + +sub print_suggestions { + my $count = scalar @suggestions; + my $pages = int ($count / 10); + my $bot = $suggestion_page * 10; + my $top = $bot + 9; + + $top = $#suggestions if $top > $#suggestions; + + my @visible = @suggestions[$bot..$top]; + my $i = 0; + + @visible = map { + '(%_' . $suggestion_colour . ($i++) . '%n) ' # bold/coloured selection num + . $suggestion_colour . $_ . '%n' # coloured selection option + } @visible; + + # disable timestamps to ensure a clean window. + my $orig_ts_level = Irssi::parse_special('$timestamp_level'); + $split_win_ref->command("^set timestamp_level $orig_ts_level -CLIENTCRAP"); + + # clear the window + $split_win_ref->command("/^scrollback clear"); + my $msg = sprintf('%s [Pg %d/%d] Select a number or <SPC> to skip this ' + . 'word. Press <i> to save this word to your personal ' + . 'dictionary. Any other key cancels%s', + '%_', $suggestion_page + 1, $pages + 1, '%_'); + + my $word = $word_pos_array[$index]->{word}; + + $split_win_ref->print($msg, PRN_LEVEL); # header + $split_win_ref->print('%_%R"' . $word . '"%n ' # erroneous word + . join(" ", @visible), PRN_LEVEL); # suggestions + + # restore timestamp settings. + $split_win_ref->command("^set timestamp_level $orig_ts_level"); + +} + +sub sig_setup_changed { + $DEBUG + = Irssi::settings_get_bool('aspell_debug'); + $suggestion_colour + = Irssi::settings_get_str('aspell_suggest_colour'); + $ignore_chan_nicks + = Irssi::settings_get_bool('aspell_ignore_chan_nicks'); + + + + my $old_lang = $aspell_language; + + $aspell_language + = Irssi::settings_get_str('aspell_language'); + + + my $old_filepath = $irssi_dict_filepath; + + $irssi_dict_filepath + = Irssi::settings_get_str('aspell_irssi_dict'); + + _debug("Filepath: $irssi_dict_filepath"); + + if ((not defined $old_filepath) or + ($irssi_dict_filepath ne $old_filepath)) { + reinit_aspell(); + } + + _debug("Language: $aspell_language"); + + if ((not defined $old_lang) or + ($old_lang ne $aspell_language)) { + reinit_aspell(); + } + +} + +sub _input { + return Irssi::parse_special('$L'); +} + +sub reinit_aspell { + $aspell = Text::Aspell->new; + $aspell->set_option('lang', $aspell_language); + $aspell->set_option('personal', $irssi_dict_filepath); + $aspell->create_speller(); +} + +# sub cmd_break_cands { +# $bacon = !$bacon; +# _print("Bacon is now: %s", $bacon?"true":"false"); +# } + +sub init { + my $default_dict_path + = File::Spec->catfile(Irssi::get_irssi_dir, "irssi.dict"); + Irssi::settings_add_bool(AS_CFG, 'aspell_debug', 0); + Irssi::settings_add_bool(AS_CFG, 'aspell_ignore_chan_nicks', 1); + Irssi::settings_add_str(AS_CFG, 'aspell_suggest_colour', '%g'); + Irssi::settings_add_str(AS_CFG, 'aspell_language', 'en_GB'); + Irssi::settings_add_str(AS_CFG, 'aspell_irssi_dict', $default_dict_path); + + sig_setup_changed(); + + Irssi::signal_add('setup changed' => \&sig_setup_changed); + + _print("%%RASpell spellchecker Version %s loaded%%n", $VERSION); + + $corrections_active = 0; + $index = 0; + + Irssi::signal_add_first('gui key pressed' => \&sig_gui_key_pressed); + Irssi::command_bind('spellcheck' => \&cmd_spellcheck_line); + #Irssi::command_bind('breakon' => \&cmd_break_cands); +} + +init(); 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/cmpchans.pl b/scripts/cmpchans.pl new file mode 100644 index 0000000..80324b0 --- /dev/null +++ b/scripts/cmpchans.pl @@ -0,0 +1,64 @@ +use strict; +use warnings; + +our $VERSION = "0.5"; +our %IRSSI = ( + authors => 'Jari Matilainen, init[1]@irc.freenode.net', + contact => 'vague@vague.se', + name => 'cmpchans', + description => 'Compare nicks in two channels', + license => 'Public Domain', + url => 'http://vague.se' +); + +use Irssi::TextUI; +use Data::Dumper; + +sub cmd_cmp { + local $/ = " "; + my ($args, $server, $witem) = @_; + my (@channels) = split /\s+/, $args; + + my $server1 = $server; + if ($channels[0] =~ s,(.*?)/,,) { + $server1 = Irssi::server_find_tag($1) || $server; + } + my $chan1 = $server1->channel_find($channels[0]); + if(!$chan1) { + Irssi::active_win()->{active}->print("You have to specify atleast one channel to compare nicks to"); + return; + } + + my @nicks_1; + my @nicks_2; + + @nicks_1 = $chan1->nicks() if(defined $chan1); + + if(not defined $channels[1]) { + @nicks_2 = $witem->nicks(); + } + else { + if ($channels[1] =~ s,(.*?)/,,) { + $server1 = Irssi::server_find_tag($1) || $server; + } + my ($chan2) = $server1->channel_find($channels[1]); + @nicks_2 = $chan2->nicks() if(defined $chan2); + } + + return if(scalar @nicks_1 == 0 || scalar @nicks_2 == 0); + + my %count = (); + my @intersection; + + foreach (@nicks_1, @nicks_2) { $count{$_->{nick}}++; } + foreach my $key (keys %count) { + if($count{$key} > 1) { + push @{\@intersection}, $key; + } + } + + my $common = join(", ", @intersection); + $witem->print("Common nicks: " . $common); +} + +Irssi::command_bind("cmp", \&cmd_cmp); 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/hlscroll.pl b/scripts/hlscroll.pl new file mode 100644 index 0000000..47a4066 --- /dev/null +++ b/scripts/hlscroll.pl @@ -0,0 +1,83 @@ +use strict; +use Irssi qw(command_bind MSGLEVEL_HILIGHT); +use vars qw($VERSION %IRSSI); + +# Recommended key bindings: alt+pgup, alt+pgdown: +# /bind meta2-5;3~ /scrollback hlprev +# /bind meta2-6;3~ /scrollback hlnext + +$VERSION = '0.02'; +%IRSSI = ( + authors => 'Juerd, Eevee', + contact => '#####@juerd.nl', + name => 'Scroll to hilights', + description => 'Scrolls to previous or next highlight', + license => 'Public Domain', + url => 'http://juerd.nl/site.plp/irssi', + changed => 'Fri Apr 13 05:48 CEST 2012', + inspiration => '@eevee on Twitter: "i really want irssi keybindings that will scroll to the next/previous line containing a highlight. why does this not exist"', +); + +sub _hlscroll{ + my ($direction, $data, $server, $witem) = @_; + $witem or return; + my $window = $witem->window or return; + + my $view = $window->view; + my $line = $view->{buffer}->{cur_line}; + my $delta = $direction eq 'prev' ? -1 : 1; + + my $linesleft = $view->{ypos} - $view->{height} + 1; + my $scrollby = 0; # how many display lines to scroll to the next highlight + + # find the line currently at the bottom of the screen + while (1) { + my $line_height = $view->get_line_cache($line)->{count}; + + if ($linesleft < $line_height) { + # found it! + if ($direction eq 'prev') { + # skip however much of $line is on the screen + $scrollby = $linesleft - $line_height; + } + else { + # skip however much of $line is off the screen + $scrollby = $linesleft; + } + + last; + } + + $linesleft -= $line_height; + + last if not $line->prev; + $line = $line->prev; + } + + while ($line->$direction) { + $line = $line->$direction; + my $line_height = $view->get_line_cache($line)->{count}; + + if ($line->{info}{level} & MSGLEVEL_HILIGHT) { + # this algorithm scrolls to the "border" between lines -- if + # scrolling down, add in the line's entire height so it's entirely + # visible + if ($direction eq 'next') { + $scrollby += $delta * $line_height; + } + + $view->scroll($scrollby); + return; + } + + $scrollby += $delta * $line_height; + } + + if ($direction eq 'next' and not $line->next) { + # scroll all the way to the bottom, after the last highlight + $view->scroll_line($line); + } +}; + +command_bind 'scrollback hlprev' => sub { _hlscroll('prev', @_) }; +command_bind 'scrollback hlnext' => sub { _hlscroll('next', @_) }; diff --git a/scripts/ido_switcher.pl b/scripts/ido_switcher.pl new file mode 100644 index 0000000..ddcf06e --- /dev/null +++ b/scripts/ido_switcher.pl @@ -0,0 +1,1166 @@ +=pod + +=head1 NAME + +ido_switcher.pl + +=head1 DESCRIPTION + +Search and select windows similar to ido-mode for emacs + +=head1 INSTALLATION + +This script requires that you have first installed and loaded F<uberprompt.pl> + +Uberprompt can be downloaded from: + +L<https://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl> + +and follow the instructions at the top of that file or its README for installation. + +If uberprompt.pl is available, but not loaded, this script will make one +attempt to load it before giving up. This eliminates the need to precisely +arrange the startup order of your scripts. + +=head2 SETUP + +C</bind ^G /ido_switch_start [options]> + +Where C<^G> is a key of your choice. + +=head2 USAGE + +C<C-g> (or whatever you've set the above bind to), enters IDO window switching mode. +You can then type either a search string, or use one of the additional key-bindings +to change the behaviour of the search. C<C-h> provides online help regarding +the possible interactive options. + +=head3 EXTENDED USAGE: + +It is possible to pass arguments to the C</ido_switch_start> command, which +correspond to some of the interactively settable parameters listed below. + +The following options are available: + +=over 4 + +=item C<-channels> + +Search through only channels. + +=item C<-queries> + +Search through only queries. + +=item C<-all> + +search both queries and channels (Default). + +=item C<-active> + +Lmit search to only window items with activity. + +=item C<-exact> + +Enable exact-substring matching + +=item C<-flex> + +Enable flex-string matching + +=back + +I<If neither of C<-exact>, C<-flex> or C<-regex> are given, the default is the value of +C</set ido_use_flex>> + +=head4 EXAMPLE + +=over 2 + +=item C</bind ^G /ido_switch_start -channels> + +=item C</bind ^F /ido_switch_start -queries -active> + +=back + +B<NOTE:> When entering window switching mode, the contents of your input line will +be saved and cleared, to avoid visual clutter whilst using the switching +interface. It will be restored once you exit the mode using either C<C-g>, C<Esc>, +or C<RET>. + +=head3 INTERACTIVE COMMANDS + +The following key-bindings are available only once the mode has been +activated: + +=over 4 + +=item C<C-g> + + Exit the mode without changing windows. + +=item C<Esc> + +Exit, as above. + +=item C<C-s> + +Rotate the list of window candidates forward by one item + +=item C<C-r> + +Rotate the list of window candidates backward by one item + +=item C<C-e> + +Toggle 'Active windows only' filter + +=item C<C-f> + +Switch between 'Flex' and 'Exact' matching. + +=item C<C-d> + +Select a network or server to filter candidates by + +=item C<C-u> + +Clear the current search string + +=item C<C-q> + +Cycle between showing only queries, channels, or all. + +=item C<C-SPC> + +Filter candidates by current search string, and then reset +the search string + +=item C<RET> + +Select the current head of the candidate list (the green one) + +=item C<SPC> + +Select the current head of the list, without exiting the +switching mode. The head is then moved one place to the right, +allowing one to cycle through channels by repeatedly pressing space. + +=item C<TAB> + +B<[currently in development]> displays all possible completions +at the bottom of the current window. + +=item I<All other keys> (C<a-z, A-Z>, etc) + +Add that character to the current search string. + +=back + +=head3 USAGE NOTES + +=over 4 + +=item * + +Using C-e (show actives), followed by repeatedly pressing space will cycle +through all your currently active windows. + +=item * + +If you enter a search string fragment, and realise that more than one candidate +is still presented, rather than delete the whole string and modify it, you +can use C-SPC to 'lock' the current matching candidates, but allow you to +search through those matches alone. + +=back + +=head1 AUTHORS + +Based originally on L<window_switcher.pl|http://scripts.irssi.org/scripts/window_switcher.pl> script Copyright 2007 Wouter Coekaerts +C<E<lt>coekie@irssi.orgE<gt>>. + +Primary functionality Copyright 2010-2011 Tom Feist +C<E<lt>shabble+irssi@metavore.orgE<gt>>. + +=head1 LICENCE + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +=head1 BUGS: + +=over 4 + +=item B<FIXED> Sometimes selecting a channel with the same name on a different + network will take you to the wrong channel. + +=back + +=head1 TODO + +=over 4 + +=item B<DONE> C-g - cancel + +=item B<DONE> C-spc - narrow + +=item B<DONE> flex matching (on by default, but optional) + +=item TODO server/network narrowing + +=item B<DONE> colourised output (via uberprompt) + +=item B<DONE> C-r / C-s rotate matches + +=item B<DONE> toggle queries/channels + +=item B<DONE> remove inputline content, restore it afterwards. + +=item TODO tab - display all possibilities in window (clean up afterwards) +how exactly will this work? + +=item B<DONE> sort by recent activity/recently used windows (separate commands?) + +=item B<TODO> need to be able to switch ordering of active ones (numerical, or most +recently active, priority to PMs/hilights, etc?) + +=item B<DONE> should space auto-move forward to next window for easy stepping + through sequential/active windows? + +=back + +=cut + +use strict; +use warnings; + +use Irssi; +use Irssi::TextUI; +use Data::Dumper; + + +our $VERSION = '2.3'; # 1dc0a53a2df38e9 +our %IRSSI = + ( + authors => 'Tom Feist, Wouter Coekaerts', + contact => 'shabble+irssi@metavore.org, shabble@#irssi/freenode', + name => 'ido_switcher', + description => 'Select window[-items] using an ido-mode like search interface', + license => 'GPLv2 or later', + url => 'http://github.com/shabble/irssi-scripts/tree/master/ido-mode/', + changed => '24/7/2010' + ); + + + +my $CMD_NAME = 'ido_switch_start'; +my $CMD_OPTS = '-channels -queries -all -active -exact -flex -regex'; + + +my $input_copy = ''; +my $input_pos_copy = 0; + +my $ido_switch_active = 0; # for intercepting keystrokes + +my @window_cache = (); +my @search_matches = (); + +my $match_index = 0; +my $search_str = ''; +my $active_only = 0; +my $regex_valid = 1; + +my $mode_type = 'ALL'; +my @mode_cache; +my $showing_help = 0; + +my $need_clear = 0; + +my $sort_ordering = "start-asc"; +my $sort_active_first = 0; + +# /set configurable settings +my $ido_show_count; +my $ido_use_flex; + +my $DEBUG_ENABLED = 0; +sub DEBUG () { $DEBUG_ENABLED } + + +sub MODE_WIN () { 0 } # windows +sub MODE_NET () { 1 } # chatnets +#sub MODE_C () { 2 } # channels +#sub MODE_S () { 3 } # select server +#sub MODE_W () { 4 } # select window + +my $MODE = MODE_WIN; + +# check we have uberprompt loaded. + +my %need_clear; + +sub _print { + my $win = Irssi::active_win; + my $str = join('', @_); + $need_clear = 1; + $win->print($str, MSGLEVEL_NEVER); + push @{ $need_clear{ $win->{_irssi} } }, $win->view->{buffer}{cur_line}; +} + +sub _debug_print { + return unless DEBUG; + my $win = Irssi::active_win; + my $str = join('', @_); + $win->print($str, MSGLEVEL_CLIENTCRAP); +} + +sub _print_clear { + return unless $need_clear; + for my $win (Irssi::windows) { + if (my $lines = delete $need_clear{ $win->{_irssi} }) { + my $view = $win->view; + my $bottom = $view->{bottom}; + for my $line (@$lines) { + $view->remove_line($line); + } + $win->command('^scrollback end') if $bottom && !$win->view->{bottom}; + $view->redraw; + } + } + %need_clear=(); +} + +# TODO: use the code from rl_history_search to put this into a disposable +# split win. +# TODO: create formats for this. +sub display_help { + + my @message = + ('%_IDO Window Switching Help:%_', + '', + '%_Ctrl-g%_ %|- cancel out of the mode without changing windows.', + '%_Esc%_ %|- cancel out, as above.', + '%_Ctrl-s%_ %|- rotate the list of window candidates forward by 1', + '%_Ctrl-r%_ %|- rotate the list of window candidates backward by 1', + '%_Ctrl-e%_ %|- Toggle \'Active windows only\' filter', + '%_Ctrl-f%_ %|- Switch between \'Regex\', \'Flex\' and \'Exact\' matching.', +# '%_Ctrl-d%_ %|- Select a network or server to filter candidates by', + '%_Ctrl-u%_ %|- Clear the current search string', + '%_Ctrl-q%_ %|- Cycle between showing only queries, channels, or all.', + '%_Ctrl-SPC%_ %|- Filter candidates by current search string, and then reset the search string', + '%_RET%_ %|- Select the current head of the candidate list (the %_green%n one)', + '%_SPC%_ %|- Select the current head of the list, without exiting switching mode. The head ' + .'is then moved one place to the right, allowing one to cycle through channels by repeatedly ' + .'pressing space.', + '%_TAB%_ %|- displays all possible completions at the bottom of the current window.', + '', + ' %_All other keys (a-z, A-Z, etc) - Add that character to the', + ' %_current search string.', + '', + '%_Press Any Key to return%_', + ); + + _print($_) for @message; + $showing_help = 1; +} + +sub print_all_matches { + my $message_header = "Windows:"; + my $win = Irssi::active_win(); + my $win_width = $win->{width} || 80; + + # TODO: needs to prefix ambig things with chatnet, or maybe order in groups + # by chatnet with newlines. + + # Also, colourise the channel list. + + my $col_width = 1; + + for (@search_matches) { + my $len = length($_->{num} . ':' . _format_display_tag($_) . $_->{name}); + $col_width = $len if $len > $col_width; + } + + my $cols = int($win_width / $col_width); + + my @lines; + my $i = 0; + my @line; + + for my $item (@search_matches) { + ++$i; + my $name = $item->{name}; + push @line, sprintf('%*s', -(10+$col_width), "\cD4/".$item->{num}.":\cD3/"._format_display_tag($item)."\cD4/".$name); + if ($i == $cols) { + push @lines, join ' ', @line; + @line = (); + $i = 0; + } + } + # flush rest out. + push @lines, join ' ', @line; + + _print($message_header); + _print($_) for (@lines); + #_print("Longtest name: $longest_name"); +} + +unless ("Irssi::Script::uberprompt"->can('init')) { + + print "Warning, this script requires '\%_uberprompt.pl\%_' in order to work. "; + +} + +sub ido_switch_init { + #Irssi::settings_add_bool('ido_switch', 'ido_switch_debug', 0); + Irssi::settings_add_str('ido_switch', 'ido_use_flex', 'flex'); + Irssi::settings_add_bool('ido_switch', 'ido_show_active_first', 1); + Irssi::settings_add_int ('ido_switch', 'ido_show_count', 5); + + + Irssi::command_bind($CMD_NAME, \&ido_switch_start); + Irssi::command_set_options($CMD_NAME, $CMD_OPTS); + + Irssi::signal_add ('setup changed' => \&setup_changed); + Irssi::signal_add_first('gui key pressed' => \&handle_keypress); + + setup_changed(); +} + +sub setup_changed { + #$DEBUG_ENABLED = Irssi::settings_get_bool('ido_switch_debug'); + $ido_show_count = Irssi::settings_get_int ('ido_show_count'); + $ido_use_flex = _flex_mode(Irssi::settings_get_str('ido_use_flex')); + $sort_active_first = Irssi::settings_get_bool('ido_show_active_first'); +} + +sub ido_switch_start { + + my ($args, $server, $witem) = @_; + + # store copy of input line to restore later. + $input_copy = Irssi::parse_special('$L'); + $input_pos_copy = Irssi::gui_input_get_pos(); + + Irssi::gui_input_set(''); + + my $options = {}; + my @opts = Irssi::command_parse_options($CMD_NAME, $args); + if (@opts and ref($opts[0]) eq 'HASH') { + $options = $opts[0]; + #print "Options: " . Dumper($options); + } + + # clear / initialise match variables. + $ido_switch_active = 1; + $search_str = ''; + $match_index = 0; + @mode_cache = (); + + # configure settings from provided arguments. + + # use provided options first, or fall back to /setting. + $ido_use_flex = _flex_mode(exists $options->{exact} + ? 'exact' + : exists $options->{flex} + ? 'flex' + : exists $options->{regex} + ? 'regex' + : Irssi::settings_get_str('ido_use_flex')); + + # only select active items + $active_only = exists $options->{active}; + + # what type of items to search. + $mode_type = exists $options->{queries} + ? 'QUERY' + : exists $options->{channels} + ? 'CHANNEL' + : 'ALL'; + + _debug_print "Win cache: " . join(", ", map { $_->{name} } @window_cache); + + _update_cache(); + + update_matches(); + update_window_select_prompt(); +} + +sub _flex_mode { + if ($_[0] =~ /flex/i) { + 'Flex' + } elsif ($_[0] =~ /regex/i) { + 'Regex' + } else { + 'Exact' + } +} + +sub _update_cache { + @window_cache = get_all_windows(); +} + +sub _build_win_obj { + my ($win, $win_item) = @_; + + my @base = ( + b_pos => -1, + e_pos => -1, + hilight_field => 'name', + active => $win->{data_level} > 0, + num => $win->{refnum}, + server => $win->{active_server}, + + ); + + if (defined($win_item)) { + return ( + @base, + name => $win_item->{visible_name}, + type => $win_item->{type}, + itemname => $win_item->{name}, + active => $win_item->{data_level} > 0, + server => $win_item->{server}, + + ) + } else { + return ( + @base, + name => $win->{name}, + type => 'WIN', + ); + } +} + +sub get_all_windows { + my @ret; + + foreach my $win (Irssi::windows()) { + my @items = $win->items(); + + if ($win->{name} ne '') { + _debug_print "Adding window: " . $win->{name}; + push @ret, { _build_win_obj($win, undef) }; + } + if (scalar @items) { + foreach my $item (@items) { + _debug_print "Adding windowitem: " . $item->{visible_name}; + push @ret, { _build_win_obj($win, $item) }; + } + } else { + if (not grep { $_->{num} == $win->{refnum} } @ret) { + my $item = { _build_win_obj($win, undef) }; + $item->{name} = "Unknown"; + push @ret, $item; + } + #_debug_print "Error occurred reading info from window: $win"; + #_debug_print Dumper($win); + } + } + @ret = _sort_windows(\@ret); + + return @ret; + +} + +sub _sort_windows { + my $list_ref = shift; + my @ret = @$list_ref; + + @ret = sort { $a->{num} <=> $b->{num} } @ret; + if ($sort_active_first) { + my @active = grep { $_->{active} } @ret; + my @inactive = grep { not $_->{active} } @ret; + + return (@active, @inactive); + } else { + return @ret; + } +} + +sub ido_switch_select { + my ($selected, $tag) = @_; + if (!$selected) { + _debug_print "Error, selection invalid"; + return; + } + _debug_print sprintf("Selecting window: %s (%d)", + $selected->{name}, $selected->{num}); + + Irssi::command("WINDOW GOTO " . $selected->{num}); + + if ($selected->{type} ne 'WIN') { + _debug_print "Selecting window item: " . $selected->{itemname}; + my $i = 1; my $found; + for (Irssi::active_win->items) { + if ($_->{name} eq $selected->{itemname}) { + if (!defined $selected->{server} && !defined $_->{server}) { + $found = 1; + last; + } + if (defined $selected->{server} && defined $_->{server} + && $selected->{server}{tag} eq $_->{server}{tag}) { + $found = 1; + last; + } + } + ++$i; + } + Irssi::command("WINDOW ITEM GOTO " . ($found ? $i : $selected->{itemname})); + } + + update_matches(); +} + +sub ido_switch_exit { + $ido_switch_active = 0; + + _print_clear(); + + Irssi::gui_input_set($input_copy); + Irssi::gui_input_set_pos($input_pos_copy); + Irssi::signal_emit('change prompt', '', 'UP_INNER'); +} + +sub _order_matches { + return @_[$match_index .. $#_, + 0 .. $match_index - 1] +} + +sub update_window_select_prompt { + + # take the top $ido_show_count entries and display them. + my $match_count = scalar @search_matches; + my $show_count = $ido_show_count; + my $match_string = '[No matches]'; + + $show_count = $match_count if $match_count < $show_count; + + if ($show_count > 0) { # otherwise, default message above. + _debug_print "Showing: $show_count matches"; + + my @ordered_matches = _order_matches(@search_matches); + + my @display = @ordered_matches[0..$show_count - 1]; + + # determine which items are non-unique, if any. + + my %uniq; + + foreach my $res (@display) { + my $name = $res->{name}; + + if (!exists $uniq{$name}) { + $uniq{$name} = []; + } + push @{$uniq{$name}}, $res; + } + + # and set a flag to ensure they have their network tag applied + # to them when drawn. + foreach my $name (keys %uniq) { + my @values = @{$uniq{$name}}; + if (@values > 1) { + $_->{display_net} = 1 for @values; + } + } + + # show the first entry in green + + my $first = shift @display; + my $formatted_first = _format_display_entry($first, '%g'); + unshift @display, $formatted_first; + + # and array-slice-map the rest to be red. + # or yellow, if they have unviewed activity + + @display[1..$#display] + = map + { + _format_display_entry($_, $_->{active}?'%y':'%r') + + } @display[1..$#display]; + + # join em all up + $match_string = join ', ', @display; + } + + my @indicators; + + # indicator if flex mode is being used (C-f to toggle) + push @indicators, $ido_use_flex; + push @indicators, 'Active' if $active_only; + push @indicators, ucfirst(lc($mode_type)); + + my $flex = sprintf(' %%b[%%n%s%%b]%%n ', join ', ', @indicators); + + my $search = ''; + $search = (sprintf '`%s\': ', $search_str) if length $search_str; + $search = (sprintf '`%%R%s%%n\': ', $search_str) if (length $search_str && !$regex_valid); + + Irssi::signal_emit('change prompt', $flex . $search . $match_string, + 'UP_INNER'); +} + + + +sub _format_display_entry { + my ($obj, $colour) = @_; + + my $field = $obj->{hilight_field}; + my $hilighted = { netname => _format_display_tag($obj).$obj->{name}, + name => $obj->{name}, num => $obj->{num} }; + my $show_tag = $obj->{display_net} || 0; + + if ($obj->{b_pos} >= 0 && $obj->{e_pos} > $obj->{b_pos}) { + substr($hilighted->{$field}, $obj->{e_pos}, 0) = '%_'; + substr($hilighted->{$field}, $obj->{b_pos}, 0) = '%_'; + _debug_print "Showing $field as: " . $hilighted->{$field} + } + + return sprintf('%s%s:%s%%n', + $colour, + $hilighted->{num}, + $hilighted->{netname}) + if $field eq 'netname'; + + return sprintf('%s%s:%s%s%%n', + $colour, + $hilighted->{num}, + $show_tag ? _format_display_tag($obj) : '', + $hilighted->{name}); +} + +sub _format_display_tag { + my $obj = shift; + if (defined $obj->{server}) { + my $server = $obj->{server}; + my $tag = $server->{tag}; + return $tag . '/' if length $tag; + } + return ''; +} + +sub _check_active { + my ($obj) = @_; + return 1 unless $active_only; + return $obj->{active}; +} + +sub update_matches { + my $current_match = get_window_match(); + + _update_cache() unless $search_str; + + if ($mode_type ne 'ALL') { + @mode_cache = @window_cache; + @window_cache = grep { $_->{type} eq $mode_type } @window_cache; + } else { + @window_cache = @mode_cache if @mode_cache; + } + + my $field = 'name'; + my $search_str2; + if ($search_str =~ m:^(.*)/(.*?)$:) { + $field = 'netname'; + $search_str2 = "$2/$1"; + } + + $regex_valid = 1; + if ($search_str =~ m/^\d+$/) { + + @search_matches = + grep { + _check_active($_) and regex_match($search_str, $_, 'num') + } @window_cache; + + } elsif ($ido_use_flex eq 'Flex') { + + @search_matches = + grep { + _check_active($_) and (flex_match($search_str, $_, $field) >= 0 + || (defined $search_str2 && flex_match($search_str2, $_, $field) >= 0)) + } @window_cache; + + } elsif ($ido_use_flex eq 'Regex') { + my $regex = do { local $@; + my $ret = eval { qr/$search_str/ } || qr/\Q$search_str/; + if ($@) { $regex_valid = 0 } + $ret; + }; + my $regex2 = defined $search_str2 ? + do { local $@; eval { qr/$search_str2/ } || qr/\Q$search_str2/ } + : undef; + @search_matches = + grep { + _check_active($_) and (regex_match($regex, $_, $field) + || (defined $regex2 && regex_match($regex2, $_, $field))) + } @window_cache; + } else { + @search_matches = + grep { + _check_active($_) and (regex_match(qr/\Q$search_str/, $_, $field) + || (defined $search_str2 && regex_match(qr/\Q$search_str2/, $_, $field))) + } @window_cache; + } + + $match_index = 0; + if ($current_match) { + for my $idx (0..$#search_matches) { + if ($search_matches[$idx]{num} == $current_match->{num} + && $search_matches[$idx]{type} eq $current_match->{type}) { + $match_index = $idx; + if ($current_match->{type} eq 'WIN') { + last; + } elsif ($search_matches[$idx]{itemname} eq $current_match->{itemname}) { + last; + } + } + } + } + +} + +sub regex_match { + my ($regex, $obj, $field) = @_; + my $data = $field eq 'netname' + ? _format_display_tag($obj).$obj->{name} : $obj->{$field}; + if ($data =~ m/$regex/i) { + $obj->{hilight_field} = $field; + $obj->{b_pos} = $-[0]; + $obj->{e_pos} = $+[0]; + return 1; + } + return 0; +} + +sub flex_match { + my ($search_str, $obj, $field) = @_; + + my $pattern = $search_str; + my $source = $field eq 'netname' + ? _format_display_tag($obj).$obj->{name} : $obj->{$field}; + + _debug_print "Flex match: $pattern / $source"; + + # default to matching everything if we don't have a pattern to compare + # against. + + return 0 unless $pattern; + + my @chars = split '', lc($pattern); + my $ret = -1; + my $first = 0; + + my $lc_source = lc($source); + + $obj->{hilight_field} = $field; + + foreach my $char (@chars) { + my $pos = index($lc_source, $char, $ret); + if ($pos > -1) { + + # store the beginning of the match + $obj->{b_pos} = $pos unless $first; + $first = 1; + + _debug_print("matched: $char at $pos in $source"); + $ret = $pos + 1; + + } else { + + $obj->{b_pos} = $obj->{e_pos} = -1; + _debug_print "Flex returning: -1"; + + return -1; + } + } + + _debug_print "Flex returning: $ret"; + + #store the end of the match. + $obj->{e_pos} = $ret; + + return $ret; +} + +sub prev_match { + + $match_index++; + if ($match_index > $#search_matches) { + $match_index = 0; + } + + _debug_print "index now: $match_index"; +} + +sub next_match { + + $match_index--; + if ($match_index < 0) { + $match_index = $#search_matches; + } + _debug_print "index now: $match_index"; +} + +sub get_window_match { + return $search_matches[$match_index]; +} + +sub handle_keypress { + my ($key) = @_; + + return unless $ido_switch_active; + + if ($showing_help) { + _print_clear(); + $showing_help = 0; + Irssi::signal_stop(); + } + + if ($key == 0) { # C-SPC? + _debug_print "\%_Ctrl-space\%_"; + + $search_str = ''; + @window_cache = @search_matches; + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + } + + if ($key == 3) { # C-c + _print_clear(); + Irssi::signal_stop(); + return; + } + if ($key == 4) { # C-d +# update_network_select_prompt(); + Irssi::signal_stop(); + return; + } + + if ($key == 5) { # C-e + $active_only = not $active_only; + Irssi::signal_stop(); + update_matches(); + update_window_select_prompt(); + return; + } + + if ($key == 6) { # C-f + + $ido_use_flex = ($ido_use_flex eq 'Regex' ? 'Flex' + : $ido_use_flex eq 'Flex' ? 'Exact' + : 'Regex'); + _update_cache(); + + update_matches(); + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + } + if ($key == 9) { # TAB + _debug_print "Tab complete"; + _print_clear(); + print_all_matches(); + Irssi::signal_stop(); + } + + if ($key == 10) { # enter + _debug_print "selecting history and quitting"; + my $selected_win = get_window_match(); + ido_switch_select($selected_win); + + ido_switch_exit(); + Irssi::signal_stop(); + return; + } + if ($key == 11) { # Ctrl-K + my $sel = get_window_match(); + _debug_print("deleting entry: " . $sel->{num}); + Irssi::command("window close " . $sel->{num}); + _update_cache(); + update_matches(); + update_window_select_prompt(); + Irssi::signal_stop(); + + } + + if ($key == 18) { # Ctrl-R + _debug_print "skipping to prev match"; + #update_matches(); + next_match(); + + update_window_select_prompt(); + Irssi::signal_stop(); # prevent the bind from being re-triggered. + return; + } + + if ($key == 17) { # Ctrl-q + if ($mode_type eq 'CHANNEL') { + $mode_type = 'QUERY'; + } elsif ($mode_type eq 'QUERY') { + $mode_type = 'ALL'; + } else { # ALL + $mode_type = 'CHANNEL'; + } + update_matches(); + update_window_select_prompt(); + Irssi::signal_stop(); + } + + if ($key == 19) { # Ctrl-s + _debug_print "skipping to next match"; + prev_match(); + + #update_matches(); + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + } + + if ($key == 7) { # Ctrl-g + _debug_print "aborting search"; + ido_switch_exit(); + Irssi::signal_stop(); + return; + } + + if ($key == 8) { # Ctrl-h + display_help(); + Irssi::signal_stop(); + return; + } + + if ($key == 21) { # Ctrl-u + $search_str = ''; + update_matches(); + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + + } + + if ($key == 127) { # DEL + + if (length $search_str) { + $search_str = substr($search_str, 0, -1); + _debug_print "Deleting char, now: $search_str"; + } + + update_matches(); + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + } + + # TODO: handle esc- sequences and arrow-keys? + + if ($key == 27) { # Esc + ido_switch_exit(); + return; + } + + if ($key == 32) { # space + my $selected_win = get_window_match(); + ido_switch_select($selected_win); + + prev_match(); + update_window_select_prompt(); + + Irssi::signal_stop(); + + return; + } + + if ($key > 32) { # printable + $search_str .= chr($key); + + update_matches(); + update_window_select_prompt(); + + Irssi::signal_stop(); + return; + } + + # ignore all other keys. + Irssi::signal_stop(); +} + +ido_switch_init(); + +sub update_network_select_prompt { + + my @servers = map + { + { + name => $_->{tag}, + type => 'SERVER', + active => 0, + e_pos => -1, + b_pos => -1, + hilight_field => 'name', + } + } Irssi::servers(); + + my $match_count = scalar @servers; + my $show_count = $ido_show_count; + my $match_string = '(no matches) '; + + $show_count = $match_count if $match_count < $show_count; + + if ($show_count > 0) { + _debug_print "Showing: $show_count matches"; + + my @ordered_matches = _order_matches(@servers); + my @display = @ordered_matches[0..$show_count - 1]; + + # show the first entry in green + + unshift(@display, _format_display_entry(shift(@display), '%g')); + + # and array-slice-map the rest to be red (or yellow for active) + @display[1..$#display] + = map + { + _format_display_entry($_, $_->{active}?'%y':'%r') + + } @display[1..$#display]; + + # join em all up + $match_string = join ', ', @display; + } + + my @indicators; + + # indicator if flex mode is being used (C-f to toggle) + push @indicators, $ido_use_flex; + push @indicators, 'Active' if $active_only; + + my $flex = sprintf(' %%k[%%n%s%%k]%%n ', join ',', @indicators); + + my $search = ''; + $search = (sprintf '`%s\': ', $search_str) if length $search_str; + $search = (sprintf '`%%R%s%%n\': ', $search_str) if (length $search_str && !$regex_valid); + + Irssi::signal_emit('change prompt', $flex . $search . $match_string, + 'UP_INNER'); + +} diff --git a/scripts/ircuwhois.pl b/scripts/ircuwhois.pl new file mode 100644 index 0000000..4a01de0 --- /dev/null +++ b/scripts/ircuwhois.pl @@ -0,0 +1,84 @@ +use strict; +use Irssi; +use vars qw($VERSION %IRSSI); + +$VERSION = '1.2'; + +%IRSSI = ( + authors => 'Valentin Batz', + contact => 'vb\@g-23.org', + name => 'ircuwhois', + description => 'show the accountname (330) and real host on ircu', + license => 'GPLv2', + url => 'http://www.hurzelgnom.homepage.t-online.de/irssi/scripts/quakenet.pl' +); + +# adapted by Nei + +Irssi::theme_register([ + 'whois_auth', '{whois account %|$1}', + 'whois_ip', '{whois actualip %|$1}', + 'whois_host', '{whois act.host %|$1}', + 'whois_oper', '{whois privile. %|$1}', + 'whois_ssl', '{whois connect. %|$1}' +]); + +sub event_whois_default_event { + #'server event', SERVER_REC, char *data, char *sender_nick, char *sender_address + my ($server, $data, $snick, $sender) = @_; + my $numeric = $server->parse_special('$H'); + if ($numeric eq '313') { &event_whois_oper } + if ($numeric eq '330') { &event_whois_auth } + if ($numeric eq '337') { &event_whois_ssl } + if ($numeric eq '338') { &event_whois_userip } +} + +sub event_whois_oper { + my ($server, $data) = @_; + my ($num, $nick, $privileges) = split(/ /, $data, 3); + $privileges =~ s/^:(?:is an? )?//; + $server->printformat($nick, MSGLEVEL_CRAP, 'whois_oper', $nick, $privileges); + Irssi::signal_stop(); +} + +sub event_whois_auth { + my ($server, $data) = @_; + my ($num, $nick, $auth_nick, $isircu) = split(/ /, $data, 4); + return unless $isircu =~ / as/; #:is logged in as + $server->printformat($nick, MSGLEVEL_CRAP, 'whois_auth', $nick, $auth_nick); + Irssi::signal_stop(); +} + +sub event_whois_ssl { + my ($server, $data) = @_; + my ($num, $nick, $connection) = split(/ /, $data, 3); + $connection =~ s/^:(?:is using an? )?//; + $server->printformat($nick, MSGLEVEL_CRAP, 'whois_ssl', $nick, $connection); + Irssi::signal_stop(); +} + +sub event_whois_userip { + my ($server, $data) = @_; + my ($num, $nick, $userhost, $ip, $isircu) = split(/ /, $data, 5); + return unless $isircu =~ /ctual /; #:Actual user@host, Actual IP + $server->printformat($nick, MSGLEVEL_CRAP, 'whois_ip', $nick, $ip); + $server->printformat($nick, MSGLEVEL_CRAP, 'whois_host', $nick, $userhost); + Irssi::signal_stop(); +} + +sub debug { + use Data::Dumper; + Irssi::print(Dumper(\@_)); +} +Irssi::signal_register({ + 'whois oper' => [ 'iobject', 'string', 'string', 'string' ], +}); # fixes oper display in 0.8.10 +Irssi::signal_add({ + 'whois oper' => 'event_whois_oper', + 'event 313' => 'event_whois_oper', + 'event 330' => 'event_whois_auth', + 'event 337' => 'event_whois_ssl', + 'event 338' => 'event_whois_userip', + 'whois default event' => 'event_whois_default_event', +}); + 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/messages_bottom.pl b/scripts/messages_bottom.pl new file mode 100644 index 0000000..8bf329d --- /dev/null +++ b/scripts/messages_bottom.pl @@ -0,0 +1,29 @@ +use strict; +use Irssi (); +use vars qw($VERSION %IRSSI); + +$VERSION = '1.0'; + +%IRSSI = ( + authors => 'Wouter Coekaerts', + contact => 'coekie@irssi.org', + name => 'messages_bottom', + description => 'makes all window text start at the bottom of windows', + license => q(send-me-beer-or-i'll-sue-you-if-you-use-it license), + url => 'http://bugs.irssi.org/index.php?do=details&id=290' +); + +########################## +# +# add this line to the very top of your ~/.irssi/startup file: +# +# script exec Irssi::active_win->print('\n' x Irssi::active_win->{'height'}, Irssi::MSGLEVEL_NEVER) +# +# + +Irssi::signal_add_last + 'window created' => sub { + my $win = shift; + $win->print( + "\n" x $win->{'height'}, + Irssi::MSGLEVEL_NEVER ) } diff --git a/scripts/mouse-awl.pl b/scripts/mouse-awl.pl new file mode 100644 index 0000000..f395199 --- /dev/null +++ b/scripts/mouse-awl.pl @@ -0,0 +1,144 @@ +# cooperates with adv_windowlist +# See http://wouter.coekaerts.be/site/irssi/mouse +# based on irssi mouse patch by mirage: http://darksun.com.pt/mirage/irssi/ + +# Copyright (C) 2005-2009 Wouter Coekaerts <wouter@coekaerts.be> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +use strict; +use Irssi qw(signal_emit settings_get_str active_win signal_stop settings_add_str settings_add_bool settings_get_bool signal_add signal_add_first); +use Math::Trig; + +use vars qw($VERSION %IRSSI); + +$VERSION = '1.0.0-awl'; +%IRSSI = ( + authors => 'Wouter Coekaerts', + contact => 'wouter@coekaerts.be', + name => 'mouse', + description => 'control irssi using mouse clicks and gestures', + license => 'GPLv2 or later', + url => 'http://wouter.coekaerts.be/irssi/', + changed => '2009-05-16', +); + +my @BUTTONS = ('', '_middle', '_right'); + +my $mouse_xterm_status = -1; # -1:off 0,1,2:filling mouse_xterm_combo +my @mouse_xterm_combo = (3, 0, 0); # 0:button 1:x 2:y +my @mouse_xterm_previous; # previous contents of mouse_xterm_combo + +sub mouse_enable { + Irssi::command '^set awl_mouse on' +} + +sub mouse_disable { + Irssi::command '^set awl_mouse off' +} + +# Handle mouse event (button press or release) +sub mouse_event { + my ($b, $x, $y, $oldb, $oldx, $oldy) = @_; + Irssi::signal_stop(); + my ($xd, $yd); + my ($distance, $angle); + + # uhm, in the patch the scrollwheel didn't work for me, but this does: + if ($b == 64) { + cmd("mouse_scroll_up"); + } elsif ($b == 65) { + cmd("mouse_scroll_down") + } + + # proceed only if a button is being released + return if ($b != 3); + + return unless (0 <= $oldb && $oldb <= 2); + my $button = $BUTTONS[$oldb]; + + # if it was a mouse click of the left button (press and release in the same position) + if ($x == $oldx && $y == $oldy) { + cmd("mouse" . $button . "_click"); + return; + } + + # otherwise, find mouse gestures + $xd = $x - $oldx; + $yd = -1 * ($y - $oldy); + $distance = sqrt($xd*$xd + $yd*$yd); + # ignore small gestures + if ($distance < 3) { + return; + } + $angle = asin($yd/$distance) * 180 / 3.14159265358979; + if ($angle < 20 && $angle > -20 && $xd > 0) { + if ($distance <= 40) { + cmd("mouse" . $button . "_gesture_right"); + } else { + cmd("mouse" . $button . "_gesture_bigright"); + } + } elsif ($angle < 20 && $angle > -20 && $xd < 0) { + if ($distance <= 40) { + cmd("mouse" . $button . "_gesture_left"); + } else { + cmd("mouse" . $button . "_gesture_bigleft"); + } + } elsif ($angle > 40) { + cmd("mouse" . $button . "_gesture_up"); + } elsif ($angle < -40) { + cmd("mouse" . $button . "_gesture_down"); + } +} + +# executes the command configured in the given setting +sub cmd +{ + my ($setting) = @_; + signal_emit("send command", settings_get_str($setting), active_win->{'active_server'}, active_win->{'active'}); +} + +Irssi::command_bind 'mouse' => sub { + my ($data, $server, $item) = @_; + $data =~ s/\s+$//g; + Irssi::command_runsub('mouse', $data, $server, $item); +}; + +# temporarily disable mouse handling. Useful for copy-pasting without touching the keyboard (pressing shift) +Irssi::command_bind 'mouse tempdisable' => sub { + my ($data, $server, $item) = @_; + my $seconds = ($data eq '') ? 5 : $data; # optional argument saying how many seconds, defaulting to 5 + mouse_disable(); + Irssi::timeout_add_once($seconds * 1000, 'mouse_enable', undef); # turn back on after $second seconds +}; + +for my $button (@BUTTONS) { + settings_add_str("lookandfeel", "mouse" . $button . "_click", "/mouse tempdisable 5"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_up", "/window last"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_down", "/window goto active"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_left", "/window prev"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_bigleft", "/eval window prev;window prev"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_right", "/window next"); + settings_add_str("lookandfeel", "mouse" . $button . "_gesture_bigright", "/eval window next;window next"); +} + +settings_add_str("lookandfeel", "mouse_scroll_up", "/scrollback goto -10"); +settings_add_str("lookandfeel", "mouse_scroll_down", "/scrollback goto +10"); + +Irssi::signal_register({ + 'gui mouse' => [qw/int int int int int int/], + }); + +Irssi::signal_add_priority('gui mouse' => 'mouse_event', 101); diff --git a/scripts/mouse_soliton.pl b/scripts/mouse_soliton.pl new file mode 100644 index 0000000..91f86e0 --- /dev/null +++ b/scripts/mouse_soliton.pl @@ -0,0 +1,146 @@ +# based on irssi mouse patch by mirage: http://darksun.com.pt/mirage/irssi/ +# It should probably indeed be done in C, and go into irssi, or as a module, +# but I translated it to perl just for the fun of it, and to prove it's possible maybe + +use strict; +use Irssi qw(signal_emit settings_get_str active_win signal_stop settings_add_str settings_add_bool settings_get_bool signal_add signal_add_first); +use Math::Trig; + +use vars qw($VERSION %IRSSI); + +$VERSION = '0.0.0'; +%IRSSI = ( + authors => 'Wouter Coekaerts', + contact => 'wouter@coekaerts.be', + name => 'trigger', + description => 'experimental perl version of the irssi mouse patch', + license => 'GPLv2', + url => 'http://wouter.coekaerts.be/irssi/', + changed => '2005-11-21', +); + +# minor changes by Soliton: +# added mouse_enable and mouse_disable functions to make for example copy & pasting possible for a second after clicking with the left mouse button +# also changed the mouse button for the gestures to the right button + +my $mouse_xterm_status = -1; # -1:off 0,1,2:filling mouse_xterm_combo +my @mouse_xterm_combo; # 0:button 1:x 2:y +my @mouse_xterm_previous; # previous contents of mouse_xterm_combo + +sub mouse_enable { + print STDERR "\e[?1000h"; # start tracking +} + +sub mouse_disable { + print STDERR "\e[?1000l"; # stop tracking + Irssi::timeout_add_once(2000, 'mouse_enable', undef); # turn back on after 1 sec +} + +# Handle mouse event (button press or release) +sub mouse_event { + my ($b, $x, $y, $oldb, $oldx, $oldy) = @_; + my ($xd, $yd); + my ($distance, $angle); + + #print "DEBUG: mouse_event $b $x $y"; + + # uhm, in the patch the scrollwheel didn't work for me, but this does: + if ($b == 64) { + cmd("mouse_scroll_up"); + } elsif ($b == 65) { + cmd("mouse_scroll_down") + } + + # proceed only if a button is being released + return if ($b != 3); + + # if it was a mouse click of the left button (press and release in the same position) + if ($x == $oldx && $y == $oldy && $oldb == 0) { + #signal_emit("mouse click", $oldb, $x, $y); + #mouse_click($oldb, $x, $y); + mouse_disable(); + return; + } + + # otherwise, find mouse gestures on button + return if ($oldb != 2); + $xd = $x - $oldx; + $yd = -1 * ($y - $oldy); + $distance = sqrt($xd*$xd + $yd*$yd); + # ignore small gestures + if ($distance < 3) { + return; + } + $angle = asin($yd/$distance) * 180 / 3.14159265358979; + if ($angle < 20 && $angle > -20 && $xd > 0) { + if ($distance <= 40) { + cmd("mouse_gesture_right"); + } else { + cmd("mouse_gesture_bigright"); + } + } elsif ($angle < 20 && $angle > -20 && $xd < 0) { + if ($distance <= 40) { + cmd("mouse_gesture_left"); + } else { + cmd("mouse_gesture_bigleft"); + } + } elsif ($angle > 40) { + cmd("mouse_gesture_up"); + } elsif ($angle < -40) { + cmd("mouse_gesture_down"); + } +} + +sub cmd +{ + my ($setting) = @_; + signal_emit("send command", settings_get_str($setting), active_win->{'active_server'}, active_win->{'active'}); +} + + +signal_add_first("gui key pressed", sub { + my ($key) = @_; + if ($mouse_xterm_status != -1) { + if ($mouse_xterm_status == 0) { + @mouse_xterm_previous = @mouse_xterm_combo; + } + $mouse_xterm_combo[$mouse_xterm_status] = $key-32; + $mouse_xterm_status++; + if ($mouse_xterm_status == 3) { + $mouse_xterm_status = -1; + # match screen coordinates + $mouse_xterm_combo[1]--; + $mouse_xterm_combo[2]--; + # TODO signal_emit("mouse event", $mouse_xterm_combo[0], $mouse_xterm_combo[1], $mouse_xterm_combo[2], $mouse_xterm_previous[0], $mouse_xterm_previous[1], $mouse_xterm_previous[2]); + mouse_event($mouse_xterm_combo[0], $mouse_xterm_combo[1], $mouse_xterm_combo[2], $mouse_xterm_previous[0], $mouse_xterm_previous[1], $mouse_xterm_previous[2]); + } + signal_stop(); + } +}); + +sub sig_command_script_unload { + my $script = shift; + if ($script =~ /(.*\/)?$IRSSI{'name'}(\.pl)? *$/) { + print STDERR "\e[?1000l"; # stop tracking + } +} +Irssi::signal_add_first('command script load', 'sig_command_script_unload'); +Irssi::signal_add_first('command script unload', 'sig_command_script_unload'); + +if ($ENV{"TERM"} !~ /^rxvt|screen|xterm(-color)?$/) { + die "Your terminal doesn't seem to support this."; +} + +print STDERR "\e[?1000h"; # start tracking + +Irssi::command("/^bind meta-[M /mouse_xterm"); # FIXME evil +Irssi::command_bind("mouse_xterm", sub {$mouse_xterm_status = 0;}); + +settings_add_str("lookandfeel", "mouse_gesture_up", "/window last"); +settings_add_str("lookandfeel", "mouse_gesture_down", "/window goto active"); +settings_add_str("lookandfeel", "mouse_gesture_left", "/window prev"); +settings_add_str("lookandfeel", "mouse_gesture_bigleft", "/eval window prev;window prev"); +settings_add_str("lookandfeel", "mouse_gesture_right", "/window next"); +settings_add_str("lookandfeel", "mouse_gesture_bigright", "/eval window next;window next"); +settings_add_str("lookandfeel", "mouse_scroll_up", "/scrollback goto -10"); +settings_add_str("lookandfeel", "mouse_scroll_down", "/scrollback goto +10"); 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/nickcolor_gay.pl b/scripts/nickcolor_gay.pl new file mode 100644 index 0000000..61f84d8 --- /dev/null +++ b/scripts/nickcolor_gay.pl @@ -0,0 +1,83 @@ +use strict; +use warnings; + +our $VERSION = '0.1'; # fc5429f1bbac061 +our %IRSSI = ( + name => 'nickcolor_gay', + description => 'colourise nicks', + license => 'ISC', + ); + +use Hash::Util qw(lock_keys); +use Irssi; + + +{ package Irssi::Nick } + +my @action_protos = qw(irc silc xmpp); +my $lastnick; + +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); + $lastnick = $nickobj ? $nickobj->{nick} : undef; +} + +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(); +} + +{my %m; my $i = 16; for my $l ( +qw(E T A O I N S H R D L C U M W F G Y P B V K J X Q Z), +qw(0 1 2 3 4 5 6 7 8 9), +qw(e t a o i n s h r d l c u m w f g y p b v k j x q z), +qw(_ - [ ] \\ ` ^ { } ~), +) { + $m{$l}=$i++; +} + +sub rainbow { + my $nick = shift; + $nick =~ s/(.)/exists $m{$1} ? sprintf "\cC%02d%s", $m{$1}, $1 : $1/ge; + $nick +} +} + +sub prnt_clear_public { + return unless defined $lastnick; + my ($dest, $txt) = @_; + if ($dest->{level} & MSGLEVEL_PUBLIC) { + my @nick_reg; + unshift @nick_reg, quotemeta substr $lastnick, 0, $_ for 1 .. length $lastnick; + for my $nick_reg (@nick_reg) { + if ($txt =~ s/($nick_reg)/rainbow($1)/e) { + Irssi::signal_continue($dest, $txt, $_[2]); + last; + } + } + clear_ref(); + } +} + +sub clear_ref { + $lastnick = undef; +} + +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', +}); 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/recentdepart.pl b/scripts/recentdepart.pl new file mode 100644 index 0000000..f0cc121 --- /dev/null +++ b/scripts/recentdepart.pl @@ -0,0 +1,332 @@ +#!/usr/bin/perl -w +# +# recentdepart.pl +# +# irssi script +# +# This script, when loaded into irssi, will filter quit and parted messages +# for channels listed in recdep_channels for any nick whos last message was +# more then a specified time ago. +# +# It also filters join messages showing only join messages for nicks who recently +# parted. +# +# [settings] +# recdep_channels +# - Should contain a list of chatnet and channel names that recentdepart +# should monitor. Its format is a spcae delimited list of chatnet/#channel +# pairs. Either chatnet or channel are optional but adding a / makes it +# explicitly recognized as a chatnet or a channel name. A special case is just a +# "*" which turns it on globally. +# +# "#irrsi #perl" - enables filtering on the #irssi and #perl +# channels on all chatnets. +# +# "freenode IRCNet/#irssi" - enables filtering for all channels on frenode +# and only the #irssi channel on IRCNet. +# +# "freenode/" - force "freenode" to be interpreted as the chatnet +# name by adding a / to the end. +# +# "/freenode" - forces "freenode" to be interpreted as the channel +# by prefixing it with the / delimiter. +# +# "*" - globally enables filtering. +# +# recdep_period +# - specifies the window of time, after a nick last spoke, for which quit/part +# notices will be let through the filter. +# +# recdep_rejoin +# - specifies a time period durring which a join notice for someone rejoining will +# be shown. Join messages are filtered if the nicks part/quit message was filtered +# or if the nick is gone longer then the rejoin period. +# Set to 0 to turn off filtering of join notices. +# +# recdep_nickperiod +# - specifies a window of time like recdep_period that is used to filter nick change +# notices. Set to 0 to turn off filtering of nick changes. +# +# recdep_use_hideshow +# - whether to use hideshow script instead of ignoring +# + +use strict; +use warnings; +use Irssi; +use Irssi::Irc; + +our $VERSION = "0.6"; +our %IRSSI = ( + authors => 'Matthew Sytsma', + contact => 'spiderpigy@yahoo.com', + name => 'Recently Departed', + description => 'Filters quit/part/join/nick notices based on time since last message. (Similar to weechat\'s smartfilter).', + license => 'GNU GPLv2 or later', + url => '', +); + +# store a hash of configure selected servers/channels +my %chanlist; +# Track recent times by server/nick/channel +# (it is more optimal to go nick/channel then channel/nick because some quit signals are by server not by channel. +# We will only have to loop through open channels that a nick has spoken in which should be less then looping +# through all the monitored channels looking for the nick. +my %nickhash=(); +# Track recent times for parted nicks by server/channel/nick +my %joinwatch=(); +my $use_hide; + +sub on_setup_changed { + my %old_chanlist = %chanlist; + %chanlist = (); + my @pairs = split(/ /, Irssi::settings_get_str("recdep_channels")); + + $use_hide = Irssi::settings_get_bool("recdep_use_hideshow"); + foreach (@pairs) + { + my ($net, $chan, $more) = split(/\//); + if ($more) + { + /\/(.+)/; + $chan = $1; + } +# Irssi::active_win()->print("Initial Net: $net Chan: $chan"); + if (!$net) + { + $net = '*'; + } + + if ($net =~ /^[#!@&]/ && !$chan) + { + $chan = $net; + $net = "*"; + } + + if (!$chan) + { + $chan = "*"; + } + + $chanlist{$net}{$chan} = 1; + } + + # empty the storage in case theres a channel or server we are no longer filtering + %nickhash=(); + %joinwatch=(); +} + +sub check_channel +{ + my ($server, $channel) = @_; + + # quits dont have a channel so we need to see if this server possibly contains this channel + if (!$channel || $channel eq '*') + { + # see if any non chatnet channel listings are open on this server + if (keys %{ $chanlist{'*'} }) + { + foreach my $chan (keys %{ $chanlist{'*'} }) + { + if ($chan eq '*' || $server->channel_find($chan)) + { + return 1; + } + } + } + + # see if there were any channels listed for this chatnet + if (keys %{ $chanlist{$server->{'chatnet'}} }) + { return 1; } + else + { return 0; } + } + + # check for global channel matches and pair matches + return (($chanlist{'*'}{'*'}) || + ($chanlist{'*'}{$channel}) || + ($chanlist{$server->{'chatnet'}}{'*'}) || + ($chanlist{$server->{'chatnet'}}{$channel})); +} + +# Hook for quitting +sub on_quit +{ + my ($server, $nick, $address, $reason) = @_; + + if ($server->{'nick'} eq $nick) + { return; } + + if (check_channel($server, '*')) + { + my $recent = 0; + foreach my $chan (keys %{ $nickhash{$server->{'tag'}}{lc($nick)} }) + { + if (time() - $nickhash{$server->{'tag'}}{lc($nick)}{$chan} < Irssi::settings_get_int("recdep_period")) + { + $recent = 1; + + if (Irssi::settings_get_int("recdep_rejoin") > 0) + { + $joinwatch{$server->{'tag'}}{$chan}{lc($nick)} = time(); + } + } + } + + delete $nickhash{$server->{'tag'}}{lc($nick)}; + + if (!$recent) + { + $use_hide ? $Irssi::scripts::hideshow::hide_next = 1 + : Irssi::signal_stop(); + } + } +} + +# Hook for parting +sub on_part +{ + my ($server, $channel, $nick, $address, $reason) = @_; + + # cleanup if its you who left a channel + if ($server->{'nick'} eq $nick) + { + # slightly painfull cleanup but we shouldn't hit this as often + foreach my $nickd (keys %{ $nickhash{$server->{'tag'}} }) + { + delete $nickhash{$server->{'tag'}}{$nickd}{$channel}; + if (!keys(%{ $nickhash{$server->{'tag'}}{$nickd} })) + { + delete $nickhash{$server->{'tag'}}{$nickd}; + } + } + delete $joinwatch{$server->{'tag'}}{$channel}; + } + elsif (check_channel($server, $channel)) + { + if (time() - $nickhash{$server->{'tag'}}{lc($nick)}{$channel} > Irssi::settings_get_int("recdep_period")) + { + $use_hide ? $Irssi::scripts::hideshow::hide_next = 1 + : Irssi::signal_stop(); + } + elsif (Irssi::settings_get_int("recdep_rejoin") > 0) + { + $joinwatch{$server->{'tag'}}{$channel}{lc($nick)} = time(); + } + + delete $nickhash{$server->{'tag'}}{lc($nick)}{$channel}; + if (!keys(%{ $nickhash{$server->{'tag'}}{lc($nick)} })) + { + delete $nickhash{$server->{'tag'}}{lc($nick)}; + } + } +} + +# Hook for public messages. +sub on_public +{ + my ($server, $msg, $nick, $addr, $target) = @_; + + if (!$target) { return; } + if ($nick =~ /^#/) { return; } + + if ($server->{'nick'} eq $nick) { return; } + + if (check_channel($server, $target)) + { + $nickhash{$server->{'tag'}}{lc($nick)}{$target} = time(); + } +} + +# Hook for people joining +sub on_join +{ + my ($server, $channel, $nick, $address) = @_; + + if ($server->{'nick'} eq $nick) + { return; } + + if (Irssi::settings_get_int("recdep_rejoin") == 0) + { return; } + + if (check_channel($server, $channel)) + { + if (time() - $joinwatch{$server->{'tag'}}{$channel}{lc($nick)} > Irssi::settings_get_int("recdep_rejoin")) + { + $use_hide ? $Irssi::scripts::hideshow::hide_next = 1 + : Irssi::signal_stop(); + } + } + + # loop through and delete all old nicks from the rejoin hash + # this should be a small loop because it will only inlude nicks who recently left channel and who + # passed the part message filter + foreach my $nickd (keys %{ $joinwatch{$server->{'tag'}}{$channel} }) + { + if (time() - $joinwatch{$server->{'tag'}}{$channel}{lc($nickd)} < Irssi::settings_get_int("recdep_rejoin")) + { next; } + + delete $joinwatch{$server->{'tag'}}{$channel}{lc($nickd)}; + } + if (!keys(%{ $joinwatch{$server->{'tag'}}{$channel} })) + { + delete $joinwatch{$server->{'tag'}}{$channel}; + } +} + +# Hook for nick changes +sub on_nick +{ + my ($server, $new, $old, $address) = @_; + + if ($server->{'nick'} eq $old || $server->{'nick'} eq $new) + { return; } + + if (check_channel($server, '*')) + { + my $recent = 0; + foreach my $chan (keys %{ $nickhash{$server->{'tag'}}{lc($old)} }) + { + if (time() - $nickhash{$server->{'tag'}}{lc($old)}{$chan} < Irssi::settings_get_int("recdep_nickperiod")) + { + $recent = 1; + } + } + + if (!$recent && Irssi::settings_get_int("recdep_nickperiod") > 0) + { + $use_hide ? $Irssi::scripts::hideshow::hide_next = 1 + : Irssi::signal_stop(); + } + + delete $nickhash{$server->{'tag'}}{lc($old)}; + } +} + + +# Hook for cleanup on server quit +sub on_serverquit +{ + my ($server, $msg) = @_; + + delete $nickhash{$server->{'tag'}}; + delete $joinwatch{$server->{'tag'}}; +} + +# Setup hooks on events +Irssi::signal_add_last("message public", "on_public"); +Irssi::signal_add_last("message part", "on_part"); +Irssi::signal_add_last("message quit", "on_quit"); +Irssi::signal_add_last("message nick", "on_nick"); +Irssi::signal_add_last("message join", "on_join"); +Irssi::signal_add_last("server disconnected", "on_serverquit"); +Irssi::signal_add_last("server quit", "on_serverquit"); +Irssi::signal_add('setup changed', "on_setup_changed"); + +# Add settings +Irssi::settings_add_str("recdentpepart", "recdep_channels", '*'); +Irssi::settings_add_int("recdentpepart", "recdep_period", 600); +Irssi::settings_add_int("recdentpepart", "recdep_rejoin", 120); +Irssi::settings_add_int("recdentpepart", "recdep_nickperiod", 600); +Irssi::settings_add_bool("recdentpepart", "recdep_use_hideshow", 0); +on_setup_changed(); diff --git a/scripts/sb_position.pl b/scripts/sb_position.pl new file mode 100644 index 0000000..a578094 --- /dev/null +++ b/scripts/sb_position.pl @@ -0,0 +1,112 @@ + +# Display current position in scrollback in a statusbar item named 'position'. + +# Copyright (C) 2010 Simon Ruderich & Tom Feist +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +use strict; +use warnings; + +use Irssi; +use Irssi::TextUI; +use POSIX qw(ceil); + +{ package Irssi::Nick } + +our $VERSION = '0.1'; # ca1d079b9abed12 +our %IRSSI = ( + authors => 'Simon Ruderich, Tom Feist', + contact => 'simon@ruderich.org, shabble+irssi@metavore.org', + name => 'sb_position', + description => 'Displays current position in scrollback.', + license => 'GPLv3 or later', + changed => '2010-12-02' +); + +my ($buf, $size, $pos, $height, $empty); +my ($pages, $cur_page, $percent); + + +init(); + +sub init { + + # (re-)register it so we can access the WIN_REC object directly. + Irssi::signal_register({'gui page scrolled' => [qw/Irssi::UI::Window/]}); + # primary update signal. + Irssi::signal_add('gui page scrolled', \&update_position); + Irssi::statusbar_item_register('position', 0, 'position_statusbar'); + + Irssi::signal_add("window changed", \&update_position); + Irssi::signal_add_last("command clear", \&update_cmd_shim); + Irssi::signal_add_last("command scrollback", \&update_cmd_shim); + # Irssi::signal_add_last("gui print text finished", sig_statusbar_more_updated); + + update_position(Irssi::active_win()); +} + +sub update_cmd_shim { + my ($cmd, $server, $witem) = @_; + + my $win = $witem ? $witem->window : Irssi::active_win; + + update_position($win); +} + +sub update_position { + + my $win = shift; + return unless $win; + + my $view = $win->view; + + $pos = $view->{ypos}; + $buf = $view->{buffer}; + $height = $view->{height}; + $size = $buf->{lines_count}; + + $empty = $view->{empty_linecount}; + $empty = 0 unless $empty; + + + $pages = ceil($size / $height); + $pages = 1 unless $pages; + + my $buf_pos_cache = $size - $pos + ($height - $empty) - 1; + + if ($pos == -1 or $size < $height) { + $cur_page = $pages; + $percent = 100; + } elsif ($pos > ($size - $height)) { + $cur_page = 1; + $percent = 0; + } else { + $cur_page = ceil($buf_pos_cache / $height); + $percent = ceil($buf_pos_cache / $size * 100); + } + + Irssi::statusbar_items_redraw('position'); +} + +sub position_statusbar { + my ($statusbar_item, $get_size_only) = @_; + + # Alternate view. + #my $sb = "p=$pos, s=$size, h=$height, pp:$cur_page/$pages $percent%%"; + my $sb = "Page: $cur_page/$pages $percent%%"; + + $statusbar_item->default_handler($get_size_only, "{sb $sb}", 0, 1); +} 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'; diff --git a/scripts/tmux-nicklist-portable.pl b/scripts/tmux-nicklist-portable.pl new file mode 100644 index 0000000..d4da3dc --- /dev/null +++ b/scripts/tmux-nicklist-portable.pl @@ -0,0 +1,390 @@ +# based on the nicklist.pl script +################################################################################ +# tmux_nicklist.pl +# This script integrates tmux and irssi to display a list of nicks in a +# vertical right pane with 20% width. Right now theres no configuration +# or setup, simply initialize the script with irssi and by default you +# will get the nicklist for every channel(customize by altering +# the regex in /set nicklist_channel_re) +# +# /set nicklist_channel_re <regex> +# * only show on channels matching this regular expression +# +# /set nicklist_max_users <num> +# * only show when the channel has so many users or less (0 = always) +# +# /set nicklist_smallest_main <num> +# * only show when main window is larger than this (0 = always) +# +# /set nicklist_pane_width <num> +# * width of the nicklist pane +# +# /set nicklist_color <ON|OFF> +# * colourise the nicks in the nicklist (required nickcolor script +# with get_nick_color2 and debug_ansicolour functions) +# +# +# It supports mouse scrolling and the following keys: +# k/up arrow: up one line +# j/down arrow: down one line +# pageup: up 20 lines +# pagedown: down 20 lines +# gg: go to top +# G: go to bottom +# +# For better integration, unrecognized sequences will be sent to irssi and +# its pane will be focused. +################################################################################ + +use strict; +use warnings; +use IO::Handle; +use IO::Select; +use POSIX; +use File::Temp qw/ :mktemp /; +use File::Basename; +our $VERSION = '0.1.3'; # 82c05732e50bb62 +our %IRSSI = ( + authors => 'Thiago de Arruda', + contact => 'tpadilha84@gmail.com', + name => 'tmux-nicklist', + description => 'displays a list of nicks in a separate tmux pane', + license => 'WTFPL', +); + +# "other" prefixes by danielg4 <daniel@gimpelevich.san-francisco.ca.us> + +{ package Irssi::Nick } + +if ($#ARGV == -1) { +require Irssi; + +my $enabled = 0; +my $script_path = __FILE__; +my $tmpdir; +my $fifo_path; +my $fifo; +my $just_launched; +my $resize_timer; + +sub enable_nicklist { + return if ($enabled); + $tmpdir = mkdtemp Irssi::get_irssi_dir()."/nicklist-XXXXXXXX"; + $fifo_path = "$tmpdir/fifo"; + POSIX::mkfifo($fifo_path, 0600) or die "can't mkfifo $fifo_path: $!"; + my $cmd = "perl $script_path $fifo_path $ENV{TMUX_PANE}"; + my $width = Irssi::settings_get_int('nicklist_pane_width'); + system('tmux', 'split-window', '-dh', '-l', $width, '-t', $ENV{TMUX_PANE}, $cmd); + open_fifo(); + Irssi::timeout_remove($just_launched) if defined $just_launched; + $just_launched = Irssi::timeout_add_once(300, sub { $just_launched = undef; }, ''); +} + +sub open_fifo { + # The next system call will block until the other pane has opened the pipe + # for reading, so synchronization is not an issue here. + open $fifo, ">", $fifo_path or do { + if ($! == 4) { + Irssi::timeout_add_once(300, \&open_fifo, ''); + $enabled = -1 unless $enabled; + return; + } + die "can't open $fifo_path: $!"; + }; + $fifo->autoflush(1); + if ($enabled < -1) { + $enabled = 1; + disable_nicklist(); + } elsif ($enabled == -1) { + $enabled = 1; + reset_nicklist(); + } else { + $enabled = 1; + } +} + +sub disable_nicklist { + return unless ($enabled); + if ($enabled > 0) { + print $fifo "EXIT\n"; + close $fifo; + $fifo = undef; + unlink $fifo_path; + rmdir $tmpdir; + } + $enabled--; +} + +sub reset_nicklist { + my $active = Irssi::active_win(); + my $channel = $active->{active}; + my ($colourer, $ansifier); + if (Irssi::settings_get_bool('nicklist_color')) { + for my $script (sort map { my $z = $_; $z =~ s/::$//; $z } grep { /^nickcolor|nm/ } keys %Irssi::Script::) { + if ($colourer = "Irssi::Script::$script"->can('get_nick_color2')) { + $ansifier = "Irssi::Script::$script"->can('debug_ansicolour'); + last; + } + } + } + my $channel_pattern = Irssi::settings_get_str('nicklist_channel_re'); + { local $@; + $channel_pattern = eval { qr/$channel_pattern/ }; + $channel_pattern = qr/(?!)/ if $@; + } + + if (!$channel || !ref($channel) + || !$channel->isa('Irssi::Channel') + || !$channel->{'names_got'} + || $channel->{'name'} !~ $channel_pattern) { + disable_nicklist; + } else { + my %colour; + my @nicks = $channel->nicks(); + my $max_nicks = Irssi::settings_get_int('nicklist_max_users'); + if ($max_nicks && @nicks > $max_nicks) { + disable_nicklist; + } else { + enable_nicklist; + return unless $enabled > 0; + foreach my $nick (sort { $a->{_irssi} <=> $b->{_irssi} } @nicks) { + $colour{$nick->{nick}} = ($ansifier && $colourer) ? $ansifier->($colourer->($active->{active}{server}{tag}, $channel->{name}, $nick->{nick}, 0)) : ''; + } + print($fifo "BEGIN\n"); + foreach my $nick (sort {(($a->{'op'}?'1':$a->{'halfop'}?'2':$a->{'voice'}?'3':$a->{'other'}>32?'0':'4').lc($a->{'nick'})) + cmp (($b->{'op'}?'1':$b->{'halfop'}?'2':$b->{'voice'}?'3':$b->{'other'}>32?'0':'4').lc($b->{'nick'}))} @nicks) { + my $colour = $colour{$nick->{nick}} || "\e[39m"; + $colour = "\e[37m" if $nick->{'gone'}; + print($fifo "NICK"); + if ($nick->{'op'}) { + print($fifo "\e[32m\@$colour$nick->{'nick'}\e[39m"); + } elsif ($nick->{'halfop'}) { + print($fifo "\e[34m%$colour$nick->{'nick'}\e[39m"); + } elsif ($nick->{'voice'}) { + print($fifo "\e[33m+$colour$nick->{'nick'}\e[39m"); + } elsif ($nick->{'other'}>32) { + print($fifo "\e[31m".(chr $nick->{'other'})."$colour$nick->{'nick'}\e[39m"); + } else { + print($fifo " $colour$nick->{'nick'}\e[39m"); + } + print($fifo "\n"); + } + print($fifo "END\n"); + } + } +} + +sub switch_channel { + print $fifo "SWITCH_CHANNEL\n" if $fifo; + reset_nicklist; +} + +sub resized_timed { + Irssi::timeout_remove($resize_timer) if defined $resize_timer; + return if defined $just_launched; + $resize_timer = Irssi::timeout_add_once(1100, \&resized, ''); + #resized(); +} +sub resized { + $resize_timer = undef; + return if defined $just_launched; + return unless $enabled > 0; + disable_nicklist; + Irssi::timeout_add_once(200, \&reset_nicklist, ''); +} +sub UNLOAD { + disable_nicklist; +} + +Irssi::settings_add_str('tmux_nicklist', 'nicklist_channel_re', '.*'); +Irssi::settings_add_int('tmux_nicklist', 'nicklist_max_users', 0); +Irssi::settings_add_int('tmux_nicklist', 'nicklist_smallest_main', 0); +Irssi::settings_add_int('tmux_nicklist', 'nicklist_pane_width', 13); +Irssi::settings_add_bool('tmux_nicklist', 'nicklist_color', 1); +Irssi::signal_add_last('window item changed', \&switch_channel); +Irssi::signal_add_last('window changed', \&switch_channel); +Irssi::signal_add_last('channel joined', \&switch_channel); +Irssi::signal_add('nicklist new', \&reset_nicklist); +Irssi::signal_add('nicklist remove', \&reset_nicklist); +Irssi::signal_add('nicklist changed', \&reset_nicklist); +Irssi::signal_add_first('nick mode changed', \&reset_nicklist); +Irssi::signal_add('gui exit', \&disable_nicklist); +Irssi::signal_add_last('terminal resized', \&resized_timed); + +} else { +my $fifo_path = $ARGV[0]; +my $irssi_pane = $ARGV[1]; +# array to store the current channel nicknames +my @nicknames = (); + +# helper functions for manipulating the terminal +# escape sequences taken from +# http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html +sub enable_mouse { print "\e[?1000h"; } +# recognized sequences +my $MOUSE_SCROLL_DOWN="\e[Ma"; +my $MOUSE_SCROLL_UP="\e[M`"; +my $ARROW_DOWN="\e[B"; +my $ARROW_UP="\e[A"; +my $UP="k"; +my $DOWN="j"; +my $PAGE_DOWN="\e[6~"; +my $PAGE_UP="\e[5~"; +my $GO_TOP="gg"; +my $GO_BOTTOM="G"; + +my $current_line = 0; +my $sequence = ''; +my ($rows, $cols); + +sub term_size { + split ' ', `stty size`; +} + +sub redraw { + my $last_nick_idx = @nicknames; + my $last_idx = $current_line + $rows; + # normalize last visible index + if ($last_idx > ($last_nick_idx)) { + $last_idx = $last_nick_idx; + } + # redraw visible nicks + for my $i (reverse 1..$rows) { + print "\e[$i;1H\e[K"; + my $idx = $current_line + $i - 1; + if ($idx < $last_idx) { + my $z = 0; my $col = $cols; + for (split /(\e\[(?:\d|;|:|\?|\s)*.)/, $nicknames[$idx]) { + if ($z ^= 1) { + print +(substr $_, 0, $col) if $col > 0; + $col -= length; + } else { + print + } + } + } + } +} + +sub move_down { + $sequence = ''; + my $count = int $_[0]; + my $nickcount = $#nicknames; + return if ($nickcount <= $rows); + if ($count == -1) { + $current_line = $nickcount - $rows - 1; + redraw; + return; + } + my $visible = $nickcount - $current_line - $count; + if ($visible > $rows) { + $current_line += $count; + redraw; + } elsif (($visible + $count) > $rows) { + # scroll the maximum we can + $current_line = $nickcount - $rows - 1; + redraw; + } +} + +sub move_up { + $sequence = ''; + my $count = int $_[0]; + if ($count == -1) { + $current_line = 0; + redraw; + return; + } + return if ($current_line == 0); + $count = 1 if $count == 0; + $current_line -= $count; + $current_line = 0 if $current_line < 0; + redraw; +} + +$SIG{INT} = 'IGNORE'; + +STDOUT->autoflush(1); +# setup terminal so we can listen for individual key presses without echo +`stty -icanon -echo`; + +# open named pipe and setup the 'select' wrapper object for listening on both +# fds(fifo and sdtin) +open my $fifo, "<", $fifo_path or die "can't open $fifo_path: $!"; +my $select = IO::Select->new(); +my @ready; +$select->add($fifo); +$select->add(\*STDIN); + +enable_mouse; +system('tput', 'smcup'); +print "\e[?7l"; #system('tput', 'rmam'); +system('tput', 'civis'); +MAIN: { + while (@ready = $select->can_read) { + foreach my $fd (@ready) { + ($rows, $cols) = term_size; + if ($fd == $fifo) { + while (<$fifo>) { + my $line = $_; + if ($line =~ /^BEGIN/) { + @nicknames = (); + } elsif ($line =~ /^SWITCH_CHANNEL/) { + $current_line = 0; + } elsif ($line =~ /^NICK(.+)$/) { + push @nicknames, $1; + } elsif ($line =~ /^END$/) { + redraw; + last; + } elsif ($line =~ /^EXIT$/) { + last MAIN; + } + } + } else { + my $key = ''; + sysread(STDIN, $key, 1); + $sequence .= $key; + if ($MOUSE_SCROLL_DOWN =~ /^\Q$sequence\E/) { + if ($MOUSE_SCROLL_DOWN eq $sequence) { + move_down 3; + # mouse scroll has two more bytes that I dont use here + # so consume them now to avoid sending unwanted bytes to + # irssi + sysread(STDIN, $key, 2); + } + } elsif ($MOUSE_SCROLL_UP =~ /^\Q$sequence\E/) { + if ($MOUSE_SCROLL_UP eq $sequence) { + move_up 3; + sysread(STDIN, $key, 2); + } + } elsif ($ARROW_DOWN =~ /^\Q$sequence\E/) { + move_down 1 if ($ARROW_DOWN eq $sequence); + } elsif ($ARROW_UP =~ /^\Q$sequence\E/) { + move_up 1 if ($ARROW_UP eq $sequence); + } elsif ($DOWN =~ /^\Q$sequence\E/) { + move_down 1 if ($DOWN eq $sequence); + } elsif ($UP =~ /^\Q$sequence\E/) { + move_up 1 if ($UP eq $sequence); + } elsif ($PAGE_DOWN =~ /^\Q$sequence\E/) { + move_down $rows/2 if ($PAGE_DOWN eq $sequence); + } elsif ($PAGE_UP =~ /^\Q$sequence\E/) { + move_up $rows/2 if ($PAGE_UP eq $sequence); + } elsif ($GO_BOTTOM =~ /^\Q$sequence\E/) { + move_down -1 if ($GO_BOTTOM eq $sequence); + } elsif ($GO_TOP =~ /^\Q$sequence\E/) { + move_up -1 if ($GO_TOP eq $sequence); + } else { + # Unrecognized sequences will be send to irssi and its pane + # will be focused + system('tmux', 'send-keys', '-t', $irssi_pane, $sequence); + system('tmux', 'select-pane', '-t', $irssi_pane); + $sequence = ''; + } + } + } + } +} + +close $fifo; + +} diff --git a/scripts/trackbar22.pl b/scripts/trackbar22.pl new file mode 100644 index 0000000..30015a2 --- /dev/null +++ b/scripts/trackbar22.pl @@ -0,0 +1,500 @@ +# trackbar.pl +# +# This little script will do just one thing: it will draw a line each time you +# switch away from a window. This way, you always know just upto where you've +# been reading that window :) It also removes the previous drawn line, so you +# don't see double lines. +# +# redraw trackbar only works on irssi 0.8.17 or higher. +# +# +# Usage: +# +# The script works right out of the box, but if you want you can change +# the working by /set'ing the following variables: +# +# Setting: trackbar_style +# Description: This setting will be the color of your trackbar line. +# By default the value will be '%K', only Irssi color +# formats are allowed. If you don't know the color formats +# by heart, you can take a look at the formats documentation. +# You will find the proper docs on http://www.irssi.org/docs. +# +# Setting: trackbar_string +# Description: This is the string that your line will display. This can +# be multiple characters or just one. For example: '~-~-' +# The default setting is '-'. +# +# Setting: trackbar_use_status_window +# Description: If this setting is set to OFF, Irssi won't print a trackbar +# in the statuswindow +# +# Setting: trackbar_ignore_windows +# Description: A list of windows where no trackbar should be printed +# +# Setting: trackbar_print_timestamp +# Description: If this setting is set to ON, Irssi will print the formatted +# timestamp in front of the trackbar. +# +# Setting: trackbar_require_seen +# Description: Only clear the trackbar if it has been scrolled to. +# +# /mark is a command that will redraw the line at the bottom. +# +# Command: /trackbar, /trackbar goto +# Description: Jump to where the trackbar is, to pick up reading +# +# Command: /trackbar keep +# Description: Keep this window's trackbar where it is the next time +# you switch windows (then this flag is cleared again) +# +# Command: /mark, /trackbar mark +# Description: Remove the old trackbar and mark the bottom of this +# window with a new trackbar +# +# Command: /trackbar markvisible +# Description: Like mark for all visible windows +# +# Command: /trackbar markall +# Description: Like mark for all windows +# +# Command: /trackbar remove +# Description: Remove this window's trackbar +# +# Command: /trackbar removeall +# Description: Remove all windows' trackbars +# +# Command: /trackbar redraw +# Description: Force redraw of trackbars +# + + +# For bugreports and other improvements contact one of the authors. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this script; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +## + +use strict; +use warnings; +use Irssi; +use Irssi::TextUI; +use Encode; + +use POSIX qw(strftime); +use vars qw($VERSION %IRSSI); + +$VERSION = "2.2"; # cb3189a33c8e5f9 + +%IRSSI = ( + authors => 'Peter Leurs and Geert Hauwaerts', + contact => 'peter@pfoe.be', + patchers => 'Johan Kiviniemi (UTF-8), Uwe Dudenhoeffer (on-upgrade-remove-line)', + name => 'trackbar', + description => 'Shows a bar where you have last read a window.', + license => 'GNU General Public License', + url => 'http://www.pfoe.be/~peter/trackbar/', + changed => 'Fri Jan 23 23:59:11 2004', + commands => 'trackbar', +); + +## Comments and remarks. +# +# This script uses settings. +# Use /SET to change the value or /TOGGLE to switch it on or off. +# +# +# Tip: The command 'trackbar' is very usefull if you bind that to a key, +# so you can easily jump to the trackbar. Please see 'help bind' for +# more information about keybindings in Irssi. +# +# Command: /BIND meta2-P key F1 +# /BIND F1 command trackbar +# +## + +## Bugfixes and new items in this rewrite. +# +# * Remove all the trackbars before upgrading. +# * New setting trackbar_use_status_window to control the statuswindow trackbar. +# * New setting trackbar_print_timestamp to print a timestamp or not. +# * New command 'trackbar' to scroll up to the trackbar. +# * When resizing your terminal, Irssi will update all the trackbars to the new size. +# * When changing trackbar settings, change all the trackbars to the new settings. +# * New command 'trackbar mark' to draw a new trackbar (The old '/mark'). +# * New command 'trackbar markall' to draw a new trackbar in each window. +# * New command 'trackbar remove' to remove the trackbar from the current window. +# * New command 'trackbar removeall' to remove all the trackbars. +# * Don't draw a trackbar in empty windows. +# * Added a version check to prevent Irssi redraw errors. +# * Fixed a bookmark NULL versus 0 bug. +# * Fixed a remove-line bug in Uwe Dudenhoeffer his patch. +# * New command 'help trackbar' to display the trackbar commands. +# * Fixed an Irssi startup bug, now processing each auto-created window. +# +## + +## Known bugs and the todolist. +# +# Todo: * Instead of drawing a line, invert the line. +# +## + +sub cmd_help { + my ($args) = @_; + if ($args =~ /^trackbar *$/i) { + print CLIENTCRAP <<HELP +%9Syntax:%9 + +TRACKBAR +TRACKBAR GOTO +TRACKBAR KEEP +TRACKBAR MARK +TRACKBAR MARKVISIBLE +TRACKBAR MARKALL +TRACKBAR REMOVE +TRACKBAR REMOVEALL +TRACKBAR REDRAW + +%9Parameters:%9 + + GOTO: Jump to where the trackbar is, to pick up reading + KEEP: Keep this window's trackbar where it is the next time + you switch windows (then this flag is cleared again) + MARK: Remove the old trackbar and mark the bottom of this + window with a new trackbar + MARKVISIBLE: Like mark for all visible windows + MARKALL: Like mark for all windows + REMOVE: Remove this window's trackbar + REMOVEALL: Remove all windows' trackbars + REDRAW: Force redraw of trackbars + +%9Description:%9 + + Manage a trackbar. Without arguments, it will scroll up to the trackbar. + +%9Examples:%9 + + /TRACKBAR MARK + /TRACKBAR REMOVE +HELP + } +} + +Irssi::theme_register([ + 'trackbar_loaded', '%R>>%n %_Scriptinfo:%_ Loaded $0 version $1 by $2.', + 'trackbar_wrong_version', '%R>>%n %_Trackbar:%_ Please upgrade your client to 0.8.17 or above if you would like to use this feature of trackbar.', + 'trackbar_all_removed', '%R>>%n %_Trackbar:%_ All the trackbars have been removed.', + 'trackbar_not_found', '%R>>%n %_Trackbar:%_ No trackbar found in this window.', +]); + +my $old_irssi = Irssi::version < 20140701; +sub check_version { + if ($old_irssi) { + Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trackbar_wrong_version'); + return; + } else { + return 1; + } +} + +sub is_utf8 { + lc Irssi::settings_get_str('term_charset') eq 'utf-8' +} + +my (%config, %keep_trackbar, %unseen_trackbar); + +sub remove_one_trackbar { + my $win = shift; + my $view = shift || $win->view; + my $line = $view->get_bookmark('trackbar'); + if (defined $line) { + my $bottom = $view->{bottom}; + $view->remove_line($line); + $win->command('^scrollback end') if $bottom && !$win->view->{bottom}; + $view->redraw; + } +} + +sub add_one_trackbar { + my $win = shift; + my $view = shift || $win->view; + $win->print(line($win->{width}), MSGLEVEL_NEVER); + $view->set_bookmark_bottom('trackbar'); + $unseen_trackbar{ $win->{_irssi} } = 1; + $view->redraw; +} + +sub update_one_trackbar { + my $win = shift; + my $view = shift || $win->view; + my $force = shift; + my $ignored = win_ignored($win, $view); + remove_one_trackbar($win, $view) + if $force || !defined $force || !$ignored; + add_one_trackbar($win, $view) + if $force || !$ignored; +} + +sub win_ignored { + my $win = shift; + my $view = shift || $win->view; + return 1 unless $view->{buffer}{lines_count}; + return 1 if $win->{name} eq '(status)' && !$config{use_status_window}; + return 1 if grep { $win->{name} eq $_ || $win->{refnum} eq $_ + || $win->get_active_name eq $_ } @{ $config{ignore_windows} }; + return 0; +} + +sub sig_window_changed { + my ($newwindow, $oldwindow) = @_; + return unless $oldwindow; + trackbar_update_seen($newwindow); + return if delete $keep_trackbar{ $oldwindow->{_irssi} }; + trackbar_update_seen($oldwindow); + return if $config{require_seen} && $unseen_trackbar{ $oldwindow->{_irssi } }; + update_one_trackbar($oldwindow, undef, 0); +} + +sub trackbar_update_seen { + my $win = shift; + return unless $win; + my $view = $win->view; + my $line = $view->get_bookmark('trackbar'); + unless ($line) { + delete $unseen_trackbar{ $win->{_irssi} }; + return; + } + my $startline = $view->{startline}; + return unless $startline; + + if ($startline->{info}{time} < $line->{info}{time} + || $startline->{_irssi} == $line->{_irssi}) { + delete $unseen_trackbar{ $win->{_irssi} }; + } +} + +sub screen_length; +{ local $@; + eval { require Text::CharWidth; }; + unless ($@) { + *screen_length = sub { Text::CharWidth::mbswidth($_[0]) }; + } + else { + *screen_length = sub { + my $temp = shift; + if (is_utf8()) { + Encode::_utf8_on($temp); + } + length($temp) + }; + } +} + +{ my %strip_table = ( + (map { $_ => '' } (split //, '04261537' . 'kbgcrmyw' . 'KBGCRMYW' . 'U9_8I:|FnN>#[' . 'pP')), + (map { $_ => $_ } (split //, '{}%')), + ); + sub c_length { + my $o = Irssi::strip_codes($_[0]); + $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; + screen_length($o) + } +} + +sub line { + my ($width, $time) = @_; + my $string = $config{string}; + $string = ' ' unless length $string; + $time ||= time; + + Encode::_utf8_on($string); + my $length = c_length($string); + + my $format = ''; + if ($config{print_timestamp}) { + $format = $config{timestamp_str}; + $format =~ y/%/\01/; + $format =~ s/\01\01/%/g; + $format = strftime($format, localtime $time); + $format =~ y/\01/%/; + } + + my $times = $width / $length; + $times += 1 if $times != int $times; + $format .= $config{style}; + $width -= c_length($format); + $string x= $times; + chop $string while length $string && c_length($string) > $width; + return $format . $string; +} + +sub remove_all_trackbars { + for my $window (Irssi::windows) { + next unless ref $window; + remove_one_trackbar($window); + } +} + +sub UNLOAD { + remove_all_trackbars(); +} + +sub redraw_trackbars { + return unless check_version(); + for my $win (Irssi::windows) { + next unless ref $win; + my $view = $win->view; + my $line = $view->get_bookmark('trackbar'); + next unless $line; + my $bottom = $view->{bottom}; + $win->print_after($line, MSGLEVEL_NEVER, line($win->{width}, $line->{info}{time}), + $line->{info}{time}); + $view->set_bookmark('trackbar', $win->last_line_insert); + $view->redraw; + $view->remove_line($line); + $win->command('^scrollback end') if $bottom && !$win->view->{bottom}; + $view->redraw; + } +} + +sub goto_trackbar { + my $win = Irssi::active_win; + my $line = $win->view->get_bookmark('trackbar'); + + if ($line) { + $win->command("scrollback goto ". strftime("%d %H:%M:%S", localtime($line->{info}{time}))); + } else { + $win->printformat(MSGLEVEL_CLIENTCRAP, 'trackbar_not_found'); + } +} + +sub cmd_mark { + update_one_trackbar(Irssi::active_win, undef, 1); +} + +sub cmd_markall { + for my $window (Irssi::windows) { + next unless ref $window; + update_one_trackbar($window); + } +} + +sub signal_stop { + Irssi::signal_stop; +} + +sub cmd_markvisible { + my @wins = Irssi::windows; + my $awin = + my $bwin = Irssi::active_win; + my $awin_counter = 0; + Irssi::signal_add_priority('window changed' => 'signal_stop', -99); + do { + Irssi::active_win->command('window up'); + $awin = Irssi::active_win; + update_one_trackbar($awin); + ++$awin_counter; + } until ($awin->{refnum} == $bwin->{refnum} || $awin_counter >= @wins); + Irssi::signal_remove('window changed' => 'signal_stop'); +} + +sub cmd_trackbar_remove_one { + remove_one_trackbar(Irssi::active_win); +} + +sub cmd_remove_all_trackbars { + remove_all_trackbars(); + Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trackbar_all_removed'); +} + +sub cmd_keep_once { + $keep_trackbar{ Irssi::active_win->{_irssi} } = 1; +} + +sub trackbar_runsub { + my ($data, $server, $item) = @_; + $data =~ s/\s+$//g; + + if ($data) { + Irssi::command_runsub('trackbar', $data, $server, $item); + } else { + goto_trackbar(); + } +} + +sub update_config { + my $was_status_window = $config{use_status_window}; + $config{style} = Irssi::settings_get_str('trackbar_style'); + $config{string} = Irssi::settings_get_str('trackbar_string'); + $config{require_seen} = Irssi::settings_get_bool('trackbar_require_seen'); + $config{ignore_windows} = [ split /[,\s]+/, Irssi::settings_get_str('trackbar_ignore_windows') ]; + $config{use_status_window} = Irssi::settings_get_bool('trackbar_use_status_window'); + $config{print_timestamp} = Irssi::settings_get_bool('trackbar_print_timestamp'); + if (defined $was_status_window && $was_status_window != $config{use_status_window}) { + if (my $swin = Irssi::window_find_name('(status)')) { + if ($config{use_status_window}) { + update_one_trackbar($swin); + } + else { + remove_one_trackbar($swin); + } + } + } + if ($config{print_timestamp}) { + my $ts_format = Irssi::settings_get_str('timestamp_format'); + my $ts_theme = Irssi::current_theme->get_format('fe-common/core', 'timestamp'); + my $render_str = Irssi::current_theme->format_expand($ts_theme); + (my $ts_escaped = $ts_format) =~ s/([%\$])/$1$1/g; + $render_str =~ s/(?|\$(.)(?!\w)|\$\{(\w+)\})/$1 eq 'Z' ? $ts_escaped : $1/ge; + $config{timestamp_str} = $render_str; + } + redraw_trackbars() unless $old_irssi; +} + +Irssi::settings_add_str('trackbar', 'trackbar_string', is_utf8() ? "\x{2500}" : '-'); +Irssi::settings_add_str('trackbar', 'trackbar_style', '%K'); +Irssi::settings_add_str('trackbar', 'trackbar_ignore_windows', ''); +Irssi::settings_add_bool('trackbar', 'trackbar_use_status_window', 1); +Irssi::settings_add_bool('trackbar', 'trackbar_print_timestamp', 0); +Irssi::settings_add_bool('trackbar', 'trackbar_require_seen', 0); + +update_config(); + +Irssi::signal_add_last( 'mainwindow resized' => 'redraw_trackbars') + unless $old_irssi; + +Irssi::signal_register({'gui page scrolled' => [qw/Irssi::UI::Window/]}); +Irssi::signal_add_last('gui page scrolled' => 'trackbar_update_seen'); + +Irssi::signal_add('setup changed' => 'update_config'); +Irssi::signal_add_priority('session save' => 'remove_all_trackbars', Irssi::SIGNAL_PRIORITY_HIGH-1); + +Irssi::signal_add('window changed' => 'sig_window_changed'); + +Irssi::command_bind('trackbar goto' => 'goto_trackbar'); +Irssi::command_bind('trackbar keep' => 'cmd_keep_once'); +Irssi::command_bind('trackbar mark' => 'cmd_mark'); +Irssi::command_bind('trackbar markvisible' => 'cmd_markvisible'); +Irssi::command_bind('trackbar markall' => 'cmd_markall'); +Irssi::command_bind('trackbar remove' => 'cmd_trackbar_remove_one'); +Irssi::command_bind('trackbar removeall' => 'cmd_remove_all_trackbars'); +Irssi::command_bind('trackbar redraw' => 'redraw_trackbars'); +Irssi::command_bind('trackbar' => 'trackbar_runsub'); +Irssi::command_bind('mark' => 'cmd_mark'); +Irssi::command_bind_last('help' => 'cmd_help'); + +Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trackbar_loaded', $IRSSI{name}, $VERSION, $IRSSI{authors}); diff --git a/scripts/typofix.pl b/scripts/typofix.pl new file mode 100644 index 0000000..49b312a --- /dev/null +++ b/scripts/typofix.pl @@ -0,0 +1,165 @@ +# typofix.pl - when someone uses s/foo/bar typofixing, this script really +# goes and modifies the original text on screen. + +use strict; +use warnings; +use Irssi qw( + settings_get_str settings_get_bool + settings_add_str settings_add_bool + signal_add signal_stop +); +use Irssi 20140701; +use Irssi::TextUI; +use Algorithm::Diff 'sdiff'; + +our $VERSION = '1.12'; # 4be3787e5717715 +our %IRSSI = ( + authors => 'Juerd (first version: Timo Sirainen, additions by: Qrczak)', + contact => 'tss@iki.fi, juerd@juerd.nl, qrczak@knm.org.pl', + name => 'Typofix', + description => 'When someone uses s/foo/bar/, this really modifies the text', + license => 'Same as Irssi', + url => 'http://juerd.nl/irssi/', + changed => 'Sat Jun 28 16:24:26 CEST 2014', + upgrade_info => '/set typofix_modify_string %wold%gnew%n', + NOTE1 => 'you need irssi 0.8.17' +); + +# /SET typofix_modify_string [fixed] - append string after replaced text +# /SET typofix_hide_replace NO - hide the s/foo/bar/ line +# (J) /SET typofix_format - format with "old" and "new" in it + + +my $chars = '/|;:\'"_=+*&^%$#@!~,.?-'; +my $regex = qq{(?x-sm: # "s/foo/bar/i # oops" + \\s* # Optional whitespace + s # Substitution operator s + ([$chars]) # Delimiter / + ( (?: \\\\. | (?!\\1). )* ) # Pattern foo + # Backslash plus any char, or a single non-delimiter char + \\1 # Delimiter / + ( (?: \\\\. | (?!\\1). )* ) # Replacementstring bar + \\1? # Optional delimiter / + ([a-z]*) # Modifiers i + \\s* # Optional whitespace + (.?) # Don't hide if there's more # oops +)}; +my $irssi_mumbo = qr/\cD[`-i]|\cD[&-@\xff]./; +my $irssi_mumbo_no_partial = qr/(?<!\cD)(?<!\cD[&-@\xff])/; + +sub replace { + my ($window, $nick, $from, $to, $opt, $screen) = @_; + + my $view = $window->view(); + my $line = $screen ? $view->{bottom_startline} : $view->{startline}; + + my $last_line; + (my $copy = $from) =~ s/\^|^/^.*\\b$nick\\b.*?\\s.*?/; + while ($line) { + my $text = $line->get_text(0); + eval { + $last_line = $line + if ($line->{info}{level} & (MSGLEVEL_PUBLIC | MSGLEVEL_MSGS)) && + $text !~ /$regex/o && $text =~ /$copy/; + 1 + } or return; + $line = $line->next(); + } + return 0 if (!$last_line); + my $text = $last_line->get_text(1); + + # variables and case insensitivity + $from = "(?i:$from)" if $opt =~ /i/; + $to = quotemeta $to; + $to =~ s{\\\\\\(.)|\\(.)([1-9])?}{ + if (defined $1) { + "\\$1" + } elsif (defined $3 && ($2 eq "\\" || $2 eq "\$")) { + "\$$3" + } else { + "\\$2".($3//"") + } }ge; + + # text replacing + $text =~ s/(.*(?:\b|$irssi_mumbo)$irssi_mumbo_no_partial$nick(?:\b|$irssi_mumbo).*?\s)//; + my $pre = $1; + $text =~ s/$irssi_mumbo//g; + my $format = settings_get_str('typofix_format'); + $format =~ s/old/\0\cA/; + $format =~ s/new/\0\cB/; + $format =~ s/%/\0\cC/g; + + my $old = $text; + eval " \$text =~ s/\$from/$to/".($opt =~ /g/ ? "g" : "")." ; 1 " + or Irssi::print "Typofix warning: $@", return 0; + my $new = ''; + my $diff = Algorithm::Diff->new([split//,$old],[split//,$text]); + while ($diff->Next()) { + local $" = ''; + if (my @it = $diff->Same()) { + $new .= "@it"; + } + else { + my %r = ("\cA" => [ $diff->Items(1) ], + "\cB" => [ $diff->Items(2) ]); + my $format_st = $format; + $format_st =~ s/\0([\cA\cB])/@{$r{$1}}/g; + $new .= $format_st; + } + } + s/%/%%/g for $pre, $new; + s/\0\cC/%/g for $new; + $text = $pre . $new . settings_get_str('typofix_modify_string'); + + my $bottom = $view->{bottom}; + my $info = $last_line->{info}; + $window->print_after($last_line, $info->{level}, $text, $info->{time}); + $view->remove_line($last_line); + $window->command('^scrollback end') if $bottom && !$window->view->{bottom}; + $view->redraw(); + + return 1; +} + +sub event_privmsg { + my ($server, $data, $nick, $address) = @_; + my ($target, $text) = $data =~ /^(\S*)\s:(.*)/ or return; + + return unless $text =~ /^$regex/o; + my ($from, $to, $opt, $extra) = ($2, $3, $4, $5); + + my $hide = settings_get_bool('typofix_hide_replace') && !$extra; + + my $ischannel = $server->ischannel($target); + my $level = $ischannel ? MSGLEVEL_PUBLIC : MSGLEVEL_MSGS; + + $target = $nick unless $ischannel; + my $window = $server->window_find_closest($target, $level); + + signal_stop() if (replace($window, $nick, $from, $to, $opt, 0) && $hide); +} + +sub event_own_public { + my ($server, $text, $target) = @_; + + return unless $text =~ /^$regex/o; + my ($from, $to, $opt, $extra) = ($2, $3, $4, $5); + + my $hide = settings_get_bool('typofix_hide_replace') && !$extra; + $hide = 0 if settings_get_bool('typofix_own_no_hide'); + + my $level = $server->ischannel($target) ? MSGLEVEL_MSGS : MSGLEVEL_PUBLIC; + my $window = $server->window_find_closest($target, $level); + + signal_stop() if (replace($window, $server->{nick}, $from, $to, $opt, 0) && $hide); +} + +settings_add_str ('typofix', 'typofix_modify_string', ' [fixed]'); +settings_add_str ('typofix', 'typofix_format', '%rold%gnew%n'); +settings_add_bool('typofix', 'typofix_hide_replace', 0); +settings_add_bool('typofix', 'typofix_own_no_hide', 0); + +signal_add { + 'event privmsg' => \&event_privmsg, + 'message own_public' => \&event_own_public +}; diff --git a/scripts/uberprompt.pl b/scripts/uberprompt.pl new file mode 100644 index 0000000..c4b8cc9 --- /dev/null +++ b/scripts/uberprompt.pl @@ -0,0 +1,787 @@ +=pod + +=head1 NAME + +uberprompt.pl + +=head1 DESCRIPTION + +This script replaces the default prompt status-bar item with one capable of +displaying additional information, under either user control or via scripts. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +It is recommended that you make it autoload in one of the +L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>. + +=head1 SETUP + +If you have a custom prompt format, you may need to copy it to the +uberprompt_format setting. See below for details. + +=head1 USAGE + +Although the script is designed primarily for other scripts to set +status information into the prompt, the following commands are available: + +=over 4 + +=item * C</prompt set [-inner|-pre|-post|only] E<lt>msgE<gt>> + +Sets the prompt to the given argument. Any use of C<$p> in the argument will +be replaced by the original prompt content. + +A parameter corresponding to the C<UP_*> constants listed below is required, in +the format C</prompt set -inner Hello!> + +=item * C</prompt clear> + +Clears the additional data provided to the prompt. + +=item * C</prompt on> + +Eenables the uberprompt (things may get confused if this is used +whilst the prompt is already enabled) + +=item * C</prompt off> + +Restore the original irssi prompt and prompt_empty statusbars. unloading the +script has the same effect. + +=item * C</help prompt> + +show help for uberprompt commands + +=back + +=head1 SETTINGS + +=head2 UBERPROMPT FORMAT + +C</set uberprompt_format E<lt>formatE<gt>> + +The default is C<[$*$uber]>, which is the same as the default provided in +F<default.theme>. + +Changing this setting will update the prompt immediately, unlike editing your theme. + +An additional variable available within this format is C<$uber>, which expands to +the content of prompt data provided with the C<UP_INNER> or C</prompt set -inner> +placement argument. + +For all other placement arguments, it will expand to the empty string. + +B<Note:> This setting completely overrides the C<prompt="...";> line in your +.theme file, and may cause unexpected behaviour if your theme wishes to set a +different form of prompt. It can be simply copied from the theme file into the +above setting. + +=head2 OTHER SETTINGS + +=over 4 + +=item * C<uberprompt_autostart> + +Boolean value, which determines if uberprompt should enable itself automatically +upon loading. If Off, it must be enabled manually with C</prompt on>. Defaults to On. + +=item * C<uberprompt_debug> + +Boolean value, which determines if uberprompt should print debugging information. +Defaults to Off, and should probably be left that way unless requested for bug-tracing +purposes. + +=item * C<uberprompt_format> + +String value containing the format-string which uberprompt uses to display the +prompt. Defaults to "C<[$*$uber] >", where C<$*> is the content the prompt would +normally display, and C<$uber> is a placeholder variable for dynamic content, as +described in the section above. + +=item * C<uberprompt_load_hook> + +String value which can contain one or more commands to be run whenever the uberprompt +is enabled, either via autostart, or C</prompt on>. Defaults to the empty string, in +which case no commands are run. Some examples include: + +C</set uberprompt_load_hook /echo prompt enabled> or + +C</^sbar prompt add -after input vim_mode> for those using vim_mode.pl who want +the command status indicator on the prompt line. + +=item * C<uberprompt_unload_hook> + +String value, defaulting to the empty string, which can contain commands which +are executed when the uberprompt is disabled, either by unloading the script, +or by the command C</prompt off>. + +=item * C<uberprompt_use_replaces> + +Boolean value, defaults to Off. If enabled, the format string for the prompt +will be subject to the I<replaces> section of the theme. The most obvious +effect of this is that bracket characters C<[ ]> are displayed in a different +colour, typically quite dark. + +=back + +B<Note:> For both C<uberprompt_*_hook> settings above, multiple commands can +be chained together in the form C</eval /^cmd1 ; /^cmd2>. The C<^> prevents +any output from the commands (such as error messages) being displayed. + +=head2 SCRIPTING USAGE + +The primary purpose of uberprompt is to be used by other scripts to +display information in a way that is not possible by printing to the active +window or using statusbar items. + +The content of the prompt can be set from other scripts via the C<"change prompt"> +signal. + +For Example: + + signal_emit 'change prompt' 'some_string', UberPrompt::UP_INNER; + +will set the prompt to include that content, by default 'C<[$* some_string]>' + +The possible position arguments are the following strings: + +=over 4 + +=item * C<UP_PRE> - place the provided string before the prompt - C<$string$prompt> + +=item * C<UP_INNER> - place the provided string inside the prompt - C<{prompt $* $string}> + +=item * C<UP_POST> - place the provided string after the prompt - C<$prompt$string> + +=item * C<UP_ONLY> - replace the prompt with the provided string - C<$string> + +=back + +All strings may use the special variable 'C<$prompt>' to include the prompt +verbatim at that position in the string. It is probably only useful for +the C<UP_ONLY> mode however. '$C<prompt_nt>' will include the prompt, minus any +trailing whitespace. + +=head2 CHANGE NOTIFICATIONS + +You can also be notified when the prompt changes in response to the previous +signal or manual C</prompt> commands via: + + signal_add 'prompt changed', sub { my ($text, $len) = @_; ... do something ... }; + +This callback will occur whenever the contents of the prompt is changed. + + +=head2 NOTES FOR SCRIPT WRITERS: + +The following code snippet can be used within your own script as a preamble +to ensure that uberprompt is loaded before your script to avoid +any issues with loading order. It first checks if uberprompt is loaded, and +if not, attempts to load it. If the load fails, the script will die +with an error message, otherwise it will call your app_init() function. + +I<---- start of snippet ----> + + my $DEBUG_ENABLED = 0; + sub DEBUG () { $DEBUG_ENABLED } + + # check we have uberprompt loaded. + + sub script_is_loaded { + return exists($Irssi::Script::{$_[0] . '::'}); + } + + if (not script_is_loaded('uberprompt')) { + + print "This script requires 'uberprompt.pl' in order to work. " + . "Attempting to load it now..."; + + Irssi::signal_add('script error', 'load_uberprompt_failed'); + Irssi::command("script load uberprompt.pl"); + + unless(script_is_loaded('uberprompt')) { + load_uberprompt_failed("File does not exist"); + } + app_init(); + } else { + app_init(); + } + + sub load_uberprompt_failed { + Irssi::signal_remove('script error', 'load_uberprompt_failed'); + + print "Script could not be loaded. Script cannot continue. " + . "Check you have uberprompt.pl installed in your path and " + . "try again."; + + die "Script Load Failed: " . join(" ", @_); + } + +I<---- end of snippet ----> + +=head1 AUTHORS + +Copyright E<copy> 2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> + +=head1 LICENCE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=head1 BUGS + +=over 4 + +=item * + +Resizing the terminal rapidly whilst using this script in debug mode may cause +irssi to crash. See bug report at http://bugs.irssi.org/index.php?do=details&task_id=772 for details. + +=back + +=head1 TODO + +=over 4 + +=item * report failure (somehow) to clients if hte prompt is disabled. + +=item * fix issue at autorun startup with sbar item doesn't exist. + +=back + +=cut + +use strict; +use warnings; + +use Irssi; +use Irssi::TextUI; +use Data::Dumper; + +{ package Irssi::Nick } # magic. + +our $VERSION = "0.2"; # 255b35bb44161e0 +our %IRSSI = + ( + authors => "shabble", + contact => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode', + name => "uberprompt", + description => "Helper script for dynamically adding text " + . "into the input-bar prompt.", + license => "MIT", + changed => "24/7/2010" + ); + + +my $DEBUG_ENABLED = 0; +sub DEBUG { $DEBUG_ENABLED } + +my $prompt_data = ''; +my $prompt_data_pos = 'UP_INNER'; + +my $prompt_last = ''; +my $prompt_format = ''; +my $prompt_format_empty = ''; + +# flag to indicate whether rendering of hte prompt should allow the replaces +# theme formats to be applied to the content. +my $use_replaces = 0; +my $trim_data = 0; + +my $emit_request = 0; + +my $expando_refresh_timer; +my $expando_vars = {}; + +my $init_callbacks = {load => '', unload => ''}; + +pre_init(); + +sub pre_init { + `stty -ixon`; + init(); +} + +sub prompt_subcmd_handler { + my ($data, $server, $item) = @_; + #$data =~ s/\s+$//g; # strip trailing whitespace. + Irssi::command_runsub('prompt', $data, $server, $item); +} + +sub _error($) { + my ($msg) = @_; + Irssi::active_win->print($msg, MSGLEVEL_CLIENTERROR); +} + +sub _debug_print($) { + return unless DEBUG; + my ($msg) = @_; + Irssi::active_win->print($msg, MSGLEVEL_CLIENTCRAP); +} + +sub _print_help { + my ($args) = @_; + if ($args =~ m/^\s*prompt/i) { + my @help_lines = + ( + "", + "PROMPT ON", + "PROMPT OFF", + "PROMPT CLEAR", + "PROMPT SET { -pre | -post | -only | -inner } <content>", + "", + "Commands for manipulating the UberPrompt.", + "", + "/PROMPT ON enables uberprompt, replacing the existing prompt ", + " statusbar-item", + "/PROMPT OFF disables it, and restores the original prompt item", + "/PROMPT CLEAR resets the value of any additional data set by /PROMPT SET", + " or a script", + "/PROMPT SET changes the contents of the prompt, according to the mode", + " and content provided.", + " { -inner sets the value of the \$uber psuedo-variable in the", + " /set uberprompt_format setting.", + " | -pre places the content before the current prompt string", + " | -post places the content after the prompt string", + " | -only replaces the entire prompt contents with the given string }", + "", + "See Also:", + '', + '/SET uberprompt_format -- defaults to "[$*$uber] "', + '/SET uberprompt_format_empty -- defaults to "[$*] "', + "/SET uberprompt_autostart -- determines whether /PROMPT ON is run", + " automatically when the script loads", + "/SET uberprompt_use_replaces -- toggles the use of the current theme", + " \"replaces\" setting. Especially", + " noticeable on brackets \"[ ]\" ", + "/SET uberprompt_trim_data -- defaults to off. Removes whitespace from", + " both sides of the \$uber result.", + "", + ); + + Irssi::print($_, MSGLEVEL_CLIENTCRAP) for @help_lines; + Irssi::signal_stop; + } +} + +sub UNLOAD { + deinit(); +} + +sub exp_lbrace() { '{' } +sub exp_rbrace() { '}' } + +sub deinit { + Irssi::expando_destroy('lbrace'); + Irssi::expando_destroy('rbrace'); + + # remove uberprompt and return the original ones. + #print "Removing uberprompt and restoring original"; + restore_prompt_items(); +} + +sub gui_exit { + restore_prompt_items(); +} + +sub init { + Irssi::statusbar_item_register('uberprompt', 0, 'uberprompt_draw'); + + # TODO: flags to prevent these from being recomputed? + Irssi::expando_create('lbrace', \&exp_lbrace, {}); + Irssi::expando_create('rbrace', \&exp_rbrace, {}); + + Irssi::settings_add_str ('uberprompt', 'uberprompt_format', '[$*$uber] '); + Irssi::settings_add_str ('uberprompt', 'uberprompt_format_empty', '[$*] '); + + Irssi::settings_add_str ('uberprompt', 'uberprompt_load_hook', ''); + Irssi::settings_add_str ('uberprompt', 'uberprompt_unload_hook', ''); + + Irssi::settings_add_bool('uberprompt', 'uberprompt_debug', 0); + Irssi::settings_add_bool('uberprompt', 'uberprompt_autostart', 1); + + Irssi::settings_add_bool('uberprompt', 'uberprompt_use_replaces', 0); + Irssi::settings_add_bool('uberprompt', 'uberprompt_trim_data', 0); + + Irssi::command_bind("prompt", \&prompt_subcmd_handler); + Irssi::command_bind('prompt on', \&replace_prompt_items); + Irssi::command_bind('prompt off', \&restore_prompt_items); + Irssi::command_bind('prompt set', \&cmd_prompt_set); + Irssi::command_bind('prompt clear', + sub { + Irssi::signal_emit 'change prompt', '$p', 'UP_POST'; + }); + + my $prompt_set_args_format = "inner pre post only"; + Irssi::command_set_options('prompt set', $prompt_set_args_format); + + Irssi::command_bind('help', \&_print_help); + + Irssi::signal_add_first('gui exit', \&gui_exit); + Irssi::signal_add('setup changed', \&reload_settings); + + # intialise the prompt format. + reload_settings(); + + # make sure we redraw when necessary. + Irssi::signal_add('window changed', \&uberprompt_refresh); + Irssi::signal_add('window name changed', \&uberprompt_refresh); + Irssi::signal_add('window changed automatic', \&uberprompt_refresh); + Irssi::signal_add('window item changed', \&uberprompt_refresh); + Irssi::signal_add('window item server changed', \&uberprompt_refresh); + Irssi::signal_add('window server changed', \&uberprompt_refresh); + Irssi::signal_add('server nick changed', \&uberprompt_refresh); + + Irssi::signal_add('nick mode changed', \&refresh_if_me); + + # install our statusbars if required. + if (Irssi::settings_get_bool('uberprompt_autostart')) { + replace_prompt_items(); + } + + # API signals + + Irssi::signal_register({'change prompt' => [qw/string string/]}); + Irssi::signal_add('change prompt' => \&change_prompt_handler); + + # other scripts (specifically overlay/visual) can subscribe to + # this event to be notified when the prompt changes. + # arguments are new contents (string), new length (int) + Irssi::signal_register({'prompt changed' => [qw/string int/]}); + Irssi::signal_register({'prompt length request' => []}); + + Irssi::signal_add('prompt length request', \&length_request_handler); +} + +sub cmd_prompt_set { + my $args = shift; + my @options_list = Irssi::command_parse_options "prompt set", $args; + if (@options_list) { + my ($options, $rest) = @options_list; + + my @opt_modes = keys %$options; + if (@opt_modes != 1) { + _error '%_/prompt set%_ must specify exactly one mode of' + . ' {-inner, -only, -pre, -post}'; + return; + } + + my $mode = 'UP_' . uc($opt_modes[0]); + + Irssi::signal_emit 'change prompt', $rest, $mode; + } else { + _error ('%_/prompt set%_ must specify a mode {-inner, -only, -pre, -post}'); + } +} + +sub refresh_if_me { + my ($channel, $nick) = @_; + + return unless $channel and $nick; + + my $server = Irssi::active_server; + my $window = Irssi::active_win; + + return unless $server and $window; + + my $my_chan = $window->{active}->{name}; + my $my_nick = $server->parse_special('$N'); + + return unless $my_chan and $my_nick; + + _debug_print "Chan: $channel->{name}, " + . "nick: $nick->{nick}, " + . "me: $my_nick, chan: $my_chan"; + + if ($my_chan eq $channel->{name} and $my_nick eq $nick->{nick}) { + uberprompt_refresh(); + } +} + +sub length_request_handler { + $emit_request = 1; + uberprompt_render_prompt(); + $emit_request = 0; +} + +sub reload_settings { + + $use_replaces = Irssi::settings_get_bool('uberprompt_use_replaces'); + $DEBUG_ENABLED = Irssi::settings_get_bool('uberprompt_debug'); + + $init_callbacks = { + load => Irssi::settings_get_str('uberprompt_load_hook'), + unload => Irssi::settings_get_str('uberprompt_unload_hook'), + }; + + if (DEBUG) { + Irssi::signal_add 'prompt changed', 'debug_prompt_changed'; + } else { + Irssi::signal_remove 'prompt changed', 'debug_prompt_changed'; + } + + my $new = Irssi::settings_get_str('uberprompt_format'); + + if ($prompt_format ne $new) { + _debug_print("Updated prompt format"); + $prompt_format = $new; + $prompt_format =~ s/\$uber/\$\$uber/; + Irssi::abstracts_register(['uberprompt', $prompt_format]); + + $expando_vars = {}; + + # TODO: something clever here to check if we need to schedule + # an update timer or something, rather than just refreshing on + # every possible activity in init() + while ($prompt_format =~ m/(?<!\$)(\$[A-Za-z,.:;][a-z_]*)/g) { + _debug_print("Detected Irssi expando variable $1"); + my $var_name = substr $1, 1; # strip the $ + $expando_vars->{$var_name} = Irssi::parse_special($1); + } + } + + $new = Irssi::settings_get_str('uberprompt_format_empty'); + + if ($prompt_format_empty ne $new) { + _debug_print("Updated prompt format"); + $prompt_format_empty = $new; + $prompt_format_empty =~ s/\$uber/\$\$uber/; + Irssi::abstracts_register(['uberprompt_empty', $prompt_format_empty]); + + $expando_vars = {}; + + # TODO: something clever here to check if we need to schedule + # an update timer or something, rather than just refreshing on + # every possible activity in init() + while ($prompt_format_empty =~ m/(?<!\$)(\$[A-Za-z,.:;][a-z_]*)/g) { + _debug_print("Detected Irssi expando variable $1"); + my $var_name = substr $1, 1; # strip the $ + $expando_vars->{$var_name} = Irssi::parse_special($1); + } + } + + $trim_data = Irssi::settings_get_bool('uberprompt_trim_data'); +} + +sub debug_prompt_changed { + my ($text, $len) = @_; + + $text =~ s/%/%%/g; + + print "DEBUG_HANDLER: Prompt Changed to: \"$text\", length: $len"; +} + +sub change_prompt_handler { + my ($text, $pos) = @_; + + # fix for people who used prompt_info and hence the signal won't + # pass the second argument. + $pos = 'UP_INNER' unless defined $pos; + _debug_print("Got prompt change signal with: $text, $pos"); + + my ($changed_text, $changed_pos); + $changed_text = defined $prompt_data ? $prompt_data ne $text : 1; + $changed_pos = defined $prompt_data_pos ? $prompt_data_pos ne $pos : 1; + + $prompt_data = $text; + $prompt_data_pos = $pos; + + if ($changed_text || $changed_pos) { + _debug_print("Redrawing prompt"); + uberprompt_refresh(); + } +} + +sub _escape_prompt_special { + my $str = shift; + $str =~ s/\$/\$\$/g; + $str =~ s/\\/\\\\/g; + #$str =~ s/%/%%/g; + $str =~ s/{/\${lbrace}/g; + $str =~ s/}/\${rbrace}/g; + + return $str; +} + +sub uberprompt_render_prompt { + + my $window = Irssi::active_win; + my $prompt_arg = ''; + + # default prompt sbar arguments (from config) + if (scalar( () = $window->items )) { + $prompt_arg = '$[.15]{itemname}'; + } else { + $prompt_arg = '${winname}'; + } + + my $prompt = ''; # rendered content of the prompt. + my $theme = Irssi::current_theme; + + my $arg = $use_replaces ? 0 : Irssi::EXPAND_FLAG_IGNORE_REPLACES; + + if ($prompt_data && (!$trim_data || trim($prompt_data))) { + $prompt = $theme->format_expand("{uberprompt $prompt_arg}", $arg); + } + else { + $prompt = $theme->format_expand("{uberprompt_empty $prompt_arg}", $arg); + } + + if ($prompt_data_pos eq 'UP_ONLY') { + $prompt =~ s/\$\$uber//; # no need for recursive prompting, I hope. + + # TODO: only compute this if necessary? + my $prompt_nt = $prompt; + $prompt_nt =~ s/\s+$//; + + my $pdata_copy = $prompt_data; + + $pdata_copy =~ s/\$prompt_nt/$prompt_nt/; + $pdata_copy =~ s/\$prompt/$prompt/; + + $prompt = $pdata_copy; + + } elsif ($prompt_data_pos eq 'UP_INNER' && defined $prompt_data) { + + my $esc = _escape_prompt_special($prompt_data); + $esc = $trim_data ? trim($esc) : $esc; + $prompt =~ s/\$\$uber/$esc/; + + } else { + # remove the $uber marker + $prompt =~ s/\$\$uber//; + + # and add the additional text at the appropriate position. + if ($prompt_data_pos eq 'UP_PRE') { + $prompt = $prompt_data . $prompt; + } elsif ($prompt_data_pos eq 'UP_POST') { + $prompt .= $prompt_data; + } + } + + _debug_print("rendering with: $prompt"); + + + if (($prompt ne $prompt_last) or $emit_request) { + + # _debug_print("Emitting prompt changed signal"); + # my $exp = Irssi::current_theme()->format_expand($text, 0); + my $ps = Irssi::parse_special($prompt); + + Irssi::signal_emit('prompt changed', $ps, length($ps)); + $prompt_last = $prompt; + } + return $prompt; +} + +sub uberprompt_draw { + my ($sb_item, $get_size_only) = @_; + + my $prompt = uberprompt_render_prompt(); + + my $ret = $sb_item->default_handler($get_size_only, $prompt, '', 0); + _debug_print("redrawing with: $prompt"); + return $ret; +} + +sub uberprompt_refresh { + Irssi::statusbar_items_redraw('uberprompt'); +} + +my $prompt_items_replaced; + +sub replace_prompt_items { + unless ($prompt_items_replaced) { + $prompt_items_replaced = 1; + + # add the new one. + _sbar_command('prompt', 'add', 'uberprompt', + qw/-alignment left -after prompt_empty -priority '-1'/); + + # remove existing ones. + _debug_print("Removing original prompt"); + + _sbar_command('prompt', 'remove', 'prompt'); + _sbar_command('prompt', 'remove', 'prompt_empty'); + + } + + my $load_hook = $init_callbacks->{load}; + if (defined $load_hook and length $load_hook) { + eval { + Irssi::command($load_hook); + }; + if ($@) { + _error("Uberprompt user load-hook command ($load_hook) failed: $@"); + } + } + +} + +sub restore_prompt_items { + if ($prompt_items_replaced) { + $prompt_items_replaced = undef; + + _debug_print("Restoring original prompt"); + + _sbar_command('prompt', 'add', 'prompt', + qw/-alignment left -after uberprompt -priority '-1'/); + _sbar_command('prompt', 'add', 'prompt_empty', + qw/-alignment left -after prompt -priority '-1'/); + + _sbar_command('prompt', 'remove', 'uberprompt'); + + } + + my $unload_hook = $init_callbacks->{unload}; + + if (defined $unload_hook and length $unload_hook) { + eval { + Irssi::command($unload_hook); + }; + if ($@) { + _error("Uberprompt user unload-hook command ($unload_hook) failed: $@"); + } + } +} + +sub _sbar_command { + my ($bar, $cmd, $item, @args) = @_; + + my $args_str = join ' ', @args; + + $args_str .= ' ' if length $args_str && defined $item; + + my $command = sprintf 'STATUSBAR %s %s %s%s', + $bar, $cmd, $args_str, defined $item ? $item : ''; + + _debug_print("Running command: $command"); + Irssi::command($command); +} + +sub trim { + my $string = shift; + + $string =~ s/^\s*//; + $string =~ s/\s*$//; + + return $string; +} |
