diff options
| -rw-r--r-- | docs/allsigs-uniq.txt | 358 | ||||
| -rwxr-xr-x | docs/buildpod.pl | 4 | ||||
| -rw-r--r-- | feature-tests/subscript.pl | 98 | ||||
| -rw-r--r-- | feature-tests/test.sub | 7 | ||||
| -rw-r--r-- | history-search/rl_history_search.pl | 63 | ||||
| -rw-r--r-- | prompt_info/uberprompt.pl | 379 | ||||
| -rw-r--r-- | prompt_info/visual.pl | 191 | ||||
| -rw-r--r-- | scrolled-reminder/scrolled_reminder.pl | 89 | ||||
| -rw-r--r-- | test/irssi/config | 15 | ||||
| -rw-r--r-- | vim-mode/TODO | 42 | ||||
| -rw-r--r-- | vim-mode/vim_mode.pl | 602 | 
11 files changed, 1669 insertions, 179 deletions
| diff --git a/docs/allsigs-uniq.txt b/docs/allsigs-uniq.txt new file mode 100644 index 0000000..4dc83e3 --- /dev/null +++ b/docs/allsigs-uniq.txt @@ -0,0 +1,358 @@ +"away mode changed" +"awaylog show" +"ban type changed" +"beep" +"channel created" +"channel destroyed" +"channel joined" +"channel rejoin new" +"channel sync" +"channel wholist" +"chanquery abort" +"chanquery ban end" +"chanquery ban" +"chanquery mode" +"chanquery who end" +"chat protocol created" +"chat protocol deinit" +"chat protocol destroyed" +"chat protocol unknown" +"chatnet destroyed" +"chatnet read" +"chatnet saved" +"complete command action" +"complete command alias" +"complete command away" +"complete command bind" +"complete command cat" +"complete command connect" +"complete command dcc send" +"complete command disconnect" +"complete command format" +"complete command help" +"complete command load" +"complete command msg" +"complete command query" +"complete command rawlog open" +"complete command rawlog save" +"complete command recode remove" +"complete command reconnect" +"complete command reload" +"complete command save" +"complete command script load" +"complete command script unload" +"complete command server add" +"complete command server remove" +"complete command server" +"complete command set" +"complete command stats" +"complete command toggle" +"complete command topic" +"complete command unalias" +"complete command window goto" +"complete command window item move" +"complete command window server" +"complete erase command action" +"complete erase command msg" +"complete erase command query" +"complete word" +"ctcp action" +"ctcp msg clientinfo" +"ctcp msg dcc accept" +"ctcp msg dcc chat" +"ctcp msg dcc resume" +"ctcp msg dcc send" +"ctcp msg dcc" +"ctcp msg ping" +"ctcp msg time" +"ctcp msg userinfo" +"ctcp msg version" +"ctcp msg" +"ctcp reply dcc reject" +"ctcp reply dcc" +"ctcp reply ping" +"ctcp reply" +"dcc chat message" +"dcc closed" +"dcc connected" +"dcc ctcp action" +"dcc ctcp dcc" +"dcc destroyed" +"dcc error close not found" +"dcc error connect" +"dcc error ctcp" +"dcc error file create" +"dcc error file open" +"dcc error get not found" +"dcc error send exists" +"dcc error send no route" +"dcc error unknown type" +"dcc error write" +"dcc list print" +"dcc rejected" +"dcc reply dcc" +"dcc reply send pasv" +"dcc request send" +"dcc request" +"dcc server started" +"default command server" +"default command" +"default ctcp msg dcc" +"default ctcp msg" +"default ctcp reply dcc" +"default ctcp reply" +"default dcc ctcp" +"default event numeric" +"default event" +"error command" +"event 001" +"event 004" +"event 005" +"event 221" +"event 254" +"event 271" +"event 272" +"event 281" +"event 301" +"event 302" +"event 303" +"event 305" +"event 306" +"event 311" +"event 312" +"event 313" +"event 314" +"event 315" +"event 317" +"event 318" +"event 319" +"event 324" +"event 326" +"event 327" +"event 328" +"event 329" +"event 330" +"event 332" +"event 333" +"event 338" +"event 341" +"event 344" +"event 345" +"event 346" +"event 347" +"event 348" +"event 349" +"event 352" +"event 353" +"event 364" +"event 365" +"event 367" +"event 368" +"event 369" +"event 372" +"event 375" +"event 376" +"event 377" +"event 378" +"event 379" +"event 381" +"event 386" +"event 387" +"event 388" +"event 389" +"event 396" +"event 401" +"event 403" +"event 404" +"event 405" +"event 407" +"event 408" +"event 421" +"event 422" +"event 432" +"event 433" +"event 436" +"event 437" +"event 438" +"event 439" +"event 442" +"event 465" +"event 470" +"event 471" +"event 472" +"event 473" +"event 474" +"event 475" +"event 476" +"event 477" +"event 478" +"event 479" +"event 482" +"event 486" +"event 489" +"event 494" +"event 506" +"event 707" +"event 716" +"event 717" +"event 728" +"event 729" +"event connected" +"event empty" +"event error" +"event invite" +"event join" +"event kick" +"event kill" +"event mode" +"event nick" +"event notice" +"event part" +"event ping" +"event pong" +"event privmsg" +"event quit" +"event silence" +"event topic" +"event wallops" +"exec input" +"expando timer" +"flood" +"gui dialog" +"gui entry redirect" +"gui exit" +"gui key pressed" +"gui page scrolled" +"gui print text finished" +"gui print text" +"gui window create override" +"gui window created" +"ignore changed" +"ignore created" +"ignore destroyed" +"irssi init finished" +"irssi init read settings" +"lag ping error" +"layout reset" +"layout restore item" +"layout restore window" +"layout restore" +"layout save item" +"layout save window" +"layout save" +"list subcommands" +"log config read" +"log config save" +"log create failed" +"log locked" +"log new" +"log written" +"mainwindow destroyed" +"mainwindow moved" +"mainwindow resized" +"message dcc action" +"message dcc ctcp" +"message dcc own" +"message dcc own_action" +"message dcc own_ctcp" +"message dcc" +"message irc mode" +"message irc own_action" +"message join" +"message own_private" +"message own_public" +"message private" +"message public" +"message quit" +"module error" +"module loaded" +"module unloaded" +"netsplit new" +"nickfind event whois" +"nicklist changed" +"nicklist host changed" +"nicklist new" +"nicklist remove" +"notifylist away changed" +"notifylist event whois away" +"notifylist event whois end" +"notifylist event whois" +"notifylist event" +"notifylist joined" +"notifylist left" +"pidwait" +"print format" +"print starting" +"print text" +"proxy client dump" +"query created" +"query destroyed" +"query nick changed" +"query server changed" +"reload" +"requested usermode change" +"script destroyed" +"script error" +"send command" +"send text" +"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" +"session restore channel" +"session restore nick" +"session restore server" +"session restore" +"session save channel" +"session save server" +"session save" +"settings errors" +"setup changed" +"setup reread" +"silent event who" +"silent event whois" +"terminal resized" +"theme changed" +"theme destroyed" +"user mode changed" +"userhost event" +"whois away" +"whois default event" +"whois end" +"whois event not found" +"whois event" +"whois oper" +"whois try whowas" +"whowas away" +"whowas event end" +"whowas event" +"window activity" +"window changed automatic" +"window changed" +"window created" +"window dehilight" +"window destroyed" +"window history changed" +"window item remove" +"window item server changed" +"window print info" +"window refnum changed" +"window server changed" diff --git a/docs/buildpod.pl b/docs/buildpod.pl index dfddf54..debaa3b 100755 --- a/docs/buildpod.pl +++ b/docs/buildpod.pl @@ -32,7 +32,7 @@ sub new {                          q(find something obviously wrong, or have requests ),                          q(for further documentation on topics not yet ),                          q(filled out, please ), -                        q(<a href="http://github.com/shabble/shab-irssi-scripts/issues#">create an issue</a>), +                        q(<a href="http://github.com/shabble/irssi-scripts/issues#">create an issue</a>),                          " on my Github page, and I'll see what I can do.</b></p>",                         ); @@ -90,7 +90,7 @@ package main;  use File::Copy; -my $output_dir = "../../tmp/shab-irssi-scripts/docs/"; +my $output_dir = "../../tmp/irssi-scripts/docs/";  my $batchconv = Pod::Simple::HTMLBatch::Custom->new;  my $css = 'podstyle.css'; diff --git a/feature-tests/subscript.pl b/feature-tests/subscript.pl new file mode 100644 index 0000000..8550409 --- /dev/null +++ b/feature-tests/subscript.pl @@ -0,0 +1,98 @@ +use strict; +use Irssi; +use Irssi::TextUI; # for sbar_items_redraw + +use vars qw($VERSION %IRSSI); + +$VERSION = "0.1"; + +%IRSSI = ( +	authors         => "shabble", +	contact         => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode', +	name            => "", +	description     => "", +	license         => "Public Domain", +	changed         => "" +); + +my $functions = {}; + +init(); + +sub _input { +    return int rand 1000; +} + +sub load { +    my $file = shift; +    my $funcs; +    if (-f $file) { +        print "Loading from file: $file"; +        $funcs = do $file; +    } +    if (not defined $funcs) { +        if ($@) { +            print "failed to parse $file: $@"; + +        } elsif ($!) { +            print "failed to read $file: $!"; + +        } +        return; +    } +    my $ref = ref $funcs; +    if ($ref ne 'HASH') { +        print "$file didn't return a hashref: ", defined $ref ? $ref : 'undef'; +        return; +    } + +    foreach my $name (keys %$funcs) { +        my $func = $funcs->{$name}; +        if (exists $functions->{$name}) { +            print "Redefining function $name"; +        } else { +            print "adding function: $name"; +        } +        $functions->{$name} = $func; +    } + +    print "Loaded " . scalar(keys(%$funcs)) . " functions"; +} + +sub init { +    Irssi::command_bind('subload', \&cmd_subload); +    Irssi::command_bind('sublist', \&cmd_sublist); +    Irssi::command_bind('subcall', \&cmd_subcall); +} + +sub cmd_subload { +    my $args = shift; +    print "Going to load: $args"; +    load($args); +} + +sub cmd_sublist { +    foreach my $name (keys %$functions) { +        my $func = $functions->{$name}; +        print "Function: $name => $func"; +    } +} + +sub cmd_subcall { +    my $args = shift; +    my ($cmd, $cmdargs); +    if ($args =~ m/^(\w+\b)(.*)$/) { +        $cmd = $1; $cmdargs = $2; +    } else { +        print "Couldn't parse $args"; +        return; +    } +    my $fun = $functions->{$cmd}; + +    if (ref $fun eq 'CODE') { +        print "Calling $cmd with $cmdargs"; +        $fun->($cmdargs); +    } else { +        print "$cmd is not a coderef. cannot run"; +    } +} diff --git a/feature-tests/test.sub b/feature-tests/test.sub new file mode 100644 index 0000000..350e89d --- /dev/null +++ b/feature-tests/test.sub @@ -0,0 +1,7 @@ +{ +    'moo' => sub { print "Moo" }, +    'hello' => sub { print join(", ", @_) }, +    'inp' => sub { print "input is : ", _input(); }, +}; + + diff --git a/history-search/rl_history_search.pl b/history-search/rl_history_search.pl index 83ff934..eebb6c7 100644 --- a/history-search/rl_history_search.pl +++ b/history-search/rl_history_search.pl @@ -1,29 +1,39 @@  # Search within your typed history as you type (like ctrl-R in bash) -# Usage: - +# +# 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 @@ -53,7 +63,7 @@ $VERSION = '1.0';     description => 'Search within your typed history as you type'                  . ' (like ctrl-R in readline applications)',     license     => 'GPLv2 or later', -   url         => 'http://github.com/shabble/shab-irssi-scripts/tree/master/history-search/', +   url         => 'http://github.com/shabble/irssi-scripts/tree/master/history-search/',     changed     => '24/7/2010'    ); @@ -65,9 +75,10 @@ my @search_matches = ();  my $match_index = 0; -sub DEBUG () { 0 } +my $DEBUG_ENABLED = 0; +sub DEBUG () { $DEBUG_ENABLED } -# check we have prompt_info loaded. +# check we have uberprompt loaded.  sub script_is_loaded {      my $name = shift; @@ -79,19 +90,29 @@ sub script_is_loaded {      return $retval;  } -unless (script_is_loaded('prompt_info')) { -    die "This script requires 'prompt_info' in order to work. " +unless (script_is_loaded('uberprompt')) { +    die "This script requires 'uberprompt.pl' in order to work. "        . "Please load it and try again";  } else {      history_init();  }  sub history_init { +    Irssi::settings_add_bool('history_search', 'histsearch_debug', 0); +      Irssi::command_bind('history_search_start', \&history_search); -    #Irssi::command_bind('history_search_exit', \&history_search_exit); + +    Irssi::signal_add      ('setup changed'   => \&setup_changed);      Irssi::signal_add_first('gui key pressed' => \&handle_keypress); + +    setup_changed();  } +sub setup_changed { +    $DEBUG_ENABLED = Irssi::settings_get_bool('histsearch_debug'); +} + +  sub history_search {      $search_active = 1;      $search_str = ''; @@ -105,16 +126,20 @@ sub history_search {  sub history_exit {      $search_active = 0; -    Irssi::signal_emit('change prompt', ''); +    Irssi::signal_emit('change prompt', '', 'UP_INNER');  }  sub update_history_prompt { -    Irssi::signal_emit('change prompt', " reverse-i-search: \`$search_str'"); +    my $col = scalar(@search_matches) ? '%g' : '%r'; +    Irssi::signal_emit('change prompt', +                       ' reverse-i-search: `' . $col . $search_str +                       . '%n' . "'", +                       'UP_INNER');  }  sub update_history_matches {      my ($match_str) = @_; -    $match_str //= $search_str; +    $match_str = $search_str unless defined $match_str;      my %unique;      my @matches = grep { m/\Q$match_str/i } @history_cache; diff --git a/prompt_info/uberprompt.pl b/prompt_info/uberprompt.pl new file mode 100644 index 0000000..0581dd3 --- /dev/null +++ b/prompt_info/uberprompt.pl @@ -0,0 +1,379 @@ +# 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. +# /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. +# +# 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. + +use strict; +use warnings; + +use Irssi; +use Irssi::TextUI;              # for sbar_items_redraw +use Data::Dumper; + +our $VERSION = "0.2"; +our %IRSSI = +  ( +   authors         => "shabble", +   contact         => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode', +   name            => "uberprompt", +   description     => "Helper script for dynamically adding text " +   . "into the input-bar prompt.", +   license         => "MIT", +   changed         => "24/7/2010" +  ); + + +my $DEBUG_ENABLED = 0; +sub DEBUG { $DEBUG_ENABLED } + +my $prompt_data     = ''; +my $prompt_data_pos = 'UP_INNER'; + +my $prompt_last     = ''; +my $prompt_format   = ''; + +pre_init(); + +sub pre_init { +    # my $start_time = Irssi::parse_special('$F'); +    # my $now = time(); +    # my $delta = $now - $start_time; + +    # print "Delta is $delta"; +    # if ($delta < 2) { +    #     print "starting a bit later"; +    #     Irssi::timeout_add_once(5000, \&init, undef); +    # } else { +    #     print "starting immediately"; + +    #     init(); +    # } +    Irssi::command('statusbar prompt reset'); +    init(); +} + +sub prompt_subcmd_handler { +    my ($data, $server, $item) = @_; +    $data =~ s/\s+$//g;         # strip trailing whitespace. +    Irssi::command_runsub('prompt', $data, $server, $item); +} + +sub UNLOAD { +    # remove uberprompt and return the original ones. +    print "Removing uberprompt and restoring original"; +    restore_prompt_items(); +} + +sub init { +    Irssi::statusbar_item_register('uberprompt', 0, 'uberprompt_draw'); + +    Irssi::settings_add_str('uberprompt', 'uberprompt_format', '[$*$uber] '); +    Irssi::settings_add_bool('uberprompt', 'uberprompt_debug', 0); +    Irssi::settings_add_bool('uberprompt', 'uberprompt_autostart', 1); + +    Irssi::command_bind("prompt",     \&prompt_subcmd_handler); +    Irssi::command_bind('prompt on',  \&replace_prompt_items); +    Irssi::command_bind('prompt off', \&restore_prompt_items); +    Irssi::command_bind('prompt set', +                        sub { +                            my $args = shift; +                            $args =~ s/^\s*(\w+)\s*(.*$)/$2/; +                            my $mode = 'UP_' . uc($1); +                            Irssi::signal_emit 'change prompt', $args, $mode; +                        }); +    Irssi::command_bind('prompt clear', +                        sub { +                            Irssi::signal_emit 'change prompt', '$p', 'UP_POST'; +                        }); + +    Irssi::signal_add('setup changed', \&reload_settings); + +    # intialise the prompt format. +    reload_settings(); + +    # make sure we redraw when necessary. +    Irssi::signal_add('window changed',           \&uberprompt_refresh); +    Irssi::signal_add('window name changed',      \&uberprompt_refresh); +    Irssi::signal_add('window changed automatic', \&uberprompt_refresh); +    Irssi::signal_add('window item changed',      \&uberprompt_refresh); + +    # install our statusbars if required. +    if (Irssi::settings_get_bool('uberprompt_autostart')) { +        replace_prompt_items(); +    } + +    # API signals + +    Irssi::signal_register({'change prompt' => [qw/string string/]}); +    Irssi::signal_add('change prompt' => \&change_prompt_handler); + +    # other scripts (specifically overlay/visual) can subscribe to +    # this event to be notified when the prompt changes. +    # arguments are new contents (string), new length (int) +    Irssi::signal_register({'prompt changed' => [qw/string int/]}); + +    if (DEBUG) { +        Irssi::signal_add 'prompt changed', \&debug_prompt_changed; +    } +} + +sub reload_settings { + +    $DEBUG_ENABLED = Irssi::settings_get_bool('uberprompt_debug'); + +    my $new = Irssi::settings_get_str('uberprompt_format'); +    if ($prompt_format ne $new) { +        print "Updated prompt format" if DEBUG; +        $prompt_format = $new; +        $prompt_format =~ s/\$uber/\$\$uber/; +        Irssi::abstracts_register(['uberprompt', $prompt_format]); +    } +} + +sub debug_prompt_changed { +    my ($text, $len) = @_; + +    $text =~ s/%/%%/g; + +    print "DEBUG: Got $text, length: $len"; +} + +sub change_prompt_handler { +    my ($text, $pos) = @_; + +    print "Got prompt change signal with: $text, $pos" if DEBUG; + +    my ($changed_text, $changed_pos); +    $changed_text = defined $prompt_data     ? $prompt_data     ne $text : 1; +    $changed_pos  = defined $prompt_data_pos ? $prompt_data_pos ne $pos  : 1; + +    $prompt_data     = $text; +    $prompt_data_pos = $pos; + +    if ($changed_text || $changed_pos) { +        print "Redrawing prompt" if DEBUG; +        uberprompt_refresh(); +    } +} + +sub _escape_prompt_special { +    my $str = shift; +    $str =~ s/\$/\$\$/g; +    $str =~ s/\\/\\\\/g; +    return $str; +} + +sub uberprompt_draw { +    my ($sb_item, $get_size_only) = @_; + +    my $window = Irssi::active_win; +    my $prompt_arg = ''; + +    # default prompt sbar arguments (from config) +    if (scalar( () = $window->items )) { +        $prompt_arg = '$[.15]{itemname}'; +    } else { +        $prompt_arg = '${winname}'; +    } + +    my $prompt = '';            # rendered content of the prompt. +    my $theme = Irssi::current_theme; + +    $prompt = $theme->format_expand("{uberprompt $prompt_arg}"); + +    if ($prompt_data_pos eq 'UP_ONLY') { +        $prompt =~ s/\$\$uber//; # no need for recursive prompting, I hope. + +        # TODO: only compute this if necessary? +        my $prompt_nt = $prompt; +        $prompt_nt =~ s/\s+$//; + +        my $pdata_copy = $prompt_data; + +        $pdata_copy =~ s/\$prompt_nt/$prompt_nt/; +        $pdata_copy =~ s/\$prompt/$prompt/; + +        $prompt = $pdata_copy; + +    } elsif ($prompt_data_pos eq 'UP_INNER' && defined $prompt_data) { +        my $esc = _escape_prompt_special($prompt_data); +        $prompt =~ s/\$\$uber/$esc/; + +    } else { +        # remove the $uber marker +        $prompt =~ s/\$\$uber//; + +        # and add the additional text at the appropriate position. +        if ($prompt_data_pos eq 'UP_PRE') { +            $prompt = $prompt_data . $prompt; +        } elsif ($prompt_data_pos eq 'UP_POST') { +            $prompt .= $prompt_data; +        } +    } + +    print "Redrawing with: $prompt, size-only: $get_size_only" if DEBUG; + + +    if ($prompt ne $prompt_last) { + +        # print "Emitting prompt changed signal" if DEBUG; +        # my $exp = Irssi::current_theme()->format_expand($text, 0); +        my $ps = Irssi::parse_special($prompt); + +        Irssi::signal_emit('prompt changed', $ps, length($ps)); +        $prompt_last = $prompt; +    } +    my $ret = $sb_item->default_handler($get_size_only, $prompt, '', 0); + +    return $ret; +} + +sub uberprompt_refresh { +    Irssi::statusbar_items_redraw('uberprompt'); +} + +sub replace_prompt_items { +    # remove existing ones. +    print "Removing original prompt" if DEBUG; + +    _sbar_command('prompt', 'remove', 'prompt'); +    _sbar_command('prompt', 'remove', 'prompt_empty'); + +    # add the new one. + +    _sbar_command('prompt', 'add', 'uberprompt', +                  qw/-alignment left -before input -priority '-1'/); + +    _sbar_command('prompt', 'position', '100'); +} + +sub restore_prompt_items { + +    _sbar_command('prompt', 'remove', 'uberprompt'); + +    print "Restoring original prompt" if DEBUG; + +    _sbar_command('prompt', 'reset'); +} + +sub _sbar_command { +    my ($bar, $cmd, $item, @args) = @_; + +    my $args_str = join ' ', @args; + +    $args_str .= ' ' if length $args_str && defined $item; + +    my $command = sprintf 'STATUSBAR %s %s %s%s', +      $bar, $cmd, $args_str, defined($item)?$item:''; + +    print "Running command: $command" if DEBUG; +    Irssi::command($command); +} + diff --git a/prompt_info/visual.pl b/prompt_info/visual.pl new file mode 100644 index 0000000..1413d0d --- /dev/null +++ b/prompt_info/visual.pl @@ -0,0 +1,191 @@ +use strict; +use warnings; + +use Irssi; +use Irssi::TextUI;              # for sbar_items_redraw +use Data::Dumper; + + + +our $VERSION = "0.1"; +our %IRSSI = +  ( +   authors         => "shabble", +   contact         => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode', +   name            => "prompt_info", +   description     => "Helper script for dynamically adding text " +   . "into the input-bar prompt.", +   license         => "Public Domain", +   changed         => "24/7/2010" +  ); + +sub DEBUG () { 1 } +#sub DEBUG () { 0 } + +my $region_active = 0; + +my ($term_w, $term_h) = (0, 0); + +# visual region selected. +my ($region_start, $region_end) = (0, 0); +my $region_content = ''; + +sub update_terminal_size { + +    my @stty_data = qx/stty -a/; +    my $line = $stty_data[0]; + +    # linux +    # speed 38400 baud; rows 36; columns 126; line = 0; +    if ($line =~ m/rows (\d+); columns (\d+);/) { +        $term_h = $1; +        $term_w = $2; +    # osx +    # speed 9600 baud; 40 rows; 235 columns; +    } elsif ($line =~ m/(\d+) rows; (\d+) columns;/) { +        $term_h = $1; +        $term_w = $2; +    } else { +        # guess? +        $term_h = 24; +        $term_w = 80; +    } + +    print "Terminal detected as $term_w cols by $term_h rows" if DEBUG; +} + +sub visual_subcmd_handler { +    my ($data, $server, $item) = @_; +    $data =~ s/\s+$//g; # strip trailing whitespace. +    Irssi::command_runsub('visual', $data, $server, $item); +} + +sub init { + +    # misc faff +    Irssi::command_bind('visual', \&visual_subcmd_handler); +    Irssi::command_bind('visual toggle', \&cmd_toggle_visual); +    Irssi::command_bind('visual clear',  \&cmd_clear_visual); + +    Irssi::command("^BIND ^F /visual toggle"); +    Irssi::command("^BIND ^G /visual clear"); + +    Irssi::command_bind 'print_test', +        sub { +            Irssi::gui_printtext(0, 0, '%8hello there%n'); +            }; + +    # redraw interception +    Irssi::signal_add_last('command redraw',   \&augment_redraw); +    Irssi::signal_add_first('gui key pressed', \&ctrl_l_intercept); + +    # for updating the overlay. +    Irssi::signal_add_last ('gui key pressed', \&key_pressed); + +    # things to refresh the overlay for. +    Irssi::signal_add('window changed',           \&uberprompt_refresh); +    Irssi::signal_add('window name changed',      \&uberprompt_refresh); +    Irssi::signal_add('window changed automatic', \&uberprompt_refresh); +    Irssi::signal_add('window item changed',      \&uberprompt_refresh); + +    Irssi::signal_add('terminal resized', \&update_terminal_size); + +    # so we know where the bottom line is +    update_terminal_size(); + + +} +sub cmd_clear_visual { +    _clear_visual_region(); +    #refresh_visual_overlay(); +    Irssi::statusbar_items_redraw('input'); +} + + +sub augment_redraw { +    print "Redraw called" if DEBUG; +    uberprompt_refresh(); +    Irssi::timeout_add_once(10, \&refresh_visual_overlay, 0); +} + + +sub cmd_toggle_visual { + +    $region_active = not $region_active; + +    if ($region_active) { +        $region_start = _pos(); +        $region_end   = 0; # reset end marker. +        print "visual mode started at $region_start" if DEBUG; +    } else { +        $region_end = _pos(); +        print "Visual mode ended at $region_end" if DEBUG; + +        if ($region_end > $region_start) { +            my $input = Irssi::parse_special('$L', 0, 0); +            my $str = substr($input, $region_start, $region_end - $region_start); +            print "Region selected: $str" if DEBUG; +        } else { +            print "Invalid region selection: [ $region_start - $region_end ]" +              if DEBUG; +            $region_start = $region_end = 0; +        } +        cmd_clear_visual(); +    } +} + +sub ctrl_l_intercept { +    my $key = shift; + +    if ($key == 12) { # C-l +        print "C-l pressed" if DEBUG; +        Irssi::command("redraw"); +        Irssi::signal_stop(); +    } elsif ($key == 10) { # RET +        _clear_visual_region(); +    } +} + +sub key_pressed { +    # this handler needs to be last so the actual character is printed by irssi +    # before we overlay on it. Otherwise things are all a bit off-by-1 +    return unless $region_active; + +    refresh_visual_overlay(); +} + +sub _clear_visual_region { +    print "Clearing Region markers" if DEBUG; +    $region_end = 0; +    $region_start = 0; +} + + +sub refresh_visual_overlay { + +    my $end_pos = $region_end; +    $end_pos  ||= _pos(); # if not set, take current position as end. + +    my $len = $end_pos - $region_start; +    return unless $len; # no point drawing an empty overlay + +    my $input = Irssi::parse_special('$L'); +    my $offset = $prompt_item->{size} + $region_start; + +    my $text = substr($input, $region_start, $len); + +    print "printing '$text' at $offset [$region_start, $end_pos] ($len)" if DEBUG; + +    $text = '%8' . $text . '%8'; +    _draw_overlay($offset, $text, $len); + +} + +sub _draw_overlay { +    my ($offset, $text, $len) = @_; +    Irssi::gui_printtext($offset, $term_h, $text); +} + +sub _pos { +    return Irssi::gui_input_get_pos(); +} diff --git a/scrolled-reminder/scrolled_reminder.pl b/scrolled-reminder/scrolled_reminder.pl index b69e3df..b1deb9c 100644 --- a/scrolled-reminder/scrolled_reminder.pl +++ b/scrolled-reminder/scrolled_reminder.pl @@ -1,3 +1,58 @@ +# ABOUT: +# +# This script attempts to prevent you from responding accidentally to long-finished +# conversations if you have scrolled back and are not viewing the current activity +# of the channel. +# +# If you attempt to send a message when you are not at the most recent point in the +# channel buffer, it will intercept the message and offer you a menu of options +# instead. +# +# USAGE: +# +# When scrolled up and sending a message, the subsequent prompt has the following +# options: +# +# * Ctrl-C - cancel sending the message. It remains in your input line, but is not +#            sent to the channel. +# * Ctrl-K - send the message to the channel. The input line is cleared. +# * Space  - Jump to the bottom (most recent) part of the channel buffer. +#            Unlike the first two, this does not cancel the prompt, so it allows +#            you to determine if your message is still appropriate before sending. +# +# +# 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. +# +# 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. +# +  use strict;  use warnings; @@ -7,16 +62,8 @@ use Irssi::Irc;  use Data::Dumper; -# Everyone is permitted to copy and distribute verbatim or modified -# copies of this license document, and changing it is allowed as long -# as the name is changed. - -#             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE -#    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -#   0. You just DO WHAT THE FUCK YOU WANT TO. - -sub DEBUG () { 1 } +my $DEBUG_ENABLED = 0; +sub DEBUG () { $DEBUG_ENABLED }  our $VERSION = '0.01';  our %IRSSI = @@ -27,8 +74,8 @@ our %IRSSI =     description => 'Requires confirmation to messages sent'                    . 'when the current window is scrolled up', -   license     => 'WTFPL; http://sam.zoy.org/wtfpl/', -   url         => 'http://github.com/shabble/shab-irssi-scripts/' +   license     => 'MIT', +   url         => 'http://github.com/shabble/irssi-scripts/'                    . 'tree/master/scrolled-reminder/',               ); @@ -44,8 +91,8 @@ sub script_is_loaded {      return $retval;  } -unless (script_is_loaded('prompt_info')) { -    die "This script requires 'prompt_info' in order to work. " +unless (script_is_loaded('uberprompt')) { +    die "This script requires the 'uberprompt.pl' script in order to work. "        . "Please load it and try again";  } else {      scroll_reminder_init(); @@ -56,11 +103,23 @@ my $pending_input = {};  my $active;  sub scroll_reminder_init { + +      $permit_pending = 0;      $active = 0; + +    Irssi::settings_add_bool('scrollminder', 'scrollminder_debug', 0); +      # we need to be first so we can intercept stuff.      Irssi::signal_add_first('send text', \&handle_send_text);      Irssi::signal_add_first('gui key pressed', \&handle_keypress); +    Irssi::signal_add('setup changed' => \&setup_changed); + +    setup_changed(); +} + +sub setup_changed { +    $DEBUG_ENABLED = Irssi::settings_get_bool('scrollminder_debug');  }  ################################################################ @@ -181,5 +240,5 @@ sub set_prompt {      my $msg = shift;      # add a leading space unless we're trying to clear it entirely.      $msg = ' ' . $msg if length $msg; -    Irssi::signal_emit('change prompt', $msg); +    Irssi::signal_emit('change prompt', $msg, 'UP_INNER');  } diff --git a/test/irssi/config b/test/irssi/config index 25101e6..5a7b307 100644 --- a/test/irssi/config +++ b/test/irssi/config @@ -165,7 +165,9 @@ aliases = {    xxx = "/dump Irssi::bindings()";    showbind = "/script exec \\%foo = Irssi::UI::bindings()\\; print \\$foo{\"\\\\$0\"}\\;";    botaway = "/away auto-away [\002BX\002-MsgLog On] (Away since 1:43 am Mon Aug 24)"; -  vim = "/script load vim_mode.pl"; +  vim = "script load vim_mode.pl"; +  unvim = "/script unload vim_mode"; +  pl = "script load prompt_replace.pl";  };  statusbar = { @@ -271,6 +273,7 @@ statusbar = {          prompt_empty = { };          input = { priority = "10"; };        }; +      position = "100";      };      promptadd = { disabled = "yes"; };    }; @@ -287,6 +290,9 @@ settings = {      autoinstall_custom_prompt = "yes";      mass_hilight_action = "/echo $nick";      mass_hilight_threshold = "0"; +    vim_mode_debug = "yes"; +    superprompt_format = "%K[%W$tag%c/%K$cumode%n$*%K]%n "; +    uberprompt_format = "[$*] ";    };    "fe-common/core" = { bell_beeps = "yes"; };    "irc/core" = { ctcp_userinfo_reply = ""; }; @@ -302,5 +308,10 @@ keyboard = (    { key = "^R"; id = "command"; data = "history_search_start "; },    { key = "meta-h"; id = "command"; data = "echo moo /echo moo"; },    { key = "meta-q"; id = "command"; data = "echo bacons"; }, -  { key = "meta-c"; id = "command"; data = "echo moo ; /echo bar"; } +  { key = "meta-c"; id = "command"; data = "echo moo ; /echo bar"; }, +  { key = "meta-l"; id = "command"; data = "visual "; }, +  { key = "meta-m"; id = "command"; data = "redraw "; }, +  { key = "^V"; id = "command"; data = "visual "; }, +  { key = "^F"; id = "command"; data = "visual toggle"; }, +  { key = "^G"; id = "command"; data = "visual clear"; }  ); diff --git a/vim-mode/TODO b/vim-mode/TODO new file mode 100644 index 0000000..e225f3e --- /dev/null +++ b/vim-mode/TODO @@ -0,0 +1,42 @@ +- fix known bugs and known todos! +- esc is a little slow as ins->cmd mode key +- 3"ap doesn't work but "a3p does +- search with / and ? in history +  first search on current line, then wrap to history +- implement tab completion for ex-mode +- recheck everything end of the line related +- marks +- text-objects +- 2daW doesn't work at end of line, should do nothing +- :imap +- custom scripts (vim-like functions) which can be mapped +    - surroundings, with an external script if somebody writes it +- Ctrl-R = to evaluate expressions, first only simple math +- Ctrl-R shouldn't have a timeout in insert mode +- % + +            # Ignore invalid operator/char combinations. +            #} elsif ($operator and ($char eq 'j' or $char eq 'k')) { +            #    # FIXME +            #    print "Invalid operator/char: $operator $char" if DEBUG; +            #    $skip = 1; + +- undo/redo positions _suck_ +- cc/dd . doesn't work + +- daw is not correctly displayed in status line, it shows d_a instead of da + +- :map ,, w +  :map ,,, b +  ,,, - very short pause - ,,, doesn't work; possibly because of +  flush_pending_map(). + +- support :map gX iINSERT TEXT<ESC> and similar stuff (suggested by estragib) +- it would be nice if :ls somehow indicated current and alternate buffer +  (maybe activity too?) (suggested by estragib) +- < estragib> oh, i can't map \<something> either +  < estragib> hehe, definitely something to do with escaping. \\ prints \ +- < estragib> there's a minimal difference to vim when yanking with yB: +              abc def ghi jkl<ESC>bhyBP +  < estragib> vim     : abc def ghighi jkl +  < estragib> vim-mode: abc def ghi ghi jkl diff --git a/vim-mode/vim_mode.pl b/vim-mode/vim_mode.pl index 28e254d..6ff8c78 100644 --- a/vim-mode/vim_mode.pl +++ b/vim-mode/vim_mode.pl @@ -22,7 +22,7 @@  # * 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> f t F T +# * 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 @@ -49,7 +49,9 @@  # * 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 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: @@ -58,45 +60,64 @@  #  # Ex-mode supports (activated by : in command mode) the following commands:  # -# * Switching buffers: :b <num> - switch to channel number -#                      :b#      - switch to last channel +# * 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) -#                      :bn[ext] - switch to next window -#                      :bp[rev] - switch to previous window -# * Close window:      :bd[elete] +#                      :[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] :di[splay] {args} +# * 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>. Mapping ex-mode and irssi commands -# is supported. Only default mappings can be used in {rhs}. +# <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 w  W      - to remap w to work like W +#     :map gb :bnext - to map gb to call :bnext  #     :map gB :bprev -#     :map <C-L> /clear # map Ctrl-L to irssi command /clear +#     :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  #  # -# The following irssi settings are available: +# 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 '' +# +# 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.  # -# * vim_mode_utf8: support UTF-8 characters, default on -# * vim_mode_debug: enable debug output, default off -# * vim_mode_cmd_seq: char that when double-pressed simulates <esc>  #  # The following statusbar items are available:  # @@ -156,6 +177,9 @@  # 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  #  # @@ -243,6 +267,13 @@ sub C_INSERT () { 4 }  sub C_EX () { 5 }  # irssi commands  sub C_IRSSI () { 6 } +# does nothing +sub C_NOP () { 7 } + +# setting types, match irssi types as they are stored as irssi settings +sub S_BOOL () { 0 } +sub S_INT  () { 1 } +sub S_STR  () { 2 }  # word and non-word regex, keep in sync with setup_changed()!  my $word     = qr/[\w_]/o; @@ -263,15 +294,19 @@ my $commands              repeatable => 1 },       # arrow like movement -      h  => { char => 'h', func => \&cmd_h, type => C_NORMAL }, -      l  => { char => 'l', func => \&cmd_l, type => C_NORMAL }, -     ' ' => { char => '<Space>', func => \&cmd_space, type => C_NORMAL }, +      h     => { char => 'h',       func => \&cmd_h, type => C_NORMAL }, +      l     => { char => 'l',       func => \&cmd_l, type => C_NORMAL }, +     "\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 }, -     k  => { char => 'k',  func => \&cmd_k,  type => C_NORMAL }, -     gg => { char => 'gg', func => \&cmd_gg, type => C_NORMAL }, +     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 }, +             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 }, @@ -289,97 +324,141 @@ my $commands       ge => { char => 'ge', func => \&cmd_ge, type => C_NORMAL,               selection_needs_move_right => 1 },       W  => { char => 'W',  func => \&cmd_W,  type => C_NORMAL }, -     B  => { char => 'B',  func => \&cmd_B,  type => C_NORMAL, -             selection_needs_move_right => 1 }, +     B  => { char => 'B',  func => \&cmd_B,  type => C_NORMAL },       E  => { char => 'E',  func => \&cmd_E,  type => C_NORMAL },       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 },       '$' => { char => '$', func => \&cmd_dollar, type => C_NORMAL },       # delete chars       x => { char => 'x', func => \&cmd_x, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       X => { char => 'X', func => \&cmd_X, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 }, +          # C_NORMAL is correct, operator c takes care of insert mode       s => { char => 's', func => \&cmd_s, type => C_NORMAL, -            repeatable => 1 }, # operator c takes care of insert mode +            repeatable => 1, no_operator => 1 }, +          # C_NORMAL is correct, operator c takes care of insert mode       S => { char => 'S', func => \&cmd_S, type => C_NORMAL, -            repeatable => 1 }, # operator c takes care of insert mode +            repeatable => 1, no_operator => 1 },       # insert mode       i => { char => 'i', func => \&cmd_i, type => C_INSERT, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       I => { char => 'I', func => \&cmd_I, type => C_INSERT, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       a => { char => 'a', func => \&cmd_a, type => C_INSERT, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       A => { char => 'A', func => \&cmd_A, type => C_INSERT, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       # replace       r => { char => 'r', func => \&cmd_r, type => C_NEEDSKEY, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       # paste       p => { char => 'p', func => \&cmd_p, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       P => { char => 'P', func => \&cmd_P, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       # to end of line       C => { char => 'C', func => \&cmd_C, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       D => { char => 'D', func => \&cmd_D, type => C_NORMAL, -            repeatable => 1 }, +            repeatable => 1, no_operator => 1 },       # scrolling -     "\x05" => { char => '<C-E>', func => \&cmd_ctrl_d, type => C_NORMAL }, +     "\x05" => { char => '<C-E>', func => \&cmd_ctrl_d, type => C_NORMAL, +                 no_operator => 1 },       "\x04" => { char => '<C-D>', func => \&cmd_ctrl_d, type => C_NORMAL, -                 needs_count => 1 }, -     "\x19" => { char => '<C-Y>', func => \&cmd_ctrl_u, type => C_NORMAL }, +                 needs_count => 1, no_operator => 1 }, +     "\x19" => { char => '<C-Y>', func => \&cmd_ctrl_u, type => C_NORMAL, +                 no_operator => 1 },       "\x15" => { char => '<C-U>', func => \&cmd_ctrl_u, type => C_NORMAL, -                 needs_count => 1 }, -     "\x06" => { char => '<C-F>', func => \&cmd_ctrl_f, type => C_NORMAL }, -     "\x02" => { char => '<C-B>', func => \&cmd_ctrl_b, type => C_NORMAL }, +                 needs_count => 1, no_operator => 1 }, +     "\x06" => { char => '<C-F>', func => \&cmd_ctrl_f, type => C_NORMAL, +                 no_operator => 1 }, +     "\x02" => { char => '<C-B>', func => \&cmd_ctrl_b, type => C_NORMAL, +                 no_operator => 1 },       # window switching -     "\x17j" => { char => '<C-W>j', func => \&cmd_ctrl_wj, type => C_NORMAL }, -     "\x17k" => { char => '<C-W>k', func => \&cmd_ctrl_wk, type => C_NORMAL }, -     "\x1e"  => { char => '<C-^>',  func => \&cmd_ctrl_6,  type => C_NORMAL }, +     "\x17j" => { char => '<C-W>j', func => \&cmd_ctrl_wj, type => C_NORMAL, +                  no_operator => 1 }, +     "\x17k" => { char => '<C-W>k', func => \&cmd_ctrl_wk, type => C_NORMAL, +                  no_operator => 1 }, +     "\x1e"  => { char => '<C-^>',  func => \&cmd_ctrl_6,  type => C_NORMAL, +                  no_operator => 1 },       # misc       '~'  => { char => '~', func => \&cmd_tilde, type => C_NORMAL, -               repeatable => 1 }, -     '"'  => { char => '"', func => \&cmd_register, type => C_NEEDSKEY }, -     '.'  => { char => '.', type => C_NORMAL, repeatable => 1 }, +               repeatable => 1, no_operator => 1 }, +     '"'  => { char => '"', func => \&cmd_register, type => C_NEEDSKEY, +               no_operator => 1 }, +     '.'  => { char => '.', type => C_NORMAL, repeatable => 1, +               no_operator => 1 },       ':'  => { char => ':', type => C_NORMAL },       "\n" => { char => '<CR>', type => C_NORMAL }, # return       # undo -     'u'    => { char => 'u',     func => \&cmd_undo, type => C_NORMAL }, -     "\x12" => { char => '<C-R>', func => \&cmd_redo, type => C_NORMAL }, +     'u'    => { char => 'u',     func => \&cmd_undo, type => C_NORMAL, +                 no_operator => 1 }, +     "\x12" => { char => '<C-R>', func => \&cmd_redo, type => C_NORMAL, +                 no_operator => 1 },      };  # All available commands in Ex-Mode.  my $commands_ex    = { -     s         => { char => ':s',         func => \&ex_substitute, type => C_EX }, -     bnext     => { char => ':bnext',     func => \&ex_bnext,      type => C_EX }, -     bn        => { char => ':bn',        func => \&ex_bnext,      type => C_EX }, -     bprev     => { char => ':bprev',     func => \&ex_bprev,      type => C_EX }, -     bp        => { char => ':bp',        func => \&ex_bprev,      type => C_EX }, -     bdelete   => { char => ':bdelete',   func => \&ex_bdelete,    type => C_EX }, -     bd        => { char => ':bd',        func => \&ex_bdelete,    type => C_EX }, -     buffer    => { char => ':buffer',    func => \&ex_buffer,     type => C_EX }, -     b         => { char => ':b',         func => \&ex_buffer,     type => C_EX }, -     registers => { char => ':registers', func => \&ex_registers,  type => C_EX }, -     reg       => { char => ':reg',       func => \&ex_registers,  type => C_EX }, -     display   => { char => ':display',   func => \&ex_registers,  type => C_EX }, -     di        => { char => ':di',        func => \&ex_registers,  type => C_EX }, -     buffers   => { char => ':buffer',    func => \&ex_buffers,    type => C_EX }, -     ls        => { char => ':ls',        func => \&ex_buffers,    type => C_EX }, -     undolist  => { char => ':undolist',  func => \&ex_undolist,   type => C_EX }, -     undol     => { char => ':undol',     func => \&ex_undolist,   type => C_EX }, -     map       => { char => ':map',       func => \&ex_map,        type => C_EX }, -     source    => { char => ':source',    func => \&ex_source,     type => C_EX }, -     so        => { char => ':so',        func => \&ex_source,     type => C_EX }, +     s         => { char => ':s',         func => \&ex_substitute, +                    type => C_EX }, +     bnext     => { char => ':bnext',     func => \&ex_bnext, +                    type => C_EX, uses_count => 1 }, +     bn        => { char => ':bn',        func => \&ex_bnext, +                    type => C_EX, uses_count => 1 }, +     bprev     => { char => ':bprev',     func => \&ex_bprev, +                    type => C_EX, uses_count => 1 }, +     bp        => { char => ':bp',        func => \&ex_bprev, +                    type => C_EX, uses_count => 1 }, +     bdelete   => { char => ':bdelete',   func => \&ex_bdelete, +                    type => C_EX, uses_count => 1 }, +     bd        => { char => ':bd',        func => \&ex_bdelete, +                    type => C_EX, uses_count => 1 }, +     buffer    => { char => ':buffer',    func => \&ex_buffer, +                    type => C_EX, uses_count => 1 }, +     b         => { char => ':b',         func => \&ex_buffer, +                    type => C_EX, uses_count => 1 }, +     registers => { char => ':registers', func => \&ex_registers, +                    type => C_EX }, +     reg       => { char => ':reg',       func => \&ex_registers, +                    type => C_EX }, +     display   => { char => ':display',   func => \&ex_registers, +                    type => C_EX }, +     di        => { char => ':di',        func => \&ex_registers, +                    type => C_EX }, +     buffers   => { char => ':buffer',    func => \&ex_buffers, +                    type => C_EX }, +     ls        => { char => ':ls',        func => \&ex_buffers, +                    type => C_EX }, +     undolist  => { char => ':undolist',  func => \&ex_undolist, +                    type => C_EX }, +     undol     => { char => ':undol',     func => \&ex_undolist, +                    type => C_EX }, +     map       => { char => ':map',       func => \&ex_map, +                    type => C_EX }, +     unmap     => { char => ':unmap',     func => \&ex_unmap, +                    type => C_EX }, +     unm       => { char => ':unm',       func => \&ex_unmap, +                    type => C_EX }, +     source    => { char => ':source',    func => \&ex_source, +                    type => C_EX }, +     so        => { char => ':so',        func => \&ex_source, +                    type => C_EX }, +     mkvimrc   => { char => ':mkvimrc',   func => \&ex_mkvimrc, +                    type => C_EX }, +     mkv       => { char => ':mkv',       func => \&ex_mkvimrc, +                    type => C_EX }, +     se        => { char => ':se',        func => \&ex_set, +                    type => C_EX }, +     set       => { char => ':set',       func => \&ex_set, +                    type => C_EX },      };  # MAPPINGS @@ -395,12 +474,20 @@ foreach my $char (keys %$commands) {  # GLOBAL VARIABLES -my $DEBUG_ENABLED = 0; - -sub DEBUG { $DEBUG_ENABLED } +# all vim_mode settings, must be enabled in vim_mode_init() before usage +my $settings +  = { +     # print debug output +     debug          => { type => S_BOOL, value => 0 }, +     # use UTF-8 internally for string calculations/manipulations +     utf8           => { type => S_BOOL, value => 1 }, +     # esc-shortcut in insert mode +     cmd_seq        => { type => S_STR,  value => '' }, +     # not used yet +     max_undo_lines => { type => S_INT,  value => 50 }, +    }; -# use UTF-8 internally for string calculations/manipulations -my $utf8 = 1; +sub DEBUG { $settings->{debug}->{value} }  # buffer to keep track of the last N keystrokes, used for Esc detection and  # insert mode mappings @@ -502,7 +589,7 @@ sub insert_ctrl_r {      my ($key) = @_;      my $char = chr($key); -    return if not defined $registers->{$char} or not $registers->{$char}; +    return if not defined $registers->{$char} or $registers->{$char} eq '';      my $pos = _insert_at_position($registers->{$char}, 1, _input_pos());      _input_pos($pos + 1); @@ -621,10 +708,6 @@ sub cmd_l {      $pos = _fix_input_pos($pos, $length);      return (undef, $pos);  } -sub cmd_space { -    my ($count, $pos, $repeat) = @_; -    return cmd_l($count, $pos); -}  # later history (down)  sub cmd_j { @@ -657,7 +740,7 @@ sub cmd_j {      } elsif ($history_index >= 0) {          my $history = $history[$history_index];          # History is not in UTF-8! -        if ($utf8) { +        if ($settings->{utf8}->{value}) {              $history = decode_utf8($history);          }          _input($history); @@ -689,7 +772,7 @@ sub cmd_k {      if ($history_index >= 0) {          my $history = $history[$history_index];          # History is not in UTF-8! -        if ($utf8) { +        if ($settings->{utf8}->{value}) {              $history = decode_utf8($history);          }          _input($history); @@ -726,7 +809,7 @@ sub cmd_G {      my $history = $history[$history_index];      # History is not in UTF-8! -    if ($utf8) { +    if ($settings->{utf8}->{value}) {          $history = decode_utf8($history);      }      _input($history); @@ -1274,7 +1357,7 @@ sub cmd_P {  sub _paste_at_position {      my ($count, $pos) = @_; -    return if not $registers->{$register}; +    return if $registers->{$register} eq '';      return _insert_at_position($registers->{$register}, $count, $pos);  } @@ -1300,6 +1383,8 @@ sub cmd_ctrl_d {          $count = $window->{height} / 2;      }      $window->view()->scroll($count); + +    Irssi::statusbar_items_redraw('more');      return (undef, undef);  }  sub cmd_ctrl_u { @@ -1311,6 +1396,8 @@ sub cmd_ctrl_u {          $count = $window->{height} / 2;      }      $window->view()->scroll($count * -1); + +    Irssi::statusbar_items_redraw('more');      return (undef, undef);  }  sub cmd_ctrl_f { @@ -1318,13 +1405,14 @@ sub cmd_ctrl_f {      my $window = Irssi::active_win();      $window->view()->scroll($count * $window->{height}); + +    Irssi::statusbar_items_redraw('more');      return (undef, undef);  }  sub cmd_ctrl_b {      my ($count, $pos, $repeat) = @_; -    cmd_ctrl_f($count * -1, $pos, $repeat); -    return (undef, undef); +    return cmd_ctrl_f($count * -1, $pos, $repeat);  }  sub cmd_ctrl_wj { @@ -1436,19 +1524,26 @@ sub _fix_input_pos {  sub cmd_ex_command {      my $arg_str = join '', @ex_buf; -    if ($arg_str !~ /^([a-z]+)/) { +    if ($arg_str !~ /^(\d*)?([a-z]+)/) {          return _warn("Invalid Ex-mode command!");      } -    if (not exists $commands_ex->{$1}) { -        return _warn("Ex-mode $1 doesn't exist!"); +    # Abort if command doesn't exist or used with count for unsupported +    # commands. +    if (not exists $commands_ex->{$2} or +        ($1 ne '' and not $commands_ex->{$2}->{uses_count})) { +        return _warn("Ex-mode $1$2 doesn't exist!");      } -    $commands_ex->{$1}->{func}($arg_str); +    my $count = $1; +    if ($count eq '') { +        $count = undef; +    } +    $commands_ex->{$2}->{func}($arg_str, $count);  }  sub ex_substitute { -    my ($arg_str) = @_; +    my ($arg_str, $count) = @_;      # :s///      if ($arg_str =~ m|^s/(.+)/(.*)/([ig]*)|) { @@ -1484,35 +1579,83 @@ sub ex_substitute {  }  sub ex_bnext { -    Irssi::command('window next'); +    my ($arg_str, $count) = @_; + +    if (not defined $count) { +        if ($arg_str =~ /^bn(?:ext)?\s(\d+)$/) { +            $count = $1; +        } else { +            $count = 1; +        } +    } + +    while ($count-- > 0) { +        Irssi::command('window next'); +    }  }  sub ex_bprev { -    Irssi::command('window previous'); +    my ($arg_str, $count) = @_; + +    if (not defined $count) { +        if ($arg_str =~ /^bp(?:rev)?\s(\d+)$/) { +            $count = $1; +        } else { +            $count = 1; +        } +    } + +    while ($count-- > 0) { +        Irssi::command('window previous'); +    }  }  sub ex_bdelete { +    my ($arg_str, $count) = @_; + +    if (not defined $count) { +        if ($arg_str =~ /^bd(?:elete)?\s(\d+)$/) { +            $count = $1; +        } +    } + +    if (defined $count) { +        my $window = Irssi::window_find_refnum($count); +        if (not $window) { +            return; +        } +        $window->set_active(); +    }      Irssi::command('window close');  }  sub ex_buffer { -    my ($arg_str) = @_; +    my ($arg_str, $count) = @_;      # :b[buffer] {args} -    if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$|) { +    if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$| or defined $count) {          my $window;          my $item;          my $buffer = $1; +        # :[N]:b[uffer] +        if (defined $count) { +            $window = Irssi::window_find_refnum($count);          # Go to window number. -        if ($buffer =~ /^[0-9]+$/) { +        } elsif ($buffer =~ /^[0-9]+$/) {              $window = Irssi::window_find_refnum($buffer);          # Go to previous window.          } elsif ($buffer eq '#') {              Irssi::command('window last');          # Go to best regex matching window.          } else { -            my $matches = _matching_windows($buffer); -            if (scalar @$matches > 0) { -                $window = @$matches[0]->{window}; -                $item = @$matches[0]->{item}; +            eval { +                my $matches = _matching_windows($buffer); +                if (scalar @$matches > 0) { +                    $window = @$matches[0]->{window}; +                    $item = @$matches[0]->{item}; +                } +            }; +            # Catch errors in /$buffer/ regex. +            if ($@) { +                _warn($@);              }          } @@ -1528,7 +1671,7 @@ sub ex_buffer {  }  sub ex_registers { -    my ($arg_str) = @_; +    my ($arg_str, $count) = @_;      # :reg[isters] {arg} and :di[splay] {arg}      if ($arg_str =~ /^(?:reg(?:isters)?|di(?:splay)?)(?:\s+(.+)$)?/) { @@ -1558,18 +1701,22 @@ sub ex_registers {  }  sub ex_buffers { +    my ($arg_str, $count) = @_; +      Irssi::command('window list');  }  sub ex_undolist { +    my ($arg_str, $count) = @_; +      _print_undo_buffer();  }  sub ex_map { -    my ($arg_str) = @_; +    my ($arg_str, $count) = @_;      # :map {lhs} {rhs} -    if ($arg_str =~ /^map (\S+) (\S.+)$/) { +    if ($arg_str =~ /^map (\S+) (\S.*)$/) {          my $lhs = _parse_mapping($1);          my $rhs = $2; @@ -1581,11 +1728,14 @@ sub ex_map {          my $command;          # Ex-mode command          if (index($rhs, ':') == 0) { -            $rhs = substr $rhs, 1; -            if (not exists $commands_ex->{$rhs}) { -                return _warn_ex('map', "$2 not found"); +            $rhs =~ /^:(\S+)(\s.+)?$/; +            if (not exists $commands_ex->{$1}) { +                return _warn_ex('map', "$rhs not found");              } else { -                $command = $commands_ex->{$rhs}; +                $command = { char => $rhs, +                             func => $commands_ex->{$1}->{func}, +                             type => C_EX, +                           };              }          # Irssi command          } elsif (index($rhs, '/') == 0) { @@ -1593,6 +1743,12 @@ sub ex_map {                           func => substr($rhs, 1),                           type => C_IRSSI,                         }; +        # <Nop> does nothing +        } elsif (lc $rhs eq '<nop>') { +            $command = { char => '<Nop>', +                         func => undef, +                         type => C_NOP, +                       };          # command-mode command          } else {              $rhs = _parse_mapping($2); @@ -1606,14 +1762,20 @@ sub ex_map {          }          add_map($lhs, $command); -    # :map -    } elsif ($arg_str eq 'map') { +    # :map [lhs] +    } elsif ($arg_str eq 'map' or $arg_str =~ /^map (\S+)$/) { +        # Necessary for case insensitive matchings. lc alone won't work. +        my $search = $1; +        $search = '' if not defined $search; +        $search = _parse_mapping_reverse(_parse_mapping($search)); +          my $active_window = Irssi::active_win();          foreach my $key (sort keys %$maps) {              my $map = $maps->{$key};              my $cmd = $map->{cmd};              if (defined $cmd) {                  next if $map->{char} eq $cmd->{char}; # skip default mappings +                next if $map->{char} !~ /^\Q$search\E/; # skip non-matches                  $active_window->print(sprintf "%-15s %s", $map->{char},                                                            $cmd->{char});              } @@ -1622,6 +1784,25 @@ sub ex_map {          _warn_ex('map');      }  } +sub ex_unmap { +    my ($arg_str, $count) = @_; + +    # :unm[ap] {lhs} +    if ($arg_str !~ /^unm(?:ap)? (\S+)$/) { +        return _warn_ex('unmap'); +    } + +    my $lhs = _parse_mapping($1); +    if (not defined $lhs) { +        return _warn_ex('unmap', 'invalid {lhs}'); +    # Prevent unmapping of unknown or default mappings. +    } elsif (not exists $maps->{$lhs} or not defined $maps->{$lhs}->{cmd} or +             ($commands->{$lhs} and $maps->{$lhs}->{cmd} == $commands->{$lhs})) { +        return _warn_ex('unmap', "$1 not found"); +    } + +    delete_map($lhs); +}  sub _parse_mapping {      my ($string) = @_; @@ -1648,6 +1829,9 @@ sub _parse_mapping_bracket {      # <CR>      } elsif ($string eq 'cr') {          $string = "\n"; +    # <BS> +    } elsif ($string eq 'bs') { +        $string = chr(127);      # Invalid char, return special string to recognize the error.      } else {          $string = '<invalid>'; @@ -1660,6 +1844,7 @@ sub _parse_mapping_reverse {      # Convert char to <char-name>.      $string =~ s/ /<Space>/g;      $string =~ s/\n/<CR>/g; +    $string =~ s/\x7F/<BS>/g;      # Convert Ctrl-X to <C-X>.      $string =~ s/([\x01-\x1A])/"<C-" . chr(ord($1) + 64) . ">"/ge;      # Convert Ctrl-6 and Ctrl-^ to <C-^>. @@ -1669,6 +1854,8 @@ sub _parse_mapping_reverse {  }  sub ex_source { +    my ($arg_str, $count) = @_; +      # :so[urce], but only loads the vim_moderc file at the moment      open my $file, '<', Irssi::get_irssi_dir() . '/vim_moderc' or return; @@ -1678,7 +1865,7 @@ sub ex_source {          chomp $line;          # :map {lhs} {rhs}, keep in sync with ex_map() -        if ($line =~ /^\s*map (\S+) (\S.+)$/) { +        if ($line =~ /^\s*map (\S+) (\S.*)$/) {              ex_map($line);          } else {              _warn_ex('source', "command not supported: $line"); @@ -1686,6 +1873,70 @@ sub ex_source {      }  } +sub ex_mkvimrc { +    my ($arg_str, $count) = @_; + +    # :mkv[imrc][!], [file] not supported + +    my $vim_moderc = Irssi::get_irssi_dir(). '/vim_moderc'; +    if (-f $vim_moderc and $arg_str !~ /^mkv(?:imrc)?!$/) { +        return _warn_ex('mkvimrc', "$vim_moderc already exists"); +    } + +    open my $file, '>', $vim_moderc or return; + +    # copied from ex_map() +    foreach my $key (sort keys %$maps) { +        my $map = $maps->{$key}; +        my $cmd = $map->{cmd}; +        if (defined $cmd) { +            next if $map->{char} eq $cmd->{char}; # skip default mappings +            print $file "map $map->{char} $cmd->{char}\n"; +        } +    } + +    close $file; +} + +sub ex_set { +    my ($arg_str, $count) = @_; + +    # :se[t] [option] [value] +    if ($arg_str =~ /^se(?:t)?(?:\s(\S+)(?:\s(.+)$)?)?/) { +        # :se[t] {option} {value} +        if (defined $1 and defined $2) { +            if (not exists $settings->{$1}) { +                return _warn_ex('map', "setting '$1' not found"); +            } +            my $name = $1; +            my $value = $2; +            # Also accept numeric values for boolean options. +            if ($settings->{$name}->{type} == S_BOOL and +                    $value !~ /^(on|off)$/) { +                $value = $value ? 'on' : 'off'; +            } +            Irssi::command("set vim_mode_$name $value"); + +        # :se[t] [option] +        } else { +            my $search = defined $1 ? $1 : ''; +            my $active_window = Irssi::active_win(); +            foreach my $setting (sort keys %$settings) { +                next if $setting !~ /^\Q$search\E/; # skip non-matches +                my $value = $settings->{$setting}->{value}; +                # Irssi only accepts 'on' and 'off' as values for boolean +                # options. +                if ($settings->{$setting}->{type} == S_BOOL) { +                    $value = $value ? 'on' : 'off'; +                } +                $active_window->print($setting . '=' . $value); +            } +        } +    } else { +        _warn_ex('map'); +    } +} +  sub _warn_ex {      my ($command, $description) = @_;      my $message = "Error in ex-mode command $command"; @@ -1783,14 +2034,21 @@ sub b_windows_cb {      my $windows = ''; -    # A little code duplication of cmd_ex_command()! +    # A little code duplication of cmd_ex_command(), but \s+ instead of \s* so +    # :bd doesn't display buffers matching d.      my $arg_str = join '', @ex_buf; -    if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$|) { +    if ($arg_str =~ m|^b(?:uffer)?\s+(.+)$|) {          my $buffer = $1;          if ($buffer !~ /^[0-9]$/ and $buffer ne '#') {              # Display matching windows. -            my $matches = _matching_windows($buffer); -            $windows = join ',', map { $_->{text} } @$matches; +            eval { +                my $matches = _matching_windows($buffer); +                $windows = join ',', map { $_->{text} } @$matches; +            }; +            # Catch errors in /$buffer/ regex. +            if ($@) { +                _warn($@); +            }          }      } @@ -1945,6 +2203,7 @@ sub flush_pending_map {                $pending_map ne $old_pending_map;      handle_command_cmd(undef); +    Irssi::statusbar_items_redraw("vim_mode");  }  sub handle_numeric_prefix { @@ -1975,8 +2234,8 @@ sub handle_command_cmd {      }      # Counts -    if (!$movement and ($char =~ m/[1-9]/ or -                        ($numeric_prefix && $char =~ m/[0-9]/))) { +    if (!$movement and !$pending_map and +        ($char =~ m/[1-9]/ or ($numeric_prefix && $char =~ m/[0-9]/))) {          print "Processing numeric prefix: $char" if DEBUG;          handle_numeric_prefix($char);          return 1; # call _stop() @@ -2028,16 +2287,26 @@ sub handle_command_cmd {          return 1; # call _stop()      } -    # Ex-mode commands can also be bound in command mode. Works only if the -    # ex-mode command doesn't need any additional arguments. +    # Ex-mode commands can also be bound in command mode.      if ($cmd->{type} == C_EX) {          print "Processing ex-command: $map->{char} ($cmd->{char})" if DEBUG; -        $cmd->{func}->($cmd->{char}); + +        $cmd->{func}->(substr($cmd->{char}, 1), $numeric_prefix); +        $numeric_prefix = undef; +          return 1; # call _stop()      # As can irssi commands.      } elsif ($cmd->{type} == C_IRSSI) {          print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG;          Irssi::command($cmd->{func}); + +        $numeric_prefix = undef; +        return 1; # call _stop(); +    # <Nop> does nothing. +    } elsif ($cmd->{type} == C_NOP) { +        print "Processing <Nop>: $map->{char}" if DEBUG; + +        $numeric_prefix = undef;          return 1; # call _stop();      } @@ -2059,8 +2328,11 @@ sub handle_command_cmd {          if ($operator) {              # But allow cc/dd/yy.              if ($operator == $cmd) { -                print "Processing line operator: $map->{char} ($cmd->{char})" +                print "Processing line operator: ", +                  $map->{char}, " (", +                  $cmd->{char} ,")"                      if DEBUG; +                  my $pos = _input_pos();                  $cmd->{func}->(0, _input_len(), undef, 0);                  # Restore position for yy. @@ -2082,8 +2354,8 @@ sub handle_command_cmd {      # Start Ex mode.      } elsif ($cmd == $commands->{':'}) { -        if (not script_is_loaded('prompt_info')) { -            _warn("Warning: Ex mode requires the 'prompt_info' script. " . +        if (not script_is_loaded('uberprompt')) { +            _warn("Warning: Ex mode requires the 'uberprompt' script. " .                      "Please load it and try again.");          } else {              _update_mode(M_EX); @@ -2120,6 +2392,13 @@ sub handle_command_cmd {              }          } +        # Ignore invalid operator/command combinations. +        if ($operator and $cmd->{no_operator}) { +            print "Invalid operator/command: $operator->{char} $cmd->{char}" +                if DEBUG; +            $skip = 1; +        } +          if ($skip) {              print "Skipping movement and operator." if DEBUG;          } else { @@ -2269,38 +2548,40 @@ sub vim_mode_init {  sub setup_changed {      my $value; -    # Delete all possible imaps created by /set vim_mode_cmd_seq. -    foreach my $char ('a' .. 'z') { -        delete $imaps->{$char}; +    if ($settings->{cmd_seq}->{value} ne '') { +        delete $imaps->{$settings->{cmd_seq}->{value}};      } -      $value = Irssi::settings_get_str('vim_mode_cmd_seq'); -    if ($value) { +    if ($value eq '') { +        $settings->{cmd_seq}->{value} = $value; +    } else {          if (length $value == 1) {              $imaps->{$value} = { 'map'  => $value,                                   'func' => sub { _update_mode(M_CMD) }                                 }; +            $settings->{cmd_seq}->{value} = $value;          } else {              _warn("Error: vim_mode_cmd_seq must be a single character");          }      } -    $DEBUG_ENABLED = Irssi::settings_get_bool('vim_mode_debug'); +    $settings->{debug}->{value} = Irssi::settings_get_bool('vim_mode_debug');      my $new_utf8 = Irssi::settings_get_bool('vim_mode_utf8'); - -    if ($new_utf8 != $utf8) { +    if ($new_utf8 != $settings->{utf8}->{value}) {          # recompile the patterns when switching to/from utf-8          $word     = qr/[\w_]/o;          $non_word = qr/[^\w_\s]/o; -    } +        $settings->{utf8}->{value} = $new_utf8; +    }      if ($new_utf8 and (!$^V or $^V lt v5.8.1)) {          _warn("Warning: UTF-8 isn't supported very well in perl < 5.8.1! " .                "Please disable the vim_mode_utf8 setting.");      } -    $utf8 = $new_utf8; +    $settings->{max_undo_lines}->{value} +        = Irssi::settings_get_int('vim_mode_max_undo_lines');  }  sub UNLOAD { @@ -2333,7 +2614,7 @@ sub _add_undo_entry {          unshift @undo_buffer, [$line, $pos];          $undo_index = 0;      } -    my $max = Irssi::settings_get_int('vim_mode_max_undo_lines'); +    my $max = $settings->{max_undo_lines}->{value};  }  sub _restore_undo_entry { @@ -2408,6 +2689,45 @@ sub add_map {      $maps->{$keys}->{char} = _parse_mapping_reverse($keys);      $maps->{$keys}->{cmd} = $command;  } +sub delete_map { +    my ($keys) = @_; + +    # Abort for non-existent mappings or placeholder mappings. +    return if not exists $maps->{$keys} or not defined $maps->{$keys}->{cmd}; + +    my @add = (); + +    # If no maps need the current key, then remove it and all other +    # unnecessary keys in the "tree". +    if (keys %{$maps->{$keys}->{maps}} == 0) { +        my $tmp = $keys; +        while (length $tmp > 1) { +            my $map = substr $tmp, -1, 1, ''; +            delete $maps->{$tmp}->{maps}->{$tmp . $map}; +            if (not $maps->{$tmp}->{cmd} and keys %{$maps->{$tmp}->{maps}} == 0) { +                push @add, $tmp; +                delete $maps->{$tmp}; +            } else { +                last; +            } +        } +    } + +    if (keys %{$maps->{$keys}->{maps}} > 0) { +        $maps->{$keys}->{cmd} = undef; +    } else { +        delete $maps->{$keys}; +    } +    push @add, $keys; + +    # Restore default keybindings in case we :unmaped a <Nop> or a remapped +    # key. +    foreach my $key (@add) { +        if (exists $commands->{$key}) { +            add_map($key, $commands->{$key}); +        } +    } +}  sub _commit_line { @@ -2420,12 +2740,12 @@ sub _input {      my $current_data = Irssi::parse_special('$L', 0, 0); -    if ($utf8) { +    if ($settings->{utf8}->{value}) {          $current_data = decode_utf8($current_data);      }      if (defined $data) { -        if ($utf8) { +        if ($settings->{utf8}->{value}) {              Irssi::gui_input_set(encode_utf8($data));          } else {              Irssi::gui_input_set($data); @@ -2535,7 +2855,7 @@ sub _set_prompt {      my $msg = shift;      # add a leading space unless we're trying to clear it entirely.      $msg = ' ' . $msg if length $msg; -    Irssi::signal_emit('change prompt', $msg); +    Irssi::signal_emit('change prompt', $msg, 'UP_INNER');  }  sub _warn { | 
