diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/buffer.pl | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/scripts/buffer.pl b/scripts/buffer.pl new file mode 100644 index 0000000..ec4fca2 --- /dev/null +++ b/scripts/buffer.pl @@ -0,0 +1,562 @@ +use strict; +use warnings; + +{ package Irssi::Nick } + +use Irssi qw(command_bind command_bind_first command_runsub +settings_add_str settings_get_str settings_add_int settings_get_int +command_set_options command_parse_options server_find_tag signal_stop +timeout_add timeout_remove); +use Irssi::TextUI; + +use Scalar::Util qw(looks_like_number); +use List::Util qw(min max); +use POSIX qw(strftime); + +our $VERSION = '1.0'; +our %IRSSI = ( + authors => 'Pablo Martín Báez Echevarría', + contact => 'pab_24n@outlook.com', + name => 'buffer', + description => 'pastes a buffer into a channel or query window line by line with a specific delay between lines', + url => 'http://reirssi.wordpress.com', + license => 'Public Domain', + changed => 'vie jun 17 15:04:25 UYT 2016', +); + +my $buffer = []; +my $pastings = {}; +my $regex = '^'; + +sub cmd_buffer_help { + print CLIENTCRAP <<HELP + +%9Syntax%9: + +BUFFER SEARCH [-filename <file>] [-regexp] [-case] [-word] [<pattern>] +BUFFER LOAD [-filename <file>] [-start <first line>] [-end <last line>] +BUFFER CLEAR +BUFFER PRINT +BUFFER PLAY [-delay <seconds>] [-continue [<id>]] +BUFFER STOP [<id>] +BUFFER REMOVE [<id>] +BUFFER RESUME + + +%9Description%9: + + Pastes a buffer into a channel or query window line by line with a + specific delay between lines. It is inspired by the mIRC /play + command. + +%9Parameters%9: + + SEARCH: Searches the active window (or a file) for a pattern and + displays the matching lines with its corresponding line + number, in order to make easy to load the desired text. + + -filename Name of the file where to look at. If omitted, + searchs the scrollback buffer of the current + window. + -regexp The given pattern is a regexp. + -case Performs case sensitive matching. + -word The text must match full words. + -pattern If omitted, it will be the pattern of the + previous search (and if there is no previous + search, the command displays everything). + + LOAD: Loads the buffer and gets it ready to be pasted into a + location. + + -filename Name of the file whose lines will be loaded. If + omitted, loads lines from the current window. + -start Number of the first line to be loaded. If + omitted, it will be 1. + -end Number of the last line to be loaded. If + omitted, it will be the total number of lines + in the current window/file. + + CLEAR: Clears the buffer. + PRINT: Displays the content of the buffer in the status window. + PLAY: Pastes the buffer into the current channel or query window + or wakes up an already existing paste. + + -delay Delay (in seconds) between each message. + -continue Continues the paste identified by <id> (run + /BUFFER RESUME to find out the correct + identifier). If this parameter is omitted, it + will paste the entire buffer previously loaded + into the current channel or query window. If + -continue is used not followed by an id, it + wakes up all the paused pastes. + + STOP: Stops an active paste. + + -id Identifier of the paste that you would like + to stop. If ommitted, it will stop all the + active pastes. + + REMOVE: Removes an existing paste. + + -id Identifier of the paste that you would like to + remove. If omitted, it will remove all the + existing pastes. + + RESUME: Lists the existings pastes and shows information about each + one. + + +%9Settings%9: + + -buffer_delay: Default delay between messages. + + -buffer_context_lines If this is set to n, then /BUFFER RESUME will + print n lines before and after the next line + to be pasted. +HELP +; + signal_stop(); # To avoid 'No help for buffer' at the end +} + +sub open_file { + my $filename = shift; + $filename =~ s/^~/$ENV{HOME}/; + open my $fh, '<', $filename or die "Could not open file '$filename': $!\n"; + die "File '$filename' does not look like a text file\n" unless -T $filename; + return $fh; +} + +sub send_line { + my ( $timeout_tag, $buff, $index, $server, $target ) = @_; + + # Get line + my $line = $buff->[$$index]; + $line .= ' ' if $line eq ''; + + # Send line to target + $server->command("MSG $target $line"); + + # Increment pointer which stores the next line to be sent + $$index++; + + # Remove the paste if it's the last line in the buffer + if ( $$index == @$buff ) { + timeout_remove( $timeout_tag ); + delete $pastings->{$timeout_tag}; + } +} + +sub timeout_function { + my ( $ref ) = @_; + my $timeout_tag = $$ref; + + my ( $buff, $servtag, $targ, $pointer ) = + @{$pastings->{$timeout_tag}}{qw/buffer network target counter/}; + + # Check server and target + my $server = server_find_tag( $servtag ); + unless ( $server && $server->{connected} ) { + printf CLIENTERROR "Not connected to server '%s'. Paste <%d> will be paused", $servtag, $timeout_tag; + timeout_remove( $timeout_tag ); + $pastings->{$timeout_tag}{'status'} = 'paused'; + return; + }; + my $witem = $server->window_find_item($targ); + unless ( $witem ) { + printf CLIENTERROR "No window named '%s'. Paste <%d> will be paused", $targ, $timeout_tag; + timeout_remove( $timeout_tag ); + $pastings->{$timeout_tag}{'status'} = 'paused'; + return; + } + + # Send line + send_line($timeout_tag, $buff, $pointer, $server, $targ); +} + +sub buffer_context_range { + my ( $index, $context_lines, $total ) = @_; + my $first = max( 0, $index - $context_lines ); + my $last = min( $total-1, $index + $context_lines ); + $first..$last; +} + +sub cmd_buffer_search { + my ( $args, $server, $witem ) = @_; + my ($options, $pattern) = command_parse_options('buffer search', $args); + + if ($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/; + }; + if ($@) { + my ($err) = $@ =~ /^(.*) at .* line \d+\.$/; + print CLIENTERROR "Pattern \/$pattern\/ did not compile: $err"; + return; + } + } else { + $regex = qr/$flags$b\Q$pattern\E$b/; + } + } + my @results; + if ( defined $options->{filename} ) { + my $filename = $options->{filename}; + my $fh; + eval { $fh = open_file($filename) }; + if ($@) { + chomp(my $err = $@); + print CLIENTERROR $err; + return; + } + my $num = 1; + while( defined (my $line = <$fh>) ) { + chomp($line); + $line =~ s/\t/' 'x4/ge; + push @results, [$num, $`, $&, $'] if $line =~ $regex; + $num++; + } + close $fh; + } else { + my $current_win = ref $witem ? $witem->window : Irssi::active_win; + my $view = $current_win->view; + my $line = $view->{buffer}->{first_line}; + my $num = 1; + while ( defined $line ) { + push @results, [$num, $`, $&, $'] if $line->get_text(0) =~ $regex; + $line = $line->next; + $num++; + } + } + if (@results) { + my $greatest_line_number = $results[-1][0]; + my $digits = length $greatest_line_number; + printf CLIENTCRAP join("\n", ("%%9%${digits}d.%%n%s%%9%%R%s%%n%s")x@results), + map { @$_[0..3] } @results; + } +} + +sub cmd_buffer_load { + my ( $args, $server, $witem ) = @_; + my ($options) = command_parse_options('buffer load', $args); + my $start = $options->{start} // 1; + my $end; + my @new_buffer; + if ( defined $options->{filename} ) { + my $filename = $options->{filename}; + my $fh; + eval { $fh = open_file($filename) }; + if ($@) { + chomp(my $err = $@); + print CLIENTERROR $err; + return; + } + my @dump = <$fh>; + close $fh; + my $lines_count = @dump; + $end = $options->{end} // $lines_count; + + if ($start<1 || $end>$lines_count || $start>$end) { + print CLIENTERROR 'Wrong -start or -end parameters (out of range)'; + return; + }; + @new_buffer = map{ chomp; s/\t/' 'x4/ge; $_ } @dump[$start-1..$end-1] + } else { + my $current_win = ref $witem ? $witem->window : Irssi::active_win; + my $view = $current_win->view; + my $line = $view->{buffer}->{first_line}; + my $lines_count = $view->{buffer}->{lines_count}; + $end = $options->{end} // $lines_count; + + if ($start<1 || $end>$lines_count || $start>$end) { + print CLIENTERROR 'Wrong -start or -end parameters (out of range)'; + return; + }; + my $num = 1; + while ( defined $line ) { + if ( $start<=$num && $num<=$end ) { + chomp(my $line_text = $line->get_text(0)); + push @new_buffer, $line_text; + last if $num == $end; + } + $line = $line->next; + $num++; + } + } + $buffer = \@new_buffer; + print CLIENTCRAP 'Buffer successfully loaded'; +} + +sub cmd_buffer_clear { + $buffer = []; + print CLIENTCRAP 'Buffer is now empty'; +} + +sub cmd_buffer_print { + print CLIENTCRAP $_ for @$buffer; + printf CLIENTCRAP "%d lines", scalar @$buffer; +} + +sub cmd_buffer_play { + my ( $args, $server, $witem ) = @_; + + my ($options) = command_parse_options('buffer play', $args); + my $delay = $options->{delay} // settings_get_str('buffer_delay'); + unless (looks_like_number($delay)) { + print CLIENTERROR 'Delay must be a number'; + return; + } + unless ( $delay >= 10e-3 ) { + print CLIENTERROR 'Delay cannot be less than 0.010 seconds (10 milliseconds)'; + return; + } + if ( defined $options->{continue} ) { + my $id = $options->{continue}; + if ( $id =~ /^\s*$/ ) { # Empty id. Wake up every paused paste + foreach my $inner_id (keys %$pastings) { + if ( $pastings->{$inner_id}{'status'} eq 'paused' ) { + wake_sleeping_paste( $inner_id, ($options->{delay} ? $delay : undef) ); + } + } + } else { # Not empty id + unless ( defined $pastings->{$id} ) { + print CLIENTERROR 'Not recognized id. See /BUFFER RESUME'; + return; + } + wake_sleeping_paste( $id, ($options->{delay} ? $delay : undef) ); + } + } else { + unless ( $server && $server->{connected} ) { + print CLIENTERROR 'Not connected to server'; + return; + } + unless ( $witem && ($witem->{type} eq "CHANNEL" || $witem->{type} eq "QUERY") ) { + print CLIENTERROR 'No active channel/query in window'; + return; + } + unless ( @$buffer ) { + print CLIENTERROR 'Buffer is empty. Nothing to paste'; + return; + } + my $servtag = $server->{tag}; + my $target = $witem->{name}; + my $counter = 0; + + my $timeout_tag; + $timeout_tag = timeout_add( $delay*1000 , 'timeout_function', \$timeout_tag ); + $pastings->{$timeout_tag} = { + buffer => $buffer, + status => 'active', + network => $servtag, + target => $target, + counter => \$counter, + delay => $delay, + timestamp => time, + }; + send_line($timeout_tag, $buffer, \$counter, $server, $target); + } +} + +sub wake_sleeping_paste { + my ( $id, $delay ) = @_; + unless ( $pastings->{$id}{'status'} eq 'paused' ) { + printf CLIENTERROR 'Paste <%d> is already active', $id; + return; + } + + my ( $buff, $servtag, $targ, $pointer ) = + @{$pastings->{$id}}{qw/buffer network target counter/}; + + # Check server and target + my $server = server_find_tag( $servtag ); + unless ( $server && $server->{connected} ) { + printf CLIENTERROR "Not connected to server '%s'. Paste <%d> will continue to be paused", $servtag, $id; + return; + }; + my $witem = $server->window_find_item($targ); + unless ( $witem ) { + printf CLIENTERROR "No window named '%s'. Paste <%d> will continue to be paused", $targ, $id; + return; + } + my $temp = $pastings->{$id}; + delete $pastings->{$id}; + $temp->{'status'} = 'active'; + $temp->{'delay'} = $delay if defined $delay; + $temp->{'timestamp'} = time; + + my $timeout_tag; + $timeout_tag = timeout_add( $temp->{'delay'} * 1000 , 'timeout_function', \$timeout_tag ); + $pastings->{$timeout_tag} = $temp; + send_line($timeout_tag, $buff, $pointer, $server, $targ); +} + +sub cmd_buffer_stop { + my ( $args, $server, $witem ) = @_; + my $id = $args; + if ( $id =~ /^\s*$/ ) { # Empty id. Stop every active paste + foreach my $inner_id (keys %$pastings) { + if ( $pastings->{$inner_id}{'status'} eq 'active' ) { + timeout_remove( $inner_id ); + $pastings->{$inner_id}{'status'} = 'paused'; + } + } + } else { # Not empty id + unless ( defined $pastings->{$id} ) { + print CLIENTERROR 'Not recognized id. See /BUFFER RESUME'; + return; + } + if ( $pastings->{$id}{'status'} eq 'active' ) { + timeout_remove( $id ); + $pastings->{$id}{'status'} = 'paused'; + } + } +} + +sub cmd_buffer_remove { + my ( $args, $server, $witem ) = @_; + my $id = $args; + if ( $id =~ /^\s*$/ ) { # Empty id. Remove every existing paste + foreach my $inner_id (keys %$pastings) { + timeout_remove( $inner_id ); + delete $pastings->{$inner_id}; + } + } else { # Not empty id + unless ( defined $pastings->{$id} ) { + print CLIENTERROR 'Not recognized id. See /BUFFER RESUME'; + return; + } + timeout_remove( $id ); + delete $pastings->{$id}; + } +} + +sub cmd_buffer_resume { + if ( keys %$pastings ) { + + my $context_lines = settings_get_int('buffer_context_lines'); + if ($context_lines < 0) { + print CLIENTERROR 'The number of context lines (surrounding the next line to be sent) must be a positive integer'; + return; + } + + my $id_string = 'ID'; + my $id_colwidth = max( + length($id_string), + max( map{length($_)} keys %$pastings ) + ); + + my $time_string = 'TIMESTAMP'; + my $time_colwidth = max( + length($time_string), + max( map{length( strftime('%c', localtime($pastings->{$_}->{'timestamp'})) )} keys %$pastings ) + ); + + my $status_string = 'STATUS'; + my $status_colwidth = max( + length($status_string), + max( map{length($pastings->{$_}->{'status'})} keys %$pastings ) + ); + + my $network_string = 'NETWORK'; + my $network_colwidth = max( + length($network_string), + max( map{length($pastings->{$_}->{'network'})} keys %$pastings ) + ); + + my $channel_string = 'TARGET'; + my $channel_colwidth = max( + length($channel_string), + max( map{length($pastings->{$_}->{'target'})} keys %$pastings ) + ); + + my $delay_string = 'DELAY'; + my $delay_colwidth = max( + length($delay_string), + max( map{length($pastings->{$_}->{'delay'})} keys %$pastings ) + ); + + my $pasted_string = 'PASTED LINES'; + my $pasted_colwidth = max( + length($pasted_string), + max( map{length( ${$pastings->{$_}->{'counter'}} )} keys %$pastings ) + ); + + my $pending_string = 'PENDING LINES'; + my $pending_colwidth = max( + length($pending_string), + max( map{length( @{$pastings->{$_}->{'buffer'}} - ${$pastings->{$_}->{'counter'}} )} keys %$pastings ) + ); + + my $str_format = join ( + " "x4, + map { "\%${_}s" } + ($id_colwidth, + $time_colwidth, + $status_colwidth, + $network_colwidth, + $channel_colwidth, + $delay_colwidth, + $pasted_colwidth, + $pending_colwidth + ) + ); + foreach my $id ( sort{$pastings->{$a}->{'timestamp'}<=>$pastings->{$b}->{'timestamp'}} keys %$pastings ) { + printf CLIENTCRAP "%%9" . $str_format, + ($id_string, + $time_string, + $status_string, + $network_string, + $channel_string, + $delay_string, + $pasted_string, + $pending_string + ); + printf CLIENTCRAP $str_format . "\n", + ($id, + strftime('%c', localtime($pastings->{$id}->{'timestamp'})), + $pastings->{$id}->{'status'}, + $pastings->{$id}->{'network'}, + $pastings->{$id}->{'target'}, + $pastings->{$id}->{'delay'}, + ${$pastings->{$id}->{'counter'}}, + @{$pastings->{$id}->{'buffer'}} - ${$pastings->{$id}->{'counter'}} + ); + print CLIENTCRAP '%9NEXT LINE TO BE PASTED:'; + my $buffref = $pastings->{$id}->{'buffer'}; + my $index = ${ $pastings->{$id}->{'counter'} }; + my $total = @$buffref; + my @range = buffer_context_range($index, $context_lines, $total); + my $digits = length( max( map{$_ + 1} @range ) ); + foreach my $i ( @range ) { + printf CLIENTCRAP "%%9%${digits}d.%%n%10s%s", + $i+1, (($i == $index) ? '===>' : ''), @$buffref[$i]; + }; + printf CLIENTCRAP "---- End paste <%s>", $id; + } + } else { + print CLIENTCRAP "There aren't any pastes"; + return; + } +} + +command_bind 'buffer' => sub { + my ( $data, $server, $item ) = @_; + $data =~ s/\s+$//g; + command_runsub('buffer', $data, $server, $item); +}; +command_bind_first help => sub { &cmd_buffer_help if $_[0] =~ /^buffer\s*$/i }; +command_bind 'buffer search' => 'cmd_buffer_search'; +command_set_options 'buffer search' => '-filename regexp case word'; +command_bind 'buffer load' => 'cmd_buffer_load'; +command_set_options 'buffer load' => '-filename @start @end'; +command_bind 'buffer clear' => 'cmd_buffer_clear'; +command_bind 'buffer print' => 'cmd_buffer_print'; +command_bind 'buffer play' => 'cmd_buffer_play'; +command_set_options 'buffer play' => '-delay -continue'; +command_bind 'buffer stop' => 'cmd_buffer_stop'; +command_bind 'buffer remove' => 'cmd_buffer_remove'; +command_bind 'buffer resume' => 'cmd_buffer_resume'; +settings_add_str 'buffer', 'buffer_delay', '1'; +settings_add_int 'buffer', 'buffer_context_lines', '2'; |
