diff options
69 files changed, 8124 insertions, 789 deletions
@@ -1,2 +1,7 @@ /test/irssi/* /nightfrog +/modules/*/*.so +/modules/*/*.o +/vim-mode/TAGS +/testing/irssi-test.log +/vim-mode/irssi/scripts/autorun/README.pod diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b351d8e --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +SCRIPT_DIRS = \ + history-search \ + ido-mode \ + prompt_info \ + quit-notify \ + sb-position \ + scrolled-reminder \ + tinyurl-tabcomplete \ + vim-mode + +# act_hide \ +# auto-server \ +# colour-popup \ +# feature-tests \ +# joinforward \ +# masshilight \ +# modules \ +# no-key-modes \ +# patches \ +# testing \ +# throttled-autojoin \ +# undo \ +# url_hilight \ + +SCRIPT_FILES = $(foreach FOO, $(SCRIPT_DIRS), $(wildcard $(FOO)/*.pl)) + +$GENERATOR = "./readme_generator" + +.PHONY: all clean rebuild + +all: + + echo making all: $(SCRIPT_DIRS) + echo files are: $(SCRIPT_FILES) + +rebuild: clean all + +clean: + -echo cleaning. + +README.pod: + -echo stuff @@ -3,6 +3,101 @@ This repository contains a collection of scripts I have written or adapted from others to improve my (and your!) irssi using experience. +## What's In Here + +I never thought I'd end up writing so many scripts, so rather than give them +each their own repository as might be sensible, they're all stuffed in here. + +The following is a brief list of the interesting scripts, and why you might want +to use them. + +* `auto_server/` -- provides a `/join+` command which allows you to join + channels on servers to which you're not currently connected, as long as you've + got everything set up correctly in your `/channels` and `/networks` config. + + +* `history_search/` -- an improved version of coekie's original history_search, it + behaves a lot more like the standard Readline incremental + search over your recent command history. + +* `ido_mode/` -- A window switching mode best described as "The bastard lovechild + of emacs ido-mode and window_switcher.pl". It supports flexible matching of + channel names, filtering by active windows, servers, and more. See the script + comments for details. + +* `feature_tests/` has a bunch of different little scripts written to test certain + aspects of Irssi behaviour. None of them are likely to be particularly useful + unless you're looking for simple examples of various things on which to build. + Some of the ones which might actually be useful are: + + * `key_test.pl` - decodes and prints keyboard inputs from the `gui key pressed` + signal. Useful for checking what keycode a particular combo generates. + + * `template.pl` - A basic outline of a script to use as a starting point + for others. + + * `pipes.pl` A barebones template for a script which uses forking and pipes + to handle async tasks in the background without hanging Irssi. + +* `modules/` contains a few half-baked attempts at loadable modules for Irssi, + none of which do very much at the moment. They might be useful as templates + for your own modules, however. + +* `no_key_modes/` provides an additional expando `$M_nopass` which you can + add to your theme to provide channel modes, but without showing the channel + key. This is probably less useful since someone pointed me at `/set + chanmode_expando_strip` which has much the same effect. + +* `patches/` contains a few patches for the irssi source, probably out of date + by now, which do various things. The easiest way to figure out what is to read + them, unless I get bored and document them sometime. + +* `prompt_info/` was the original "How can I put dynamic status crap into my + prompt string" script. This has since been superceded by the far nicer + `uberprompt.pl` which also lives in that dir. Many of my other scripts + require uberprompt as a way to provide part of their user interface. + +* `sb_position/` gives you an additional status bar item which indicates + approximately how far you are scrolled in a given window buffer, in terms + of pages and percentage. It is configurable as the comments in the script + explain. + +* `scrolled_reminder/` is a script that tries to prevent one of the more + common accidental IRC screwups -- replying to a conversation that happened + hours ago because you didn't notice you were scrolled up in the history buffer. + If you try to send a message whilst scrolled, it'll warn you and check you + really want to first. + +* `quit-notify/` is based on a script by vague@#irssi/Freenode. It is most + useful when you are ignoring joins/parts/quits from a channel, and hence + don't realise when the person you are responding to has left. If they have + (or if you prefix your message with a nonexistent nick), the script will + ask you to confirm that you want to send the message, much like + `scrolled_reminder`. + +* `undo/` contains 2 scripts, `undo.pl` and `kill-ring.pl`. See their header + for further documentation. + +* `testing/` is a Perl framework external to Irssi (ie: not a script) that will + ultimately allow you to write unit tests for your scripts, and run them in a + real irssi instance, and check that they behave correctly. Still undergoing + heavy development, and not really ready for public usage. + +* `throttled_autojoin/` doesn't work. It's meant to allow people with huuuuuuge + numbers of autojoin channels and servers to connect slowly, so as not to time + themselves out when reconnecting. Mostly dead. + +* `tinyurl_tabcomplete/` is a script which lets you hit tab following anything + that looks like a URL to replace it with a tinyurl'd version. Handy if you + regularly paste Google-maps links or other massive horrible links. + +* `url_hilight/` I can't remember what this one does. Suggestions welcome. + +* `vim_mode/` The most exciting (and by far the largest) script here. Gives + vim-addicted users a way to use most of their finger-memory in Irssi. Still + under active development, features and bug-requests welcome. Also supported + in #irssi_vim on Freenode. + ## Documentation If you are here looking for my Irssi scripting documentation, please note that diff --git a/act_hide/act_hide.pl b/act_hide/act_hide.pl new file mode 100644 index 0000000..95c10b9 --- /dev/null +++ b/act_hide/act_hide.pl @@ -0,0 +1,353 @@ +=pod + +=head1 NAME + +act_hide.pl + +=head1 DESCRIPTION + +A minimalist template useful for basing actual scripts on. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +=head1 USAGE + +None, since it doesn't actually do anything. + +=head1 AUTHORS + +Derived from the L<hide.pl|http://scripts.irssi.org/scripts/hide.pl> script, +Original Copyright E<copy> 2002 Marcus Rueckert C<E<lt>darix@irssi.deE<gt>> + +Modifications Copyright E<copy> 2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> + +=head1 LICENCE + +B<TODO:> Is this Public Domain enough? + +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 + +=head1 TODO + +=cut + +use strict; +use warnings; + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'Marcus Rueckert, shabble', + contact => 'shabble+irssi@metavore.org', + name => 'act_hide', + description => 'Improved interface to activity_hide_* settings', + license => 'Public Domain', + ); + +# Globals. + +my $CMD_NAME = "hide"; +my $SUB_CMDS = { + add => \&cmd_hide_add, + del => \&cmd_hide_del, + list => \&cmd_hide_list, + }; + +my $CMD_OPTS = "-target -level"; + + +act_hide_init(); + +sub act_hide_init { + + Irssi::command_bind($CMD_NAME, \&subcmd_handler); + + foreach my $subcmd (keys %$SUB_CMDS) { + my $coderef = $SUB_CMDS->{$subcmd}; + Irssi::command_bind("$CMD_NAME $subcmd", $coderef); + Irssi::command_set_options("$CMD_NAME $subcmd", $CMD_OPTS); + } +} + +sub subcmd_handler { + my ($data, $server, $w_item) = @_; + $data =~ s/\s+$//g; # strip trailing whitespace. + Irssi::command_runsub($CMD_NAME, $data, $server, $w_item); +} + +sub parse_args { + my ($cmd, $data) = @_; + my $options = {}; + my @opts = Irssi::command_parse_options($cmd, $data); + + if (@opts and ref($opts[0]) eq 'HASH') { + $options = $opts[0]; + $options->{__remainder} = $opts[1]; + print "Options: " . Dumper($options); + } else { + $options->{__remainder} = $data; + } + return $options; +} + +sub cmd_hide_add { + my ($args, $server, $w_item) = @_; + my $opts = parse_args("$CMD_NAME add", $args); + + if (exists $opts->{target}) { + my $target = $opts->{__remainder}; + $target =~ s/^\s*(\S+)\s*$/$1/; + + if (add_item_to_targets($target)) { + $w_item->print("added $target"); + } else { + $w_item->print("failed to add $target"); + } + } elsif (exists $opts->{level}) { + + } else { + print "Bah"; + } + +} + +sub cmd_hide_del { + my ($args, $server, $w_item) = @_; + my $opts = parse_args("$CMD_NAME del", $args); + if (exists $opts->{target}) { + my $target = $opts->{__remainder}; + $target =~ s/^\s*(\S+)\s*$/$1/; + + if (remove_item_from_targets($target)) { + $w_item->print("removed $target"); + } else { + $w_item->print("failed to remove $target"); + } + } elsif (exists $opts->{level}) { + + } else { + print "Bah"; + } + +} + +sub cmd_hide_list { + my ($args, $server, $w_item) = @_; + my $opts = parse_args("$CMD_NAME list", $args); + if (exists $opts->{target}) { + $w_item->print("Targets: " + . Irssi::settings_get_str('activity_hide_targets')); + } elsif (exists $opts->{level}) { + $w_item->print("Levels: " + . Irssi::settings_get_level('activity_hide_level')); + } else { + $w_item->print("Bah"); + } + +} + +sub add_item_to_targets { + my ($item) = @_; + my $current = Irssi::settings_get_str('activity_hide_targets'); + my %map = map { $_ => 1 } split /\s+/, $current; + + if (not exists $map{$item}) { + Irssi::settings_set_str('activity_hide_targets', $current . ' ' . $item); + return 1; + } else { + print "Cannot add $item, already exists"; + return 0; + } +} + +sub remove_item_from_targets { + my ($item) = @_; + my $current = Irssi::settings_get_str('activity_hide_targets'); + my %map = map { $_ => 1 } split /\s+/, $current; + + if (exists $map{$item}) { + my $new_str = join ' ', grep { $_ ne $item } keys %map; + Irssi::settings_set_str('activity_hide_targets', $new_str); + return 1; + } else { + print "Cannot remove $item, doesn't exist"; + return 0; + } +} + + + + +# sub add_item { +# my ($target_type, $data) = @_; +# my $target = target_check ($target_type); +# return 0 unless $target; +# if ($data =~ /^\s*$/ ) { +# print (CRAP "\cBNo target specified!\cB"); +# print (CRAP "\cBUsage:\cB hide $target_type add [$target_type]+"); +# } +# else { +# my $set = settings_get_str($target); +# for my $item ( split (/\s+/, $data) ) { +# if ($set =~ m/^\Q$item\E$/i) { +# print (CRAP "\cBWarning:\cB $item is already in in $target_type hide list.") +# } +# else { +# print (CRAP "$item added to $target_type hide list."); +# $set = join (' ', $set, $item); +# } +# }; +# settings_set_str ($target, $set); +# signal_emit('setup changed'); +# } +# return 1; +# } + +# sub remove_item { +# my ($target_type, $data) = @_; +# my $target = target_check ($target_type); +# if ( not ( $target )) { return 0 }; +# if ($data =~ /^\s*$/ ) { +# print (CRAP "\cBNo target specified!\cB"); +# print (CRAP "\cBUsage:\cB hide $target_type remove [$target_type]+"); +# } +# else { +# my $set = settings_get_str($target); +# for my $item ( split (/\s+/, $data) ) { +# if ($set =~ s/$item//i) { +# print (CRAP "$item removed from $target_type hide list.") +# } +# else { +# print (CRAP "\cBWarning:\cB $item was not in $target_type hide list.") +# } +# }; +# settings_set_str ($target, $set); +# signal_emit('setup changed'); +# } +# return 1; +# } + +# sub target_check { +# my ($target_type) = @_; +# my $target = ''; +# if ($target_type eq 'level') { +# $target = 'activity_hide_level'; +# } +# elsif ($target_type eq 'target') { +# $target = 'activity_hide_targets'; +# } +# else { +# print (CLIENTERROR "\cBadd_item: no such target_type $target_type\cB"); +# } +# return $target; +# } + +# sub print_usage { +# print (CRAP "\cBUsage:\cB"); +# print (CRAP " hide target [add|remove] [targets]+"); +# print (CRAP " hide level [add|remove] [levels]+"); +# print (CRAP " hide usage"); +# print (CRAP " hide print"); +# print (CRAP "See also: levels"); +# }; + +# sub print_items { +# my ($target_type) = @_; +# my $delimiter = settings_get_str('hide_print_delimiter'); +# my $target = target_check ($target_type); +# if ( not ( $target )) { return 0 }; +# print ( CRAP "\cB$target_type hide list:\cB$delimiter", join ( $delimiter, sort ( split ( " ", settings_get_str($target) ) ) ) ); +# return 1; +# } + +# # +# # targets +# # + +# command_bind 'hide target' => sub { +# my ($data, $server, $item) = @_; +# if ($data =~ m/^[(add)|(remove)]/i ) { +# command_runsub ('hide target', $data, $server, $item); +# } +# else { +# print (CRAP "\cBUsage:\cB hide target [add|remove] [targets]+"); +# } +# }; + +# command_bind 'hide target add' => sub { +# my ($data, $server, $item) = @_; +# add_item ('target', $data); +# }; + +# command_bind 'hide target remove' => sub { +# my ($data, $server, $item) = @_; +# remove_item ('target', $data); +# }; + +# # +# # levels +# # +# command_bind 'hide level' => sub { +# my ($data, $server, $item) = @_; +# if ($data =~ m/^[(add)|(remove)]/i ) { +# command_runsub ('hide level', $data, $server, $item); +# } +# else { +# print (CRAP "\cBUsage:\cB hide level [add|remove] [levels]+"); +# print (CRAP "See also: levels"); +# } +# }; + +# command_bind 'hide level add' => sub { +# my ($data, $server, $item) = @_; +# add_item ('level', $data); +# }; + +# command_bind 'hide level remove' => sub { +# my ($data, $server, $item) = @_; +# remove_item ('level', $data); +# }; + +# # +# # general +# # + +# command_bind 'hide' => sub { +# my ($data, $server, $item) = @_; +# if ($data =~ m/^[(target)|(level)|(help)|(usage)|(print)]/i ) { +# command_runsub ('hide', $data, $server, $item); +# } +# else { +# print_usage(); +# } +# }; + +# command_bind 'hide print' => sub { +# print_items ('level'); +# print_items ('target'); +# }; + +# command_bind 'hide usage' => sub { print_usage (); }; +# command_bind 'hide help' => sub { print_usage (); }; + +# # +# # settings +# # + +# settings_add_str ( 'script', 'hide_print_delimiter', "\n - "); diff --git a/feature-tests/augment_inputline.pl b/feature-tests/augment_inputline.pl index 577f756..97a129e 100644 --- a/feature-tests/augment_inputline.pl +++ b/feature-tests/augment_inputline.pl @@ -1,3 +1,12 @@ +=pod + +=head1 NAME + +test + +=cut + + use strict; use Irssi; use Irssi::TextUI; # for sbar_items_redraw diff --git a/feature-tests/bindings.pl b/feature-tests/bindings.pl index 006eaf1..ece220d 100644 --- a/feature-tests/bindings.pl +++ b/feature-tests/bindings.pl @@ -23,11 +23,39 @@ our %IRSSI = ( my $keymap; +sub STATE_HEADER () { 0 } +sub STATE_BODY () { 1 } +sub STATE_END () { 2 } +my $parse_state = STATE_HEADER; + +my $binding_formats = {}; + init(); sub init { - update_keymap(); + + $keymap = {}; + Irssi::command_bind('showbinds', 'cmd_showbinds'); + Irssi::signal_add('command bind' => 'watch_keymap'); + + $binding_formats = get_binding_formats(); + + capture_bind_data(); +} + +sub get_binding_formats { + my $theme = Irssi::current_theme(); + my @keys = qw/bind_header bind_list bind_command_list + bind_footer bind_unknown_id/; + + my $ret = {}; + foreach my $key (@keys) { + my $tmp = $theme->get_format('fe-common/core', $key); + #$tmp =~ s/%/%%/g; # escape colour codes? + $ret->{$key} = $tmp; + } + return $ret; } sub cmd_showbinds { @@ -42,35 +70,44 @@ sub cmd_showbinds { $win->print("Done showing window bindings:", Irssi::MSGLEVEL_CLIENTCRAP); } -sub get_keymap { + +sub sig_print_text { my ($text_dest, $str, $str_stripped) = @_; - if ($text_dest->{level} == Irssi::MSGLEVEL_CLIENTCRAP and $text_dest->{target} eq '') { - if (not defined($text_dest->{'server'})) { - if ($str_stripped =~ m/((?:meta-)+)(.)\s+change_window (\d+)/) { - my ($level, $key, $window) = ($1, $2, $3); - #my $numlevel = ($level =~ y/-//) - 1; - my $kk = $level . $key; - $keymap->{$kk} = $window; - } - Irssi::signal_stop(); - } + return unless $text_dest->{level} == Irssi::MSGLEVEL_CLIENTCRAP; + return unless $text_dest->{target} eq ''; + return unless not defined $text_dest->{server}; + + # if ($parse_state = STATE_HEADER) { + # if ($str =~ m/\Q$binding_formats->{bind_header}\E/) { + # $parse_state = STATE_BODY; + # } + # } elsif ($parse_state = STATE_BODY) { + print "Data is: $str_stripped"; + if ($str_stripped =~ m/^.*?(\S{,20})\s+(\S+)\s+(\S+)/) { + $keymap->{$1} = "$2, $3"; + print "Parsed $1 as $2, $3"; } + Irssi::signal_stop(); + # } elsif ($str =~ m/$binding_formats->{bind_footer}\E/) { + # $parse_state = STATE_END; + # } + # } } -sub update_keymap { - $keymap = {}; + +sub capture_bind_data { Irssi::signal_remove('command bind' => 'watch_keymap'); - Irssi::signal_add_first('print text' => 'get_keymap'); + Irssi::signal_add_first('print text' => 'sig_print_text'); Irssi::command('bind'); # stolen from grep - Irssi::signal_remove('print text' => 'get_keymap'); - Irssi::signal_add('command bind' => 'watch_keymap'); - #Irssi::timeout_add_once(100, 'eventChanged', undef); + Irssi::signal_remove('print text' => 'sig_print_text'); + } + # watch keymap changes sub watch_keymap { - Irssi::timeout_add_once(1000, 'update_keymap', undef); + Irssi::timeout_add_once(1000, 'capture_bind_data', undef); } diff --git a/feature-tests/easy_exec.pl b/feature-tests/easy_exec.pl index 669a00b..d342516 100644 --- a/feature-tests/easy_exec.pl +++ b/feature-tests/easy_exec.pl @@ -2,7 +2,7 @@ use strict; use warnings; # export everything. -use Irssi; #(@Irssi::EXPORT_OK); +use Irssi; use Irssi::Irc; use Irssi::TextUI; @@ -18,33 +18,38 @@ our %IRSSI = ( license => 'Public Domain', ); -#Irssi::signal_add_first 'command script exec', \&better_exec; -Irssi::command_bind('script exec', \&better_exec); - -sub better_exec { - my ($args, $serv, $witem) = @_; - # todo: handle permanent arg? - my $perm = 0; - print "Args: $args"; - if ($args =~ s/^\s*-permanent\s*(.*)$/$1/) { - $perm = 1; - } - print "Args now: $args"; - -# eval $args; - my $str = "//script exec " . - ($perm ? '-permanent' : '') - . 'use Irssi (@Irssi::EXPORT_OK); ' . $args; - print "Running: $str"; - -# Irssi::command($str); - Irssi::signal_continue($str, @_[1..$#_]); -} +# TODO: make this more tab-complete friendly +init(); + +sub init { + Irssi::command('/alias se script exec use Data::Dumper\;' + .' use Irssi (@Irssi::EXPORT_OK)\; $0-'); + Irssi::command('/alias sep script exec -permanent ' + . 'use Data::Dumper\; use Irssi (@Irssi::EXPORT_OK)\; $0-'); -sub Dump { - print Dumper(\@_); + Irssi::signal_add_last ('complete word', 'sig_complete_word'); } -sub test() { - print "This is a test"; +sub sig_complete_word { + my ($strings, $window, $word, $linestart, $want_space) = @_; + # only provide these completions if the input line is otherwise empty. + my $cmdchars = Irssi::settings_get_str('cmdchars'); + my $quoted = quotemeta($cmdchars); + #print "Linestart: $linestart"; + return unless ($linestart =~ /^${quoted}(?:se|sep)/); + + my $clean_word = $word; + $clean_word =~ s/^"//g; + $clean_word =~ s/"$//g; + $clean_word =~ s/->$//g; + + + + my @expansions = @Irssi::EXPORT_OK; + push @$strings, grep { $_ =~ m/^\Q$clean_word\E/ } @expansions; + print "Sebug: " . join(", ", @$strings); + $$want_space = 0; + + + Irssi::signal_stop() if (@$strings); } diff --git a/feature-tests/exec.pl b/feature-tests/exec.pl new file mode 100644 index 0000000..f6d6377 --- /dev/null +++ b/feature-tests/exec.pl @@ -0,0 +1,442 @@ +# exec.pl +# a (currently stupid) alternative to the built-in /exec, because it's broken +# on OSX. This thing stll needs a whole bunch of actual features, but for now, +# you can actually run commands. + +# Obviously, that's pretty dangerous. Use at your own risk. + +# EXEC [-] [-nosh] [-out | -msg <target> | -notice <target>] [-name <name>] <cmd line> +# EXEC -out | -window | -msg <target> | -notice <target> | -close | -<signal> %<id> +# EXEC -in %<id> <text to send to process> +# +# -: Don't print "process terminated ..." message +# +# -nosh: Don't start command through /bin/sh +# +# -out: Send output to active channel/query +# +# -msg: Send output to specified nick/channel +# +# -notice: Send output to specified nick/channel as notices +# +# -name: Name the process so it could be accessed easier +# +# -window: Move the output of specified process to active window +# +# -close: Forcibly close (or "forget") a process that doesn't die. +# This only removes all information from irssi concerning the +# process, it doesn't send SIGKILL or any other signal +# to the process. +# +# -<signal>: Send a signal to process. <signal> can be either numeric +# or one of the few most common ones (hup, term, kill, ...) +# +# -in: Send text to standard input of the specified process +# +# -interactive: Creates a query-like window item. Text written to it is +# sent to executed process, like /EXEC -in. +# +# Execute specified command in background. Output of process is printed to +# active window by default, but can be also sent as messages or notices to +# specified nick or channel. +# +# Processes can be accessed either by their ID or name if you named it. Process +# identifier must always begin with '%' character, like %0 or %name. +# +# Once the process is started, its output can still be redirected elsewhere with +# the -window, -msg, etc. options. You can send text to standard input of the +# process with -in option. +# +# -close option shouldn't probably be used if there's a better way to kill the +# process. It is meant to remove the processes that don't die even with +# SIGKILL. This option just closes the pipes used to communicate with the +# process and frees all memory it used. +# +# EXEC without any arguments displays the list of started processes. +# + + + +use 5.010; # 5.10 or above, necessary to get the return value from a command. + +use strict; +use warnings; +use English '-no_match_vars'; + +use Irssi; +use POSIX; +use Time::HiRes qw/sleep/; +use IO::Handle; +use IO::Pipe; +use IPC::Open3; +use Symbol 'geniosym'; + +use Data::Dumper; + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => 'exec.pl', + description => '', + license => 'Public Domain', + ); + +my @processes = (); +sub get_processes { return @processes } + +# the /exec command, nothing to do with the actual command being run. +my $command; +my $command_options; + +sub get_new_id { + my $i = 1; + foreach my $proc (@processes) { + if ($proc->{id} != $i) { + next; + } + $i++; + } + return $i; +} + +sub add_process { + #my ($pid) = @_; + my $id = get_new_id(); + + my $new = { + id => $id, + pid => 0, + in_tag => 0, + out_tag => 0, + err_tag => 0, + s_in => geniosym(), #IO::Handle->new, + s_err => geniosym(), #IO::Handle->new, + s_out => geniosym(), #IO::Handle->new, + cmd => '', + opts => {}, + }; + + # $new->{s_in}->autoflush(1); + # $new->{s_out}->autoflush(1); + # $new->{s_err}->autoflush(1); + + push @processes, $new; + + _msg("New process item created: $id"); + return $new; +} + +sub find_process_by_id { + my ($id) = @_; + my @matches = grep { $_->{id} == $id } @processes; + _error("wtf, multiple id matches for $id. BUG") if @matches > 1; + + return $matches[0]; + +} +sub find_process_by_pid { + my ($pid) = @_; + my @matches = grep { $_->{pid} == $pid } @processes; + _error("wtf, multiple pid matches for $pid. BUG") if @matches > 1; + + return $matches[0]; +} + +sub remove_process { + my ($id, $verbose) = @_; + my $del_index = 0; + foreach my $proc (@processes) { + if ($id == $proc->{id}) { + last; + } + $del_index++; + } + print "remove: del index: $del_index"; + if ($del_index <= $#processes) { + my $dead = splice(@processes, $del_index, 1, ()); + #_msg("removing " . Dumper($dead)); + + Irssi::input_remove($dead->{err_tag}); + Irssi::input_remove($dead->{out_tag}); + + close $dead->{s_out}; + close $dead->{s_in}; + close $dead->{s_err}; + + } else { + $verbose = 1; + if ($verbose) { + print "remove: No such process with ID $id"; + } + } +} + +sub show_current_processes { + if (@processes == 0) { + print "No processes running"; + return; + } + foreach my $p (@processes) { + printf("ID: %d, PID: %d, Command: %s", $p->{id}, $p->{pid}, $p->{cmd}); + } +} + +sub parse_options { + my ($args) = @_; + my @options = Irssi::command_parse_options($command, $args); + if (@options) { + my $opt_hash = $options[0]; + my $rest = $options[1]; + + $rest =~ s/^\s*(.*?)\s*$/$1/; # trim surrounding space. + + #print Dumper([$opt_hash, $rest]); + if (length $rest) { + return ($opt_hash, $rest); + } else { + show_current_processes(); + return (); + } + } else { + _error("Error parsing $command options"); + return (); + } +} + +sub schedule_cleanup { + my $fd = shift; + Irssi::timeout_add_once(100, sub { $_[0]->close }, $fd); +} + +sub do_fork_and_exec { + my ($rec) = @_; + + #Irssi::timeout_add_once(100, sub { die }, {}); + + return unless exists $rec->{cmd}; + drop_privs(); + + _msg("Executing command " . join(", ", @{ $rec->{cmd} })); + my $c = join(" ", @{ $rec->{cmd} }); + my $pid = open3($rec->{s_sin}, $rec->{s_out}, $rec->{s_err}, $c); + + _msg("PID is $pid"); + $rec->{pid} = $pid; + + # _msg("Pid %s, in: %s, out: %s, err: %s, cmd: %s", + # $pid, $sin, $sout, $serr, $cmd); + + # _msg("filenos, Pid %s, in: %s, out: %s, err: %s", + # $pid, $sin->fileno, $sout->fileno, $serr->fileno); + + if (not defined $pid) { + + _error("open3 failed: $! Aborting"); + + close($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out}); + undef($_) for ($rec->{s_in}, $rec->{s_err}, $rec->{s_out}); + + return; + } + + # parent + if ($pid) { + +# eval { + print "fileno is " . fileno($rec->{s_out}); + $rec->{out_tag} = Irssi::input_add( fileno($rec->{s_out}), + Irssi::INPUT_READ, + \&child_output, + $rec); + #die unless $rec->{out_tag}; + + $rec->{err_tag} = Irssi::input_add(fileno($rec->{s_err}), + Irssi::INPUT_READ, + \&child_error, + $rec); + #die unless $rec->{err_tag}; + + # }; + + + Irssi::pidwait_add($pid); + die "input_add failed to initialise: $@" if $@; + } +} + +sub drop_privs { + my @temp = ($EUID, $EGID); + my $orig_uid = $UID; + my $orig_gid = $GID; + $EUID = $UID; + $EGID = $GID; + # Drop privileges + $UID = $orig_uid; + $GID = $orig_gid; + # Make sure privs are really gone + ($EUID, $EGID) = @temp; + die "Can't drop privileges" + unless $UID == $EUID && $GID eq $EGID; +} + +sub child_error { + my $rec = shift; + + my $err_fh = $rec->{s_err}; + + my $done = 0; + + while (not $done) { + my $data = ''; + _msg("Stderr: starting sysread"); + my $bytes_read = sysread($err_fh, $data, 256); + if (not defined $bytes_read) { + _error("stderr: sysread failed:: $!"); + $done = 1; + } elsif ($bytes_read == 0) { + _msg("stderr: sysread got EOF"); + $done = 1; + } elsif ($bytes_read < 256) { + # that's all, folks. + _msg("%%_stderr:%%_ read %d bytes: %s", $bytes_read, $data); + } else { + # we maybe need to read some more + _msg("%%_stderr:%%_ read %d bytes: %s, maybe more", $bytes_read, $data); + } + } + + _msg('removing input stderr tag'); + Irssi::input_remove($rec->{err_tag}); + +} + +sub sig_pidwait { + my ($pidwait, $status) = @_; + my @matches = grep { $_->{pid} == $pidwait } @processes; + foreach my $m (@matches) { + _msg("PID %d has terminated. Status %d (or maybe %d .... %d)", + $pidwait, $status, $?, ${^CHILD_ERROR_NATIVE} ); + + remove_process($m->{id}); + } +} + +sub child_output { + my $rec = shift; + my $out_fh = $rec->{s_out}; + + my $done = 0; + + while (not $done) { + my $data = ''; + _msg("Stdout: starting sysread"); + my $bytes_read = sysread($out_fh, $data, 256); + if (not defined $bytes_read) { + _error("stdout: sysread failed:: $!"); + $done = 1; + } elsif ($bytes_read == 0) { + _msg("stdout: sysread got EOF"); + $done = 1; + } elsif ($bytes_read < 256) { + # that's all, folks. + _msg("%%_stdout:%%_ read %d bytes: %s", $bytes_read, $data); + } else { + # we maybe need to read some more + _msg("%%_stdout:%%_ read %d bytes: %s, maybe more", $bytes_read, $data); + } + } + + _msg('removing input stdout tag'); + Irssi::input_remove($rec->{out_tag}); + + #schedule_cleanup($stdout_reader); + #$stdout_reader->close; +} + +sub _error { + my ($msg, @params) = @_; + my $win = Irssi::active_win(); + my $str = sprintf($msg, @params); + $win->print($str, Irssi::MSGLEVEL_CLIENTERROR); +} + +sub _msg { + my ($msg, @params) = @_; + my $win = Irssi::active_win(); + my $str = sprintf($msg, @params); + $win->print($str, Irssi::MSGLEVEL_CLIENTCRAP); +} + +sub cmd_exec { + + my ($args, $server, $witem) = @_; + Irssi::signal_stop; + my @options = parse_options($args); + + if (@options) { + my $rec = add_process(); + my ($options, $cmd) = @options; + + $cmd = [split ' ', $cmd]; + + if (not exists $options->{nosh}) { + unshift @$cmd, ("/bin/sh -c"); + } + + $rec->{opts} = $options; + $rec->{cmd} = $cmd; + + do_fork_and_exec($rec) + } + +} + +sub cmd_input { + my ($args) = @_; + my $rec = $processes[0]; # HACK, make them specify. + if ($rec->{pid}) { + print "INput writing to $rec->{pid}"; + my $fh = $rec->{s_in}; + + my $ret = syswrite($fh, "$args\n"); + if (not defined $ret) { + print "Error writing to process $rec->{pid}: $!"; + } else { + print "Wrote $ret bytes to $rec->{pid}"; + } + + } else { + _error("no execs are running to accept input"); + } +} + +sub exec_init { + $command = "exec"; + $command_options = join ' ', + ( + '!-', 'interactive', 'nosh', '+name', '+msg', + '+notice', 'window', 'close', '+level', 'quiet' + ); + + Irssi::command_bind($command, \&cmd_exec); + Irssi::command_set_options($command, $command_options); + Irssi::command_bind('input', \&cmd_input); + + Irssi::signal_add('pidwait', \&sig_pidwait); +} + + exec_init(); + +package Irssi::UI; + +{ + no warnings 'redefine'; + + sub processes() { + return Irssi::Script::exec::get_processes(); + } + +} + +1; diff --git a/feature-tests/key_sig.pl b/feature-tests/key_sig.pl new file mode 100644 index 0000000..ef69d45 --- /dev/null +++ b/feature-tests/key_sig.pl @@ -0,0 +1,51 @@ +use strict; +use warnings 'all'; + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; + + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => '', + description => '', + license => 'Public Domain', + ); + +my $bacon = 10; + +Irssi::signal_register({'key created' => [qw/Irssi::UI::Key/ ] }); + +Irssi::signal_add('key created', \&sig_key_created); +Irssi::signal_register({'key command' => [qw/string/]}); +Irssi::signal_add_first('key command' => \&sig_key_cmd); + +Irssi::signal_register({'key nothing' => [qw/string/]}); +Irssi::signal_add_first('key nothing' => \&sig_key_cmd); + +Irssi::signal_register({'keyboard created' => [qw/Irssi::UI::Keyboard/]}); +Irssi::signal_add_first('keyboard created' => \&sig_keyboard); + +sub sig_keyboard { + my ($data) = @_; + print "keyboard: " . Dumper($data); +} + +sub sig_key_cmd { + my ($data) = @_; + print "key cmd: " . Dumper($data); + +} + +sub sig_key_created { + my @args = @_; + + print "Key Created, Args: " . Dumper(\@args); +} + +Irssi::command("bind meta-q /echo moo"); diff --git a/feature-tests/redir-input.pl b/feature-tests/redir-input.pl index 94ca523..ed77d3c 100644 --- a/feature-tests/redir-input.pl +++ b/feature-tests/redir-input.pl @@ -20,11 +20,14 @@ our %IRSSI = ( Irssi::command_bind("ri", \&cmd_ri); - +Irssi::signal_register({ 'gui entry redirect' => [ qw/string string intptr intptr/]}); sub cmd_ri { my ($args, $server, $witem) = @_; my $win = Irssi::active_win(); - + my ($x, $y) = (0, 0); + Irssi::signal_emit('gui entry redirect', 'sub_blah', "bacon", $x, $y); #my $ref = Irssi::windows_refnum_last - $win->format_create_dest(Irssi::MSGLEVEL_ClIENTCRAP()); +# $win->format_create_dest(Irssi::MSGLEVEL_ClIENTCRAP()); } + +sub sub_blah { print "Moo" } diff --git a/feature-tests/sig_unbind.pl b/feature-tests/sig_unbind.pl new file mode 100644 index 0000000..3123182 --- /dev/null +++ b/feature-tests/sig_unbind.pl @@ -0,0 +1,58 @@ +use strict; +use warnings; + + +use Irssi (@Irssi::EXPORT_OK); +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; + + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => '', + description => '', + license => 'Public Domain', + ); + +command_bind("dosig_r", + sub { + my $ref = \&cmd_oink; + _print("binding oink to $ref"); + signal_add("command oink", $ref); + }); + +command_bind("undosig_r", + sub { + my $ref = \&cmd_oink; + + _print("unbinding oink from $ref"); + + signal_remove("command oink", $ref); + }); + +command_bind("dosig_s", + sub { + signal_add("command oink", 'cmd_oink'); + }); + +command_bind("undosig_s", + sub { + signal_remove("command oink", 'cmd_oink'); + }); + +sub cmd_oink { + Irssi::active_win()->print("Oink:"); +} + +sub _print { + Irssi::active_win()->print($_[0]); +} + +command("dosig_r"); +command("oink"); +command("undosig_r"); +command("oink"); diff --git a/feature-tests/signal_logger.pl b/feature-tests/signal_logger.pl new file mode 100644 index 0000000..3b3b9ad --- /dev/null +++ b/feature-tests/signal_logger.pl @@ -0,0 +1,170 @@ +use strict; +use warnings; + + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; +use Time::HiRes qw/time/; + +use Data::Dumper; + + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => '', + description => '', + license => 'Public Domain', + ); + +my $enabled = 0; +my $depth = 0; +my $handlers = { }; +my @log = (); +my @signals = +( +'send text', +'send command', +#'print text', +#'gui print text', +'beep', +#'complete word', +#'gui key pressed', +'window changed', + "server add fill", + "server connect copy", + "server connect failed", + "server connected", + "server connecting", + "server disconnected", + "server event", + "server incoming", + "server lag disconnect", + "server lag", + "server looking", + "server nick changed", + "server quit", + "server reconnect not found", + "server reconnect remove", + "server reconnect save status", + "server sendmsg", + "server setup fill chatnet", + "server setup fill connect", + "server setup fill reconn", + "server setup read", + "server setup saved", + "default event", +#'gui print text finished', + +); + +init(); + +sub init { + + @log = (); + $handlers = {}; + + Irssi::command_bind('siglog_on', \&cmd_register_all_signals); + Irssi::command_bind('siglog_off', \&cmd_unregister_all_signals); + Irssi::command_bind('siglog_dump', \&cmd_log_dump); + Irssi::command_bind('siglog_stats', \&cmd_log_stats); +} + +sub cmd_register_all_signals { + + + Irssi::active_win->print("Starting to log all signals"); + $enabled = 1; + + foreach my $sig_name (@signals) { + + my $first_func = build_init_func($sig_name); + my $last_func = build_end_func($sig_name); + + $handlers->{$sig_name} = [ $first_func, $last_func ]; + + Irssi::signal_add_first($sig_name, $first_func); + Irssi::signal_add_last($sig_name, $last_func); + } +} + +sub cmd_unregister_all_signals { + + foreach my $sig_name (@signals) { + + my ($first_func, $last_func) = @{ $handlers->{$sig_name} }; + + Irssi::signal_remove($sig_name, $first_func); + Irssi::signal_remove($sig_name, $last_func); + } + $enabled = 0; + Irssi::active_win->print("Signal logging disabled"); + +} + +sub cmd_log_dump { + + my $win = Irssi::active_win(); + if ($enabled) { + cmd_unregister_all_signals(); + $win->print("Disabled logging"); + } + foreach my $lref (@log) { + my ($line, $indent) = @$lref; + my $xx = " " x $indent; + $win->print($xx . $line); + } +} + +sub cmd_log_stats { + + my $win = Irssi::active_win(); + if ($enabled) { + cmd_unregister_all_signals(); + $win->print("Disabled logging"); + } +} + +sub build_init_func { + my ($sig_name) = @_; + + return sub { + my @args = @_; + my $args_str = ''; + my $n = 0; + + foreach my $arg (@args) { + $args_str .= "[$n] "; + + if (not defined $arg) { + $args_str .= "undef, "; + next; + } + + if (ref $arg) { + $args_str .= ref($arg) . ", " + } else { + $arg =~ s/^(.{20})/$1/; + $args_str .= "$arg, "; + } + $n++; + } + my $msg = sprintf("%f: %s - First %s", time(), $sig_name, $args_str); + push @log, [$msg, $depth]; + $depth++; + } +} + +sub build_end_func { + my ($sig_name) = @_; + + return sub { + my $msg = sprintf("%f: %s - End", time(), $sig_name); + push @log, [$msg, $depth]; + $depth--; + } +} + diff --git a/feature-tests/signal_redir.pl b/feature-tests/signal_redir.pl new file mode 100644 index 0000000..89da34c --- /dev/null +++ b/feature-tests/signal_redir.pl @@ -0,0 +1,116 @@ +# mangled from cmpusers.pl (unpublished, afaik) by Bazerka <bazerka@quakenet.org>. +# He is not to blame for any problems, contact me instead. + +use strict; +use warnings; + +use Irssi; + +our $VERSION = "0.1"; +our %IRSSI = + ( + authors => "shabble, Bazerka", + contact => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode,' + . 'bazerka@quakenet.org', + name => "signal_redir", + description => "Demonstration showing how to redirect a remote WHOIS" + . "command so the results can be captured by a script.", + license => "BSD", + url => "https://github.com/shabble/irssi-scripts/", + changed => "Fri Apr 1 00:05:39 2011" + ); + +my $running = 0; # flag to prevent overlapping requests. +my $debug = 1; +sub redir_init { + # set up event to handler mappings + Irssi::signal_add + ({ + 'redir test_redir_whois_user' => 'event_whois_user', + 'redir test_redir_whois_channels' => 'event_whois_channels', + 'redir test_redir_whois_end' => 'event_whois_end', + 'redir test_redir_whois_nosuchnick' => 'event_whois_nosuchnick', + 'redir test_redir_whois_timeout' => 'event_whois_timeout', + }); +} + +sub request_whois { + my ($server, $nick) = @_; + + $server->redirect_event + ( + 'whois', 1, $nick, 0, # command, remote, arg, timeout + 'redir test_redir_whois_timeout', # error handler + { + 'event 311' => 'redir test_redir_whois_user', # event mappings + 'event 318' => 'redir test_redir_whois_end', + 'event 319' => 'redir test_redir_whois_channels', + 'event 401' => 'redir test_redir_whois_nosuchnick', + '' => 'event empty', + } + ); + Irssi::print("Sending Command: WHOIS $nick", MSGLEVEL_CLIENTCRAP) if $debug; + # send the actual command directly to the server, rather than + # with $server->command() + $server->send_raw("WHOIS $nick"); +} + +sub event_whois_user { + my ($server, $data) = @_; + my ($nick, $user, $host) = ( split / +/, $data, 6 )[ 1, 2, 3 ]; + Irssi::print("test_redir whois_user: $nick!$user\@$host", MSGLEVEL_CLIENTCRAP); +} + +sub event_whois_channels { + my ($server, $data) = @_; + my ($nick, $channels) = ( split / +/, $data, 3 )[ 1, 2 ]; + my $prefix = 'cowu.be'; # server name + my $args = "shabble"; # match criteria + my $event = 'event 319'; # triggering event + my $sig = $server->redirect_get_signal($prefix, $event, $args); + Irssi::print("test_redir whois_channels: $nick, $channels", MSGLEVEL_CLIENTCRAP); + Irssi::print("test_redir get_signal: $sig", MSGLEVEL_CLIENTCRAP); + +} + +sub event_whois_end { + my ($server, $data) = @_; + my ($nick) = ( split / +/, $data, 3 )[1]; + Irssi::print("test_redir whois_end: $nick", MSGLEVEL_CLIENTCRAP); + + return if $running == 0; # catch 318 -> 401 (nosuchnick followed by endofwhois) + $running = 0; +} + +sub event_whois_nosuchnick { + my ($server, $data) = @_; + my $nick = ( split / +/, $data, 4)[1]; + Irssi::active_win->print("test_redir error: no such nick $nick - aborting.", + MSGLEVEL_CLIENTCRAP); + $running = 0; +} + +sub event_whois_timeout { + my ($server, $data) = @_; + Irssi::print("test_redir whois_timeout", MSGLEVEL_CLIENTCRAP); + $running = 0; +} + +sub cmd_test_redir { + my ($args, $server, $witem) = @_; + $args = lc $args; + my @nicks = split /\s+/, $args; + + if ($running) { + Irssi::active_win->print + ("test_redir error: a request is currently being processed " + . "- please try again shortly.", MSGLEVEL_CLIENTCRAP); + return; + } + $running = 1; + request_whois($server, $nicks[0]); +} + +redir_init(); +Irssi::command_bind("test_redir", \&cmd_test_redir); + diff --git a/feature-tests/template.pl b/feature-tests/template.pl index f3ae68a..a49d742 100644 --- a/feature-tests/template.pl +++ b/feature-tests/template.pl @@ -1,19 +1,69 @@ +=pod + +=head1 NAME + +template.pl + +=head1 DESCRIPTION + +A minimalist template useful for basing actual scripts on. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +=head1 USAGE + +None, since it doesn't actually do anything. + +=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 + +=head1 TODO + +Use this template to make an actual script. + +=cut + use strict; use warnings; - use Irssi; use Irssi::Irc; use Irssi::TextUI; use Data::Dumper; - our $VERSION = '0.1'; our %IRSSI = ( authors => 'shabble', contact => 'shabble+irssi@metavore.org', name => '', description => '', - license => 'Public Domain', + license => 'MIT', + updated => '$DATE' ); diff --git a/history-search/README.pod b/history-search/README.pod new file mode 100644 index 0000000..ce0a42f --- /dev/null +++ b/history-search/README.pod @@ -0,0 +1,135 @@ +=pod + +=head1 NAME + +rl_history_search.pl + +=head1 DESCRIPTION + +Search within your typed history as you type (something like ctrl-R in bash or other +readline-type applications.) + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +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. + +=head1 SETUP + +C</bind ^R /history_search_start> + +Where C<^R> is a key of your choice. + +=head1 USAGE + +Type C<ctrl-R> followed by your search string. The prompt line will show +you the most recent command to match the string you've typed. + +B<Tip:> You can cycle through multiple matches with C<Ctrl-R> (to select older +matches), and <Ctrl-S> (to select newer matches). Cycling off the end of the +history list returns you to the other end again. + +B<NOTE:> C<Ctrl-S> may not work if you have software flow control configured for +your terminal. It may appear to freeze irssi entirely. If this happens, it can +be restored with C<Ctrl-Q>, but you will be unable to use the C<Ctrl-S> binding. +You can disable flow control by running the command C<stty -ixon> in your +terminal, or setting C<defflow off> in your F<~/.screenrc>if using GNU Screen. + +=head2 COMMANDS + +=over 4 + +=item C<Enter> selects a match and terminates search mode. B<It will also run +the selected command.> + +=item C<Ctrl-G> exits search mode without selecting. + +=item C<E<lt>TABE<gt>> will open a new split window, showing all matching + completions. C<E<lt>EscE<gt>> will close the window again, as will any other + action that exits history search mode. Possible candidates can be cycled + through as normal using C<C-r> and <C-s>. + +=item Any other ctrl- or meta- key binding will terminate search mode, leaving + the selected item in the input line. + +=back + +=head1 AUTHORS + +Original script L<history_search.pl|https://github.com/coekie/irssi-scripts/blob/master/history_search.pl> Copyright E<copy> 2007 Wouter Coekaerts <coekie@irssi.org> + +Copyright E<copy> 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 + +faff + + +=head1 TODO + +=over 1 + +=item * DONE document tab behaviour + +=item * add keys (C-n/C-p) to scroll history list if necessary. + +=item * DONE if list is bigger than split size, centre it so selected item is visible + +=item * INPROG allow a mechanism to select by number from list + +=item * steal more of the code from ido_switcher to hilight match positions. + +=item * make flex matching optional (key or setting) + +=item * add some online help (? or C-h triggered, maybe?) + +=item * make temp_split stuff more generic (to be used by help, etc) + +=item * figure out why sometimes the split list doesn't update correctly (eg: no matches) + +=item * consider tracking history manually (via send command/send text) + +=item + +=over 4 + +=item * Pro: we could timestamp it. + +=item * Con: Would involve catching up/down and all the other history selection mechanisms. + +=item * Compromise - tag history as it comes it (match it with data via sig handlers?) + +=back + +=item * Possibility of saving/restoring history over sessions? + +=back + diff --git a/history-search/rl_history_search.pl b/history-search/rl_history_search.pl index ca45fe5..7561386 100644 --- a/history-search/rl_history_search.pl +++ b/history-search/rl_history_search.pl @@ -1,79 +1,185 @@ -# Search within your typed history as you type (like ctrl-R in bash) -# -# INSTALL: -# -# This script requires that you have first installed and loaded 'uberprompt.pl' -# Uberprompt can be downloaded from: -# -# http://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl -# -# and follow the instructions at the top of that file for installation. -# -# USAGE: -# -# * Setup: /bind ^R /history_search_start -# -# * Then type ctrl-R and type what you're searching for -# -# * You can cycle through multiple matches with ^R (older matches), and -# ^S (newer matches) -# -# NOTE: Ctrl-S may not work if you have software flow control configured for -# your terminal. It may appear to freeze irssi entirely. If this happens, it can -# be restored with Ctrl-Q, but you will be unable to use the Ctrl-S binding. -# You can disable flow control by running the command `stty -ixon' in your -# terminal, or setting `defflow off' in your ~/.screenrc if using GNU Screen. -# -# * Hitting enter selects a match and terminates search mode. -# -# * You can use ^G to exit search mode without selecting. -# -# * Any other ctrl- or meta- key binding will terminate search mode, leaving the -# selected item in the input line. -# -# Original script Copyright 2007 Wouter Coekaerts <coekie@irssi.org> -# Heavy modifications by Shabble <shabble+irssi@metavore.org>, 2010. -# -# 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 +=pod + +=head1 NAME + +rl_history_search.pl + +=head1 DESCRIPTION + +Search within your typed history as you type (something like ctrl-R in bash or other +readline-type applications.) + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<filename>>. + +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. + +=head1 SETUP + +C</bind ^R /history_search_start> + +Where C<^R> is a key of your choice. + +=head1 USAGE + +Type C<ctrl-R> followed by your search string. The prompt line will show +you the most recent command to match the string you've typed. + +B<Tip:> You can cycle through multiple matches with C<Ctrl-R> (to select older +matches), and <Ctrl-S> (to select newer matches). Cycling off the end of the +history list returns you to the other end again. + +B<NOTE:> C<Ctrl-S> may not work if you have software flow control configured for +your terminal. It may appear to freeze irssi entirely. If this happens, it can +be restored with C<Ctrl-Q>, but you will be unable to use the C<Ctrl-S> binding. +You can disable flow control by running the command C<stty -ixon> in your +terminal, or setting C<defflow off> in your F<~/.screenrc>if using GNU Screen. + +=head2 COMMANDS + +=over 4 + +=item C<Enter> + +Selects a match and terminates search mode. +B<It will also run the currently selected command.> + +=item C<Ctrl-G> + +Exits search mode without selecting. + +=item C<E<lt>TABE<gt> + +Opens a new split window, showing all matching completions. C<E<lt>EscE<gt>> +will close the window again, as will any other action that exits history search +mode. Possible candidates can be cycled through as normal using C<C-r> and +<C-s>. + +=item Any other ctrl- or meta- key + +This will terminate search mode, leaving the selected item in the input line. +It will not run the command (Except for C<Ctrl-J> and C<Ctrl-M>, which are +functionally equivalent to C<Enter>). + +=back + +=head1 AUTHORS + +Original script L<history_search.pl|https://github.com/coekie/irssi-scripts/blob/master/history_search.pl> Copyright E<copy> 2007 Wouter Coekaerts <coekie@irssi.org> + +Most of the other fancy stuff Copyright E<copy> 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 + +Yeah, probably. + +=head1 TODO + +=over 1 + +=item * B<DONE> document tab behaviour + +=item * add keys (C-n/C-p) to scroll history list if necessary. + +=item * B<DONE> if list is bigger than split size, centre it so selected item is visible + +=item * I<INPROG> allow a mechanism to select by number from list + +=item * steal more of the code from ido_switcher to hilight match positions. + +=item * make flex matching optional (key or setting) + +=item * add some online help (? or C-h triggered, maybe?) + +=item * make temp_split stuff more generic (to be used by help, etc) + +=item * figure out why sometimes the split list doesn't update correctly (eg: no matches) + +=item * consider tracking history manually (via send command/send text) + +=over 4 + +=item * Pro: we could timestamp it. + +=item * Con: Would involve catching up/down and all the other history selection mechanisms. + +=item * Compromise - tag history as it comes it (match it with data via sig handlers?) + +=back + +=item * Possibility of saving/restoring history over sessions? + +=back + +=cut + use strict; use Irssi; use Irssi::TextUI; use Data::Dumper; -use vars qw($VERSION %IRSSI); -$VERSION = '2.0'; -%IRSSI = +our $VERSION = '2.5'; +our %IRSSI = ( authors => 'Tom Feist, Wouter Coekaerts', contact => 'shabble+irssi@metavore.org, shabble@#irssi/freenode', name => 'rl_history_search', description => 'Search within your typed history as you type' - . ' (like ctrl-R in readline applications)', + . ' (like ctrl-R in readline applications)', license => 'GPLv2 or later', url => 'http://github.com/shabble/irssi-scripts/tree/master/history-search/', - changed => '24/7/2010' + changed => '14/4/2011', + requires => [qw/uberprompt.pl/], ); my $search_str = ''; my $search_active = 0; +my $select_num_active = 0; +my $num_buffer; + my @history_cache = (); my @search_matches = (); my $match_index = 0; +# split info +my $split_ref; +my $original_win_ref; + +# formats +my $list_format; + +my $use_flex_match = 1; my $DEBUG_ENABLED = 0; sub DEBUG () { $DEBUG_ENABLED } @@ -81,13 +187,7 @@ sub DEBUG () { $DEBUG_ENABLED } # check we have uberprompt loaded. sub script_is_loaded { - my $name = shift; - print "Checking if $name is loaded" if DEBUG; - no strict 'refs'; - my $retval = defined %{ "Irssi::Script::${name}::" }; - use strict 'refs'; - - return $retval; + return exists($Irssi::Script::{$_[0] . '::'}) ; } if (not script_is_loaded('uberprompt')) { @@ -103,14 +203,14 @@ if (not script_is_loaded('uberprompt')) { } history_init(); } else { - history_init(); + history_init(); } sub load_uberprompt_failed { Irssi::signal_remove('script error', 'load_prompt_failed'); print "Script could not be loaded. Script cannot continue. " - . "Check you have uberprompt.pl installed in your path and " + . "Check you have uberprompt.pl installed in your path and " . "try again."; die "Script Load Failed: " . join(" ", @_); @@ -124,6 +224,7 @@ sub history_init { Irssi::signal_add ('setup changed' => \&setup_changed); Irssi::signal_add_first('gui key pressed' => \&handle_keypress); + $list_format = Irssi::theme_register([ list_format => '$*' ]); setup_changed(); } @@ -131,29 +232,34 @@ sub setup_changed { $DEBUG_ENABLED = Irssi::settings_get_bool('histsearch_debug'); } +sub temp_split_active () { + return defined $split_ref; +} sub history_search { $search_active = 1; $search_str = ''; - $match_index = -1; + $match_index = 0; - @history_cache = Irssi::active_win()->get_history_lines(); + my $win = Irssi::active_win; + @history_cache = $win->get_history_lines(); @search_matches = (); + $original_win_ref = $win; + + update_history_matches(); update_history_prompt(); } sub history_exit { $search_active = 0; - Irssi::signal_emit('change prompt', '', 'UP_INNER'); + close_temp_split(); + _set_prompt(''); } sub update_history_prompt { my $col = scalar(@search_matches) ? '%g' : '%r'; - Irssi::signal_emit('change prompt', - ' reverse-i-search: `' . $col . $search_str - . '%n' . "'", - 'UP_INNER'); + _set_prompt(' reverse-i-search: `' . $col . $search_str . '%n' . "'"); } sub update_history_matches { @@ -161,11 +267,18 @@ sub update_history_matches { $match_str = $search_str unless defined $match_str; my %unique; - my @matches = grep { m/\Q$match_str/i } @history_cache; + my @matches; + + if ($use_flex_match) { + @matches = grep { flex_match($_) >= 0 } @history_cache; + } else { + @matches = grep { m/\Q$match_str/i } @history_cache; + } @search_matches = (); # uniquify the results, whilst maintaining order. + # TODO: duplicates should keep teh most recent one? foreach my $m (@matches) { unless (exists($unique{$m})) { # add them in reverse order. @@ -178,6 +291,80 @@ sub update_history_matches { join(", ", @search_matches) if DEBUG; } +sub flex_match { + my ($obj) = @_; + + my $pattern = $search_str; + my $source = $obj; #->{name}; + + #_debug("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} = 'name'; + + 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 enter_select_num_mode { + # require that the list be shown. + return unless temp_split_active(); + return if $select_num_active; # TODO: should we prevent restarting? + + $num_buffer = 0; + _set_prompt("Num select: "); + $select_num_active = 1; +} + +sub exit_select_num_mode { + + $select_num_active = 0; + $num_buffer = 0; + update_history_prompt(); +} + +sub history_select_num { + if ($num_buffer > 0 && $num_buffer <= $#search_matches) { + $match_index = $num_buffer; + my $match = get_history_match(); + _debug("Num select: got $match"); + } +} + sub get_history_match { return $search_matches[$match_index]; } @@ -211,65 +398,116 @@ sub handle_keypress { return unless $search_active; - if ($key == 10) { # enter + if ($select_num_active) { + if ($key >= 48 and $key <= 57) { # Number key + + $num_buffer = ($num_buffer * 10) + $key - 48; + _set_prompt("Num select: $num_buffer"); + + } elsif ($key == 10) { # ENTER + + history_select_num(); + update_input(); + exit_select_num_mode(); + history_exit(); + + } else { # anything else quits. + exit_select_num_mode(); + } + Irssi::signal_stop(); + return; + } + + if ($key == 6) { # Ctrl-F + enter_select_num_mode(); + Irssi::signal_stop(); + return; + } + + if ($key == 7) { # Ctrl-G + print "aborting search" if DEBUG; + history_exit(); + + # cancel empties the inputline. + Irssi::gui_input_set(''); + Irssi::gui_input_set_pos(0); + + Irssi::signal_stop(); + return; + } + if ($key == 8) { # C-h + $original_win_ref->print("This would show help, " + . "if there was any. Coming soon!"); + } + + if ($key == 9) { # TAB + update_history_matches(); + if (not temp_split_active()) { + create_temp_split() if @search_matches > 0; + } else { + print_current_matches(); + } + + Irssi::signal_stop(); + return; + } + if ($key == 10) { # enter print "selecting history and quitting" if DEBUG; history_exit(); return; } - if ($key == 18) { # Ctrl-R + if ($key == 18) { # Ctrl-R print "skipping to prev match" if DEBUG; prev_match(); update_input(); update_history_prompt(); - Irssi::signal_stop(); # prevent the bind from being re-triggered. + print_current_matches(); + Irssi::signal_stop(); # prevent the bind from being re-triggered. return; } - if ($key == 19) { # Ctrl-S + if ($key == 19) { # Ctrl-S print "skipping to next match" if DEBUG; next_match(); update_input(); update_history_prompt(); + print_current_matches(); Irssi::signal_stop(); return; } - if ($key == 7) { # Ctrl-G - print "aborting search" if DEBUG; - history_exit(); - - # cancel empties the inputline. - Irssi::gui_input_set(''); - Irssi::gui_input_set_pos(0); + # TODO: handle arrow-keys? + if ($key == 27) { + close_temp_split(); Irssi::signal_stop(); return; } - if ($key == 127) { # DEL + if ($key >= 32 and $key < 127) { # printable + $search_str .= chr($key); - if (length $search_str) { - $search_str = substr($search_str, 0, -1); - print "Deleting char, now: $search_str" if DEBUG; - } update_history_matches(); update_history_prompt(); update_input(); + print_current_matches(); Irssi::signal_stop(); return; } - # TODO: handle esc- sequences and arrow-keys? - - if ($key >= 32) { # printable - $search_str .= chr($key); + if ($key == 127) { # DEL + if (length $search_str) { + $search_str = substr($search_str, 0, -1); + print "Deleting char, now: $search_str" if DEBUG; + } update_history_matches(); update_history_prompt(); update_input(); + print_current_matches(); Irssi::signal_stop(); return; @@ -279,3 +517,134 @@ sub handle_keypress { history_exit(); #Irssi::signal_stop(); } + +sub create_temp_split { + + Irssi::signal_add_first('window created', 'sig_win_created'); + Irssi::command('window new split'); + Irssi::signal_remove('window created', 'sig_win_created'); +} + +sub close_temp_split { + + if (temp_split_active()) { + Irssi::command("window close $split_ref->{refnum}"); + undef $split_ref; + } + + # restore original window focus + if (Irssi::active_win()->{refnum} != $original_win_ref->{refnum}) { + $original_win_ref->set_active(); + } +} + +sub sig_win_created { + my ($win) = @_; + $split_ref = $win; + # printing directly from this handler causes irssi to segfault. + Irssi::timeout_add_once(10, \&print_current_matches, {}); +} + +sub print_current_matches { + + return unless temp_split_active(); + + my $num_matches = scalar(@search_matches); + #return unless $num_matches > 0; + + # for some woefully unobvious reason, we need to refetch + # the window reference in order for its attribute hash + # to be regenerated. + my $s_win = Irssi::window_find_refnum($split_ref->{refnum}); + + my $split_height = $s_win->{height}; + + $s_win->command("^scrollback clear"); + + # disable timestamps to ensure a clean window. + my $orig_ts_level = Irssi::parse_special('$timestamp_level'); + $s_win->command("^set timestamp_level $orig_ts_level -CLIENTCRAP"); + + + $original_win_ref->print("Num matches: $num_matches, height: $split_height") + if DEBUG; + + # print header + # TODO: make this a format? + $s_win->print('%_Current history matches. Press <esc> to close.%_', + MSGLEVEL_CLIENTCRAP | MSGLEVEL_NEVER); + + if ($num_matches == 0) { + $s_win->print('(No Matches)', MSGLEVEL_CLIENTCRAP | MSGLEVEL_NEVER); + return; + } + $split_height -= 2; # account for header line; + + my $hist_entry = get_history_match(); + + my ($start, $end); + + if ($num_matches > $split_height) { + # we have too many matches to fit in the window. decide on a new + # start and end point. + + my $half_height = int ($split_height / 2); + + # initial start pos is in the middle of the screen. + $start = $match_index >= $half_height + ? $match_index - $half_height + : 0; + # and ends with the max number of matches we can fit + $end = $start + $split_height > $num_matches - 1 + ? $num_matches - 1 + : $start + $split_height; + + # readjust start if the screen isn't filled. + if ($end - $start < $split_height) { + $start = $end - $split_height; + } + + _debug("sh: $split_height, hh: $half_height, " + . "mi: $match_index, start: $start, end: $end"); + } else { + $start = 0; + $end = $#search_matches; + } + + foreach my $i ($start..$end) { + my $j = $num_matches - $i; + my $entry = $search_matches[$i]; + + my $hilight = $hist_entry eq $entry + ? '%g' + : ''; + $hilight = Irssi::parse_special($hilight); + my $str = sprintf("%s%-6d %s%%n", $hilight, $j, $entry); + $s_win->print($str, MSGLEVEL_CLIENTCRAP|MSGLEVEL_NEVER); + } + + # restore timestamp settings. + $s_win->command("^set timestamp_level $orig_ts_level"); +} + +sub print_help { + +} + +sub _print_to_active { + my ($msg, @args) = @_; + my $str = sprintf($msg, @args); + $original_win_ref->print($str,MSGLEVEL_CLIENTCRAP | MSGLEVEL_NEVER); +} + +sub _debug { + return unless DEBUG; + my ($msg, @args) = @_; + my $str = sprintf($msg, @args); + $original_win_ref->print($str, MSGLEVEL_CLIENTCRAP | MSGLEVEL_NEVER); +} +sub _set_prompt { + my ($str) = @_; + $str = ' ' . $str if length $str; + Irssi::signal_emit('change prompt', $str, 'UP_INNER'); +} diff --git a/ido-mode/README.pod b/ido-mode/README.pod new file mode 100644 index 0000000..62f7190 --- /dev/null +++ b/ido-mode/README.pod @@ -0,0 +1,247 @@ +=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> or C<-flex> 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 + diff --git a/ido-mode/ido_switcher.pl b/ido-mode/ido_switcher.pl index 66628f1..8194f8a 100644 --- a/ido-mode/ido_switcher.pl +++ b/ido-mode/ido_switcher.pl @@ -1,87 +1,256 @@ -# Search and select windows similar to ido-mode for emacs -# -# INSTALL: -# -# This script requires that you have first installed and loaded 'uberprompt.pl' -# Uberprompt can be downloaded from: -# -# http://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl -# -# and follow the instructions at the top of that file for installation. -# -# SETUP: -# -# * Setup: /bind ^G /ido_switch_start -# -# * Then type ctrl-G and type what you're searching for -# -# USAGE: -# -# C-g (or whatever you've set the above bind to), enters window switching mode. -# -# NB: 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-g, Esc, -# or RET. - -# The following key-bindings are available only once the mode has been -# activated: -# -# * C-g - cancel out of the mode without changing windows. -# * Esc - cancel out, as above. -# * C-s - rotate the list of window candidates forward by 1 -# * C-r - rotate the list of window candidates backward by 1 -# * C-e - Toggle 'Active windows only' filter -# * C-f - Switch between 'Flex' and 'Exact' matching. -# * C-d - Select a network or server to filter candidates by -# * C-u - Clear the current search string -# * C-q - Cycle between showing only queries, channels, or all. -# * C-SPC - Filter candidates by current search string, and then reset -# the search string -# * RET - Select the current head of the candidate list (the green one) -# * 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. -# * TAB - [currently in development] 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. -# -# USAGE NOTES: -# -# * Using C-e (show actives), followed by repeatedly pressing space will cycle -# through all your currently active windows. -# -# * 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. -# -# Based in part on window_switcher.pl script Copyright 2007 Wouter Coekaerts -# <coekie@irssi.org> -# -# 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 +=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> or C<-flex> 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 Irssi; use Irssi::TextUI; use Data::Dumper; -use vars qw($VERSION %IRSSI); -$VERSION = '2.0'; -%IRSSI = + +our $VERSION = '2.1'; +our %IRSSI = ( authors => 'Tom Feist, Wouter Coekaerts', contact => 'shabble+irssi@metavore.org, shabble@#irssi/freenode', @@ -93,22 +262,10 @@ $VERSION = '2.0'; ); -# TODO: -# DONE C-g - cancel -# DONE C-spc - narrow -# DONE flex matching (on by default, but optional) -# TODO server/network narrowing -# DONE colourised output (via uberprompt) -# DONE C-r / C-s rotate matches -# DONE toggle queries/channels -# DONE remove inputline content, restore it afterwards. -# TODO tab - display all possibilities in window (clean up afterwards) -# how exactly will this work? -# DONE sort by recent activity/recently used windows (separate commands?) -# TODO need to be able to switch ordering of active ones (numerical, or most recently -# active, priority to PMs/hilights, etc?) -# DONE should space auto-move forward to next window for easy stepping through -# sequential/active windows? + +my $CMD_NAME = 'ido_switch_start'; +my $CMD_OPTS = '-channels -queries -all -active -exact -flex'; + my $input_copy = ''; my $input_pos_copy = 0; @@ -153,22 +310,25 @@ sub _print { my $win = Irssi::active_win; my $str = join('', @_); $need_clear = 1; - $win->print($str, Irssi::MSGLEVEL_NEVER); + $win->print($str, MSGLEVEL_NEVER); } sub _debug_print { return unless DEBUG; my $win = Irssi::active_win; my $str = join('', @_); - $win->print($str, Irssi::MSGLEVEL_CLIENTCRAP); + $win->print($str, MSGLEVEL_CLIENTCRAP); } sub _print_clear { return unless $need_clear; my $win = Irssi::active_win(); - $win->command('/scrollback levelclear -level NEVER'); + $win->command('/^scrollback levelclear -level NEVER'); } +# 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 = @@ -243,15 +403,9 @@ sub print_all_matches { #_print("Longtest name: $longest_name"); } - sub script_is_loaded { - my $name = shift; - _debug_print "Checking if $name is loaded"; - no strict 'refs'; - my $retval = defined %{ "Irssi::Script::${name}::" }; - use strict 'refs'; - - return $retval; - } +sub script_is_loaded { + return exists($Irssi::Script::{$_[0] . '::'}); +} unless (script_is_loaded('uberprompt')) { @@ -281,7 +435,9 @@ sub ido_switch_init { Irssi::settings_add_bool('ido_switch', 'ido_show_active_first', 1); Irssi::settings_add_int ('ido_switch', 'ido_show_count', 5); - Irssi::command_bind('ido_switch_start', \&ido_switch_start); + + 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); @@ -296,23 +452,46 @@ sub setup_changed { $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(''); - # set startup flags + 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_type = 'ALL'; - # refresh in case we toggled it last time. - $ido_use_flex = Irssi::settings_get_bool('ido_use_flex'); - $active_only = 0; + # configure settings from provided arguments. + + # use provided options first, or fall back to /setting. + $ido_use_flex = exists $options->{exact} + ? 0 + : exists $options->{flex} + ? 1 + : Irssi::settings_get_bool('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); @@ -404,11 +583,12 @@ sub get_all_windows { } sub ido_switch_select { - my ($selected) = @_; + my ($selected, $tag) = @_; - _debug_print "Selecting window: " . $selected->{name}; + _debug_print sprintf("Selecting window: %s (%d)", + $selected->{name}, $selected->{num}); - Irssi::command("WINDOW GOTO " . $selected->{name}); + Irssi::command("WINDOW GOTO " . $selected->{num}); if ($selected->{type} ne 'WIN') { _debug_print "Selecting window item: " . $selected->{itemname}; @@ -438,7 +618,7 @@ sub get_all_windows { # 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 match'; + my $match_string = '[No matches]'; $show_count = $match_count if $match_count < $show_count; @@ -500,7 +680,7 @@ sub get_all_windows { push @indicators, 'Active' if $active_only; push @indicators, ucfirst(lc($mode_type)); - my $flex = sprintf(' %%k[%%n%s%%k]%%n ', join ',', @indicators); + my $flex = sprintf(' %%b[%%n%s%%b]%%n ', join ', ', @indicators); my $search = ''; $search = (sprintf '`%s\': ', $search_str) if length $search_str; @@ -553,7 +733,7 @@ sub get_all_windows { if ($mode_type ne 'ALL') { @mode_cache = @window_cache; - @window_cache = grep { print "Type: " . $_->{type}; $_->{type} eq $mode_type } @window_cache; + @window_cache = grep { $_->{type} eq $mode_type } @window_cache; } else { @window_cache = @mode_cache if @mode_cache; } diff --git a/joinforward/joinforward.pl b/joinforward/joinforward.pl index 26630b2..1acd72a 100644 --- a/joinforward/joinforward.pl +++ b/joinforward/joinforward.pl @@ -24,26 +24,37 @@ my $forwards; init(); -sub init() { - Irssi::signal_add('event 470', 'sig_470'); - Irssi::signal_add('event 473', 'sig_473'); +sub init { + Irssi::signal_add('event 470', 'sig_470'); # forwarding (on freenode, anhywya) + Irssi::signal_add('event 473', 'sig_473'); # notinvited. + # or better to just overload /join? + Irssi::command_bind('fwdlist', 'cmd_fwdlist'); + print "Joinforward loaded"; +} + +sub cmd_fwdlist { + print "Known Forwards:"; + foreach my $fwd (sort keys %$forwards) { + print "$fwd -> " . $forwards->{$fwd}; + } } sub sig_470 { my ($server, $args, $sender) = @_; #'shibble #mac ##mac :Forwarding to another channel', - my $nick = quotemeta(Irssi::parse_special('$N')); - if ($args =~ m/^$nick (#.*?)\s+(#.*?)\s+(.*)$/) { + print "Sig 470: $args"; + if ($args =~ m/(#.*?)\s+(#.*?)/) { $forwards->{$1} = $2; + print "adding $1 -> $2"; } } sub sig_473 { my ($server, $args, $sender) = @_; + print "Sig 473: $args"; #" shibble #mac :Cannot join channel (+i) - you must be invited'," if ($server->{version} =~ m/ircd-seven/) { # assume freenode - my $nick = quotemeta(Irssi::parse_special('$N')); - if ($args =~ m/^$nick\s+(#.*?)\s+/) { + if ($args =~ m/^(#.*?)\s+/) { if (exists $forwards->{$1}) { $server->command("join " . $forwards->{$1}); } diff --git a/modules/overlays/COPYING b/modules/overlays/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/modules/overlays/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/modules/overlays/Makefile b/modules/overlays/Makefile new file mode 100644 index 0000000..ba5d787 --- /dev/null +++ b/modules/overlays/Makefile @@ -0,0 +1,59 @@ + +### Edit these parameters ### + +# change this to 'find' if you're on a decent system. +FIND = gfind # stupid OSX non-gnu defaults. + +# Where your irssi include files live. You might need to install an +# 'irssi-dev' package or something like that. +IRSSI_DIST = /opt/stow/repo/irssi-debug/include/irssi + +# probably $(HOME)/.irssi for most people. +IRSSI_USER_DIR = $(HOME)/projects/tmp/test/irssi-debug +MODULE_NAME = overlay + +### You shouldn't need to edit anything beyond this point ### + +LIB_NAME = lib$(MODULE_NAME).so +CFLAGS = -Wall -O2 -Werror -g -DMODULE_NAME=\"$(MODULE_NAME)\" +LDFLAGS = -avoid-version -module -bundle -flat_namespace -undefined suppress + +# When you start adding more components to your module, add them here. +OBJECTS = overlay_core.o \ + overlay_impl.o + +IRSSI_INC = $(HOME)/sources/irssi-git + +IRSSI_INCLUDE = -I$(IRSSI_INC) \ + -I$(IRSSI_INC)/src \ + -I$(IRSSI_INC)/src/fe-common/core \ + -I$(IRSSI_INC)/src/core \ + -I$(IRSSI_INC)/src/fe-text \ + -I$(IRSSI_INC)/src/irc \ + -I$(IRSSI_INC)/src/irc/core \ + -I$(IRSSI_INC)/src/irc/dcc \ + -I$(IRSSI_INC)/src/irc/notifylist + + +GLIB_CFLAGS = $(shell pkg-config glib-2.0 --cflags) + +all: $(LIB_NAME) + +%.o: %.c Makefile + $(CC) $(CFLAGS) $(GLIB_CFLAGS) $(IRSSI_INCLUDE) -I. -fPIC -c $< + +$(LIB_NAME): $(OBJECTS) + $(CC) $(CFLAGS) $(LDFLAGS) $(OBJECTS) -o $@ + +install: $(LIB_NAME) + install $< $(IRSSI_USER_DIR)/modules + +clean: + rm -rf *~ *.o *.so core || true + +TAGS: + $(FIND) -type f -exec etags -a -o TAGS {} \; + +.default: all + +.phony: clean install TAGS diff --git a/modules/overlays/README.md b/modules/overlays/README.md new file mode 100644 index 0000000..3fdbae7 --- /dev/null +++ b/modules/overlays/README.md @@ -0,0 +1,10 @@ +## What is this? + +A thing that allows mroe direct access to windows and curses information within irssi, via various +signals. + +## Authors + + * Tom Feist [shabble+irssi@metavore.org](mailto://shabble+irssi@metavore.org) + Chief plunderer of the modular goodness. + diff --git a/modules/overlays/overlay_core.c b/modules/overlays/overlay_core.c new file mode 100644 index 0000000..590a1be --- /dev/null +++ b/modules/overlays/overlay_core.c @@ -0,0 +1,46 @@ + +/* Originally this code was part of the irssi-lua module + * <https://github.com/ahf/irssi-lua> and copyright as below: + * + * Copyright (c) 2009 Alexander Færøy <ahf@irssi.org> + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include <overlay_core.h> +#include <overlay_irssi.h> +#include <overlay_impl.h> +#include <terminfo-core.h> + +static void cmd_overlay(const char *data, SERVER_REC *server) { + + terminfo_move(current_term->width, current_term->height); + //terminfo_clear(); + print_random_message("What is going on here?"); +} + +void overlay_init() { + module_register(MODULE_NAME, "core"); + print_load_message(); + + command_bind("overlay", NULL, (SIGNAL_FUNC) cmd_overlay); +} + + +void overlay_deinit() { + command_unbind("overlay", (SIGNAL_FUNC) cmd_overlay); + print_unload_message(); +} diff --git a/modules/overlays/overlay_core.h b/modules/overlays/overlay_core.h new file mode 100644 index 0000000..39fc743 --- /dev/null +++ b/modules/overlays/overlay_core.h @@ -0,0 +1,27 @@ +/* vim: set sw=4 sts=4 et foldmethod=syntax : */ + +/* + * Copyright (c) 2009 Alexander Færøy <ahf@irssi.org> + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef GUARD_KEY_EMITTER_CORE_H +#define GUARD_KEY_EMITTER_CORE_H 1 + +void overlay_init(); +void overlay_deinit(); + +#endif diff --git a/modules/overlays/overlay_impl.c b/modules/overlays/overlay_impl.c new file mode 100644 index 0000000..dbb9748 --- /dev/null +++ b/modules/overlays/overlay_impl.c @@ -0,0 +1,23 @@ +#include <overlay_irssi.h> +#include <overlay_impl.h> +#include <overlay_core.h> + +void print_load_message(void) { + + printtext(NULL, NULL, MSGLEVEL_CLIENTERROR, + "Hello, World, ~~ \"%s\"", MODULE_NAME); + +} + +void print_random_message(char *str) { + printtext(NULL, NULL, MSGLEVEL_CLIENTCRAP, + "%s", str); +} + +void print_unload_message(void) { + + printtext(NULL, NULL, MSGLEVEL_CLIENTERROR, + "Goodbye, Cruel World. ~~ \"%s\"", MODULE_NAME); + +} + diff --git a/modules/overlays/overlay_impl.h b/modules/overlays/overlay_impl.h new file mode 100644 index 0000000..9f2d6ee --- /dev/null +++ b/modules/overlays/overlay_impl.h @@ -0,0 +1,9 @@ +#ifndef _OVERLAY_IMPL_H_ +#define _OVERLAY_IMPL_H_ 1 + +void print_load_message(void); +void print_unload_message(void); +void print_random_message(char *str); + + +#endif /* _OVERLAY_IMPL_H_ */ diff --git a/modules/overlays/overlay_irssi.h b/modules/overlays/overlay_irssi.h new file mode 100644 index 0000000..8c80623 --- /dev/null +++ b/modules/overlays/overlay_irssi.h @@ -0,0 +1,52 @@ + +/* + * Copyright (c) 2009 Alexander Færøy <ahf@irssi.org> + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef GUARD_TEST_IRSSI_H +#define GUARD_TEST_IRSSI_H 1 + +/* FIXME: bug */ +#define UOFF_T_LONG_LONG 1 + +/* #include "config.h" */ + +#include "core.h" +#include "common.h" +#include "modules.h" +#include "commands.h" +#include "settings.h" +#include "printtext.h" +#include "window-items.h" +#include "window-activity.h" +#include "levels.h" +#include "servers.h" +#include "chat-protocols.h" +#include "channels.h" +#include "queries.h" +#include "nicklist.h" +#include "chatnets.h" +#include "servers-reconnect.h" +#include "masks.h" +#include "misc.h" +#include "rawlog.h" +#include "log.h" +#include "ignore.h" +#include "fe-exec.h" +#include "pidwait.h" + +#endif diff --git a/patches/fix-cumode_space.patch b/patches/fix-cumode_space.patch new file mode 100644 index 0000000..9d1e728 --- /dev/null +++ b/patches/fix-cumode_space.patch @@ -0,0 +1,32 @@ +From 35ddd45044388fe1f26e95c43ca0d8bcf30462fe Mon Sep 17 00:00:00 2001 +From: Tom Feist <shabble@metavore.org> +Date: Sat, 19 Mar 2011 08:09:37 +0000 +Subject: [PATCH] fix bug #795 by setting free_arg parameter correctly to false where the expando + expansion is a static string. + +--- + src/irc/core/irc-expandos.c | 8 +++++++- + 1 files changed, 7 insertions(+), 1 deletions(-) + +diff --git a/src/irc/core/irc-expandos.c b/src/irc/core/irc-expandos.c +index 0c0da64..df692cc 100644 +--- a/src/irc/core/irc-expandos.c ++++ b/src/irc/core/irc-expandos.c +@@ -106,7 +106,13 @@ static char *expando_cumode_space(SERVER_REC *server, void *item, int *free_ret) + return ""; + + ret = expando_cumode(server, item, free_ret); +- return *ret == '\0' ? " " : ret; ++ ++ if (*ret == '\0') { ++ free_ret = FALSE; ++ return " "; ++ } else { ++ return ret; ++ } + } + + static void event_join(IRC_SERVER_REC *server, const char *data, +-- +1.7.4.1 + diff --git a/patches/fix-signal-remove-coderef.patch b/patches/fix-signal-remove-coderef.patch new file mode 100644 index 0000000..e2ecd19 --- /dev/null +++ b/patches/fix-signal-remove-coderef.patch @@ -0,0 +1,22 @@ +From f1f66db22e732ca3e5d920c64c8b904e2fb92762 Mon Sep 17 00:00:00 2001 +From: Tom Feist <shabble@metavore.org> +Date: Fri, 8 Apr 2011 20:28:41 +0100 +Subject: [PATCH] bugfix: allow Irssi::signal_remove to work properly with coderefs + + +diff --git a/src/perl/perl-signals.c b/src/perl/perl-signals.c +index a455cfd..1652d09 100644 +--- a/src/perl/perl-signals.c ++++ b/src/perl/perl-signals.c +@@ -434,8 +434,9 @@ static void perl_signal_remove_list_one(GSList **siglist, PERL_SIGNAL_REC *rec) + } + + #define sv_func_cmp(f1, f2) \ +- (f1 == f2 || (SvPOK(f1) && SvPOK(f2) && \ +- strcmp((char *) SvPV_nolen(f1), (char *) SvPV_nolen(f2)) == 0)) ++ ((SvROK(f1) && SvROK(f2) && SvRV(f1) == SvRV(f2)) || \ ++ (SvPOK(f1) && SvPOK(f2) && \ ++ strcmp((char *) SvPV_nolen(f1), (char *) SvPV_nolen(f2)) == 0)) + + static void perl_signal_remove_list(GSList **list, SV *func) + { diff --git a/prompt_info/README.pod b/prompt_info/README.pod new file mode 100644 index 0000000..d8e3b3e --- /dev/null +++ b/prompt_info/README.pod @@ -0,0 +1,273 @@ +=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_prompt_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 + diff --git a/prompt_info/input_overlay.pl b/prompt_info/old/input_overlay.pl index 7a8dd9d..9d2a42b 100644 --- a/prompt_info/input_overlay.pl +++ b/prompt_info/old/input_overlay.pl @@ -27,8 +27,9 @@ my @regions; my ($term_w, $term_h) = (0, 0); -my $key_capture = 0; +my $overlay_active = 0; my $prompt_len = 5; +my $region_id = 0; sub DEBUG () { 1 } @@ -56,6 +57,16 @@ sub update_terminal_size { print "Terminal detected as $term_w cols by $term_h rows" if DEBUG; } +sub find_region { + my ($pos) = @_; + foreach my $region (@regions) { + next unless $pos > $region->{start}; + return $region if $pos <= $region->{end}; + } + print "failed to find region for pos: $pos"; + return undef; +} + sub redraw_overlay { # TODO: we can't assume the active win is the only one with overlays. #Irssi::active_win->view->redraw(); @@ -90,55 +101,25 @@ sub intercept_keypress { } -sub new_region { - my $key = shift; - print "Creating new Region"; - my $new_region - = { - text => '', - start => _pos() -1, - style => '%_', - open => 1, - draw => 1, - }; - - insert_into_region($key, $new_region); - - push @regions, $new_region; -} - -sub insert_into_region { - my ($key, $region) = @_; - - # if ($key == 127) { # backspace - # substr($region->{text}, -1, 1) = ''; - # } else { - # $region->{text} .= chr $key; - # } - my $input = Irssi::parse_special('$L'); - my $len = _pos() - $region->{start}; -# print "Input: $input, len: $len" if DEBUG; - - my $str = substr($input, $region->{start} , $len); -# print "Str: $str" if DEBUG; - $region->{text} = $str; - - # printf("region [%d-%d] now contains '%s'", - # $region->{start}, _pos(), - # $region->{text}) if DEBUG; -} - sub observe_keypress { my $key = shift; - if ($key_capture && $key > 31 && $key <= 127) { + print "Key " . chr ($key) . " pressed, pos: " . _pos(); + if ($key > 31 && $key <= 127) { # see if we're still appending to the last region: #print "Observed printable key: " . chr($key) if DEBUG; #print ''; my $latest_region = $regions[-1]; $latest_region = {} unless defined $latest_region; - if (not $latest_region->{open}) { - new_region($key); + + my $pos = _pos(); + my $reg = find_region($pos); + + if (defined $reg) { + insert_into_region($key, $reg); + } elsif (not $latest_region->{open}) { + my $style = $overlay_active?'%_':''; + new_region($style, $key); } else { insert_into_region($key, $latest_region); } @@ -148,6 +129,7 @@ sub observe_keypress { redraw_overlay(); } } + sub init { die "This script requires uberprompt.pl" @@ -160,8 +142,7 @@ sub init { Irssi::signal_add ('terminal resized', \&update_terminal_size); Irssi::signal_add_first('gui print text finished', \&augment_redraw); - Irssi::command_bind('region_start', \®ion_toggle); - Irssi::command('/bind ^C /region_start'); + setup_bindings(); Irssi::signal_add('prompt changed', sub { print "Updated prompt length: $_[1]"; @@ -173,22 +154,119 @@ sub init { update_terminal_size(); } +sub setup_bindings { + + Irssi::command_bind('region_start', \®ion_toggle); + Irssi::command_bind('region_clear', \®ion_clear); + Irssi::command_bind('region_print', \&print_regions); + + + Irssi::command('/bind ^C /region_start'); + ##Irssi::command('/bind ^D /region_clear'); + Irssi::command('/bind ^D /region_print'); + +} + + +################################################################################ + +sub escape_style { + my ($style) = @_; + $style =~ s/%/%%/g; + + return $style; +} + +sub print_regions { + foreach my $reg (@regions) { + printf("start: %d end: %d style: %s, text: \"%s\", open: %d, draw: %d", + $reg->{start}, $reg->{end}, escape_style($reg->{style}), + $reg->{text}, $reg->{open}, $reg->{draw}); + } +} + +sub new_region { + my ($style, $key) = @_; + + my $new_id = $region_id++; + _debug("Creating new Region: $new_id"); + + my $new_region + = { + id => $region_id++, + text => '', + start => _pos(), + end => _pos(), + style => $style, + open => 1, + draw => 1, + }; + + insert_into_region($key, $new_region); + + push @regions, $new_region; +} + +sub delete_region { + my ($region) = @_; + my $idx = 0; + foreach my $i (0..$#regions) { + if ($regions[$i]->{id} == $region->{id}) { + $idx = $i; + last; + } + } + print "Deleting region: $idx"; + splice(@regions, $idx, 1); # remove the selected region. +} + +sub insert_into_region { + my ($key, $region) = @_; + + my $pos = _pos(); + + if ($key == 127) { # backspace + substr($region->{text}, -1, 1) = ''; + $region->{end}--; + if ($region->{end} <= $region->{start}) { + delete_region($region); + } + } else { + printf("text: '%s', pos: %d, offset: %d", + $region->{text}, $pos, $pos - $region->{start}); + if ( $region->{end} < $pos) { + $region->{text} .= chr $key; + } else { + substr($region->{text}, $pos - $region->{start}, 0) = chr $key; + } + $region->{end}++; + } +} + +sub region_clear { + @regions = (); + Irssi::signal_emit('command redraw'); +} + sub region_toggle { - $key_capture = not $key_capture; - printf("Region is %sactive", $key_capture?'':'in'); + $overlay_active = not $overlay_active; + _debug("Region is %sactive", $overlay_active?'':'in'); #@regions = (); # terminate the previous region - my $latest_region = $regions[-1]; - if (defined $latest_region) { - $latest_region->{open} = 0; + + my $region = find_region(_pos()); + if (defined $region) { + $region->{open} = 0; + $region->{end} = _pos(); + debug("Region closed: %d-%d", $region->{start}, $region->{end}); } } sub script_is_loaded { my $name = shift; - print "Checking if $name is loaded" if DEBUG; + _debug("Checking if $name is loaded"); no strict 'refs'; - my $retval = defined %{ "Irssi::Script::${name}::" }; + my $retval = %{ "Irssi::Script::${name}::" }; use strict 'refs'; return $retval; @@ -198,5 +276,13 @@ sub _pos { return Irssi::gui_input_get_pos(); } +sub _input { + return Irssi::parse_special('$L'); +} + +sub _debug { + printf @_ if DEBUG(); +} + init(); diff --git a/prompt_info/overlays.pl b/prompt_info/old/overlays.pl index b3299e9..b3299e9 100644 --- a/prompt_info/overlays.pl +++ b/prompt_info/old/overlays.pl diff --git a/prompt_info/prompt_info.pl b/prompt_info/old/prompt_info.pl index 8ad63ba..8ad63ba 100644 --- a/prompt_info/prompt_info.pl +++ b/prompt_info/old/prompt_info.pl diff --git a/prompt_info/prompt_replace.pl b/prompt_info/old/prompt_replace.pl index 30120f7..30120f7 100644 --- a/prompt_info/prompt_replace.pl +++ b/prompt_info/old/prompt_replace.pl diff --git a/prompt_info/visual.pl b/prompt_info/old/visual.pl index b29875b..b29875b 100644 --- a/prompt_info/visual.pl +++ b/prompt_info/old/visual.pl diff --git a/prompt_info/uberprompt.pl b/prompt_info/uberprompt.pl index 0706d29..62bda13 100644 --- a/prompt_info/uberprompt.pl +++ b/prompt_info/uberprompt.pl @@ -1,127 +1,282 @@ -# This script replaces the default prompt status-bar item with one capable -# of displaying additional information, under either user control or via -# scripts. -# -# INSTALL: -# -# Place script in ~/.irssi/scripts/ and potentially symlink into autorun/ -# to ensure it starts at irssi startup. -# -# If not using autorun, manually load the script via: -# -# /script load uberprompt.pl -# -# If you have a custom prompt format, you may need to copy it to the -# uberprompt_format setting. See below for details. -# -# USAGE: -# -# Although the script is designed primarily for other scripts to set -# status information into the prompt, the following commands are available: -# -# TODO: Document positional settings. -# -# /prompt set - sets the prompt to the given argument. $p in the argument will -# be replaced by the original prompt content. -# A parameter corresponding to the UP_* constants listed below -# is required, in the format `/prompt set -inner Hello!' -# /prompt clear - clears the additional data provided to the prompt. -# /prompt on - enables the uberprompt (things may get confused if this is used -# whilst the prompt is already enabled) -# /prompt off - restore the original irssi prompt and prompt_empty statusbars. -# unloading the script has the same effect. -# -# /help prompt - show help for uberprompt commands -# -# Additionally, the format for the prompt can be set via: -# -# UBERPROMPT FORMAT: -# -# /set uberprompt_format <format> -# -# The default is [$*], which is the same as the default provided in default.theme. -# Changing this setting will update the prompt immediately, unlike editing your theme. -# -# An additional variable available within this format is '$uber', which expands to -# the content of prompt data provided with the UP_INNER placement argument. For all -# other placement arguments, it will expand to the empty string ''. -# -# NOTE: this setting completely overrides the 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. -# -# Usage from other Scripts: signal 'change prompt' => 'string' => position -# -# eg: -# -# signal_emit 'change prompt' 'some_string', UberPrompt::UP_INNER; -# -# will set the prompt to include that content, by default '[$* some_string]' -# -# The possible position arguments are the following strings: -# -# UP_PRE - place the provided string before the prompt -- $string$prompt -# UP_INNER - place the provided string inside the prompt -- {prompt $* $string} -# UP_POST - place the provided string after the prompt -- $prompt$string -# UP_ONLY - replace the prompt with the provided string -- $string -# -# All strings may use the special variable '$prompt' to include the prompt -# verbatim at that position in the string. It is probably only useful for -# the UP_ONLY mode however. '$prompt_nt' will include the prompt, minus any -# trailing whitespace. -# -# NOTIFICATIONS: -# -# You can also be notified when the prompt changes in response to the previous -# signal or manual commands via: -# -# signal_add 'prompt changed', sub { my ($text, $len) = @_; ... do something ... }; -# -# Bugs: -# -# * 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. -# -# TODO: -# -# * report failure (somehow) to clients if hte prompt is disabled. -# * fix issue at autorun startup with sbar item doesn't exist. -# -# -# -# -# LICENCE: -# -# Copyright (c) 2010 Tom Feist -# -# 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. +=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_prompt_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; # for sbar_items_redraw +use Irssi::TextUI; use Data::Dumper; -{ package Irssi::Nick } +{ package Irssi::Nick } # magic. our $VERSION = "0.2"; our %IRSSI = @@ -154,6 +309,8 @@ my $emit_request = 0; my $expando_refresh_timer; my $expando_vars = {}; +my $init_callbacks = {load => '', unload => ''}; + pre_init(); sub pre_init { @@ -169,13 +326,13 @@ sub prompt_subcmd_handler { sub _error($) { my ($msg) = @_; - Irssi::active_win->print($msg, Irssi::MSGLEVEL_CLIENTERROR); + Irssi::active_win->print($msg, MSGLEVEL_CLIENTERROR); } sub _debug_print($) { return unless DEBUG; my ($msg) = @_; - Irssi::active_win->print($msg, Irssi::MSGLEVEL_CLIENTCRAP); + Irssi::active_win->print($msg, MSGLEVEL_CLIENTCRAP); } sub _print_help { @@ -198,11 +355,11 @@ sub _print_help { " 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", + " { -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", + " | -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:", '', @@ -215,7 +372,7 @@ sub _print_help { "", ); - Irssi::print($_, Irssi::MSGLEVEL_CLIENTCRAP) for @help_lines; + Irssi::print($_, MSGLEVEL_CLIENTCRAP) for @help_lines; Irssi::signal_stop; } } @@ -228,7 +385,9 @@ sub exp_lbrace() { '{' } sub exp_rbrace() { '}' } sub deinit { - Irssi::expando_destroy('brace'); + Irssi::expando_destroy('lbrace'); + Irssi::expando_destroy('rbrace'); + # remove uberprompt and return the original ones. print "Removing uberprompt and restoring original"; restore_prompt_items(); @@ -241,7 +400,11 @@ sub init { 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', '[$*$uber] '); + + 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); @@ -349,17 +512,20 @@ sub length_request_handler { sub reload_settings { - $use_replaces = Irssi::settings_get_bool('uberprompt_use_replaces'); - + $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) { @@ -415,8 +581,8 @@ sub _escape_prompt_special { $str =~ s/\$/\$\$/g; $str =~ s/\\/\\\\/g; #$str =~ s/%/%%/g; - $str =~ s/{/\$lbrace/g; - $str =~ s/}/\$rbrace/g; + $str =~ s/{/\${lbrace}/g; + $str =~ s/}/\${rbrace}/g; return $str; } @@ -434,7 +600,7 @@ sub uberprompt_render_prompt { } my $prompt = ''; # rendered content of the prompt. - my $theme = Irssi::current_theme; + my $theme = Irssi::current_theme; my $arg = $use_replaces ? 0 : Irssi::EXPAND_FLAG_IGNORE_REPLACES; $prompt = $theme->format_expand("{uberprompt $prompt_arg}", $arg); @@ -470,7 +636,7 @@ sub uberprompt_render_prompt { } } - #_debug_print("Redrawing with: $prompt, size-only: $get_size_only"); + _debug_print("rendering with: $prompt"); if (($prompt ne $prompt_last) or $emit_request) { @@ -491,7 +657,7 @@ sub uberprompt_draw { my $prompt = uberprompt_render_prompt(); my $ret = $sb_item->default_handler($get_size_only, $prompt, '', 0); - + _debug_print("redrawing with: $prompt"); return $ret; } @@ -512,6 +678,17 @@ sub replace_prompt_items { qw/-alignment left -before input -priority '-1'/); _sbar_command('prompt', 'position', '100'); + + 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 { @@ -521,6 +698,17 @@ sub restore_prompt_items { _debug_print("Restoring original prompt"); _sbar_command('prompt', 'reset'); + + 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 { diff --git a/quit-notify/README.pod b/quit-notify/README.pod new file mode 100644 index 0000000..d2c0978 --- /dev/null +++ b/quit-notify/README.pod @@ -0,0 +1,92 @@ +=pod + +=head1 NAME + +notifyquit.pl + +=head1 DESCRIPTION + +A script intended to alert people to the fact that their conversation partners +have quit or left the channel, especially useful in high-traffic channels, or +where you have C<JOINS PARTS QUITS> ignored. + +=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. + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<notifyquit.pl>>. + +=head1 SETUP + +This script provides a single setting: + +C</SET notifyquit_exceptions>, which defaults to "C</^https?/ /^ftp/>" + +The setting is a space-separated list of regular expressions in the format +C</EXPR/>. If the extracted nickname matches any of these patterns, it isa +assumed to be a false-positive match, and is sent to the channel with no +further confirmation. + +=head1 USAGE + +When responding to users in a channel in the format C<$theirnick: some message> +(where the C<:> is not necessarily a colon, but the value of your +C<completion_char> setting), this script will check that the nickname still +exists in the channel, and will prompt you for confirmation if they have +since left. + +It is intended for use for people who ignore C<JOINS PARTS QUITS>, etc, and +try to respond to impatient people, or those with a bad connection. + +To send the message once prompted, either hit C<enter>, or C<y>. Pressing C<n> +will abort sending, but leave the message in your input buffer just in case +you want to keep it. + +=head1 AUTHORS + +Original Copyright E<copy> 2011 Jari Matilainen C<E<lt>vague!#irssi@freenodeE<gt>> + +Some extra bits +Copyright E<copy> 2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> + +=head1 LICENCE + +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 + +I<None known.> + +Please report any problems to L<https://github.com/shabble/irssi-scripts/issues/new> +or moan about it in C<#irssi@Freenode>. + +=head1 TODO + +=over 4 + +=item * Keep a watchlist of nicks in the channel, and only act to confirm if +they quit shortly before/during you typing a response. + +=back + + + +=cut + diff --git a/quit-notify/notifyquit.pl b/quit-notify/notifyquit.pl new file mode 100644 index 0000000..eb08d88 --- /dev/null +++ b/quit-notify/notifyquit.pl @@ -0,0 +1,466 @@ +=pod + +=head1 NAME + +notifyquit.pl + +=head1 DESCRIPTION + +A script intended to alert people to the fact that their conversation partners +have quit or left the channel, especially useful in high-traffic channels, or +where you have C<JOINS PARTS QUITS> ignored. + +=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. + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD F<notifyquit.pl>>. + +=head1 SETUP + +This script provides a single setting: + +C</SET notifyquit_exceptions>, which defaults to "C</^https?/ /^ftp/>" + +The setting is a space-separated list of regular expressions in the format +C</EXPR/>. If the extracted nickname matches any of these patterns, it isa +assumed to be a false-positive match, and is sent to the channel with no +further confirmation. + +=head1 USAGE + +When responding to users in a channel in the format C<$theirnick: some message> +(where the C<:> is not necessarily a colon, but the value of your +C<completion_char> setting), this script will check that the nickname still +exists in the channel, and will prompt you for confirmation if they have +since left. + +It is intended for use for people who ignore C<JOINS PARTS QUITS>, etc, and +try to respond to impatient people, or those with a bad connection. + +To send the message once prompted, either hit C<enter>, or C<y>. Pressing C<n> +will abort sending, but leave the message in your input buffer just in case +you want to keep it. + +=head1 AUTHORS + +Original Copyright E<copy> 2011 Jari Matilainen C<E<lt>vague!#irssi@freenodeE<gt>> + +Some extra bits +Copyright E<copy> 2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> + +=head1 LICENCE + +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 + +I<None known.> + +Please report any problems to L<https://github.com/shabble/irssi-scripts/issues/new> +or moan about it in C<#irssi@Freenode>. + +=head1 TODO + +=over 4 + +=item * Keep a watchlist of nicks in the channel, and only act to confirm if +they quit shortly before/during you typing a response. + +keep track of the most recent departures, and upon sending, see if one of them +is your target. If so, prompt for confirmation. + +So, add them on quit/kick/part, and remove them after a tiemout. + +=back + +=cut + +### +# +# Parts of the script pertaining to uberprompt borrowed from +# shabble (shabble!#irssi/@Freenode), thanks for letting me steal from you :P +# +### + +use strict; +use warnings; +use Data::Dumper; + +our $VERSION = "0.2"; +our %IRSSI = ( + authors => "Jari Matilainen", + contact => 'vague!#irssi@freenode', + name => "notifyquit", + description => "Notify if user has left the channel", + license => "Public Domain", + url => "http://vague.se" + ); + +my $active = 0; +my $permit_pending = 0; +my $pending_input = {}; +my $verbose = 0; +my @match_exceptions; +my $watchlist = {}; + +sub script_is_loaded { + return exists($Irssi::Script::{$_[0] . '::'}); +} + +if (script_is_loaded('uberprompt')) { + app_init(); +} else { + 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(); +} + +sub load_uberprompt_failed { + Irssi::signal_remove('script error', 'load_prompt_failed'); + + print "Script could not be loaded. Script cannot continue. " + . "Check you have uberprompt.pl installed in your scripts directory and " + . "try again. Otherwise, it can be fetched from: "; + print "https://github.com/shabble/irssi-scripts/raw/master/" + . "prompt_info/uberprompt.pl"; + + die "Script Load Failed: " . join(" ", @_); +} + +sub extract_nick { + my ($str) = @_; + + my $completion_char + = quotemeta(Irssi::settings_get_str("completion_char")); + + # from BNF grammar at http://www.irchelp.org/irchelp/rfc/chapter2.html + # special := '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}' + + my $pattern = qr/^( [[:alpha:]] # starts with a letter + (?: [[:alpha:]] # then letter + | \d # or number + | [\[\]\\`^\{\}-]) # or special char + *? ) # any number of times + $completion_char/x; # followed by completion char. + + if ($str =~ m/$pattern/) { + return $1; + } else { + return undef; + } + +} + +sub check_nick_exemptions { + my ($nick) = @_; + foreach my $except (@match_exceptions) { + _debug("Testing nick $nick against $except"); + if ($nick =~ $except) { + _debug( "FAiled match $except"); + return 0; # fail + } + } + _debug("match ok"); + + return 1; +} + +sub sig_send_text { + my ($data, $server, $witem) = @_; + + return unless($witem); + + return unless $witem->{type} eq 'CHANNEL'; + + # shouldn't need escaping, but it doesn't hurt to be paranoid. + my $target_nick = extract_nick($data); + + if ($target_nick) { + if (check_watchlist($target_nick, $witem, $server) + and not $witem->nick_find($target_nick)) { + + #return if $target_nick =~ m/^(?:https?)|ftp/i; + return unless check_nick_exemptions($target_nick); + + if ($permit_pending) { + + $pending_input = {}; + $permit_pending = 0; + Irssi::signal_continue(@_); + + } else { + my $text + = "$target_nick isn't in this channel, send anyway? [Y/n]"; + $pending_input + = { + text => $data, + server => $server, + win_item => $witem, + }; + + Irssi::signal_stop; + require_confirmation($text); + } + } + } +} + +sub sig_gui_keypress { + my ($key) = @_; + + return if not $active; + + my $char = chr($key); + + # Enter, y, or Y. + if ($char =~ m/^y?$/i) { + $permit_pending = 1; + Irssi::signal_stop; + Irssi::signal_emit('send text', + $pending_input->{text}, + $pending_input->{server}, + $pending_input->{win_item}); + $active = 0; + set_prompt(''); + + } elsif ($char =~ m/^n?$/i or $key == 3 or $key == 7) { + # we support n, N, Ctrl-C, and Ctrl-G for no. + + Irssi::signal_stop; + set_prompt(''); + + $permit_pending = 0; + $active = 0; + $pending_input = {}; + + } else { + Irssi::signal_stop; + return; + } +} + + +sub add_to_watchlist { + my ($nick, $channel, $server) = @_; + my $tag = $server->{tag}; + _debug("Adding $nick to $channel/$tag"); + + $watchlist->{$tag}->{$channel}->{$nick} = time(); +} + +sub check_watchlist { + my ($nick, $channel, $server) = @_; + my $tag = $server->{tag}; + + my $check = exists ($watchlist->{$tag}->{$channel}->{$nick}); + _debug("Check for $nick in $channel/$tag is " .( $check ? 'true' : 'false')); + + return $check; +} + +sub remove_from_watchlist { + my ($nick, $channel, $server) = @_; + my $tag = $server->{tag}; + + if (exists($watchlist->{$tag}->{$channel}->{$nick})) { + delete($watchlist->{$tag}->{$channel}->{$nick}); + _debug("Deleted $nick from $channel/$tag"); + } +} + +sub start_watchlist_expire_timer { + my ($nick, $channel, $server, $callback) = @_; + + my $tag = $server->{tag}; + my $timeout = Irssi::settings_get_time('notifyquit_timeout'); + + Irssi::timeout_add_once($timeout, + $callback, + { nick => $nick, + channel => $channel, + server => $server, + }); +} + +sub sig_message_quit { + my ($server, $nick, $address, $reason) = @_; + + my $tag = $server->{tag}; + + _debug( "$nick quit from $tag"); + add_to_watchlist($nick, "***", $server); + + my $quit_cb = sub { + + # remove from all channels. + foreach my $chan (keys %{ $watchlist->{$tag} }) { + # if (exists $chan->{$nick}) { + # delete $watchlist->{$tag}->{$chan}->{$nick}; + # } + remove_from_watchlist($nick, $chan, $server) + } + }; + + start_watchlist_expire_timer($nick, '***', $server, $quit_cb); + +} + +sub sig_message_part { + my ($server, $channel, $nick, $address, $reason) = @_; + + my $tag = $server->{tag}; + + _debug( "$nick parted from $channel/$tag"); + add_to_watchlist($nick, $channel, $server); + my $part_cb = sub { + remove_from_watchlist($nick, $channel, $server); + }; + + start_watchlist_expire_timer($nick, $channel, $server, $part_cb); + +} + +sub sig_message_kick { + my ($server, $channel, $nick, $kicker, $address, $reason) = @_; + _debug( "$nick kicked from $channel by $kicker"); + + my $tag = $server->{tag}; + add_to_watchlist($nick, $channel, $server); + + my $kick_cb = sub { + remove_from_watchlist($nick, $channel, $server); + }; + + start_watchlist_expire_timer($nick, $channel, $server, $kick_cb); +} + +sub sig_message_nick { + my ($server, $newnick, $oldnick, $address) = @_; + my $tag = $server->{tag}; + + _debug("$oldnick changed nick to $newnick ($tag)"); + #_debug( "Not bothering with this for now."); + add_to_watchlist($newnick, '***', $server); + remove_from_watchlist($oldnick, '***', $server); + + my $nick_cb = sub { + remove_from_watchlist($newnick, '***', $server); + }; + + start_watchlist_expire_timer($newnick, '***', $server, $nick_cb); +} + +sub sig_message_join { + my ($server, $channel, $nick) = @_; + add_to_watchlist($nick, $channel, $server); + +} + +sub app_init { + Irssi::signal_add('setup changed' => \&sig_setup_changed); + Irssi::signal_add_first('message quit' => \&sig_message_quit); + #Irssi::signal_add_first('message join' => \&sig_message_join); + Irssi::signal_add_first('message part' => \&sig_message_part); + Irssi::signal_add_first('message kick' => \&sig_message_kick); + Irssi::signal_add_first('message nick' => \&sig_message_nick); + Irssi::signal_add_first("send text" => \&sig_send_text); + Irssi::signal_add_first('gui key pressed' => \&sig_gui_keypress); + Irssi::settings_add_str($IRSSI{name}, 'notifyquit_exceptions', '/^https?/ /^ftp/'); + Irssi::settings_add_bool($IRSSI{name}, 'notifyquit_verbose', 0); + Irssi::settings_add_time($IRSSI{name}, 'notifyquit_timeout', '30s'); + + # horrible name, but will serve. + Irssi::command_bind('notifyquit_show_exceptions', \&cmd_show_exceptions); + Irssi::command_bind('notifyquit_show_watchlist', \&cmd_show_watchlist); + + sig_setup_changed(); +} + +sub cmd_show_exceptions { + + foreach my $e (@match_exceptions) { + print "Exception: $e"; + } +} + +sub cmd_show_watchlist { + Irssi::print Dumper($watchlist); +} + +sub sig_setup_changed { + + my $except_str = Irssi::settings_get_str('notifyquit_exceptions'); + $verbose = Irssi::settings_get_bool('notifyquit_verbose'); + my @except_list = split( m{(?:^|(?<=/))\s+(?:(?=/)|$)}, $except_str); + + @match_exceptions = (); + + foreach my $except (@except_list) { + + _debug("Exception regex str: $except"); + $except =~ s|^/||; + $except =~ s|/$||; + + next if $except =~ m/^\s*$/; + + my $regex; + + eval { + $regex = qr/$except/i; + }; + + if ($@ or not defined $regex) { + print "Regex failed to parse: \"$except\": $@"; + } else { + _debug("Adding match exception: $regex"); + push @match_exceptions, $regex; + } + } +} + + +sub require_confirmation { + $active = 1; + set_prompt(shift); +} + +sub set_prompt { + my ($msg) = @_; + $msg = ': ' . $msg if length $msg; + Irssi::signal_emit('change prompt', $msg, 'UP_INNER'); +} + +sub _debug { + + return unless $verbose; + + my ($msg, @params) = @_; + my $str = sprintf($msg, @params); + print $str; + +} diff --git a/readme_generator.pl b/readme_generator.pl new file mode 100755 index 0000000..e93239e --- /dev/null +++ b/readme_generator.pl @@ -0,0 +1,77 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +# Goal: extract the comments from the top of each script file, and +# turn them into some sort of markdown-style README.md for github to nom on. +# +# Not sure how it's going to work with multiple files in a dir though. Sections? + +# Change of plan! Github supports POD, so we just use Pod::Select to scrape it. + +use File::Find; +use File::Spec; +use Pod::Select; + +use feature qw/say/; +use Cwd; + +my $overwrite = $ARGV[0]; +if ($overwrite =~ m/--overwrite/) { + shift @ARGV; # remove it form list of dirs. + $overwrite = 1; +} else { + $overwrite = 0; +} + +my @dirs = map { File::Spec->catdir(getcwd(), $_) } @ARGV; + +die unless @dirs; + +find(\&wanted, @dirs); + +sub wanted { + my ($file, $dir, $path) = ($_, $File::Find::dir, $File::Find::name); + return unless $file =~ m/\.pl$/; + return if $file =~ m/^\./; + + _err("processing file: $path"); + create_output_file($dir, $file); +} + +sub create_output_file { + my ($dir, $in_file) = @_; + + my $parser = Pod::Select->new; + + my $out_file = "README.pod"; + + my $in_file_path = File::Spec->catfile($dir, $in_file); + my $out_file_path = File::Spec->catfile($dir, $out_file); + my $sec_sep = ''; + + if (-f $out_file_path and not $overwrite) { + _err("$out_file_path already exists, going to append") unless $overwrite; + $sec_sep = "\n\n=for html <br />\n\n"; + } + + my $mode = $overwrite ? '>' : '>>'; + + _err("Writing to $mode $out_file_path"); + + open my $wfh, $mode, $out_file_path + or die "Couldn't open $out_file_path for $mode output: $!"; + + $parser->parse_from_file($in_file_path, $wfh); + + print $wfh "\n\n=cut\n\n"; + + close $wfh; +} + +sub _err { + my ($msg, @args) = @_; + my $str = sprintf($msg, @args); + say STDERR $str; +} diff --git a/scrolled-reminder/scrolled_reminder.pl b/scrolled-reminder/scrolled_reminder.pl index 48da622..efe6630 100644 --- a/scrolled-reminder/scrolled_reminder.pl +++ b/scrolled-reminder/scrolled_reminder.pl @@ -82,13 +82,7 @@ our %IRSSI = # check we have prompt_info loaded. sub script_is_loaded { - my $name = shift; - print "Checking if $name is loaded" if DEBUG; - no strict 'refs'; - my $retval = defined %{ "Irssi::Script::${name}::" }; - use strict 'refs'; - - return $retval; + return exists($Irssi::Script::{$_[0] . '::'}) ; } unless (script_is_loaded('uberprompt')) { diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 0000000..119fe08 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,5 @@ +Makefile +Makefile.old +blib/ +irssi.log +pm_to_blib diff --git a/testing/Changes b/testing/Changes new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/testing/Changes diff --git a/testing/MANIFEST b/testing/MANIFEST new file mode 100644 index 0000000..1810c3e --- /dev/null +++ b/testing/MANIFEST @@ -0,0 +1,13 @@ +Makefile.PL +MANIFEST +MANIFEST.SKIP +README +Changes + +t/001-use.t +t/002-init.t + +lib/Test/Irssi.pm +lib/Test/Irssi/Callbacks.pm +lib/Test/Irssi/Driver.pm +lib/Test/Irssi/Test.pm
\ No newline at end of file diff --git a/testing/MANIFEST.SKIP b/testing/MANIFEST.SKIP new file mode 100644 index 0000000..1bfcbf3 --- /dev/null +++ b/testing/MANIFEST.SKIP @@ -0,0 +1,9 @@ +.*\.git.* +pm_to_blib +.*\.old +.*\.bak +.*\.swp +blib/.* +^Makefile$ +\d+_local_ + diff --git a/testing/Makefile.PL b/testing/Makefile.PL new file mode 100644 index 0000000..3312c95 --- /dev/null +++ b/testing/Makefile.PL @@ -0,0 +1,30 @@ +use strict; +use warnings; +use Cwd; +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. + +WriteMakefile( + NAME => 'Test::Irssi', + AUTHOR => 'shabble <shabble+cpan@metavore.org>', + VERSION_FROM => 'lib/Test/Irssi.pm', # finds $VERSION + ABSTRACT_FROM => 'lib/Test/Irssi.pm', + PL_FILES => {}, + # LIBS => ["-L/opt/local/lib -lcprops"], + # INC => "-I/opt/local/include/cprops", + PREREQ_PM => { + 'Test::More' => 0, + 'Carp' => 0, + 'MooseX::Declare' => 0, + 'IO::File' => 0, + 'Term::VT102' => 0, + 'Term::Terminfo' => 0, + 'strictures' => 0, + 'Data::Dump' => 0, + }, + dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, + # clean => { + # FILES => 'CProps-Trie-* Trie.inl _Inline' + # }, + ); diff --git a/testing/README b/testing/README new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/testing/README diff --git a/testing/lib/Test/Irssi.pm b/testing/lib/Test/Irssi.pm new file mode 100644 index 0000000..0db7ee0 --- /dev/null +++ b/testing/lib/Test/Irssi.pm @@ -0,0 +1,314 @@ +use strictures 1; +use MooseX::Declare; + +our $VERSION = 0.02; + +class Test::Irssi { + + # requires the latest pre-release POE from + # https://github.com/rcaputo/poe until a new release is...released. + use lib $ENV{HOME} . "/projects/poe/lib"; + use POE; + + use Term::VT102; + use Term::Terminfo; + use feature qw/say switch/; + use Data::Dump; + use IO::File; + + use Test::Irssi::Driver; + use Test::Irssi::Callbacks; + use Test::Irssi::Test; + + has 'generate_tap' + => ( + is => 'rw', + isa => 'Bool', + required => 1, + default => 1, + ); + + has 'irssi_binary' + => ( + is => 'ro', + isa => 'Str', + required => 1, + ); + + has 'irssi_homedir' + => ( + is => 'ro', + isa => 'Str', + required => 1, + ); + + has 'terminal_width' + => ( + is => 'ro', + isa => 'Int', + required => 1, + default => 80, + ); + + has 'terminal_height' + => ( + is => 'ro', + isa => 'Int', + required => 1, + default => 24, + ); + + has 'vt' + => ( + is => 'ro', + isa => 'Term::VT102', + required => 1, + lazy => 1, + builder => '_build_vt_obj', + ); + + has 'logfile' + => ( + is => 'ro', + isa => 'Str', + required => 1, + default => 'irssi-test.log', + ); + + has '_logfile_fh' + => ( + is => 'ro', + isa => 'IO::File', + required => 1, + lazy => 1, + builder => '_build_logfile_fh', + ); + + has 'driver' + => ( + is => 'ro', + isa => 'Test::Irssi::Driver', + required => 1, + lazy => 1, + builder => '_build_driver_obj', + handles => { + run_headless => 'headless', + } + ); + + has '_callbacks' + => ( + is => 'ro', + isa => 'Test::Irssi::Callbacks', + required => 1, + lazy => 1, + builder => '_build_callback_obj', + ); + + has 'pending_tests' + => ( + is => 'ro', + isa => "ArrayRef", + required => 1, + default => sub { [] }, + traits => [qw/Array/], + handles => { + add_pending_test => 'push', + next_pending_test => 'shift', + tests_remaining => 'count', + } + ); + + has 'completed_tests' + => ( + is => 'ro', + isa => "ArrayRef", + required => 1, + default => sub { [] }, + traits => [qw/Array/], + handles => { + add_completed_test => 'push', + tests_completed => 'count', + }, + ); + + has 'active_test' + => ( + is => 'rw', + isa => 'Test::Irssi::Test', + ); + + sub new_test { + my ($self, $name, @params) = @_; + my $new = Test::Irssi::Test->new(name => $name, + parent => $self, + @params); + $self->add_pending_test($new); + return $new; + } + + method _build_callback_obj { + Test::Irssi::Callbacks->new(parent => $self); + } + + method _build_driver_obj { + Test::Irssi::Driver->new(parent => $self); + } + + method _build_vt_obj { + my $rows = $self->terminal_height; + my $cols = $self->terminal_width; + + Term::VT102->new($cols, $rows); + } + + method _build_logfile_fh { + + my $logfile = $self->logfile; + + my $fh = IO::File->new($logfile, 'w'); + die "Couldn't open $logfile for writing: $!" unless defined $fh; + $fh->autoflush(1); + + return $fh; + } + + method _vt_setup { + # options + my $vt = $self->vt; + + $vt->option_set(LINEWRAP => 1); + $vt->option_set(LFTOCRLF => 1); + + $self->_callbacks->register_callbacks; + + } + method screenshot { + my $data = ''; + my $vt = $self->vt; + foreach my $row (1 .. $vt->rows) { + $data .= $vt->row_plaintext($row) . "\n"; + } + return $data; + } + + method complete_test { + # put the completed one onto the completed pile + my $old_test = $self->active_test; + $self->add_completed_test($old_test); + + # TAP: print status. + if ($self->generate_tap) { + my $pass = $old_test->passed; + my $tap = sprintf("%s %d - %s", $pass?'ok':'not ok', + $self->tests_completed, + $old_test->description); + say STDOUT $tap; + if (not $pass) { + $old_test->details; + $self->log("-------------------"); + $self->log($self->screenshot); + $self->log("-------------------"); + + } + } + } + + method run_test { + # and make the next pending one active. + my $test = $self->next_pending_test; + $self->active_test($test); + + # signal to the driver to start executing it. + $poe_kernel->post(IrssiTestDriver => execute_test => $test); + } + + method run { + + $self->driver->setup; + $self->_vt_setup; + $self->log("Driver setup complete"); + ### Start a session to encapsulate the previous features. + + # TAP: print number of tests. + if ($self->generate_tap) { + print STDOUT "1.." . $self->tests_remaining . "\n"; + } + + $poe_kernel->run(); + } + + sub apply_delay { + my ($self, $delay, $next_index) = @_; + $poe_kernel->post(IrssiTestDriver + => create_delay + => $delay, $next_index); + } + + # TODO: pick one. + sub inject_text { + my ($self, $text) = @_; + $poe_kernel->post(IrssiTestDriver => got_terminal_stdin + => $text); + } + + sub simulate_keystroke { + my ($self, $text) = @_; + $poe_kernel->post(IrssiTestDriver => got_terminal_stdin + => $text); + + } + + method get_topic_line { + return $self->vt->row_plaintext(1); + } + + method get_prompt_line { + return $self->vt->row_plaintext($self->terminal_height); + } + + method get_window_statusbar_line { + return $self->vt->row_plaintext($self->terminal_height() - 1); + } + + method get_window_contents { + my $buf = ''; + for (2..$self->terminal_height() - 2) { + $buf .= $self->vt->row_plaintext($_); + } + return $buf; + } + + method get_cursor_position { + return ($self->vt->x(), $self->vt->y()); + } + + method load_script { + my ($script_name) = @_; + + } + + method summarise_test_results { + foreach my $test (@{$self->completed_tests}) { + my $name = $test->name; + printf("Test %s\t\t-\t%s\n", $name, $test->passed?"pass":"fail"); + $test->details(); + } + } + + sub log { + my ($self, $msg) = @_; + $self->_logfile_fh->say($msg); + } + +} + + __END__ + +=head1 NAME + +Test::Irssi - A cunning testing system for Irssi scripts + +=head1 SYNOPSIS + +blah blah blah diff --git a/testing/lib/Test/Irssi/Callbacks.pm b/testing/lib/Test/Irssi/Callbacks.pm new file mode 100644 index 0000000..8321ace --- /dev/null +++ b/testing/lib/Test/Irssi/Callbacks.pm @@ -0,0 +1,123 @@ +use strictures 1; + +package Test::Irssi::Callbacks; + +use Moose; +use Data::Dump qw/dump/; +use Data::Dumper; + +has 'parent' + => ( + is => 'ro', + isa => 'Test::Irssi', + required => 1, + ); + +sub register_callbacks { + my ($self) = @_; + + my $vt = $self->parent->vt; + $self->log("Callbacks registered"); + + $vt->callback_set(OUTPUT => sub { $self->vt_output(@_) }, undef); + $vt->callback_set(ROWCHANGE => sub { $self->vt_rowchange(@_) }, undef); + $vt->callback_set(CLEAR => sub { $self->vt_clear(@_) }, undef); + $vt->callback_set(SCROLL_DOWN => sub { $self->vt_scr_up(@_) }, undef); + $vt->callback_set(SCROLL_UP => sub { $self->vt_scr_dn(@_) }, undef); + $vt->callback_set(GOTO => sub { $self->vt_goto(@_) }, undef); + +} + +sub vt_output { + my ($self, $vt, $cb_name, $cb_data) = @_; + $self->log( "OUTPUT: " . dump([@_[1..$#_]])); +} + +sub vt_rowchange { + my $self = shift; + my ($vt, $cb_name, $arg1, $arg2) = @_; + + $arg1 //= '?'; + $arg2 //= '?'; + + $self->log( "-" x 100); + $self->log( "Row $arg1 changed: "); + + my $bottom_line = $vt->rows(); + + $self->log( "-" x 100); + $self->log( "Window Line"); + $self->log( "-" x 100); + $self->log( $vt->row_plaintext($bottom_line - 1)); + $self->log( "-" x 100); + $self->log( "Prompt line"); + $self->log( "-" x 100); + $self->log( $vt->row_plaintext($bottom_line)); + +} + +sub vt_clear { + my $self = shift; + my ($vt, $cb_name, $arg1, $arg2) = @_; + $arg1 //= '?'; + $arg2 //= '?'; + + $self->log( "VT Cleared"); +} + +sub vt_scr_dn { + my $self = shift; + my ($vt, $cb_name, $arg1, $arg2) = @_; + $arg1 //= '?'; + $arg2 //= '?'; + + $self->log( "Scroll Down"); +} + +sub vt_scr_up { + my $self = shift; + my ($vt, $cb_name, $arg1, $arg2) = @_; + $arg1 //= '?'; + $arg2 //= '?'; + + $self->log( "Scroll Up"); +} + + +sub vt_goto { + my $self = shift; + my ($vt, $cb_name, $arg1, $arg2) = @_; + $arg1 //= '?'; + $arg2 //= '?'; + + $self->log( "Goto: $arg1, $arg2"); +} + +sub vt_dump { + my ($self) = @_; + my $vt = $self->parent->vt; + my $rows = $self->parent->terminal_height; + my $str = ''; + for my $y (1..$rows) { + $str .= $vt->row_sgrtext($y) . "\n"; + } + + return $str; +} + +sub log { + my ($self, $msg) = @_; + #$self->parent->_logfile_fh->say($msg); +} + +__PACKAGE__->meta->make_immutable; + +no Moose; + + + +# # delegate to Callbacks. +# sub vt_dump { +# my ($self) = @_; +# my $cb = $self->parent->_callbacks->vt_dump(); +# } diff --git a/testing/lib/Test/Irssi/Driver.pm b/testing/lib/Test/Irssi/Driver.pm new file mode 100644 index 0000000..6b4e5e5 --- /dev/null +++ b/testing/lib/Test/Irssi/Driver.pm @@ -0,0 +1,258 @@ +use strictures 1; + +package Test::Irssi::Driver; + +use Moose; +use lib $ENV{HOME} . "/projects/poe/lib"; + +use POE qw( Wheel::ReadWrite Wheel::Run Filter::Stream ); +use POSIX; +use feature qw/say/; +use Data::Dump qw/dump/; + +has 'parent' + => ( + is => 'ro', + isa => 'Test::Irssi', + required => 1, + ); + +has 'headless' + => ( + is => 'rw', + isa => 'Bool', + default => 0, + ); + +sub START { + my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP]; + + $kernel->alias_set("IrssiTestDriver"); + + $self->log("Start handler called"); + + $self->save_term_settings($heap); + + # Set a signal handler. + $kernel->sig(CHLD => "got_sigchld"); + + $self->make_raw_terminal; + + my @stdio_options = + ( + InputHandle => \*STDIN, + OutputHandle => \*STDOUT, + InputEvent => "got_terminal_stdin", + Filter => POE::Filter::Stream->new(), + ); + + $self->log("stdio options: " . dump(@stdio_options)); + + # Start the terminal reader/writer. + $heap->{stdio} = POE::Wheel::ReadWrite->new(@stdio_options); + + $self->log("Created stdio wheel"); + + my $rows = $self->parent->terminal_height; + my $cols = $self->parent->terminal_width; + + my @program_options = + ( + Program => $self->parent->irssi_binary, + ProgramArgs => ['--noconnect', '--home=' . $self->parent->irssi_homedir ], + Conduit => "pty", + Winsize => [$rows, $cols, 0, 0], + StdoutEvent => "got_child_stdout", + StdioFilter => POE::Filter::Stream->new(), + ); + + $self->log("wheel options: " . dump(@program_options)); + + # Start the asynchronous child process. + $heap->{program} = POE::Wheel::Run->new(@program_options); + + $self->log("Created child run wheel"); + $poe_kernel->yield('testing_ready'); +} + +sub STOP { + my ($self, $heap) = @_[OBJECT,HEAP]; + $self->log("STOP called"); + $self->restore_term_settings($heap); + $self->parent->_logfile_fh->close(); + + if (not $self->parent->generate_tap) { + $self->parent->summarise_test_results(); + } +} + +### Handle terminal STDIN. Send it to the background program's STDIN. +### If the user presses ^C, then echo a little string + +sub terminal_stdin { + my ($self, $heap, $input) = @_[OBJECT, HEAP, ARG0]; + + if ($input =~ m/\003/g) { # C-c + $input = "/echo I like cakes\n"; + } elsif ($input =~ m/\x17/g) { # C-w + $input = "/quit\n"; + } + + $heap->{program}->put($input); +} + +### Handle STDOUT from the child program. +sub child_stdout { + my ($self, $heap, $input) = @_[OBJECT, HEAP, ARG0]; + # process via vt + $self->parent->vt->process($input); + + if (not $self->headless) { + # send to terminal + $heap->{stdio}->put($input); + } +} + +### Handle SIGCHLD. Shut down if the exiting child process was the +### one we've been managing. + +sub shutdown { + my ($self, $heap, $kernel) = @_[OBJECT, HEAP, KERNEL]; + $self->log("Shutdown called"); + $heap->{program}->kill(15); + $kernel->alias_remove("IrssiTestDriver"); +} + +sub CHILD { + my ($self, $heap, $child_pid) = @_[OBJECT, HEAP, ARG1]; + if ($child_pid == $heap->{program}->PID) { + delete $heap->{program}; + delete $heap->{stdio}; + } + return 0; +} + +sub setup { + my $self = shift; + + my @states = + ( + object_states => + [ $self => + { + _start => 'START', + _stop => 'STOP', + got_sigchld => 'CHILD', + + got_terminal_stdin => 'terminal_stdin', + got_child_stdout => 'child_stdout', + + got_delay => 'timer_expired', + create_delay => 'timer_created', + + + testing_ready => 'testing_ready', + test_complete => 'test_complete', + execute_test => 'execute_test', + + shutdown => 'shutdown', + } + ] + ); + $self->log("creating root session"); + + POE::Session->create(@states); + $self->log("session created"); + +} + +sub testing_ready { + my ($self) = $_[OBJECT]; + # begin by fetching a test from the pending queue. + $self->log("Starting to run tests"); + $self->log("-" x 80); + $self->parent->run_test; +} + +sub execute_test { + my ($self, $heap, $kernel, $test) = @_[OBJECT,HEAP, KERNEL, ARG0]; + # do some stuff here to evaluate it. + + $test->evaluate_test; + +} + +sub test_complete { + my ($self, $kernel) = @_[OBJECT, KERNEL]; + + $self->parent->complete_test; + + if ($self->parent->tests_remaining) { + $self->parent->run_test; + } + + # otherwise, we're done, and can shutdown. + #kernel->yield('shutdown'); + +} + +sub timer_created { + my ($self, $heap, $kernel, $duration) = @_[OBJECT, HEAP, KERNEL, ARG0]; + $kernel->delay(got_delay => $duration); + $self->log("Timer created for $duration"); +} + +sub timer_expired { + my ($self, $data) = @_[OBJECT,ARG0]; + $self->log("Timeout invoking test again."); + $self->parent->active_test->resume_from_timer; +} + +sub save_term_settings { + my ($self, $heap) = @_; + # Save the original terminal settings so they can be restored later. + $heap->{stdin_tio} = POSIX::Termios->new(); + $heap->{stdin_tio}->getattr(0); + $heap->{stdout_tio} = POSIX::Termios->new(); + $heap->{stdout_tio}->getattr(1); + $heap->{stderr_tio} = POSIX::Termios->new(); + $heap->{stderr_tio}->getattr(2); +} + +sub restore_term_settings { + my ($self, $heap) = @_; + + $heap->{stdin_tio}->setattr (0, TCSANOW); + $heap->{stdout_tio}->setattr(1, TCSANOW); + $heap->{stderr_tio}->setattr(2, TCSANOW); +} + +sub make_raw_terminal { + my ($self) = @_; + # Put the terminal into raw input mode. Otherwise discrete + # keystrokes will not be read immediately. + my $tio = POSIX::Termios->new(); + $tio->getattr(0); + my $lflag = $tio->getlflag; + $lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | ICANON | IEXTEN | ISIG); + $tio->setlflag($lflag); + my $iflag = $tio->getiflag; + $iflag &= ~(BRKINT | INPCK | ISTRIP | IXON); + $tio->setiflag($iflag); + my $cflag = $tio->getcflag; + $cflag &= ~(CSIZE | PARENB); + $tio->setcflag($cflag); + $tio->setattr(0, TCSANOW); +} + +sub log { + my ($self, $msg) = @_; + my $fh = $self->parent->_logfile_fh; + $fh->say($msg); +} + + +__PACKAGE__->meta->make_immutable; + +no Moose; + diff --git a/testing/lib/Test/Irssi/Misc.pm b/testing/lib/Test/Irssi/Misc.pm new file mode 100644 index 0000000..a6339e0 --- /dev/null +++ b/testing/lib/Test/Irssi/Misc.pm @@ -0,0 +1,35 @@ +package Test::Irssi::Misc; +use strictures 1; + + + +sub keycombo_to_code { + my ($key_combo) = @_; + my $output = ''; + my $ctrl = 0; + my $meta = 0; + if ($key_combo =~ m/[cC](?:trl)?-(.+)/) { + $ctrl = 1; + _parse_rest($1); + } + if ($key_combo =~ m/[Mm](?:eta)?-(.+)/) { + $meta = 1; + _parse_rest($1); + } +} + +sub _parse_key { + my ($rest) = @_; + my $special = { + left => '', + right => '', + up => '', + down => '', + tab => '', + space => '', + spc => '', + }; +} + + +1; diff --git a/testing/lib/Test/Irssi/Test.pm b/testing/lib/Test/Irssi/Test.pm new file mode 100644 index 0000000..cd0a6f9 --- /dev/null +++ b/testing/lib/Test/Irssi/Test.pm @@ -0,0 +1,308 @@ +use strictures 1; +use MooseX::Declare; + +class Test::Irssi::Test { + + use POE; + use Test::Irssi; + use Test::Irssi::Driver; + use feature qw/say/; + use Data::Dump qw/dump/; + + has 'parent' + => ( + is => 'ro', + isa => 'Test::Irssi', + required => 1, + ); + + has 'name' + => ( + is => 'ro', + isa => 'Str', + required => 1, + ); + + has 'description' + => ( + is => 'rw', + isa => 'Str', + default => '', + ); + + has 'states' + => ( + is => 'ro', + isa => 'ArrayRef', + traits => [qw/Array/], + default => sub { [] }, + lazy => 1, + handles => { + add_state => 'push', + state_count => 'count', + get_state => 'get', + }, + ); + + has 'results' + => ( + is => 'ro', + isa => 'ArrayRef', + default => sub { [] }, + ); + + has 'complete' + => ( + is => 'rw', + isa => 'Bool', + default => 0, + ); + + + has '_next_state' + => ( + is => 'rw', + isa => 'Int', + default => 0, + traits => [qw/Counter/], + handles => { + _increment_state_counter => 'inc', + _clear_state => 'reset', + }, + ); + + # TODO: should only be valid when complete is set. + sub passed { + my $self = shift; + my $pass = 0; + foreach my $result (@{$self->results}) { + $pass = $result; + } + return $pass and $self->complete; + } + + sub failed { + my $self = shift; + return not $self->passed; + } + + + sub details { + my ($self) = shift; + my $state_count = $self->state_count; + for (0..$state_count-1) { + my $state = $self->states->[$_]; + my $result = $self->results->[$_]; + say( "#\t" . $state->{type} . " - " . $state->{desc} . " " + . " = " .( $result?"ok":"not ok")); + } + } + ############# API FUNCTIONS ########################################## + + + method add_input_sequence(Str $input) { + $self->add_state({type => 'command', + of => 'input', + input => $input, + desc => 'input'}); + + $self->log("Adding $input as input"); + } + + method add_delay (Num $delay) { + $self->add_state({type => 'command', + of => 'delay', + desc => 'delay', + delay => $delay }); + $self->log("Adding $delay as delay"); + + } + + method add_keycode(Str $code) { + my $input = $self->translate_keycode($code); + $self->add_state({type => 'command', + desc => 'input', + input => $input }); + $self->log("Adding $input ($code) as input"); + + } + sub add_diag { + my ($self, $diag) = @_; + $self->add_state({type => 'command', + of => 'diag', + desc => $diag }); + } + + sub add_pattern_match { + my ($self, $pattern, $constraints, $desc) = @_; + $self->add_state({type => 'test', + of => 'pattern', + pattern => $pattern, + constraints => $constraints, + desc => $desc}); + + $self->log("Adding $pattern as output match "); + } + + sub test_cursor_position { + my ($self, $x, $y, $desc) = @_; + $self->add_state({type => 'test', + of => 'cursor', + x => $x, + y => $y, + desc => $desc }); + $self->log("Adding cursor [$x, $y] test "); + + } + + sub add_evaluation_function { + my ($self, $coderef, $desc) = @_; + $self->add_state({type => 'test', + of => 'function', + code => $coderef, + desc => $desc}); + } + + + ############# END OF API FUNCTIONS #################################### + + + + method translate_keycode(Str $code) { + my $seq = ''; + if ($code =~ m/M-([a-z])/i) { + $seq = "\x1b" . $1; + } elsif ($code =~ m/C-([a-z])/i) { + $seq = chr ( ord(lc $1) - 64 ); + } + return $seq; + } + + method this_state { + return $self->_next_state - 1; + } + + sub check_output { + my ($self, $data) = @_; + + my ($pattern, $constraints) = ($data->{pattern}, $data->{constraints}); + + my $ok = 0; + my $line = ''; + if ($constraints eq 'prompt') { + $line = $self->parent->get_prompt_line; + } elsif ($constraints eq 'window_sbar') { + $line = $self->parent->get_window_statusbar_line; + } elsif ($constraints eq 'window') { + # NOTE: not actually a line. + $line = $self->parent->get_window_contents; + } elsif ($constraints eq 'topic') { + $line = $self->parent->get_topic_line; + } + + $self->log("Testing pattern against: '$line'"); + + if ($line =~ m/$pattern/) { + $self->log("Pattern $pattern passed"); + $self->results->[$self->this_state] = 1; + } else { + $self->log("Pattern $pattern failed"); + $self->results->[$self->this_state] = 0;; + } + } + + sub get_next_state { + my ($self) = @_; + my $item = $self->get_state($self->_next_state); + $self->_increment_state_counter; + + return $item; + } + + sub evaluate_test { + my ($self) = @_; + + while (my $state = $self->get_next_state) { + + $self->log("Evaluating Test: " . dump($state)); + + my $type = $state->{type}; + + if ($type eq 'command') { + my $subtype = $state->{of}; + + if ($subtype eq 'diag') { + if ($self->parent->generate_tap) { + say STDOUT '#' . $state->{desc}; + } + } + if ($subtype eq 'input') { + $self->parent->inject_text($state->{input}); + $self->log("input: ". $state->{input}); + } + if ($subtype eq 'delay') { + $self->log("inserting delay"); + $self->parent->apply_delay($state->{delay}); + $self->results->[$self->this_state] = 1; + return; + } + + # all commands are considered to succeed. + $self->results->[$self->this_state] = 1; + + } elsif ($type eq 'test') { + + my $test_type = $state->{of}; + + if ($test_type eq 'pattern') { + my $pattern = $state->{pattern}; + $self->check_output($state); + } + if ($test_type eq 'cursor') { + my ($curs_x, $curs_y) = $self->parent->get_cursor_position; + + my $ret = 0; + if ($state->{x} == $curs_x and $state->{y} == $curs_y) { + $ret = 1; + } + + $self->results->[$self->this_state] = $ret; + + } + + if ($test_type eq 'function') { + # code evaluation + my @args = ($self, $self->parent, $self->parent->vt); + my $ret = $state->{code}->(@args); + $ret //= 0; # ensure that undef failures are + # marked as such. + $self->results->[$self->this_state] = $ret; + } + } else { + # wtf? + } + } + + $poe_kernel->post(IrssiTestDriver => 'test_complete'); + + $self->complete(1); + + $self->log("Test Execution Finished"); + } + + sub resume_from_timer { + my ($self) = @_; + $self->log("Resuming after timeout"); + $self->evaluate_test; + } + sub log { + my ($self, $msg) = @_; + $self->parent->_logfile_fh->say($msg); + } + + sub _all { $_ || return 0 for @_; 1 } +} + + + + __END__ diff --git a/testing/lib/Test/Irssi/VirtualIrssi.pm b/testing/lib/Test/Irssi/VirtualIrssi.pm new file mode 100644 index 0000000..dc3bfc7 --- /dev/null +++ b/testing/lib/Test/Irssi/VirtualIrssi.pm @@ -0,0 +1,32 @@ +use strictures 1; +use MooseX::Declare; + +class Test::Irssi::VirtualIrssi { + +# class that pretends to be irssi which you can pull out various data from. + + +has cursor + => ( + is => 'ro', + writer => '_set_cursor', + isa => 'ArrayRef[Int]', + default => sub { [0, 0] }, + ); + +has topic_row + => ( + ); + +has window_row + => ( + ); + +has prompt_row + => ( + ); + +has window + => ( + ); +} diff --git a/testing/t/001-use.t b/testing/t/001-use.t new file mode 100755 index 0000000..6ebbb5a --- /dev/null +++ b/testing/t/001-use.t @@ -0,0 +1,27 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Test::More; +use Data::Dumper; + +BEGIN { + use_ok 'Test::Irssi'; +} + + +my $test = new_ok 'Test::Irssi', + [irssi_binary => 'null', irssi_homedir => 'null']; + +my @methods = qw/logfile terminal_height terminal_width irssi_homedir irssi_binary/; +can_ok($test, @methods); + +undef $test; + +done_testing; + +__END__ + + + diff --git a/testing/t/002-init.t b/testing/t/002-init.t new file mode 100755 index 0000000..b688f9f --- /dev/null +++ b/testing/t/002-init.t @@ -0,0 +1,33 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Test::More; +use Data::Dumper; + +BEGIN { + use_ok 'Test::Irssi'; +} + + +my $test = new_ok 'Test::Irssi', + [irssi_binary => "/opt/stow/repo/irssi-debug/bin/irssi", + irssi_homedir => $ENV{HOME} . "/projects/tmp/test/irssi-debug"]; + +if (-f $test->logfile) { + ok(unlink $test->logfile, 'deleted old logfile'); +} + +my $drv = $test->driver; +isa_ok($drv, 'Test::Irssi::Driver', 'driver created ok'); + +diag "Starting POE session"; +$test->run(); + +done_testing; + +__END__ + + + diff --git a/testing/test-shim.pl b/testing/test-shim.pl new file mode 100644 index 0000000..628f7af --- /dev/null +++ b/testing/test-shim.pl @@ -0,0 +1,114 @@ +use strict; +use warnings; + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; +use POSIX; +use Time::HiRes qw/sleep/; +use JSON::Any; + + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => 'test-shim', + description => '', + license => 'Public Domain', + ); + + +my $forked = 0; + +sub pipe_and_fork { + my ($read_handle, $write_handle); + pipe($read_handle, $write_handle); + + my $oldfh = select($write_handle); + $| = 1; + select $oldfh; + + return if $forked; + + my $pid = fork(); + + if (not defined $pid) { + _error("Can't fork: Aborting"); + close($read_handle); + close($write_handle); + return; + } + + $forked = 1; + + if ($pid > 0) { # this is the parent (Irssi) + close ($write_handle); + Irssi::pidwait_add($pid); + my $job = $pid; + my $tag; + my @args = ($read_handle, \$tag, $job); + $tag = Irssi::input_add(fileno($read_handle), + Irssi::INPUT_READ, + \&child_input, + \@args); + + } else { # child + child_process($write_handle); + close $write_handle; + + POSIX::_exit(1); + } +} +sub _cleanup_child { + my ($read_handle, $input_tag_ref) = @_; + close $read_handle; + Irssi::input_remove($$input_tag_ref); + _msg("child finished"); + $forked = 0; +} +sub child_input { + my $args = shift; + my ($read_handle, $input_tag_ref, $job) = @$args; + + my $input = <$read_handle>; + my $data = JSON::Any::jsonToObj($input); + if (ref $data ne 'HASH') { + _error("Invalid data received: $input"); + _cleanup_child($read_handle, $input_tag_ref); + } + + if (exists $data->{connection}) { + if ($data->{connection} eq 'close') { + _cleanup_child($read_handle, $input_tag_ref); + } + } else { + parent_process_response($data); + } +} + +sub parent_process_response { + my ($data) = @_; +} + + +sub child_process { + my ($handle) = @_; + +} + +sub _error { + my ($msg) = @_; + my $win = Irssi::active_win(); + $win->print($msg, Irssi::MSGLEVEL_CLIENTERROR); +} + +sub _msg { + my ($msg) = @_; + my $win = Irssi::active_win(); + $win->print($msg, Irssi::MSGLEVEL_CLIENTCRAP); +} + +Irssi::command_bind("start_pipes", \&pipe_and_fork); diff --git a/testing/test.pl b/testing/test.pl new file mode 100755 index 0000000..bf01530 --- /dev/null +++ b/testing/test.pl @@ -0,0 +1,17 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use feature qw/say/; +#use lib 'blib/lib'; + +use TAP::Harness; +my $harness = TAP::Harness->new({ verbosity => 1, + lib => 'blib/lib', + color => 1, + }); + +my @tests = glob($ARGV[0]); +say "Tests: " . join (", ", @tests); +$harness->runtests(@tests); diff --git a/testing/tests/001-basic.t b/testing/tests/001-basic.t new file mode 100755 index 0000000..60578d8 --- /dev/null +++ b/testing/tests/001-basic.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use feature qw/say/; +use Test::Irssi; + +my $tester = Test::Irssi->new + (irssi_binary => "/opt/stow/repo/irssi-debug/bin/irssi", + irssi_homedir => $ENV{HOME} . "/projects/tmp/test/irssi-debug"); + +if (exists $ENV{IRSSI_TEST_HEADLESS} and $ENV{IRSSI_TEST_NOHEADLESS} == 1) { + $tester->run_headless(0); + $tester->generate_tap(0); +} else { + $tester->run_headless(1); + $tester->generate_tap(1); +} + +my $test = $tester->new_test('test1'); +$test->description("simple echo tests"); + +$test->add_input_sequence("/echo Hello cats\n"); +$test->add_delay(1); +$test->add_input_sequence("/echo Hello Again\n"); +$test->add_input_sequence("this is a long test"); +$test->add_delay(0.5); +$test->add_pattern_match(qr/long/, 'prompt', 'prompt contains long'); +$test->add_delay(1); + +$test->add_pattern_match(qr/this is a .*? test/, 'prompt', 'prompt matches'); + +my $test2 = $tester->new_test('test2'); +$test2->description("cursor movement and deletion"); + +$test2->add_delay(1); +$test2->add_input_sequence("\x01"); +$test2->add_delay(0.1); +$test2->add_input_sequence("\x0b"); +$test2->add_delay(0.1); +$test2->add_input_sequence("/clear\n"); +$test2->add_delay(0.1); +$test2->add_input_sequence("/echo moo\n"); + +my $quit = $tester->new_test('quit'); +$quit->description('quitting'); +$quit->add_input_sequence("/quit\n"); + +$tester->run; diff --git a/testing/tests/002-cursor-test.t b/testing/tests/002-cursor-test.t new file mode 100755 index 0000000..eb35170 --- /dev/null +++ b/testing/tests/002-cursor-test.t @@ -0,0 +1,29 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use feature qw/say/; +use Test::Irssi; + +my $tester = Test::Irssi->new + (irssi_binary => "/opt/stow/repo/irssi-debug/bin/irssi", + irssi_homedir => $ENV{HOME} . "/projects/tmp/test/irssi-debug"); + +if (exists $ENV{IRSSI_TEST_HEADLESS} and $ENV{IRSSI_TEST_NOHEADLESS} == 1) { + $tester->run_headless(0); + $tester->generate_tap(0); +} else { + $tester->run_headless(1); + $tester->generate_tap(1); +} + +my $test = $tester->new_test('test1'); +$test->description("simple echo tests"); +$test->add_diag("Testing 123"); + +my $quit = $tester->new_test('quit'); +$quit->description('quitting'); +$quit->add_input_sequence("/quit\n"); + +$tester->run; diff --git a/tinyurl-tabcomplete/README.pod b/tinyurl-tabcomplete/README.pod new file mode 100644 index 0000000..f29ace0 --- /dev/null +++ b/tinyurl-tabcomplete/README.pod @@ -0,0 +1,56 @@ +=pod + +=head1 NAME + +complete-tiny-url.pl + +=head1 DESCRIPTION + +Shortens web links from your Irssi input field by pressing tab directly after +them. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD complete-tiny-url.pl>. You may wish to have it autoload in one of the +L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>. + +=head1 USAGE + +Type or paste a URL into your input field, then immediately following the last +character of it, press the C<E<lt>TABE<gt>> key. After a few seconds, the +URL will be replaced with an appropriate L<http://tinyurl.com/> address. + +=head1 AUTHORS + +Copyright E<copy> 2010-2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> and + +=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 + +None Known. Please report any at +L<https://github.com/shabble/irssi-scripts/issues/new> + + + +=cut + diff --git a/tinyurl-tabcomplete/complete-tiny-url.pl b/tinyurl-tabcomplete/complete-tiny-url.pl index 79d1161..b2d448f 100644 --- a/tinyurl-tabcomplete/complete-tiny-url.pl +++ b/tinyurl-tabcomplete/complete-tiny-url.pl @@ -1,17 +1,72 @@ +=pod + +=head1 NAME + +complete-tiny-url.pl + +=head1 DESCRIPTION + +Shortens web links from your Irssi input field by pressing tab directly after +them. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD complete-tiny-url.pl>. You may wish to have it autoload in one of the +L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>. + +=head1 USAGE + +Type or paste a URL into your input field, then immediately following the last +character of it, press the C<E<lt>TABE<gt>> key. After a few seconds, the +URL will be replaced with an appropriate L<http://tinyurl.com/> address. + +=head1 AUTHORS + +Copyright E<copy> 2010-2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> and + +=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 + +None Known. Please report any at +L<https://github.com/shabble/irssi-scripts/issues/new> + +=cut + + use strict; -use vars qw($VERSION %IRSSI); +use warnings; use Irssi; use WWW::Shorten::TinyURL; -$VERSION = '1.0'; -%IRSSI = ( +our $VERSION = '1.0'; +our %IRSSI = ( authors => 'Shabble', contact => 'shabble+irssi /at/ metavore /dot/ org', name => 'Shorten URLs using Tab', description => 'Hitting Tab after typing/pasting a long URL will replace it with' . ' its tinyURL.com equivalent', - license => 'WTFPL', + license => 'MIT', ); sub do_complete { diff --git a/undo/kill-ring.pl b/undo/kill-ring.pl new file mode 100644 index 0000000..c3600a6 --- /dev/null +++ b/undo/kill-ring.pl @@ -0,0 +1,129 @@ +# DOCUMENTATION: +# +# +# +# +# LICENCE: +# +# Copyright (c) 2011 Tom Feist <shabble+irssi@metavore.org> +# +# 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. +# + +use strict; +use warnings; + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => 'kill-ring', + description => 'keeps track of changes to the cutbuffer' + . ' and allows you to cycle through them', + license => 'MIT', + updated => '$DATE' + ); + +my @cut_buf_history; + +sub cut_buffer_init { + Irssi::signal_add_first('gui key pressed' => \&sig_cmd_undo); + Irssi::command_bind('cut_buf_cycle' => \&cmd_cut_buf_cycle); +} + +sub add_to_history { + my ($str) = @_; + + return unless defined $str; + + my $head = $cut_buf_history[-1]; + + if (defined $head and $str ne $head) { + push @cut_buf_history, $str; + } elsif (not defined $head) { + push @cut_buf_history, $str; + } + + # enforce maximum size + # TODO: make this a setting? + + if (@cut_buf_history > 100) { + shift @cut_buf_history; + } +} + +sub cmd_cut_buf_cycle { + print '%_Cut buffer contains:%_'; + foreach my $buf (@cut_buf_history) { + print "$buf" + } +} + +sub sig_cmd_undo { + my ($key) = @_; + add_to_history(_cut_buf()); +} + +sub _cut_buf { + return Irssi::parse_special('$U', 0, 0); +} + +sub _input { + my ($data) = @_; + + my $current_data = Irssi::parse_special('$L', 0, 0); + + # TODO: set this back up. + + # if ($settings->{utf8}->{value}) { + # $current_data = decode_utf8($current_data); + # } + + if (defined $data) { + # if ($settings->{utf8}->{value}) { + # Irssi::gui_input_set(encode_utf8($data)); + # } else { + Irssi::gui_input_set($data); + #} + } else { + $data = $current_data; + } + + return $data; +} + +sub _input_pos { + my ($pos) = @_; + my $cur_pos = Irssi::gui_input_get_pos(); + if (defined $pos) { + Irssi::gui_input_set_pos($pos) if $pos != $cur_pos; + } else { + $pos = $cur_pos; + } + + return $pos; +} + +cut_buffer_init(); diff --git a/undo/undo.pl b/undo/undo.pl new file mode 100644 index 0000000..5daa16e --- /dev/null +++ b/undo/undo.pl @@ -0,0 +1,134 @@ +# LICENCE: +# +# Copyright (c) 2011 Tom Feist <shabble+irssi@metavore.org> +# +# 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. +# + +# TODO: attempt to create an undo function for short-term history in +# the input-bar. + +use strict; +use warnings; + +use Irssi; +use Irssi::Irc; +use Irssi::TextUI; + +use Data::Dumper; + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'shabble', + contact => 'shabble+irssi@metavore.org', + name => '', + description => '', + license => 'MIT', + ); + +my @undo_list; + +sub undo_init { + Irssi::signal_add_first('gui key pressed' => \&sig_cmd_undo);} +} + +sub add_to_undo { + my ($str, $pos) = @_; + + return unless defined $str and defined $pos; + + my $undo_head = $undo_list[-1]; + + if (ref $undo_head) { + if ($str ne $undo_head->{str} && $pos != $undo_head->{pos}) { + push @undo_list, { str => $str, pos => $pos }; + } + } else { + push @undo_list, { str => $str, pos => $pos }; + } + + # enforce maximum size + # TODO: make this a setting? + + if (@undo_list > 100) { + shift @undo_list; + } +} + +sub sig_cmd_undo { + my ($key) = @_; + + if ($key == 10) { + @undo_list = (); + return; + } elsif ($key != 28) { + add_to_undo(_input(), _input_pos()); + return; + } + + Irssi::signal_stop(); + + my $prev_state = pop @undo_list; + + if (not defined $prev_state) { + print "No further undo"; + return; + } + + _input($prev_state->{str}); + _input_pos($prev_state->{pos}); +} + +sub _input { + my ($data) = @_; + + my $current_data = Irssi::parse_special('$L', 0, 0); + + # TODO: set this back up. + + # if ($settings->{utf8}->{value}) { + # $current_data = decode_utf8($current_data); + # } + + if (defined $data) { + # if ($settings->{utf8}->{value}) { + # Irssi::gui_input_set(encode_utf8($data)); + # } else { + Irssi::gui_input_set($data); + #} + } else { + $data = $current_data; + } + + return $data; +} + +sub _input_pos { + my ($pos) = @_; + my $cur_pos = Irssi::gui_input_get_pos(); + if (defined $pos) { + Irssi::gui_input_set_pos($pos) if $pos != $cur_pos; + } else { + $pos = $cur_pos; + } + + return $pos; +} + +undo_init(); diff --git a/vim-mode/README.pod b/vim-mode/README.pod new file mode 100644 index 0000000..83962ff --- /dev/null +++ b/vim-mode/README.pod @@ -0,0 +1,584 @@ +=pod + +=head1 NAME + +vim_mode.pl + +=head1 DESCRIPTION + +An Irssi script to emulate some of the vi(m) features for the Irssi inputline. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD vim_mode.pl>. You may wish to have it autoload in one of the +L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>. + +=head2 DEPENDENCIES + +For proper :ex mode support, vim-mode requires the installation of 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 for installation. + +If you don't need Ex-mode, you can run vim_mode.pl without the +uberprompt.pl script, but it is strongly recommended that you use it. + +=head3 Irssi requirements + +0.8.12 and above should work fine. However the following features are +disabled in irssi < 0.8.13: + +=over 4 + +=item * C<j> C<k> (only with count, they work fine without count in older versions) + +=item * C<gg>, C<G> + +=back + +It is intended to work with at Irssi 0.8.12 and later versions. However some +features are disabled in older versions (see below for details). + +Perl >= 5.8.1 is recommended for UTF-8 support (which can be disabled if +necessary). Please report bugs in older versions as well, we'll try to fix +them. Later versions of Perl are also faster, which is probably beneficial +to a script of this size and complexity. + +=head2 SETUP + +Vim Mode provides certain feedback to the user, such as the currently active +mode (command, insert, ex), and if switching buffers, which buffer(s) currently +match the search terms. + +There are two ways to go about displaying this information, as described in +the following sections: + +=head3 Statusbar Items + +Run the following command to add a statusbar item that shows which mode +you're in. + +C</statusbar window add vim_mode> + +And the following to let C<:b [str]> display a list of channels matching your +search string. + +C</statusbar window add vim_windows> + +B<Note:> Remember to C</save> after adding these statusbar items to make them +permanent. + +B<Note:> If you would rather have these statusbar items on your prompt +line rather than thte window statusbar, please follow these steps. + +For technical reasons, I<uberprompt> must occasionally call C</statusbar prompt +reset>, which will remove or deactivate any manually added items on the prompt +statusbar. To get around this, uberprompt provides two command hooks, +C<uberprompt_load_hook> and C<uberprompt_unload_hook>. Both of these settings +can contain one (or more, using C</EVAL>) commands to be executed when the prompt +is enabled and disabled, respectively. + +See the L<uberprompt documentation|https://github.com/shabble/irssi-scripts/blob/master/prompt_info/README.pod> for further details. + +For I<right-aligned> items (that is, after the input field: + +=over 4 + +=item 1 C</alias vm_add /^statusbar prompt add -after input -alignment right vim_mode> + +=item 2 C</alias vm_del /^statusbar prompt remove vim_mode> + +=item 3 C</set uberprompt_load_hook /^vm_add> + +=item 4 C</set uberprompt_unload_hook /^vm_del> + +=back + +For I<left-aligned> items (before the prompt): + +=over 4 + +=item 1 C</alias vm_add /^statusbar prompt add -before prompt -alignment left vim_mode> + +=item 2 C</alias vm_del /^statusbar prompt remove vim_mode> + +=item 3 C</set uberprompt_load_hook /^vm_add> + +=item 4 C</set uberprompt_unload_hook /^vm_del> + +=back + +If you wish to add both C<vim_mode> and C<vim_windows> items, replace steps 1 and 2 +above with the following (right-aligned): + +=over 4 + +=item 1 C</alias vm_add /^eval /^statusbar prompt add -after input -alightment right vim_mode ; /^statusbar prompt add -after input -alignment right vim_windows> + +=item 2 C</alias vm_del /^eval /^statusbar prompt remove vim_mode ; /^statusbar prompt remove vim_windows> + +=back + +and then complete stages 3 and 4 as above. Replace the C<-after> and C<-alignment> +to suit your location preferences. + +B<Note:> It is also possible to place the items between the prompt and input field, +by specifying C<-after prompt> or C<-before input> as appropriate. + +=head3 Expando Variables + +Vim mode augments the existing Irssi expando (automatic variables) with two +additional ones: C<$vim_cmd_mode> and C<$vim_wins>. + +C<$vim_cmd_mode> is the equivalent of the C<vim_mode> statusbar item, and +C<$vim_wins> is the counterpart of C<vim_windows>. + +They can be added to your theme, or inserted into your uberprompt using +a command such as: + +"C</set uberprompt_format [$vim_cmd_mode] $*$uber] >" + +=head3 FILE-BASED CONFIGURATION + +Additionally to the irssi settings described in L<settings|/SETTINGS>, vim_mode +can be configured through an external configuration file named "vim_moderc" +located in F<~/.irssi/vim_moderc>. If available it's loaded on startup and every +supported ex-command is run. Its syntax is similar to "vimrc". To (re)load it +while vim_mode is running use C<:so[urce]>. + +Currently Supported ex-commands: + +=over 4 + +=item * C<:map> + +=back + +=head1 USAGE + +The following section is divided into the different modes as supported by Vim itself: + +=head2 COMMAND MODE + +It supports most commonly used command mode features: + +=over 2 + +=item * Insert/Command mode. + +C<Esc> and C<Ctrl-C> enter command mode. C</set vim_mode_cmd_seq j> allows +to use C<jj> as Escape (any other character can be used as well). + +=item * Cursor motion: + +C<h l 0 ^ $ E<lt>SpaceE<gt> E<lt>BSE<gt> f t F T> + +=item * History motion: + +C<j k gg G> C<gg> moves to the oldest (first) history line. C<G> without a +count moves to the current input line, with a count it goes to the I<count-th> +history line (1 is the oldest). + +=item * Cursor word motion: + +C<w b ge e W gE B E> + +=item * Word objects (only the following work yet): + +C<aw aW> + +=item * Yank and paste: + + C<y p P> + +=item * Change and delete: + +C<c d> + +=item * Delete at cursor: + +C<x X> + +=item * Replace at cursor: + +C<r> + +=item * Insert mode: + +C<i a I A> + +=item * Switch case: + +C<~> + +=item * Repeat change: + +C<.> + +=item * Repeat + +C<ftFT: ; ,> + +=item * Registers: + +C<"a-"z "" "0 "* "+ "_> (black hole) + +=item * Line-wise shortcuts: + +C<dd cc yy> + +=item * Shortcuts: + +C<s S C D> + +=item * Scroll the scrollback buffer: + +C<Ctrl-E Ctrl-D Ctrl-Y Ctrl-U Ctrl-F Ctrl-B> + +=item * Switch to last active window: + +C<Ctrl-6/Ctrl-^> + +=item * Switch split windows: + +<Ctrl-W j Ctrl-W k> + +=item * Undo/Redo: + +C<u Ctrl-R> + +=back + +Counts and combinations work as well, e.g. C<d5fx> or C<3iabcE<lt>escE<gt>>. Counts also work with mapped ex-commands (see below), e.g. if you map C<gb> to do C<:bn>, then C<2gb> will switch to the second next buffer. Repeat also supports counts. + +=head3 REGISTERS + +=over 4 + +=item * Appending to register with C<"A-"Z> + +=item * C<""> is the default yank/delete register. + +=item * C<"0> contains the last yank (if no register was specified). + +=item * The special registers C<"* "+> both contain irssi's internal cut-buffer. + +=back + +=head2 INSERT MODE + +The following insert mode mappings are supported: + +=over 4 + +=item * Insert register content: Ctrl-R x (where x is the register to insert) + +=back + +=head2 EX-MODE + +Ex-mode (activated by C<:> in command mode) supports the following commands: + +=over 4 + +=item * Command History: + +C<E<lt>uparrowE<gt>> - cycle backwards through history + +C<E<lt>downarrowE<gt>> - cycle forwards through history + +C<:eh> - show ex history + +=item * Switching buffers: + +C<:[N]b [N]> - switch to channel number + +C<:b#> - switch to last channel + +C<:b> E<lt>partial-channel-nameE<gt> + +C<:b> <partial-server>/<partial-channel> + +C<:buffer {args}> (same as C<:b>) + +C<:[N]bn[ext] [N]> - switch to next window + +C<:[N]bp[rev] [N]> - switch to previous window + +=item * Close window: + +C<:[N]bd[elete] [N]> + +=item * Display windows: + +C<:ls>, C<:buffers> + +=item * Display registers: + +C<:reg[isters] {args}>, C<:di[splay] {args}> + +=item * Display undolist: + +C<:undol[ist]> (mostly used for debugging) + +=item * Source files: + +C<:so[urce]> - only sources vim_moderc at the moment, + F<{file}> not supported + +=item * Mappings: + +C<:map> - display custom mappings + +=item * Saving mappings: + +C<:mkv[imrc][!]> - like in Vim, but [file] not supported + +=item * Substitution: + +C<:s///> - I<i> and I<g> are supported as flags, only C<///> can be used as +eparator, and it uses Perl regex syntax instead of Vim syntax. + +=item * Settings: + +C<:se[t]> - display all options + +C<:se[t] {option}> - display all matching options + +C<:se[t] {option} {value}> - change option to value + +=back + +=head3 MAPPINGS + +=head4 Commands + +=over 4 + +=item * C<:map {lhs}> - display mappings starting with {lhs} + +=item * C<:map {lhs} {rhs}> - add mapping + +=item * C<:unm[ap] {lhs}> - remove custom mapping + +=back + +=head4 Parameters + +I<{lhs}> is the key combination to be mapped, I<{rhs}> the target. The +C<E<lt>E<gt>> notation is used + +(e.g. C<E<lt>C-FE<gt>> is I<Ctrl-F>), case is ignored. + Supported C<E<lt>E<gt>> keys are: + +=over 4 + +=item * C<E<lt>C-AE<gt>> - C<E<lt>C-ZE<gt>>, + +=item * C<E<lt>C-^E<gt>>, C<E<lt>C-6E<gt>> + +=item * C<E<lt>SpaceE<gt>> + +=item * C<E<lt>CRE<gt>> - Enter + +=item * C<E<lt>BSE<gt>> - Backspace + +=item * C<E<lt>NopE<gt>> - No-op (Do Nothing). + +=back + +Mapping ex-mode and irssi commands is supported. When mapping ex-mode commands +the trailing C<E<lt>CrE<gt>> is not necessary. Only default mappings can be used +in I<{rhs}>. + +Examples: + +=over 4 + +=item * C<:map w W> - to remap w to work like W + +=item * C<:map gb :bnext> - to map gb to call :bnext + +=item * C<:map gB :bprev> + +=item * C<:map g1 :b 1> - to map g1 to switch to buffer 1 + +=item * C<:map gb :b> - to map gb to :b, 1gb switches to buffer 1, 5gb to 5 + +=item * C<:map E<lt>C-LE<gt> /clear> - map Ctrl-L to irssi command /clear + +=item * C<:map E<lt>C-GE<gt> /window goto 1> + +=item * C<:map E<lt>C-EE<gt> <Nop>> - disable E<lt>C-EE<gt>, it does nothing now + +=item * C<:unmap E<lt>C-EE<gt>> - restore default behavior of C<E<lt>C-EE<gt>> +after disabling it + +=back + +Note that you must use C</> for irssi commands (like C</clear>), the current value +of Irssi's cmdchars does B<not> work! This is necessary do differentiate between +ex-commands and irssi commands. + +=head2 SETTINGS + +The settings are stored as irssi settings and can be set using C</set> as usual +(prepend C<vim_mode_> to setting name) or using the C<:set> ex-command. The +following settings are available: + +=over 4 + +=item * utf8 - Support UTF-8 characters, boolean, default on + +=item * debug - Enable debug output, boolean, default off + +=item * cmd_seq - Char that when double-pressed simulates C<E<lt>EscE<gt>>, string, default '' (disabled) + +=item * start_cmd - Start every line in command mode, boolean, default off + +=item * max_undo_lines - Sze of the undo buffer. Integer, default 50 items. + +=item * ex_history_size - Number of items stored in the ex-mode history. Integer, default 100. + +=item * prompt_leading_space - Ddetermines whether ex mode prepends a space to the displayed input. Boolean, default on + +=back + +In contrast to irssi's settings, C<:set> accepts 0 and 1 as values for boolean +settings, but only vim_mode's settings can be set/displayed. + +Examples: + + :set cmd_seq=j # set cmd_seq to j + :set cmd_seq= # disable cmd_seq + :set debug=on # enable debug + :set debug=off # disable debug + +=head1 SUPPORT + +Any behavior different from Vim (unless explicitly documented) should be +considered a bug and reported. + +B<NOTE:> This script is still under heavy development, and there may be bugs. +Please submit reproducible sequences to the bug-tracker at: +L<http://github.com/shabble/irssi-scripts/issues/new> + +or contact rudi_s or shabble on irc.freenode.net (#irssi and #irssi_vim) + +=head1 AUTHORS + +Copyright E<copy> 2010-2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> and + +Copyright E<copy> 2010-2011 Simon Ruderich C<E<lt>simon@ruderich.orgE<gt>> + +=head1 THANKS + +Particular thanks go to + +=over 4 + +=item * estragib: a lot of testing and many bug reports and feature requests + +=item * iaj: testing + +=item * tmr: explaining how various bits of vim work + +=back + +as well as the rest of C<#irssi> and C<#irssi_vim> on Freenode IRC. + +=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 * + +count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does + +=item * + +mapping an incomplete ex-command doesn't open the ex-mode with the partial +command (e.g. C<:map gb :b> causes an error instead of opening the ex-mode and +displaying C<:bE<lt>cursorE<gt>>) + +=item * + + undo/redo cursor positions are mostly wrong + +=back + +=head1 TODO + +=over 4 + +=item * + +History: + +=over 4 + +=item * + + C< * /,?,n,N> to search through history (like rl_history_search.pl) + +=back + +=item * + +Window switching (C<:b>) + +=over 4 + +=item * + +Tab completion of window(-item) names + +=item * + +non-sequential matches(?) + +=back + +=back + +See also the TODO file at +L<github|https://github.com/shabble/irssi-scripts/blob/master/vim-mode/TODO> for +many many more things. + +=head2 WONTFIX + +Things we're not ever likely to do: + +=over 4 + +=item * Macros + +=back + + + +=cut + diff --git a/vim-mode/input.txt b/vim-mode/input.txt new file mode 100644 index 0000000..790060a --- /dev/null +++ b/vim-mode/input.txt @@ -0,0 +1,58 @@ +How the input system for vim_mode should work. +============================================= + +* insert mode mappings +* normal (command) mode mappings +* timeout for ambiguous mappings +** but support for no timeout for some mappings (like Ctrl-R in insert mode) +* maybe 'timeout' and 'ttimeout' settings (and 'timeoutlen' and 'ttimeoutlen') +* partial commands/mappings (like :map a :b a which inserts ex mode and +* displays :b a there without running any command) +* support for mappings entering insert mode: :map gX itexthere<esc> (also without esc!) +** (.) (repeat) for all (most) commands +* more general command order: 3"ap doesn't work but "a3p does +* arbitrary mappings, like 3dwjjihi<esc>10G (at the moment even :map Y y$ doesn't work) +* support to disable mappings temporarily (:set paste, 'pastetoggle') +* (better) support multiple bindings for the same action (especially <BS> and <C-H>) +* unmapping of internal mappings (for example <C-C> to <Nop> as it's also used for colors in irssi) +* better approach to detecting single ESC push (compared to F-keys or meta-* keys) + +We base the input model on a simple state machine. +It can exist in any of 4 (5) states: + - insert mode + - insert escape mode + a transitional mode to distingush that we've received an escape char. + - command mode + - ex mode + - visual mode (can be mostly ignored for now, until I sort my overlays stuff + out) + +to swap between these modes, there are various bindings available. +The most challenging is going from insert -> command. Typically this is done +by a single push of the <esc> key, but C-c and <jj> are both alternatives that +people use. + +The single entrypoint we have into the input system is detecting keystrokes. +when a 'gui key pressed' signal is fired, we can receive, process and interrupt +it such that it never gets passed to irssi proper. + +in order to detect single <esc> presses from their meta comrades, we operate +as the following: + +if (key == 27) { + mode = insert_escape + add_timeout(short, check_escape_buffer) +} elsif (mode == insert_escape) { + insert(key, escape_buffer) +} else { + # do nothing +} + +sub check_escape_buffer { +# when the timeout fires, we can assume that only meta-keys would be fast enough +# to insert additional characters into the buffer. In which case, we replay +# them and clear the buffer. + mode = insert_mode; +# if the buffer is otherwise empty, we have received a single escape press. + mode = command_mode; +} diff --git a/vim-mode/irssi/config b/vim-mode/irssi/config new file mode 100644 index 0000000..d0b0805 --- /dev/null +++ b/vim-mode/irssi/config @@ -0,0 +1,38 @@ +# Minimal default irssi configuration file - with vim-mode statusbars. + + +servers = ( ); + +chatnets = { }; + +channels = ( ); + +aliases = { }; + +statusbar = { + default = { + window = { + items = { + barstart = { priority = "100"; }; + time = { }; + user = { }; + window = { }; + window_empty = { }; + lag = { priority = "-1"; }; + act = { priority = "10"; }; + more = { priority = "-1"; alignment = "right"; }; + barend = { priority = "100"; alignment = "right"; }; + vim_mode = { }; + vim_windows = { }; + }; + }; + + prompt = { + items = { + uberprompt = { priority = "-1"; }; + input = { priority = "10"; }; + }; + position = "100"; + }; + }; +}; diff --git a/vim-mode/irssi/scripts/autorun/uberprompt.pl b/vim-mode/irssi/scripts/autorun/uberprompt.pl new file mode 120000 index 0000000..3d4d97a --- /dev/null +++ b/vim-mode/irssi/scripts/autorun/uberprompt.pl @@ -0,0 +1 @@ +../../../../prompt_info/uberprompt.pl
\ No newline at end of file diff --git a/vim-mode/irssi/scripts/autorun/vim_mode.pl b/vim-mode/irssi/scripts/autorun/vim_mode.pl new file mode 120000 index 0000000..ed78131 --- /dev/null +++ b/vim-mode/irssi/scripts/autorun/vim_mode.pl @@ -0,0 +1 @@ +../../../../vim-mode/vim_mode.pl
\ No newline at end of file diff --git a/vim-mode/tests.pl b/vim-mode/tests.pl new file mode 100644 index 0000000..9007171 --- /dev/null +++ b/vim-mode/tests.pl @@ -0,0 +1,171 @@ +#!/usr/bin/env perl + +# Must be run in a 80x24 terminal unless a fixed POE is released. + + +use strict; +use warnings; + +use lib '../testing/blib/lib'; + +use Test::Irssi; + +# Mode constants: C(ommand), I(nsert). +sub C () { 0 } +sub I () { 1 } + + +sub statusbar_mode { + my ($test, $mode) = @_; + + $test->add_pattern_match(qr/^ \[\d{2}:\d{2}\] \[\] \[1\] \[$mode\]\s+$/, + 'window_sbar', "[$mode] in vim-mode statusbar"); +} +sub cursor_position { + my ($test, $position) = @_; + + $test->test_cursor_position($position, 24, "Checking cursor position"); +} + +sub check { + my ($test, $input, $mode, $position, $delay) = @_; + + if (defined $input) { + $test->add_input_sequence($input); + if (not defined $delay) { + $delay = 0.1; + $delay += 0.4 if $input =~ /\e/; # esc needs a longer delay + } + $test->add_delay($delay); + } + + cursor_position($test, $position); + if ($mode == C) { + statusbar_mode($test, 'Command'); + } elsif ($mode == I) { + statusbar_mode($test, 'Insert'); + } else { + die "Wrong mode: $mode"; + } +} + +my $tester = Test::Irssi->new + (irssi_binary => "irssi", + irssi_homedir => "./irssi/"); + + +$tester->run_headless(1); +$tester->generate_tap(1); + +my $test = $tester->new_test('insert-command-mode'); +$test->description("switching between insert and command mode"); +# Make sure irssi is finished - not entirely sure why this is necessary. +$test->add_delay(2); + +# We start in insert mode. +check $test, undef, I, 12 + 0; +check $test, "\e", C, 12 + 0; +check $test, 'i', I, 12 + 0; + +# Quit irssi, necessary to terminate the test. +#$test->add_input_sequence("\n/quit\n"); + + +# FIXME: multiple tests don't work +#$test = $tester->new_test('basic-movement'); +#$test->description('basic movement'); +#$test->add_delay(2); + +my $test_string = + 'Test $tring. with a 4711, words , w.r#s42 etc. and more123! ..'; + +check $test, $test_string, I, 12 + length $test_string; +check $test, "\e", C, 12 + 64; + +# h l +check $test, "0", C, 12 + 0; +check $test, "l", C, 12 + 1; +check $test, "l", C, 12 + 2; +check $test, "l", C, 12 + 3; +check $test, "l", C, 12 + 4; +check $test, "l", C, 12 + 5; +check $test, "l", C, 12 + 6; +check $test, "l", C, 12 + 7; +check $test, "h", C, 12 + 6; +check $test, "h", C, 12 + 5; +check $test, "h", C, 12 + 4; +check $test, "h", C, 12 + 3; +check $test, "h", C, 12 + 2; +check $test, "10l", C, 12 + 12; +check $test, "7l", C, 12 + 19; +check $test, "3l", C, 12 + 22; +check $test, "3h", C, 12 + 19; +check $test, "50l", C, 12 + 64; +check $test, "10l", C, 12 + 64; +check $test, "24h", C, 12 + 40; +check $test, "100h", C, 12 + 0; + +# 0 ^ $ +check $test, "I \e", C, 12 + 4; # insert test string for ^ +check $test, "0", C, 12 + 0; +check $test, "^", C, 12 + 5; +check $test, "3^", C, 12 + 5; +check $test, "12^", C, 12 + 5; +check $test, "\$", C, 12 + 46; +check $test, "05x", C, 12 + 0; # remove test string for ^ + +# <Space> <BS> +check $test, "0", C, 12 + 0; +check $test, " ", C, 12 + 1; +check $test, " ", C, 12 + 2; +check $test, " ", C, 12 + 3; +check $test, " ", C, 12 + 4; +check $test, " ", C, 12 + 5; +check $test, " ", C, 12 + 6; +check $test, " ", C, 12 + 7; +check $test, " ", C, 12 + 8; +check $test, "5 ", C, 12 + 13; +check $test, "2 ", C, 12 + 15; +check $test, "10 ", C, 12 + 25; +check $test, "30 ", C, 12 + 55; +check $test, "20 ", C, 12 + 64; +check $test, "10 ", C, 12 + 64; +check $test, " ", C, 12 + 64; +check $test, "\x7f", C, 12 + 63; +check $test, "\x7f", C, 12 + 62; +check $test, "\x7f", C, 12 + 61; +check $test, "\x7f", C, 12 + 60; +check $test, "1\x7f", C, 12 + 59; +check $test, "3\x7f", C, 12 + 56; +check $test, "5\x7f", C, 12 + 51; +check $test, "10\x7f", C, 12 + 41; +check $test, "50\x7f", C, 12 + 0; +check $test, "\x7f", C, 12 + 0; +check $test, "5\x7f", C, 12 + 0; + +# f t +check $test, "0", C, 12 + 0; +check $test, "fe", C, 12 + 1; +check $test, "fs", C, 12 + 2; +check $test, "ft", C, 12 + 3; +check $test, "f ", C, 12 + 4; +check $test, "2f ", C, 12 + 17; +check $test, "5f,", C, 12 + 17; +check $test, "2f,", C, 12 + 32; +check $test, "tw", C, 12 + 33; +check $test, "t ", C, 12 + 40; +check $test, "t ", C, 12 + 40; +check $test, "t ", C, 12 + 40; +check $test, "2t ", C, 12 + 41; +check $test, "2t ", C, 12 + 46; +check $test, "3t ", C, 12 + 48; +check $test, "t!", C, 12 + 60; +check $test, "t ", C, 12 + 61; +check $test, "t.", C, 12 + 62; +check $test, "t.", C, 12 + 62; +check $test, "5t.", C, 12 + 62; +check $test, "\$", C, 12 + 64; + +$test->add_input_sequence("\n/quit\n"); + +$tester->run; diff --git a/vim-mode/vim_mode.pl b/vim-mode/vim_mode.pl index 428156c..7c5f957 100644 --- a/vim-mode/vim_mode.pl +++ b/vim-mode/vim_mode.pl @@ -1,238 +1,584 @@ -# A script to emulate some of the vi(m) features for the Irssi inputline. -# -# It should work fine with at least 0.8.12 and later versions. However some -# features are disabled in older versions (see below for details). Perl >= -# 5.8.1 is recommended for UTF-8 support (which can be disabled if necessary). -# Please report bugs in older versions as well, we'll try to fix them. -# -# Any behavior different from Vim (unless explicitly documented) should be -# considered a bug and reported. -# -# NOTE: This script is still under heavy development, and there may be bugs. -# Please submit reproducible sequences to the bug-tracker at: -# http://github.com/shabble/irssi-scripts/issues -# -# or contact rudi_s or shabble on irc.freenode.net (#irssi and #irssi_vim) -# -# -# Features: -# -# It supports most commonly used command mode features: -# -# * Insert/Command mode. Escape and Ctrl-C enter command mode. -# /set vim_mode_cmd_seq j allows to use jj as Escape (any other character -# can be used as well). -# * Cursor motion: h l 0 ^ $ <Space> <BS> f t F T -# * History motion: j k gg G -# gg moves to the oldest (first) history line. -# G without a count moves to the current input line, with a count it goes to -# the count-th history line (1 is the oldest). -# * Cursor word motion: w b ge e W gE B E -# * Word objects (only the following work yet): aw aW -# * Yank and paste: y p P -# * Change and delete: c d -# * Delete at cursor: x X -# * Replace at cursor: r -# * Insert mode: i a I A -# * Switch case: ~ -# * Repeat change: . -# * Repeat ftFT: ; , -# * Registers: "a-"z "" "0 "* "+ "_ (black hole) -# Appending to register with "A-"Z -# "" is the default yank/delete register. -# "0 contains the last yank (if no register was specified). -# The special registers "* "+ contain both irssi's cut-buffer. -# * Line-wise shortcuts: dd cc yy -# * Shortcuts: s S C D -# * Scroll the scrollback buffer: Ctrl-E Ctrl-D Ctrl-Y Ctrl-U Ctrl-F Ctrl-B -# * Switch to last active window: Ctrl-6/Ctrl-^ -# * Switch split windows: Ctrl-W j Ctrl-W k -# * Undo/Redo: u Ctrl-R -# -# Counts and combinations work as well, e.g. d5fx or 3iabc<esc>. Counts also -# work with mapped ex-commands (see below), e.g. if you map gb to do :bn, then -# 2gb will switch to the second next buffer. -# Repeat also supports counts. -# -# The following insert mode mappings are supported: -# -# * Insert register content: Ctrl-R x (where x is the register to insert) -# -# Ex-mode supports (activated by : in command mode) the following commands: -# -# * Switching buffers: :[N]b [N] - switch to channel number -# :b# - switch to last channel -# :b <partial-channel-name> -# :b <partial-server>/<partial-channel> -# :buffer {args} (same as :b) -# :[N]bn[ext] [N] - switch to next window -# :[N]bp[rev] [N] - switch to previous window -# * Close window: :[N]bd[elete] [N] -# * Display windows: :ls :buffers -# * Display registers: :reg[isters] {args} :di[splay] {args} -# * Display undolist: :undol[ist] (mostly used for debugging) -# * Source files :so[urce] - only sources vim_moderc at the moment, -# {file} not supported -# * Mappings: :map - display custom mappings -# :map {lhs} - display mappings starting with {lhs} -# :map {lhs} {rhs} - add mapping -# :unm[ap] {lhs} - remove custom mapping -# * Save mappings: :mkv[imrc][!] - like in Vim, but [file] not supported -# * Substitute: :s/// - i and g are supported as flags, only /// can be -# used as separator, uses Perl regex instead of -# Vim regex -# * Settings: :se[t] - display all options -# :se[t] {option} - display all matching options -# :se[t] {option} {value} - change option to value -# -# -# Mappings: -# -# {lhs} is the key combination to be mapped, {rhs} the target. The <> notation -# is used (e.g. <C-F> is Ctrl-F), case is ignored. Supported <> keys: -# <C-A>-<C-Z>, <C-^>, <C-6>, <Space>, <CR>, <BS>, <Nop>. Mapping ex-mode and -# irssi commands is supported. When mapping ex-mode commands the trailing <Cr> -# is not necessary. Only default mappings can be used in {rhs}. -# Examples: -# :map w W - to remap w to work like W -# :map gb :bnext - to map gb to call :bnext -# :map gB :bprev -# :map g1 :b 1 - to map g1 to switch to buffer 1 -# :map gb :b - to map gb to :b, 1gb switches to buffer 1, 5gb to 5 -# :map <C-L> /clear - map Ctrl-L to irssi command /clear -# :map <C-G> /window goto 1 -# :map <C-E> <Nop> - disable <C-E>, it does nothing now -# :unmap <C-E> - restore default behavior of <C-E> after disabling it -# -# Note that you must use / for irssi commands (like /clear), the current value -# of cmdchars does _not_ work! This is necessary do differentiate between -# ex-commands and irssi commands. -# -# -# Settings: -# -# The settings are stored as irssi settings and can be set using /set as usual -# (prepend vim_mode_ to setting name) or using the :set ex-command. The -# following settings are available: -# -# * utf8: support UTF-8 characters, boolean, default on -# * debug: enable debug output, boolean, default off -# * cmd_seq: char that when double-pressed simulates <esc>, string, default '' -# * start_cmd: start every line in command mode, boolean, default off -# -# In contrast to irssi's settings, :set accepts 0 and 1 as values for boolean -# settings, but only vim_mode's settings can be set/displayed. -# Examples: -# :set cmd_seq=j # set cmd_seq to j -# :set cmd_seq= # disable cmd_seq -# :set debug=on # enable debug -# :set debug=off # disable debug -# -# -# The following statusbar items are available: -# -# * vim_mode: displays current mode -# * vim_windows: displays windows selected with :b -# -# -# Configuration -# -# Additionally to the irssi settings described above vim_mode can be -# configured through an external configuration file named "vim_moderc" located -# in ~/.irssi/vim_moderc. If available it's loaded on startup and every -# supported ex-command is run. It's syntax is similar to "vimrc". To (re)load -# it while vim_mode is running use :so[urce]. -# -# Supported ex-commands: -# -# * :map -# -# -# Installation: -# -# As always copy the script into .irssi/scripts and load it with -# /script load # vim_mode.pl -# -# Use the following command to get a statusbar item that shows which mode -# you're in. -# -# /statusbar window add vim_mode -# -# And the following to let :b name display a list of matching channels -# -# /statusbar window add vim_windows -# -# -# Dependencies: -# -# For proper :ex mode support, requires the installation of uberprompt.pl -# Uberprompt can be downloaded from: -# -# http://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl -# -# and follow the instructions at the top of that file for installation. -# -# If you don't need Ex-mode, you can run vim_mode.pl without the -# uberprompt.pl script, but it is recommended. -# -# -# Irssi requirements: -# -# 0.8.12 and above should work fine. However the following features are -# disabled in irssi < 0.8.13: -# -# * j k (only with count, they work fine without count in older versions) -# * gg G -# -# -# Known bugs: -# -# * count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does -# * mapping an incomplete ex-command doesn't open the ex-mode with the partial -# command (e.g. :map gb :b causes an error instead of opening the ex-mode -# and displaying :b<cursor>) -# * undo/redo positions are mostly wrong -# -# -# TODO: -# * History: -# * /,?,n,N to search through history (like history_search.pl) -# * Window switching (:b) -# * Tab completion of window(-item) names -# * non-sequential matches(?) -# -# WONTFIX - things we're not ever likely to do -# * Macros -# -# THANKS: -# -# * estragib: a lot of testing and many bug reports and feature requests -# * iaj: testing -# -# LICENCE: -# -# Copyright (c) 2010 Tom Feist & Simon Ruderich -# -# 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. -# -# -# Have fun! +=pod + +=head1 NAME + +vim_mode.pl + +=head1 DESCRIPTION + +An Irssi script to emulate some of the vi(m) features for the Irssi inputline. + +=head1 INSTALLATION + +Copy into your F<~/.irssi/scripts/> directory and load with +C</SCRIPT LOAD vim_mode.pl>. You may wish to have it autoload in one of the +L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>. + +=head2 DEPENDENCIES + +For proper :ex mode support, vim-mode requires the installation of 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 for installation. + +If you don't need Ex-mode, you can run vim_mode.pl without the +uberprompt.pl script, but it is strongly recommended that you use it. + +=head3 Irssi requirements + +0.8.12 and above should work fine. However the following features are +disabled in irssi < 0.8.13: + +=over 4 + +=item * C<j> C<k> (only with count, they work fine without count in older versions) + +=item * C<gg>, C<G> + +=back + +It is intended to work with at Irssi 0.8.12 and later versions. However some +features are disabled in older versions (see below for details). + +Perl >= 5.8.1 is recommended for UTF-8 support (which can be disabled if +necessary). Please report bugs in older versions as well, we'll try to fix +them. Later versions of Perl are also faster, which is probably beneficial +to a script of this size and complexity. + +=head2 SETUP + +Vim Mode provides certain feedback to the user, such as the currently active +mode (command, insert, ex), and if switching buffers, which buffer(s) currently +match the search terms. + +There are two ways to go about displaying this information, as described in +the following sections: + +=head3 Statusbar Items + +Run the following command to add a statusbar item that shows which mode +you're in. + +C</statusbar window add vim_mode> + +And the following to let C<:b [str]> display a list of channels matching your +search string. + +C</statusbar window add vim_windows> + +B<Note:> Remember to C</save> after adding these statusbar items to make them +permanent. + +B<Note:> If you would rather have these statusbar items on your prompt +line rather than thte window statusbar, please follow these steps. + +For technical reasons, I<uberprompt> must occasionally call C</statusbar prompt +reset>, which will remove or deactivate any manually added items on the prompt +statusbar. To get around this, uberprompt provides two command hooks, +C<uberprompt_load_hook> and C<uberprompt_unload_hook>. Both of these settings +can contain one (or more, using C</EVAL>) commands to be executed when the prompt +is enabled and disabled, respectively. + +See the L<uberprompt documentation|https://github.com/shabble/irssi-scripts/blob/master/prompt_info/README.pod> for further details. + +For I<right-aligned> items (that is, after the input field: + +=over 4 + +=item 1 C</alias vm_add /^statusbar prompt add -after input -alignment right vim_mode> + +=item 2 C</alias vm_del /^statusbar prompt remove vim_mode> + +=item 3 C</set uberprompt_load_hook /^vm_add> + +=item 4 C</set uberprompt_unload_hook /^vm_del> + +=back + +For I<left-aligned> items (before the prompt): + +=over 4 + +=item 1 C</alias vm_add /^statusbar prompt add -before prompt -alignment left vim_mode> + +=item 2 C</alias vm_del /^statusbar prompt remove vim_mode> + +=item 3 C</set uberprompt_load_hook /^vm_add> + +=item 4 C</set uberprompt_unload_hook /^vm_del> + +=back + +If you wish to add both C<vim_mode> and C<vim_windows> items, replace steps 1 and 2 +above with the following (right-aligned): + +=over 4 + +=item 1 C</alias vm_add /^eval /^statusbar prompt add -after input -alightment right vim_mode ; /^statusbar prompt add -after input -alignment right vim_windows> + +=item 2 C</alias vm_del /^eval /^statusbar prompt remove vim_mode ; /^statusbar prompt remove vim_windows> + +=back + +and then complete stages 3 and 4 as above. Replace the C<-after> and C<-alignment> +to suit your location preferences. + +B<Note:> It is also possible to place the items between the prompt and input field, +by specifying C<-after prompt> or C<-before input> as appropriate. + +=head3 Expando Variables + +Vim mode augments the existing Irssi expando (automatic variables) with two +additional ones: C<$vim_cmd_mode> and C<$vim_wins>. + +C<$vim_cmd_mode> is the equivalent of the C<vim_mode> statusbar item, and +C<$vim_wins> is the counterpart of C<vim_windows>. + +They can be added to your theme, or inserted into your uberprompt using +a command such as: + +"C</set uberprompt_format [$vim_cmd_mode] $*$uber] >" + +=head3 FILE-BASED CONFIGURATION + +Additionally to the irssi settings described in L<settings|/SETTINGS>, vim_mode +can be configured through an external configuration file named "vim_moderc" +located in F<~/.irssi/vim_moderc>. If available it's loaded on startup and every +supported ex-command is run. Its syntax is similar to "vimrc". To (re)load it +while vim_mode is running use C<:so[urce]>. + +Currently Supported ex-commands: + +=over 4 + +=item * C<:map> + +=back + +=head1 USAGE + +The following section is divided into the different modes as supported by Vim itself: + +=head2 COMMAND MODE + +It supports most commonly used command mode features: + +=over 2 + +=item * Insert/Command mode. + +C<Esc> and C<Ctrl-C> enter command mode. C</set vim_mode_cmd_seq j> allows +to use C<jj> as Escape (any other character can be used as well). + +=item * Cursor motion: + +C<h l 0 ^ $ E<lt>SpaceE<gt> E<lt>BSE<gt> f t F T> + +=item * History motion: + +C<j k gg G> C<gg> moves to the oldest (first) history line. C<G> without a +count moves to the current input line, with a count it goes to the I<count-th> +history line (1 is the oldest). + +=item * Cursor word motion: + +C<w b ge e W gE B E> + +=item * Word objects (only the following work yet): + +C<aw aW> + +=item * Yank and paste: + + C<y p P> + +=item * Change and delete: + +C<c d> + +=item * Delete at cursor: + +C<x X> + +=item * Replace at cursor: + +C<r> + +=item * Insert mode: + +C<i a I A> + +=item * Switch case: + +C<~> + +=item * Repeat change: + +C<.> + +=item * Repeat + +C<ftFT: ; ,> + +=item * Registers: + +C<"a-"z "" "0 "* "+ "_> (black hole) + +=item * Line-wise shortcuts: + +C<dd cc yy> + +=item * Shortcuts: + +C<s S C D> + +=item * Scroll the scrollback buffer: + +C<Ctrl-E Ctrl-D Ctrl-Y Ctrl-U Ctrl-F Ctrl-B> + +=item * Switch to last active window: + +C<Ctrl-6/Ctrl-^> + +=item * Switch split windows: + +<Ctrl-W j Ctrl-W k> + +=item * Undo/Redo: + +C<u Ctrl-R> + +=back + +Counts and combinations work as well, e.g. C<d5fx> or C<3iabcE<lt>escE<gt>>. Counts also work with mapped ex-commands (see below), e.g. if you map C<gb> to do C<:bn>, then C<2gb> will switch to the second next buffer. Repeat also supports counts. + +=head3 REGISTERS + +=over 4 + +=item * Appending to register with C<"A-"Z> + +=item * C<""> is the default yank/delete register. + +=item * C<"0> contains the last yank (if no register was specified). + +=item * The special registers C<"* "+> both contain irssi's internal cut-buffer. + +=back + +=head2 INSERT MODE + +The following insert mode mappings are supported: + +=over 4 + +=item * Insert register content: Ctrl-R x (where x is the register to insert) + +=back + +=head2 EX-MODE + +Ex-mode (activated by C<:> in command mode) supports the following commands: + +=over 4 + +=item * Command History: + +C<E<lt>uparrowE<gt>> - cycle backwards through history + +C<E<lt>downarrowE<gt>> - cycle forwards through history + +C<:eh> - show ex history + +=item * Switching buffers: + +C<:[N]b [N]> - switch to channel number + +C<:b#> - switch to last channel + +C<:b> E<lt>partial-channel-nameE<gt> + +C<:b> <partial-server>/<partial-channel> + +C<:buffer {args}> (same as C<:b>) + +C<:[N]bn[ext] [N]> - switch to next window + +C<:[N]bp[rev] [N]> - switch to previous window + +=item * Close window: + +C<:[N]bd[elete] [N]> + +=item * Display windows: + +C<:ls>, C<:buffers> + +=item * Display registers: + +C<:reg[isters] {args}>, C<:di[splay] {args}> + +=item * Display undolist: + +C<:undol[ist]> (mostly used for debugging) + +=item * Source files: + +C<:so[urce]> - only sources vim_moderc at the moment, + F<{file}> not supported + +=item * Mappings: + +C<:map> - display custom mappings + +=item * Saving mappings: + +C<:mkv[imrc][!]> - like in Vim, but [file] not supported + +=item * Substitution: + +C<:s///> - I<i> and I<g> are supported as flags, only C<///> can be used as +eparator, and it uses Perl regex syntax instead of Vim syntax. + +=item * Settings: + +C<:se[t]> - display all options + +C<:se[t] {option}> - display all matching options + +C<:se[t] {option} {value}> - change option to value + +=back + +=head3 MAPPINGS + +=head4 Commands + +=over 4 + +=item * C<:map {lhs}> - display mappings starting with {lhs} + +=item * C<:map {lhs} {rhs}> - add mapping + +=item * C<:unm[ap] {lhs}> - remove custom mapping + +=back + +=head4 Parameters + +I<{lhs}> is the key combination to be mapped, I<{rhs}> the target. The +C<E<lt>E<gt>> notation is used + +(e.g. C<E<lt>C-FE<gt>> is I<Ctrl-F>), case is ignored. + Supported C<E<lt>E<gt>> keys are: + +=over 4 + +=item * C<E<lt>C-AE<gt>> - C<E<lt>C-ZE<gt>>, + +=item * C<E<lt>C-^E<gt>>, C<E<lt>C-6E<gt>> + +=item * C<E<lt>SpaceE<gt>> + +=item * C<E<lt>CRE<gt>> - Enter + +=item * C<E<lt>BSE<gt>> - Backspace + +=item * C<E<lt>NopE<gt>> - No-op (Do Nothing). + +=back + +Mapping ex-mode and irssi commands is supported. When mapping ex-mode commands +the trailing C<E<lt>CrE<gt>> is not necessary. Only default mappings can be used +in I<{rhs}>. + +Examples: + +=over 4 + +=item * C<:map w W> - to remap w to work like W + +=item * C<:map gb :bnext> - to map gb to call :bnext + +=item * C<:map gB :bprev> + +=item * C<:map g1 :b 1> - to map g1 to switch to buffer 1 + +=item * C<:map gb :b> - to map gb to :b, 1gb switches to buffer 1, 5gb to 5 + +=item * C<:map E<lt>C-LE<gt> /clear> - map Ctrl-L to irssi command /clear + +=item * C<:map E<lt>C-GE<gt> /window goto 1> + +=item * C<:map E<lt>C-EE<gt> <Nop>> - disable E<lt>C-EE<gt>, it does nothing now + +=item * C<:unmap E<lt>C-EE<gt>> - restore default behavior of C<E<lt>C-EE<gt>> +after disabling it + +=back + +Note that you must use C</> for irssi commands (like C</clear>), the current value +of Irssi's cmdchars does B<not> work! This is necessary do differentiate between +ex-commands and irssi commands. + +=head2 SETTINGS + +The settings are stored as irssi settings and can be set using C</set> as usual +(prepend C<vim_mode_> to setting name) or using the C<:set> ex-command. The +following settings are available: + +=over 4 + +=item * utf8 - Support UTF-8 characters, boolean, default on + +=item * debug - Enable debug output, boolean, default off + +=item * cmd_seq - Char that when double-pressed simulates C<E<lt>EscE<gt>>, string, default '' (disabled) + +=item * start_cmd - Start every line in command mode, boolean, default off + +=item * max_undo_lines - Sze of the undo buffer. Integer, default 50 items. + +=item * ex_history_size - Number of items stored in the ex-mode history. Integer, default 100. + +=item * prompt_leading_space - Ddetermines whether ex mode prepends a space to the displayed input. Boolean, default on + +=back + +In contrast to irssi's settings, C<:set> accepts 0 and 1 as values for boolean +settings, but only vim_mode's settings can be set/displayed. + +Examples: + + :set cmd_seq=j # set cmd_seq to j + :set cmd_seq= # disable cmd_seq + :set debug=on # enable debug + :set debug=off # disable debug + +=head1 SUPPORT + +Any behavior different from Vim (unless explicitly documented) should be +considered a bug and reported. + +B<NOTE:> This script is still under heavy development, and there may be bugs. +Please submit reproducible sequences to the bug-tracker at: +L<http://github.com/shabble/irssi-scripts/issues/new> + +or contact rudi_s or shabble on irc.freenode.net (#irssi and #irssi_vim) + +=head1 AUTHORS + +Copyright E<copy> 2010-2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> and + +Copyright E<copy> 2010-2011 Simon Ruderich C<E<lt>simon@ruderich.orgE<gt>> + +=head1 THANKS + +Particular thanks go to + +=over 4 + +=item * estragib: a lot of testing and many bug reports and feature requests + +=item * iaj: testing + +=item * tmr: explaining how various bits of vim work + +=back + +as well as the rest of C<#irssi> and C<#irssi_vim> on Freenode IRC. + +=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 * + +count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does + +=item * + +mapping an incomplete ex-command doesn't open the ex-mode with the partial +command (e.g. C<:map gb :b> causes an error instead of opening the ex-mode and +displaying C<:bE<lt>cursorE<gt>>) + +=item * + + undo/redo cursor positions are mostly wrong + +=back + +=head1 TODO + +=over 4 + +=item * + +History: + +=over 4 + +=item * + + C< * /,?,n,N> to search through history (like rl_history_search.pl) + +=back + +=item * + +Window switching (C<:b>) + +=over 4 + +=item * + +Tab completion of window(-item) names + +=item * + +non-sequential matches(?) + +=back + +=back + +See also the TODO file at +L<github|https://github.com/shabble/irssi-scripts/blob/master/vim-mode/TODO> for +many many more things. + +=head2 WONTFIX + +Things we're not ever likely to do: + +=over 4 + +=item * Macros + +=back + +=cut use strict; use warnings; @@ -245,9 +591,9 @@ use Irssi::TextUI; # for sbar_items_redraw use Irssi::Irc; # necessary for 0.8.14 -use vars qw($VERSION %IRSSI); -$VERSION = "1.0.1"; -%IRSSI = + +our $VERSION = "1.0.2"; +our %IRSSI = ( authors => "Tom Feist (shabble), Simon Ruderich (rudi_s)", contact => 'shabble+irssi@metavore.org, ' @@ -267,24 +613,24 @@ sub M_CMD () { 1 } # insert mode sub M_INS () { 0 } # extended mode (after a :?) -sub M_EX () { 2 } +sub M_EX () { 2 } # operator command -sub C_OPERATOR () { 0 } +sub C_OPERATOR () { 0 } # normal command, no special handling necessary -sub C_NORMAL () { 1 } +sub C_NORMAL () { 1 } # command taking another key as argument -sub C_NEEDSKEY () { 2 } +sub C_NEEDSKEY () { 2 } # text-object command (i a) sub C_TEXTOBJECT () { 3 } # commands entering insert mode -sub C_INSERT () { 4 } +sub C_INSERT () { 4 } # ex-mode commands -sub C_EX () { 5 } +sub C_EX () { 5 } # irssi commands -sub C_IRSSI () { 6 } +sub C_IRSSI () { 6 } # does nothing -sub C_NOP () { 7 } +sub C_NOP () { 7 } # setting types, match irssi types as they are stored as irssi settings sub S_BOOL () { 0 } @@ -316,14 +662,14 @@ my $commands "\x7F" => { char => '<BS>', func => \&cmd_h, type => C_NORMAL }, ' ' => { char => '<Space>', func => \&cmd_l, type => C_NORMAL }, # history movement - j => { char => 'j', func => \&cmd_j, type => C_NORMAL, - no_operator => 1 }, - k => { char => 'k', func => \&cmd_k, type => C_NORMAL, - no_operator => 1 }, - gg => { char => 'gg', func => \&cmd_gg, type => C_NORMAL, - no_operator => 1 }, - G => { char => 'G', func => \&cmd_G, type => C_NORMAL, - needs_count => 1, no_operator => 1 }, + j => { char => 'j', func => \&cmd_j, type => C_NORMAL, + no_operator => 1 }, + k => { char => 'k', func => \&cmd_k, type => C_NORMAL, + no_operator => 1 }, + gg => { char => 'gg', func => \&cmd_gg, type => C_NORMAL, + no_operator => 1 }, + G => { char => 'G', func => \&cmd_G, type => C_NORMAL, + needs_count => 1, no_operator => 1 }, # char movement, take an additional parameter and use $movement f => { char => 'f', func => \&cmd_f, type => C_NEEDSKEY, selection_needs_move_right => 1 }, @@ -346,11 +692,11 @@ my $commands gE => { char => 'gE', func => \&cmd_gE, type => C_NORMAL, selection_needs_move_right => 1 }, # text-objects, leading _ means can't be mapped! - _i => { char => 'i', func => \&cmd__i, type => C_TEXTOBJECT }, - _a => { char => 'a', func => \&cmd__a, type => C_TEXTOBJECT }, + _i => { char => 'i', func => \&cmd__i, type => C_TEXTOBJECT }, + _a => { char => 'a', func => \&cmd__a, type => C_TEXTOBJECT }, # line movement - '0' => { char => '0', func => \&cmd_0, type => C_NORMAL }, - '^' => { char => '^', func => \&cmd_caret, type => C_NORMAL }, + '0' => { char => '0', func => \&cmd_0, type => C_NORMAL }, + '^' => { char => '^', func => \&cmd_caret, type => C_NORMAL }, '$' => { char => '$', func => \&cmd_dollar, type => C_NORMAL }, # delete chars x => { char => 'x', func => \&cmd_x, type => C_NORMAL, @@ -424,6 +770,16 @@ my $commands # All available commands in Ex-Mode. my $commands_ex = { + # arrow keys - not actually used, see handle_input_buffer() + # TODO: make these work. + "\e[A" => { char => ':exprev', func => \&ex_history_back, + type => C_EX }, + "\e[B" => { char => ':exnext', func => \&ex_history_fwd, + type => C_EX }, + + # normal Ex mode commands. + eh => { char => ':exhist', func => \&ex_history_show, + type => C_EX }, s => { char => ':s', func => \&ex_substitute, type => C_EX }, bnext => { char => ':bnext', func => \&ex_bnext, @@ -500,11 +856,16 @@ my $commands_ex # default command mode mappings my $maps = {}; -# Add all default mappings. -foreach my $char (keys %$commands) { - next if $char =~ /^_/; # skip private commands (text-objects for now) - add_map($char, $commands->{$char}); -} +# current imap still pending (first character entered) +my $imap = undef; + +# maps for insert mode +my $imaps + = { + # CTRL-R, insert register + "\x12" => { map => undef, func => \&insert_ctrl_r }, + }; + # GLOBAL VARIABLES @@ -521,8 +882,12 @@ my $settings start_cmd => { type => S_BOOL, value => 0 }, # not used yet max_undo_lines => { type => S_INT, value => 50 }, + # size of history buffer for Ex mode. + ex_history_size => { type => S_INT, value => 100 }, # prompt_leading_space prompt_leading_space => { type => S_BOOL, value => 1 }, + # <Leader> value for prepending to commands. + map_leader => { type => S_STR, value => '\\' }, }; sub DEBUG { $settings->{debug}->{value} } @@ -542,6 +907,10 @@ my $should_ignore = 0; # ex mode buffer my @ex_buf; +# ex mode history storage. +my @ex_history; +my $ex_history_index = 0; + # we are waiting for another mapped key (e.g. g pressed, but there are # multiple mappings like gg gE etc.) my $pending_map = undef; @@ -583,19 +952,6 @@ my $registers '*' => '', # same '_' => '', # black hole register, always empty }; -foreach my $char ('a' .. 'z') { - $registers->{$char} = ''; -} - -# current imap still pending (first character entered) -my $imap = undef; - -# maps for insert mode -my $imaps - = { - # CTRL-R, insert register - "\x12" => { map => undef, func => \&insert_ctrl_r }, - }; # index into the history list (for j,k) my $history_index = undef; @@ -616,24 +972,20 @@ my $completion_active = 0; my $completion_string = ''; sub script_is_loaded { - my $name = shift; - print "Checking if $name is loaded" if DEBUG; - no strict 'refs'; - my $retval = defined %{ "Irssi::Script::${name}::" }; - use strict 'refs'; - - return $retval; + return exists($Irssi::Script::{shift(@_) . '::'}); } -vim_mode_init(); + # INSERT MODE COMMANDS sub insert_ctrl_r { my ($key) = @_; - + _debug("ctrl-r called"); my $char = chr($key); + _debug("ctrl-r called with $char"); + return if not defined $registers->{$char} or $registers->{$char} eq ''; my $pos = _insert_at_position($registers->{$char}, 1, _input_pos()); @@ -1641,6 +1993,10 @@ sub cmd_ex_command { return _warn("Ex-mode $1$2 doesn't exist!"); } + # add this item to the ex mode history + ex_history_add($arg_str); + $ex_history_index = 0; # and reset the history position. + my $count = $1; if ($count eq '') { $count = undef; @@ -2025,6 +2381,7 @@ sub _parse_mapping { my ($string) = @_; $string =~ s/<([^>]+)>/_parse_mapping_bracket($1)/ge; + _warn("Parse mapping: $string"); if (index($string, '<invalid>') != -1) { return undef; } @@ -2050,6 +2407,8 @@ sub _parse_mapping_bracket { # <BS> } elsif ($string eq 'bs') { $string = chr(127); + } elsif ($string eq 'leader') { + $string = $settings->{map_leader}->{value}; # Invalid char, return special string to recognize the error. } else { $string = '<invalid>'; @@ -2059,6 +2418,14 @@ sub _parse_mapping_bracket { sub _parse_mapping_reverse { my ($string) = @_; + if (not defined $string) { + _warn("Unable to reverse-map command: " . join('', @ex_buf)); + return; + } + + my $escaped_leader = quotemeta($settings->{map_leader}->{value}); + $string =~ s/$escaped_leader/<Leader>/g; + # Convert char to <char-name>. $string =~ s/ /<Space>/g; $string =~ s/\n/<CR>/g; @@ -2073,6 +2440,9 @@ sub _parse_mapping_reverse { sub _parse_partial_command_reverse { my ($string) = @_; + my $escaped_leader = quotemeta($settings->{map_leader}->{value}); + $string =~ s/$escaped_leader/<Leader>/g; + # Convert Ctrl-X to ^X. $string =~ s/([\x01-\x1A])/"^" . chr(ord($1) + 64)/ge; # Convert Ctrl-6 and Ctrl-^ to <C-^>. @@ -2230,9 +2600,9 @@ sub _matching_windows { # STATUS ITEMS -# vi mode status item. -sub vim_mode_cb { - my ($sb_item, $get_size_only) = @_; +#TODO: give these things better names. +sub vim_mode_cmd { + my $mode_str = ''; if ($mode == M_INS) { $mode_str = 'Insert'; @@ -2241,7 +2611,7 @@ sub vim_mode_cb { } else { $mode_str = '%_Command%_'; if ($register ne '"' or $numeric_prefix or $operator or $movement or - $pending_map) { + $pending_map) { my $partial = ''; if ($register ne '"') { $partial .= '"' . $register; @@ -2263,13 +2633,10 @@ sub vim_mode_cb { $mode_str .= " ($partial)"; } } - $sb_item->default_handler($get_size_only, "{sb $mode_str}", '', 0); + return $mode_str; } -# :b window list item. -sub b_windows_cb { - my ($sb_item, $get_size_only) = @_; - +sub vim_wins_data { my $windows = ''; # A little code duplication of cmd_ex_command(), but \s+ instead of \s* so @@ -2289,6 +2656,31 @@ sub b_windows_cb { } } } + return $windows; +} + +sub vim_exp_mode { + my ($server, $witem, $arg) = @_; + return vim_mode_cmd(); +} + +sub vim_exp_wins { + my ($server, $witem, $arg) = @_; + return vim_wins_data(); +} + +# vi mode status item. +sub vim_mode_cb { + my ($sb_item, $get_size_only) = @_; + my $mode_str = vim_mode_cmd(); + $sb_item->default_handler($get_size_only, "{sb $mode_str}", '', 0); +} + +# :b window list item. +sub b_windows_cb { + my ($sb_item, $get_size_only) = @_; + + my $windows = vim_wins_data(); $sb_item->default_handler($get_size_only, "{sb $windows}", '', 0); } @@ -2312,8 +2704,10 @@ sub got_key { $input_buf_timer = Irssi::timeout_add_once(10, \&handle_input_buffer, undef); print "Buffer Timer tag: $input_buf_timer" if DEBUG; + } elsif ($mode == M_INS) { - if ($key == 3) { # Ctrl-C enter command mode + + if ($key == 3) { # Ctrl-C enters command mode _update_mode(M_CMD); _stop(); return; @@ -2373,6 +2767,13 @@ sub got_key { Irssi::statusbar_items_redraw("vim_mode"); } elsif ($mode == M_EX) { + + if ($key == 3) { # C-c cancels Ex mdoe as well. + _update_mode(M_CMD); + _stop(); + return; + } + handle_command_ex($key); } } @@ -2392,20 +2793,33 @@ sub handle_input_buffer { _update_mode(M_CMD); } else { - # we need to identify what we got, and either replay it - # or pass it off to the command handler. - # if ($mode == M_CMD) { - # # command - # my $key_str = join '', map { chr } @input_buf; - # if ($key_str =~ m/^\e\[([ABCD])/) { - # print "Arrow key: $1" if DEBUG; - # } else { - # print "Dunno what that is." if DEBUG; - # } - # } else { - # _emulate_keystrokes(@input_buf); - # } - _emulate_keystrokes(@input_buf); + # we have more than a single esc, implying an escape sequence + # (meta-* or esc-*) + + # currently, we only extract escape sequences if: + # a) we're in ex mode + # b) they're arrow keys (for history control) + + if ($mode == M_EX) { + # ex mode + my $key_str = join '', map { chr } @input_buf; + if ($key_str =~ m/^\e\[([ABCD])/) { + my $arrow = $1; + _debug( "Arrow key: $arrow"); + if ($arrow eq 'A') { # up + ex_history_back(); + } elsif ($arrow eq 'B') { # down + ex_history_fwd(); + } else { + $arrow =~ s/C/right/; + $arrow =~ s/D/left/; + _debug("Arrow key $arrow not supported"); + } + } + } else { + # otherwise, we just forward them to irssi. + _emulate_keystrokes(@input_buf); + } # Clear insert buffer, pressing "special" keys (like arrow keys) # resets it. @@ -2417,7 +2831,7 @@ sub handle_input_buffer { } sub flush_input_buffer { - Irssi::timeout_remove($input_buf_timer); + Irssi::timeout_remove($input_buf_timer) if defined $input_buf_timer; $input_buf_timer = undef; # see what we've collected. print "Input buffer flushed" if DEBUG; @@ -2540,13 +2954,7 @@ sub handle_command_cmd { } elsif ($cmd->{type} == C_IRSSI) { print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG; - # TODO: fix me more better (general server/win/none context?) - my $server = Irssi::active_server; - if (defined $server) { - $server->command($cmd->{func}); - } else { - Irssi::command($cmd->{func}); - } + _command_with_context($cmd->{func}); $numeric_prefix = undef; return 1; # call _stop(); @@ -2749,7 +3157,7 @@ sub handle_command_ex { # BS key (8) or DEL key (127) - remove last character. if ($key == 8 || $key == 127) { print "Delete" if DEBUG; - if (scalar @ex_buf > 0) { + if (@ex_buf > 0) { pop @ex_buf; _set_prompt(':' . join '', @ex_buf); # Backspacing over : exits ex-mode. @@ -2767,14 +3175,21 @@ sub handle_command_ex { print "Tab pressed" if DEBUG; print "Ex buf contains: " . join('', @ex_buf) if DEBUG; @tab_candidates = _tab_complete(join('', @ex_buf), [keys %$commands_ex]); - + _debug("Candidates: " . join(", ", @tab_candidates)); + if (@tab_candidates == 1) { + @ex_buf = ( split('', $tab_candidates[0]), ' '); + _set_prompt(':' . join '', @ex_buf); + } # Ignore control characters for now. - } elsif ($key < 32) { + } elsif ($key > 0 && $key < 32) { # TODO: use them later, e.g. completion # Append entered key } else { - push @ex_buf, chr $key; + if ($key != -1) { + # check we're not called from an ex_history_* function + push @ex_buf, chr $key; + } _set_prompt(':' . join '', @ex_buf); } @@ -2797,15 +3212,32 @@ sub _tab_complete { sub vim_mode_init { Irssi::signal_add_first 'gui key pressed' => \&got_key; - Irssi::signal_add 'setup changed' => \&setup_changed; - Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb'); + Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb'); Irssi::statusbar_item_register ('vim_windows', 0, 'b_windows_cb'); + Irssi::expando_create('vim_cmd_mode' => \&vim_exp_mode, {}); + Irssi::expando_create('vim_wins' => \&vim_exp_wins, {}); + + # Register all available settings. foreach my $name (keys %$settings) { _setting_register($name); } + foreach my $char ('a' .. 'z') { + $registers->{$char} = ''; + } + + setup_changed(); + + Irssi::signal_add 'setup changed' => \&setup_changed; + + # Add all default mappings. + foreach my $char (keys %$commands) { + next if $char =~ /^_/; # skip private commands (text-objects for now) + add_map($char, $commands->{$char}); + } + # Load the vim_moderc file if it exists. ex_source('source'); @@ -2814,6 +3246,8 @@ sub vim_mode_init { if ($settings->{start_cmd}->{value}) { _update_mode(M_CMD); + } else { + _update_mode(M_INS); } } @@ -2998,7 +3432,7 @@ sub delete_map { } push @add, $keys; - # Restore default keybindings in case we :unmaped a <Nop> or a remapped + # Restore default keybindings in case we :unmapped a <Nop> or a remapped # key. foreach my $key (@add) { if (exists $commands->{$key}) { @@ -3132,6 +3566,8 @@ sub _update_mode { } Irssi::statusbar_items_redraw("vim_mode"); + Irssi::statusbar_items_redraw ('uberprompt'); + } sub _set_prompt { @@ -3209,3 +3645,105 @@ sub _warn { print '%_vim_mode: ', $warning, '%_'; } + +sub _debug { + return unless DEBUG; + + my ($format, @args) = @_; + my $str = sprintf($format, @args); + print $str; +} + +sub _command_with_context { + my ($command) = @_; + my $context; + my $window = Irssi::active_win; + if (defined $window) { + my $witem = $window->{active}; + if (defined $witem and ref($witem) eq 'Irssi::Windowitem') { + $context = $witem; + } else { + $context = $window; + } + } else { + my $server = Irssi::active_server; + if (defined $server) { + $context = $server; + } + } + if (defined $context) { + print "Command $command Using context: " . ref($context) if DEBUG; + $context->command($command); + } else { + print "Command $command has no context" if DEBUG; + Irssi::command($command); + } +} + +sub ex_history_add { + my ($line) = @_; + + # check it's not an exact dupe of the previous history line + + my $last_hist = $ex_history[$ex_history_index]; + $last_hist = '' unless defined $last_hist; + + return if $last_hist eq $line; + + _debug("Adding $line to ex command history"); + + # add it to the history + unshift @ex_history, $line; + + if ($settings->{ex_history_size}->{value} < @ex_history) { + pop @ex_history; # junk the last entry if we've hit the max. + } +} + +sub ex_history_fwd { + + my $hist_max = $#ex_history; + $ex_history_index++; + if ($ex_history_index > $hist_max) { + $ex_history_index = 0; + _debug("ex history hit top, wrapping to 0"); + } + + my $line = $ex_history[$ex_history_index]; + $line = '' if not defined $line; + + _debug("Ex history line: $line"); + + @ex_buf = split '', $line; + handle_command_ex(-1); +} + +sub ex_history_back { + my $hist_max = $#ex_history; + $ex_history_index--; + if ($ex_history_index == -1) { + $ex_history_index = $hist_max; + _debug("ex history hit bottom, wrapping to $hist_max"); + + } + + my $line = $ex_history[$ex_history_index]; + $line = '' if not defined $line; + + _debug("Ex history line: $line"); + @ex_buf = split '', $line; + handle_command_ex(-1); + +} + +sub ex_history_show { + my $win = Irssi::active_win(); + $win->print("Ex command history:"); + for my $i (0 .. $#ex_history) { + my $flag = $i == $ex_history_index + ? ' <' + : ''; + $win->print("$i " . $ex_history[$i] . $flag); + } +} +vim_mode_init(); |