# 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 # * only show on channels matching this regular expression # # /set nicklist_max_users # * only show when the channel has so many users or less (0 = always) # # /set nicklist_smallest_main # * only show when main window is larger than this (0 = always) # # /set nicklist_pane_width # * width of the nicklist pane # # /set nicklist_color # * colourise the nicks in the nicklist (required nickcolor script # with get_nick_color2 and debug_ansicolour functions) # # /set nicklist_gone_sort # * sort away people below # # It supports mouse scrolling and the following keys: # k/up arrow: up one line # j/down arrow: down one line # u/pageup: up 50% lines # d/pagedown: down 50% 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. # # to toggle the nicklist if it is in the way you can make a key binding: # /bind meta-Z /script exec Irssi::Script::tmux_nicklist_portable::toggle_nicklist ################################################################################ use strict; use warnings; use IO::Handle; use IO::Select; use POSIX; use File::Temp qw/ :mktemp /; use File::Basename; our $VERSION = '0.1.8'; 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 # added 'd' and 'u' navigation as in vim, by @gerardbm (github) { package Irssi::Nick } if ($#ARGV == -1) { require Irssi; my $enabled = 0; my $nicklist_toggle = 1; 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("enabled"); } 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 $event = shift; my $active = Irssi::active_win(); my $channel = $active->{active}; return disable_nicklist unless $channel && ref $channel; if ($event =~ /^nick/) { # check if that nick event is for the current channel/nicklist my ($event_channel) = @_; return unless $channel->{_irssi} == $event_channel->{_irssi}; } 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 $@; } my $smallest_main = Irssi::settings_get_int('nicklist_smallest_main'); if (!$nicklist_toggle || !$channel || !ref($channel) || !$channel->isa('Irssi::Channel') || !$channel->{'names_got'} || $channel->{'name'} !~ $channel_pattern || ($smallest_main && $channel->window->{width} < $smallest_main)) { 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"); my $gone_sort = Irssi::settings_get_bool('nicklist_gone_sort'); my $prefer_real; if (exists $Irssi::Script::{'realnames::'}) { my $code = "Irssi::Script::realnames"->can('use_realnames'); $prefer_real = $code && $code->($channel); } my $_real = sub { my $nick = shift; $prefer_real && length $nick->{'realname'} ? $nick->{'realname'} : $nick->{'nick'} }; foreach my $nick (sort {($a->{'op'}?'1':$a->{'halfop'}?'2':$a->{'voice'}?'3':$a->{'other'}>32?'0':'4').($gone_sort?($a->{'gone'}?1:0):'').lc($_real->($a)) cmp ($b->{'op'}?'1':$b->{'halfop'}?'2':$b->{'voice'}?'3':$b->{'other'}>32?'0':'4').($gone_sort?($b->{'gone'}?1:0):'').lc($_real->($b))} @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".$_real->($nick)."\e[39m"); } elsif ($nick->{'halfop'}) { print($fifo "\e[34m%$colour".$_real->($nick)."\e[39m"); } elsif ($nick->{'voice'}) { print($fifo "\e[33m+$colour".$_real->($nick)."\e[39m"); } elsif ($nick->{'other'}>32) { print($fifo "\e[31m".(chr $nick->{'other'})."$colour".$_real->($nick)."\e[39m"); } else { print($fifo " $colour".$_real->($nick)."\e[39m"); } print($fifo "\n"); } print($fifo "END\n"); } } } sub toggle_nicklist { if ($enabled) { $nicklist_toggle = undef } else { $nicklist_toggle = 1; } reset_nicklist("toggle"); } 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, sub{reset_nicklist("terminal resized")}, ''); } 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::settings_add_bool('tmux_nicklist', 'nicklist_gone_sort', 0); Irssi::signal_add_last('window item changed', sub{switch_channel("window item changed",@_)}); Irssi::signal_add_last('window changed', sub{switch_channel("window changed",@_)}); Irssi::signal_add_last('channel joined', sub{switch_channel("channel joined",@_)}); Irssi::signal_add('nicklist new', sub{reset_nicklist("nicklist new",@_)}); Irssi::signal_add('nicklist remove', sub{reset_nicklist("nicklist remove",@_)}); Irssi::signal_add('nicklist changed', sub{reset_nicklist("nicklist changed",@_)}); Irssi::signal_add_first('nick mode changed', sub{reset_nicklist("nick mode changed",@_)}); 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 $DOWN="j"; my $UP="k"; my $PAGE_DOWN="\e[6~"; my $PAGE_UP="\e[5~"; my $PAGE_DOWN_D="d"; my $PAGE_UP_U="u"; 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 + 1; 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 ($PAGE_DOWN_D =~ /^\Q$sequence\E/) { move_down $rows/2 if ($PAGE_DOWN_D eq $sequence); } elsif ($PAGE_UP_U =~ /^\Q$sequence\E/) { move_up $rows/2 if ($PAGE_UP_U 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; }