zsh_dots 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3783 @@
1
+ =pod
2
+
3
+ =head1 NAME
4
+
5
+ vim_mode.pl
6
+
7
+ =head1 DESCRIPTION
8
+
9
+ An Irssi script to emulate some of the vi(m) features for the Irssi inputline.
10
+
11
+ =head1 INSTALLATION
12
+
13
+ Copy into your F<~/.irssi/scripts/> directory and load with
14
+ C</SCRIPT LOAD vim_mode.pl>. You may wish to have it autoload in one of the
15
+ L<usual ways|https://github.com/shabble/irssi-docs/wiki/Guide#Autorunning_Scripts>.
16
+
17
+ =head2 DEPENDENCIES
18
+
19
+ For proper :ex mode support, vim-mode requires the installation of F<uberprompt.pl>
20
+ Uberprompt can be downloaded from:
21
+
22
+ L<https://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl>
23
+
24
+ and follow the instructions at the top of that file for installation.
25
+
26
+ If you don't need Ex-mode, you can run vim_mode.pl without the
27
+ uberprompt.pl script, but it is strongly recommended that you use it.
28
+
29
+ =head3 Irssi requirements
30
+
31
+ 0.8.12 and above should work fine. However the following features are
32
+ disabled in irssi < 0.8.13:
33
+
34
+ =over 4
35
+
36
+ =item * C<j> C<k> (only with count, they work fine without count in older versions)
37
+
38
+ =item * C<gg>, C<G>
39
+
40
+ =back
41
+
42
+ It is intended to work with at Irssi 0.8.12 and later versions. However some
43
+ features are disabled in older versions (see below for details).
44
+
45
+ Perl >= 5.8.1 is recommended for UTF-8 support (which can be disabled if
46
+ necessary). Please report bugs in older versions as well, we'll try to fix
47
+ them. Later versions of Perl are also faster, which is probably beneficial
48
+ to a script of this size and complexity.
49
+
50
+ =head2 SETUP
51
+
52
+ Vim Mode provides certain feedback to the user, such as the currently active
53
+ mode (command, insert, ex), and if switching buffers, which buffer(s) currently
54
+ match the search terms.
55
+
56
+ There are two ways to go about displaying this information, as described in
57
+ the following sections:
58
+
59
+ =head3 Statusbar Items
60
+
61
+ Run the following command to add a statusbar item that shows which mode
62
+ you're in.
63
+
64
+ C</statusbar window add vim_mode>
65
+
66
+ And the following to let C<:b [str]> display a list of channels matching your
67
+ search string.
68
+
69
+ C</statusbar window add vim_windows>
70
+
71
+ B<Note:> Remember to C</save> after adding these statusbar items to make them
72
+ permanent.
73
+
74
+ B<Note:> If you would rather have these statusbar items on your prompt
75
+ line rather than thte window statusbar, please follow these steps.
76
+
77
+ For technical reasons, I<uberprompt> must occasionally call C</statusbar prompt
78
+ reset>, which will remove or deactivate any manually added items on the prompt
79
+ statusbar. To get around this, uberprompt provides two command hooks,
80
+ C<uberprompt_load_hook> and C<uberprompt_unload_hook>. Both of these settings
81
+ can contain one (or more, using C</EVAL>) commands to be executed when the prompt
82
+ is enabled and disabled, respectively.
83
+
84
+ See the L<uberprompt documentation|https://github.com/shabble/irssi-scripts/blob/master/prompt_info/README.pod> for further details.
85
+
86
+ For I<right-aligned> items (that is, after the input field:
87
+
88
+ =over 4
89
+
90
+ =item 1 C</alias vm_add /^statusbar prompt add -after input -alignment right vim_mode>
91
+
92
+ =item 2 C</alias vm_del /^statusbar prompt remove vim_mode>
93
+
94
+ =item 3 C</set uberprompt_load_hook /^vm_add>
95
+
96
+ =item 4 C</set uberprompt_unload_hook /^vm_del>
97
+
98
+ =back
99
+
100
+ For I<left-aligned> items (before the prompt):
101
+
102
+ =over 4
103
+
104
+ =item 1 C</alias vm_add /^statusbar prompt add -before prompt -alignment left vim_mode>
105
+
106
+ =item 2 C</alias vm_del /^statusbar prompt remove vim_mode>
107
+
108
+ =item 3 C</set uberprompt_load_hook /^vm_add>
109
+
110
+ =item 4 C</set uberprompt_unload_hook /^vm_del>
111
+
112
+ =back
113
+
114
+ If you wish to add both C<vim_mode> and C<vim_windows> items, replace steps 1 and 2
115
+ above with the following (right-aligned):
116
+
117
+ =over 4
118
+
119
+ =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>
120
+
121
+ =item 2 C</alias vm_del /^eval /^statusbar prompt remove vim_mode ; /^statusbar prompt remove vim_windows>
122
+
123
+ =back
124
+
125
+ and then complete stages 3 and 4 as above. Replace the C<-after> and C<-alignment>
126
+ to suit your location preferences.
127
+
128
+ B<Note:> It is also possible to place the items between the prompt and input field,
129
+ by specifying C<-after prompt> or C<-before input> as appropriate.
130
+
131
+ =head3 Expando Variables
132
+
133
+ Vim mode augments the existing Irssi expando (automatic variables) with two
134
+ additional ones: C<$vim_cmd_mode> and C<$vim_wins>.
135
+
136
+ C<$vim_cmd_mode> is the equivalent of the C<vim_mode> statusbar item, and
137
+ C<$vim_wins> is the counterpart of C<vim_windows>.
138
+
139
+ They can be added to your theme, or inserted into your uberprompt using
140
+ a command such as:
141
+
142
+ "C</set uberprompt_format [$vim_cmd_mode] $*$uber] >"
143
+
144
+ =head3 FILE-BASED CONFIGURATION
145
+
146
+ Additionally to the irssi settings described in L<settings|/SETTINGS>, vim_mode
147
+ can be configured through an external configuration file named "vim_moderc"
148
+ located in F<~/.irssi/vim_moderc>. If available it's loaded on startup and every
149
+ supported ex-command is run. Its syntax is similar to "vimrc". To (re)load it
150
+ while vim_mode is running use C<:so[urce]>.
151
+
152
+ Currently Supported ex-commands:
153
+
154
+ =over 4
155
+
156
+ =item * C<:map>
157
+
158
+ =back
159
+
160
+ =head1 USAGE
161
+
162
+ The following section is divided into the different modes as supported by Vim itself:
163
+
164
+ =head2 COMMAND MODE
165
+
166
+ It supports most commonly used command mode features:
167
+
168
+ =over 2
169
+
170
+ =item * Insert/Command mode.
171
+
172
+ C<Esc> and C<Ctrl-C> enter command mode. C</set vim_mode_cmd_seq j> allows
173
+ to use C<jj> as Escape (any other character can be used as well).
174
+
175
+ =item * Cursor motion:
176
+
177
+ C<h l 0 ^ $ E<lt>SpaceE<gt> E<lt>BSE<gt> f t F T>
178
+
179
+ =item * History motion:
180
+
181
+ C<j k gg G> C<gg> moves to the oldest (first) history line. C<G> without a
182
+ count moves to the current input line, with a count it goes to the I<count-th>
183
+ history line (1 is the oldest).
184
+
185
+ =item * Cursor word motion:
186
+
187
+ C<w b ge e W gE B E>
188
+
189
+ =item * Word objects (only the following work yet):
190
+
191
+ C<aw aW>
192
+
193
+ =item * Yank and paste:
194
+
195
+ C<y p P>
196
+
197
+ =item * Change and delete:
198
+
199
+ C<c d>
200
+
201
+ =item * Delete at cursor:
202
+
203
+ C<x X>
204
+
205
+ =item * Replace at cursor:
206
+
207
+ C<r>
208
+
209
+ =item * Insert mode:
210
+
211
+ C<i a I A>
212
+
213
+ =item * Switch case:
214
+
215
+ C<~>
216
+
217
+ =item * Repeat change:
218
+
219
+ C<.>
220
+
221
+ =item * Repeat
222
+
223
+ C<ftFT: ; ,>
224
+
225
+ =item * Registers:
226
+
227
+ C<"a-"z "" "0 "* "+ "_> (black hole)
228
+
229
+ =item * Line-wise shortcuts:
230
+
231
+ C<dd cc yy>
232
+
233
+ =item * Shortcuts:
234
+
235
+ C<s S C D>
236
+
237
+ =item * Scroll the scrollback buffer:
238
+
239
+ C<Ctrl-E Ctrl-D Ctrl-Y Ctrl-U Ctrl-F Ctrl-B>
240
+
241
+ =item * Switch to last active window:
242
+
243
+ C<Ctrl-6/Ctrl-^>
244
+
245
+ =item * Switch split windows:
246
+
247
+ <Ctrl-W j Ctrl-W k>
248
+
249
+ =item * Undo/Redo:
250
+
251
+ C<u Ctrl-R>
252
+
253
+ =back
254
+
255
+ 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.
256
+
257
+ =head3 REGISTERS
258
+
259
+ =over 4
260
+
261
+ =item * Appending to register with C<"A-"Z>
262
+
263
+ =item * C<""> is the default yank/delete register.
264
+
265
+ =item * C<"0> contains the last yank (if no register was specified).
266
+
267
+ =item * The special registers C<"* "+> both contain irssi's internal cut-buffer.
268
+
269
+ =back
270
+
271
+ =head2 INSERT MODE
272
+
273
+ The following insert mode mappings are supported:
274
+
275
+ =over 4
276
+
277
+ =item * Insert register content: Ctrl-R x (where x is the register to insert)
278
+
279
+ =back
280
+
281
+ =head2 EX-MODE
282
+
283
+ Ex-mode (activated by C<:> in command mode) supports the following commands:
284
+
285
+ =over 4
286
+
287
+ =item * Command History:
288
+
289
+ C<E<lt>uparrowE<gt>> - cycle backwards through history
290
+
291
+ C<E<lt>downarrowE<gt>> - cycle forwards through history
292
+
293
+ C<:eh> - show ex history
294
+
295
+ =item * Switching buffers:
296
+
297
+ C<:[N]b [N]> - switch to channel number
298
+
299
+ C<:b#> - switch to last channel
300
+
301
+ C<:b> E<lt>partial-channel-nameE<gt>
302
+
303
+ C<:b> <partial-server>/<partial-channel>
304
+
305
+ C<:buffer {args}> (same as C<:b>)
306
+
307
+ C<:[N]bn[ext] [N]> - switch to next window
308
+
309
+ C<:[N]bp[rev] [N]> - switch to previous window
310
+
311
+ =item * Close window:
312
+
313
+ C<:[N]bd[elete] [N]>
314
+
315
+ =item * Display windows:
316
+
317
+ C<:ls>, C<:buffers>
318
+
319
+ =item * Display registers:
320
+
321
+ C<:reg[isters] {args}>, C<:di[splay] {args}>
322
+
323
+ =item * Display undolist:
324
+
325
+ C<:undol[ist]> (mostly used for debugging)
326
+
327
+ =item * Source files:
328
+
329
+ C<:so[urce]> - only sources vim_moderc at the moment,
330
+ F<{file}> not supported
331
+
332
+ =item * Mappings:
333
+
334
+ C<:map> - display custom mappings
335
+
336
+ =item * Saving mappings:
337
+
338
+ C<:mkv[imrc][!]> - like in Vim, but [file] not supported
339
+
340
+ =item * Substitution:
341
+
342
+ C<:s///> - I<i> and I<g> are supported as flags, only C<///> can be used as
343
+ eparator, and it uses Perl regex syntax instead of Vim syntax.
344
+
345
+ =item * Settings:
346
+
347
+ C<:se[t]> - display all options
348
+
349
+ C<:se[t] {option}> - display all matching options
350
+
351
+ C<:se[t] {option} {value}> - change option to value
352
+
353
+ =back
354
+
355
+ =head3 MAPPINGS
356
+
357
+ =head4 Commands
358
+
359
+ =over 4
360
+
361
+ =item * C<:map {lhs}> - display mappings starting with {lhs}
362
+
363
+ =item * C<:map {lhs} {rhs}> - add mapping
364
+
365
+ =item * C<:unm[ap] {lhs}> - remove custom mapping
366
+
367
+ =back
368
+
369
+ =head4 Parameters
370
+
371
+ I<{lhs}> is the key combination to be mapped, I<{rhs}> the target. The
372
+ C<E<lt>E<gt>> notation is used
373
+
374
+ (e.g. C<E<lt>C-FE<gt>> is I<Ctrl-F>), case is ignored.
375
+ Supported C<E<lt>E<gt>> keys are:
376
+
377
+ =over 4
378
+
379
+ =item * C<E<lt>C-AE<gt>> - C<E<lt>C-ZE<gt>>,
380
+
381
+ =item * C<E<lt>C-^E<gt>>, C<E<lt>C-6E<gt>>
382
+
383
+ =item * C<E<lt>SpaceE<gt>>
384
+
385
+ =item * C<E<lt>CRE<gt>> - Enter
386
+
387
+ =item * C<E<lt>BSE<gt>> - Backspace
388
+
389
+ =item * C<E<lt>NopE<gt>> - No-op (Do Nothing).
390
+
391
+ =back
392
+
393
+ Mapping ex-mode and irssi commands is supported. When mapping ex-mode commands
394
+ the trailing C<E<lt>CrE<gt>> is not necessary. Only default mappings can be used
395
+ in I<{rhs}>.
396
+
397
+ Examples:
398
+
399
+ =over 4
400
+
401
+ =item * C<:map w W> - to remap w to work like W
402
+
403
+ =item * C<:map gb :bnext> - to map gb to call :bnext
404
+
405
+ =item * C<:map gB :bprev>
406
+
407
+ =item * C<:map g1 :b 1> - to map g1 to switch to buffer 1
408
+
409
+ =item * C<:map gb :b> - to map gb to :b, 1gb switches to buffer 1, 5gb to 5
410
+
411
+ =item * C<:map E<lt>C-LE<gt> /clear> - map Ctrl-L to irssi command /clear
412
+
413
+ =item * C<:map E<lt>C-GE<gt> /window goto 1>
414
+
415
+ =item * C<:map E<lt>C-EE<gt> <Nop>> - disable E<lt>C-EE<gt>, it does nothing now
416
+
417
+ =item * C<:unmap E<lt>C-EE<gt>> - restore default behavior of C<E<lt>C-EE<gt>>
418
+ after disabling it
419
+
420
+ =back
421
+
422
+ Note that you must use C</> for irssi commands (like C</clear>), the current value
423
+ of Irssi's cmdchars does B<not> work! This is necessary do differentiate between
424
+ ex-commands and irssi commands.
425
+
426
+ =head2 SETTINGS
427
+
428
+ The settings are stored as irssi settings and can be set using C</set> as usual
429
+ (prepend C<vim_mode_> to setting name) or using the C<:set> ex-command. The
430
+ following settings are available:
431
+
432
+ =over 4
433
+
434
+ =item * utf8 - Support UTF-8 characters, boolean, default on
435
+
436
+ =item * debug - Enable debug output, boolean, default off
437
+
438
+ =item * cmd_seq - Char that when double-pressed simulates C<E<lt>EscE<gt>>, string, default '' (disabled)
439
+
440
+ =item * start_cmd - Start every line in command mode, boolean, default off
441
+
442
+ =item * max_undo_lines - Sze of the undo buffer. Integer, default 50 items.
443
+
444
+ =item * ex_history_size - Number of items stored in the ex-mode history. Integer, default 100.
445
+
446
+ =item * prompt_leading_space - Ddetermines whether ex mode prepends a space to the displayed input. Boolean, default on
447
+
448
+ =back
449
+
450
+ In contrast to irssi's settings, C<:set> accepts 0 and 1 as values for boolean
451
+ settings, but only vim_mode's settings can be set/displayed.
452
+
453
+ Examples:
454
+
455
+ :set cmd_seq=j # set cmd_seq to j
456
+ :set cmd_seq= # disable cmd_seq
457
+ :set debug=on # enable debug
458
+ :set debug=off # disable debug
459
+
460
+ =head1 KNOWN ISSUES
461
+
462
+ If you use tmux and want to use <esc> to exit insert mode you might want to
463
+ reduce the escape-time for a better experience (500 is the default value):
464
+
465
+ set -s escape-time 100
466
+
467
+ A similar problem exist in GNU screen, the following settings in ~/.screenrc
468
+ fix it (thanks to jsbronder for reporting the screen issue and fix):
469
+
470
+ maptimeout 0
471
+ defc1 off
472
+
473
+ defc1 might not be necessary.
474
+
475
+ =head1 SUPPORT
476
+
477
+ Any behavior different from Vim (unless explicitly documented) should be
478
+ considered a bug and reported.
479
+
480
+ B<NOTE:> This script is still under heavy development, and there may be bugs.
481
+ Please submit reproducible sequences to the bug-tracker at:
482
+ L<http://github.com/shabble/irssi-scripts/issues/new>
483
+
484
+ or contact rudi_s or shabble on irc.freenode.net (#irssi and #irssi_vim)
485
+
486
+ =head1 AUTHORS
487
+
488
+ Copyright E<copy> 2010-2011 Tom Feist C<E<lt>shabble+irssi@metavore.orgE<gt>> and
489
+
490
+ Copyright E<copy> 2010-2011 Simon Ruderich C<E<lt>simon@ruderich.orgE<gt>>
491
+
492
+ =head1 THANKS
493
+
494
+ Particular thanks go to
495
+
496
+ =over 4
497
+
498
+ =item * estragib: a lot of testing and many bug reports and feature requests
499
+
500
+ =item * iaj: testing
501
+
502
+ =item * tmr: explaining how various bits of vim work
503
+
504
+ =back
505
+
506
+ as well as the rest of C<#irssi> and C<#irssi_vim> on Freenode IRC.
507
+
508
+ =head1 LICENCE
509
+
510
+ Permission is hereby granted, free of charge, to any person obtaining a copy
511
+ of this software and associated documentation files (the "Software"), to deal
512
+ in the Software without restriction, including without limitation the rights
513
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
514
+ copies of the Software, and to permit persons to whom the Software is
515
+ furnished to do so, subject to the following conditions:
516
+
517
+ The above copyright notice and this permission notice shall be included in
518
+ all copies or substantial portions of the Software.
519
+
520
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
521
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
522
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
523
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
524
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
525
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
526
+ THE SOFTWARE.
527
+
528
+ =head1 BUGS
529
+
530
+ =over 4
531
+
532
+ =item *
533
+
534
+ count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does
535
+
536
+ =item *
537
+
538
+ mapping an incomplete ex-command doesn't open the ex-mode with the partial
539
+ command (e.g. C<:map gb :b> causes an error instead of opening the ex-mode and
540
+ displaying C<:bE<lt>cursorE<gt>>)
541
+
542
+ =item *
543
+
544
+ undo/redo cursor positions are mostly wrong
545
+
546
+ =back
547
+
548
+ =head1 TODO
549
+
550
+ =over 4
551
+
552
+ =item *
553
+
554
+ Make sure the input line is empty when entering ex mode. Stuff hanging around
555
+ just looks silly.
556
+
557
+ =item *
558
+
559
+ History:
560
+
561
+ =over 4
562
+
563
+ =item *
564
+
565
+ C< * /,?,n,N> to search through history (like rl_history_search.pl)
566
+
567
+ =back
568
+
569
+ =item *
570
+
571
+ Window switching (C<:b>)
572
+
573
+ =over 4
574
+
575
+ =item *
576
+
577
+ Tab completion of window(-item) names
578
+
579
+ =item *
580
+
581
+ non-sequential matches(?)
582
+
583
+ =back
584
+
585
+ =back
586
+
587
+ See also the TODO file at
588
+ L<github|https://github.com/shabble/irssi-scripts/blob/master/vim-mode/TODO> for
589
+ many many more things.
590
+
591
+ =head2 WONTFIX
592
+
593
+ Things we're not ever likely to do:
594
+
595
+ =over 4
596
+
597
+ =item * Macros
598
+
599
+ =back
600
+
601
+ =cut
602
+
603
+ use strict;
604
+ use warnings;
605
+
606
+ use Encode;
607
+ use List::Util;
608
+
609
+ use Irssi;
610
+ use Irssi::TextUI; # for sbar_items_redraw
611
+ use Irssi::Irc; # necessary for 0.8.14
612
+
613
+
614
+
615
+ our $VERSION = "1.1.0";
616
+ our %IRSSI =
617
+ (
618
+ authors => "Tom Feist (shabble), Simon Ruderich (rudi_s)",
619
+ contact => 'shabble+irssi@metavore.org, '
620
+ . 'shabble@#irssi/Freenode, simon@ruderich.org'
621
+ . 'rudi_s@#irssi/Freenode',
622
+ name => "vim_mode",
623
+ description => "Give Irssi Vim-like commands for editing the inputline",
624
+ license => "MIT",
625
+ changed => "3/2/2012"
626
+ );
627
+
628
+
629
+ # CONSTANTS
630
+
631
+ # command mode
632
+ sub M_CMD () { 1 }
633
+ # insert mode
634
+ sub M_INS () { 0 }
635
+ # extended mode (after a :?)
636
+ sub M_EX () { 2 }
637
+
638
+ # operator command
639
+ sub C_OPERATOR () { 0 }
640
+ # normal command, no special handling necessary
641
+ sub C_NORMAL () { 1 }
642
+ # command taking another key as argument
643
+ sub C_NEEDSKEY () { 2 }
644
+ # text-object command (i a)
645
+ sub C_TEXTOBJECT () { 3 }
646
+ # commands entering insert mode
647
+ sub C_INSERT () { 4 }
648
+ # ex-mode commands
649
+ sub C_EX () { 5 }
650
+ # irssi commands
651
+ sub C_IRSSI () { 6 }
652
+ # does nothing
653
+ sub C_NOP () { 7 }
654
+
655
+ # setting types, match irssi types as they are stored as irssi settings
656
+ sub S_BOOL () { 0 }
657
+ sub S_INT () { 1 }
658
+ sub S_STR () { 2 }
659
+ sub S_TIME () { 3 }
660
+
661
+ # word and non-word regex, keep in sync with setup_changed()!
662
+ my $word = qr/[\w_]/o;
663
+ my $non_word = qr/[^\w_\s]/o;
664
+
665
+ # COMMANDS
666
+
667
+ # All available commands in command mode, they are stored with a char as key,
668
+ # but this is not necessarily the key the command is currently mapped to.
669
+ my $commands
670
+ = {
671
+ # operators
672
+ c => { char => 'c', func => \&cmd_operator_c, type => C_OPERATOR,
673
+ repeatable => 1 },
674
+ d => { char => 'd', func => \&cmd_operator_d, type => C_OPERATOR,
675
+ repeatable => 1 },
676
+ y => { char => 'y', func => \&cmd_operator_y, type => C_OPERATOR,
677
+ repeatable => 1 },
678
+
679
+ # arrow like movement
680
+ h => { char => 'h', func => \&cmd_h, type => C_NORMAL },
681
+ l => { char => 'l', func => \&cmd_l, type => C_NORMAL },
682
+ "\x08" => { char => '<BS>', func => \&cmd_h, type => C_NORMAL },
683
+ "\x7F" => { char => '<BS>', func => \&cmd_h, type => C_NORMAL },
684
+ ' ' => { char => '<Space>', func => \&cmd_l, type => C_NORMAL },
685
+ # history movement
686
+ j => { char => 'j', func => \&cmd_j, type => C_NORMAL,
687
+ no_operator => 1 },
688
+ k => { char => 'k', func => \&cmd_k, type => C_NORMAL,
689
+ no_operator => 1 },
690
+ gg => { char => 'gg', func => \&cmd_gg, type => C_NORMAL,
691
+ no_operator => 1 },
692
+ G => { char => 'G', func => \&cmd_G, type => C_NORMAL,
693
+ needs_count => 1, no_operator => 1 },
694
+ # char movement, take an additional parameter and use $movement
695
+ f => { char => 'f', func => \&cmd_f, type => C_NEEDSKEY,
696
+ selection_needs_move_right => 1 },
697
+ t => { char => 't', func => \&cmd_t, type => C_NEEDSKEY,
698
+ selection_needs_move_right => 1 },
699
+ F => { char => 'F', func => \&cmd_F, type => C_NEEDSKEY },
700
+ T => { char => 'T', func => \&cmd_T, type => C_NEEDSKEY },
701
+ ';' => { char => ';', func => \&cmd_semicolon, type => C_NORMAL },
702
+ ',' => { char => ',', func => \&cmd_comma, type => C_NORMAL },
703
+ # word movement
704
+ w => { char => 'w', func => \&cmd_w, type => C_NORMAL },
705
+ b => { char => 'b', func => \&cmd_b, type => C_NORMAL },
706
+ e => { char => 'e', func => \&cmd_e, type => C_NORMAL,
707
+ selection_needs_move_right => 1 },
708
+ ge => { char => 'ge', func => \&cmd_ge, type => C_NORMAL,
709
+ selection_needs_move_right => 1 },
710
+ W => { char => 'W', func => \&cmd_W, type => C_NORMAL },
711
+ B => { char => 'B', func => \&cmd_B, type => C_NORMAL },
712
+ E => { char => 'E', func => \&cmd_E, type => C_NORMAL },
713
+ gE => { char => 'gE', func => \&cmd_gE, type => C_NORMAL,
714
+ selection_needs_move_right => 1 },
715
+ # text-objects, leading _ means can't be mapped!
716
+ _i => { char => 'i', func => \&cmd__i, type => C_TEXTOBJECT },
717
+ _a => { char => 'a', func => \&cmd__a, type => C_TEXTOBJECT },
718
+ # line movement
719
+ '0' => { char => '0', func => \&cmd_0, type => C_NORMAL },
720
+ '^' => { char => '^', func => \&cmd_caret, type => C_NORMAL },
721
+ '$' => { char => '$', func => \&cmd_dollar, type => C_NORMAL },
722
+ # delete chars
723
+ x => { char => 'x', func => \&cmd_x, type => C_NORMAL,
724
+ repeatable => 1, no_operator => 1 },
725
+ X => { char => 'X', func => \&cmd_X, type => C_NORMAL,
726
+ repeatable => 1, no_operator => 1 },
727
+ # C_NORMAL is correct, operator c takes care of insert mode
728
+ s => { char => 's', func => \&cmd_s, type => C_NORMAL,
729
+ repeatable => 1, no_operator => 1 },
730
+ # C_NORMAL is correct, operator c takes care of insert mode
731
+ S => { char => 'S', func => \&cmd_S, type => C_NORMAL,
732
+ repeatable => 1, no_operator => 1 },
733
+ # insert mode
734
+ i => { char => 'i', func => \&cmd_i, type => C_INSERT,
735
+ repeatable => 1, no_operator => 1 },
736
+ I => { char => 'I', func => \&cmd_I, type => C_INSERT,
737
+ repeatable => 1, no_operator => 1 },
738
+ a => { char => 'a', func => \&cmd_a, type => C_INSERT,
739
+ repeatable => 1, no_operator => 1 },
740
+ A => { char => 'A', func => \&cmd_A, type => C_INSERT,
741
+ repeatable => 1, no_operator => 1 },
742
+ # replace
743
+ r => { char => 'r', func => \&cmd_r, type => C_NEEDSKEY,
744
+ repeatable => 1, no_operator => 1 },
745
+ # paste
746
+ p => { char => 'p', func => \&cmd_p, type => C_NORMAL,
747
+ repeatable => 1, no_operator => 1 },
748
+ P => { char => 'P', func => \&cmd_P, type => C_NORMAL,
749
+ repeatable => 1, no_operator => 1 },
750
+ # to end of line
751
+ C => { char => 'C', func => \&cmd_C, type => C_NORMAL,
752
+ repeatable => 1, no_operator => 1 },
753
+ D => { char => 'D', func => \&cmd_D, type => C_NORMAL,
754
+ repeatable => 1, no_operator => 1 },
755
+ # scrolling
756
+ "\x05" => { char => '<C-E>', func => \&cmd_ctrl_d, type => C_NORMAL,
757
+ no_operator => 1 },
758
+ "\x04" => { char => '<C-D>', func => \&cmd_ctrl_d, type => C_NORMAL,
759
+ needs_count => 1, no_operator => 1 },
760
+ "\x19" => { char => '<C-Y>', func => \&cmd_ctrl_u, type => C_NORMAL,
761
+ no_operator => 1 },
762
+ "\x15" => { char => '<C-U>', func => \&cmd_ctrl_u, type => C_NORMAL,
763
+ needs_count => 1, no_operator => 1 },
764
+ "\x06" => { char => '<C-F>', func => \&cmd_ctrl_f, type => C_NORMAL,
765
+ no_operator => 1 },
766
+ "\x02" => { char => '<C-B>', func => \&cmd_ctrl_b, type => C_NORMAL,
767
+ no_operator => 1 },
768
+ # window switching
769
+ "\x17j" => { char => '<C-W>j', func => \&cmd_ctrl_wj, type => C_NORMAL,
770
+ no_operator => 1 },
771
+ "\x17k" => { char => '<C-W>k', func => \&cmd_ctrl_wk, type => C_NORMAL,
772
+ no_operator => 1 },
773
+ "\x1e" => { char => '<C-^>', func => \&cmd_ctrl_6, type => C_NORMAL,
774
+ no_operator => 1 },
775
+ # misc
776
+ '~' => { char => '~', func => \&cmd_tilde, type => C_NORMAL,
777
+ repeatable => 1, no_operator => 1 },
778
+ '"' => { char => '"', func => \&cmd_register, type => C_NEEDSKEY,
779
+ no_operator => 1 },
780
+ '.' => { char => '.', type => C_NORMAL, repeatable => 1,
781
+ no_operator => 1 },
782
+ ':' => { char => ':', type => C_NORMAL },
783
+ "\n" => { char => '<CR>', type => C_NORMAL }, # return
784
+ # undo
785
+ 'u' => { char => 'u', func => \&cmd_undo, type => C_NORMAL,
786
+ no_operator => 1 },
787
+ "\x12" => { char => '<C-R>', func => \&cmd_redo, type => C_NORMAL,
788
+ no_operator => 1 },
789
+ };
790
+
791
+ # All available commands in Ex-Mode.
792
+ my $commands_ex
793
+ = {
794
+ # arrow keys - not actually used, see handle_input_buffer()
795
+ # TODO: make these work.
796
+ "\e[A" => { char => ':exprev', func => \&ex_history_back,
797
+ type => C_EX },
798
+ "\e[B" => { char => ':exnext', func => \&ex_history_fwd,
799
+ type => C_EX },
800
+
801
+ # normal Ex mode commands.
802
+ eh => { char => ':exhist', func => \&ex_history_show,
803
+ type => C_EX },
804
+ s => { char => ':s', func => \&ex_substitute,
805
+ type => C_EX },
806
+ bnext => { char => ':bnext', func => \&ex_bnext,
807
+ type => C_EX, uses_count => 1 },
808
+ bn => { char => ':bn', func => \&ex_bnext,
809
+ type => C_EX, uses_count => 1 },
810
+ bprev => { char => ':bprev', func => \&ex_bprev,
811
+ type => C_EX, uses_count => 1 },
812
+ bp => { char => ':bp', func => \&ex_bprev,
813
+ type => C_EX, uses_count => 1 },
814
+ bdelete => { char => ':bdelete', func => \&ex_bdelete,
815
+ type => C_EX, uses_count => 1 },
816
+ bd => { char => ':bd', func => \&ex_bdelete,
817
+ type => C_EX, uses_count => 1 },
818
+ buffer => { char => ':buffer', func => \&ex_buffer,
819
+ type => C_EX, uses_count => 1 },
820
+ b => { char => ':b', func => \&ex_buffer,
821
+ type => C_EX, uses_count => 1 },
822
+ registers => { char => ':registers', func => \&ex_registers,
823
+ type => C_EX },
824
+ reg => { char => ':reg', func => \&ex_registers,
825
+ type => C_EX },
826
+ display => { char => ':display', func => \&ex_registers,
827
+ type => C_EX },
828
+ di => { char => ':di', func => \&ex_registers,
829
+ type => C_EX },
830
+ buffers => { char => ':buffer', func => \&ex_buffers,
831
+ type => C_EX },
832
+ ls => { char => ':ls', func => \&ex_buffers,
833
+ type => C_EX },
834
+ undolist => { char => ':undolist', func => \&ex_undolist,
835
+ type => C_EX },
836
+ undol => { char => ':undol', func => \&ex_undolist,
837
+ type => C_EX },
838
+ map => { char => ':map', func => \&ex_map,
839
+ type => C_EX },
840
+ unmap => { char => ':unmap', func => \&ex_unmap,
841
+ type => C_EX },
842
+ unm => { char => ':unm', func => \&ex_unmap,
843
+ type => C_EX },
844
+ source => { char => ':source', func => \&ex_source,
845
+ type => C_EX },
846
+ so => { char => ':so', func => \&ex_source,
847
+ type => C_EX },
848
+ mkvimrc => { char => ':mkvimrc', func => \&ex_mkvimrc,
849
+ type => C_EX },
850
+ mkv => { char => ':mkv', func => \&ex_mkvimrc,
851
+ type => C_EX },
852
+ se => { char => ':se', func => \&ex_set,
853
+ type => C_EX },
854
+ set => { char => ':set', func => \&ex_set,
855
+ type => C_EX },
856
+ itemnext => { char => ':itemnext', func => \&ex_item_next,
857
+ type => C_EX },
858
+ inext => { char => ':itemnext', func => \&ex_item_next,
859
+ type => C_EX },
860
+ itemprev => { char => ':itemprev', func => \&ex_item_prev,
861
+ type => C_EX },
862
+ iprev => { char => ':itemprev', func => \&ex_item_prev,
863
+ type => C_EX },
864
+ servernext => { char => ':servernext', func => \&ex_server_next,
865
+ type => C_EX },
866
+ snext => { char => ':servernext', func => \&ex_server_next,
867
+ type => C_EX },
868
+ serverprev => { char => ':serverprev', func => \&ex_server_prev,
869
+ type => C_EX },
870
+ sprev => { char => ':serverprev', func => \&ex_server_prev,
871
+ type => C_EX },
872
+
873
+ };
874
+
875
+ # MAPPINGS
876
+
877
+ # default command mode mappings
878
+ my $maps = {};
879
+
880
+ # current imap still pending (first character entered)
881
+ my $imap = undef;
882
+
883
+ # maps for insert mode
884
+ my $imaps
885
+ = {
886
+ # CTRL-R, insert register
887
+ "\x12" => { map => undef, func => \&insert_ctrl_r },
888
+ };
889
+
890
+
891
+ # GLOBAL VARIABLES
892
+
893
+ # all vim_mode settings, must be enabled in vim_mode_init() before usage
894
+ my $settings
895
+ = {
896
+ # print debug output
897
+ debug => { type => S_BOOL, value => 0 },
898
+ # use UTF-8 internally for string calculations/manipulations
899
+ utf8 => { type => S_BOOL, value => 1 },
900
+ # esc-shortcut in insert mode
901
+ cmd_seq => { type => S_STR, value => '' },
902
+ # start every line in command mode
903
+ start_cmd => { type => S_BOOL, value => 0 },
904
+ # not used yet
905
+ max_undo_lines => { type => S_INT, value => 50 },
906
+ # size of history buffer for Ex mode.
907
+ ex_history_size => { type => S_INT, value => 100 },
908
+ # prompt_leading_space
909
+ prompt_leading_space => { type => S_BOOL, value => 1 },
910
+ # <Leader> value for prepending to commands.
911
+ map_leader => { type => S_STR, value => '\\' },
912
+ # timeout for keys following esc. In milliseconds.
913
+ esc_buf_timeout => { type => S_TIME, value => '10ms' },
914
+
915
+ };
916
+
917
+ sub DEBUG { $settings->{debug}->{value} }
918
+
919
+ # buffer to keep track of the last N keystrokes, used for Esc detection and
920
+ # insert mode mappings
921
+ my @input_buf;
922
+ my $input_buf_timer;
923
+ my $input_buf_enabled = 0;
924
+
925
+ # insert mode repeat buffer, used to repeat (.) last insert
926
+ my @insert_buf;
927
+
928
+ # flag to allow us to emulate keystrokes without re-intercepting them
929
+ my $should_ignore = 0;
930
+
931
+ # ex mode buffer
932
+ my @ex_buf;
933
+
934
+ # ex mode history storage.
935
+ my @ex_history;
936
+ my $ex_history_index = 0;
937
+
938
+ # we are waiting for another mapped key (e.g. g pressed, but there are
939
+ # multiple mappings like gg gE etc.)
940
+ my $pending_map = undef;
941
+
942
+ # for commands like 10x
943
+ my $numeric_prefix = undef;
944
+ # current operator as $command hash
945
+ my $operator = undef;
946
+ # vi movements, only used when a movement needs more than one key (like f t).
947
+ my $movement = undef;
948
+ # last vi command, used by .
949
+ my $last
950
+ = {
951
+ 'cmd' => $commands->{i}, # = i to support . when loading the script
952
+ 'numeric_prefix' => undef,
953
+ 'operator' => undef,
954
+ 'movement' => undef,
955
+ 'register' => '"',
956
+ };
957
+ # last ftFT movement, used by ; and ,
958
+ my $last_ftFT
959
+ = {
960
+ type => undef, # f, t, F or T
961
+ char => undef,
962
+ };
963
+
964
+ # what Vi mode we're in. We start in insert mode.
965
+ my $mode = M_INS;
966
+
967
+ # current active register
968
+ my $register = '"';
969
+
970
+ # vi registers
971
+ my $registers
972
+ = {
973
+ '"' => '', # default register
974
+ '0' => '', # yank register
975
+ '+' => '', # contains irssi's cut buffer
976
+ '*' => '', # same
977
+ '_' => '', # black hole register, always empty
978
+ };
979
+
980
+ # index into the history list (for j,k)
981
+ my $history_index = undef;
982
+ # current line, necessary for j,k or the current input line gets destroyed
983
+ my $history_input = undef;
984
+ # position in input line
985
+ my $history_pos = 0;
986
+
987
+ # Undo/redo buffer.
988
+
989
+ my @undo_buffer;
990
+ my $undo_index = undef;
991
+
992
+ # tab completion state vars
993
+
994
+ my @tab_candidates;
995
+ my $completion_active = 0;
996
+ my $completion_string = '';
997
+
998
+ sub script_is_loaded {
999
+ return exists($Irssi::Script::{shift(@_) . '::'});
1000
+ }
1001
+
1002
+
1003
+
1004
+
1005
+ # INSERT MODE COMMANDS
1006
+
1007
+ sub insert_ctrl_r {
1008
+ my ($key) = @_;
1009
+ _debug("ctrl-r called");
1010
+ my $char = chr($key);
1011
+ _debug("ctrl-r called with $char");
1012
+
1013
+ return if not defined $registers->{$char} or $registers->{$char} eq '';
1014
+
1015
+ my $pos = _insert_at_position($registers->{$char}, 1, _input_pos());
1016
+ _input_pos($pos + 1);
1017
+ }
1018
+
1019
+
1020
+ # COMMAND MODE OPERATORS
1021
+
1022
+ sub cmd_operator_c {
1023
+ my ($old_pos, $new_pos, $move_cmd, $repeat) = @_;
1024
+
1025
+ # Changing a word or WORD doesn't delete the last space before a word, but
1026
+ # not if we are on that whitespace before the word.
1027
+ if ($move_cmd and ($move_cmd == $commands->{w} or
1028
+ $move_cmd == $commands->{W})) {
1029
+ my $input = _input();
1030
+ if ($new_pos - $old_pos > 1 and
1031
+ substr($input, $new_pos - 1, 1) =~ /\s/) {
1032
+ $new_pos--;
1033
+ }
1034
+ }
1035
+
1036
+ cmd_operator_d($old_pos, $new_pos, $move_cmd, $repeat, 1);
1037
+
1038
+ if (!$repeat) {
1039
+ _update_mode(M_INS);
1040
+ } else {
1041
+ my $pos = _input_pos();
1042
+ $pos = _insert_buffer(1, $pos);
1043
+ _input_pos($pos);
1044
+ }
1045
+ }
1046
+
1047
+ sub cmd_operator_d {
1048
+ my ($old_pos, $new_pos, $move_cmd, $repeat, $change) = @_;
1049
+
1050
+ my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move_cmd);
1051
+
1052
+ # Remove the selected string from the input.
1053
+ my $input = _input();
1054
+ my $string = substr $input, $pos, $length, '';
1055
+ if ($register =~ /[A-Z]/) {
1056
+ $registers->{lc $register} .= $string;
1057
+ print "Deleted into $register: ", $registers->{lc $register} if DEBUG;
1058
+ } else {
1059
+ $registers->{$register} = $string;
1060
+ print "Deleted into $register: ", $registers->{$register} if DEBUG;
1061
+ }
1062
+ _input($input);
1063
+
1064
+ # Prevent moving after the text when we delete the last character. But not
1065
+ # when changing (C).
1066
+ $pos-- if $pos == length($input) and !$change;
1067
+
1068
+ _input_pos($pos);
1069
+ }
1070
+
1071
+ sub cmd_operator_y {
1072
+ my ($old_pos, $new_pos, $move_cmd, $repeat) = @_;
1073
+
1074
+ my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move_cmd);
1075
+
1076
+ # Extract the selected string and put it in the " register.
1077
+ my $input = _input();
1078
+ my $string = substr $input, $pos, $length;
1079
+ if ($register =~ /[A-Z]/) {
1080
+ $registers->{lc $register} .= $string;
1081
+ print "Yanked into $register: ", $registers->{lc $register} if DEBUG;
1082
+ } else {
1083
+ $registers->{$register} = $string;
1084
+ print "Yanked into $register: ", $registers->{$register} if DEBUG;
1085
+ if ($register eq '"') {
1086
+ $registers->{0} = $string;
1087
+ print "Yanked into 0: ", $registers->{0} if DEBUG;
1088
+ }
1089
+ }
1090
+
1091
+ # Always move to the lower position.
1092
+ if ($old_pos > $new_pos) {
1093
+ _input_pos($new_pos);
1094
+ } else {
1095
+ _input_pos($old_pos);
1096
+ }
1097
+ }
1098
+
1099
+ sub _get_pos_and_length {
1100
+ my ($old_pos, $new_pos, $move_cmd) = @_;
1101
+
1102
+ my $length = $new_pos - $old_pos;
1103
+ # We need a positive length and $old_pos must be smaller.
1104
+ if ($length < 0) {
1105
+ $old_pos = $new_pos;
1106
+ $length *= -1;
1107
+ }
1108
+
1109
+ # Some commands don't move one character after the deletion area which is
1110
+ # necessary for all commands moving to the right. Fix it.
1111
+ if ($move_cmd->{selection_needs_move_right}) {
1112
+ $length += 1;
1113
+ }
1114
+
1115
+ return ($old_pos, $length);
1116
+ }
1117
+
1118
+ # COMMAND MODE COMMANDS
1119
+
1120
+ sub cmd_h {
1121
+ my ($count, $pos, $repeat) = @_;
1122
+
1123
+ $pos -= $count;
1124
+ $pos = 0 if $pos < 0;
1125
+ return (undef, $pos);
1126
+ }
1127
+
1128
+ sub cmd_l {
1129
+ my ($count, $pos, $repeat) = @_;
1130
+
1131
+ my $length = _input_len();
1132
+ $pos += $count;
1133
+ $pos = _fix_input_pos($pos, $length);
1134
+ return (undef, $pos);
1135
+ }
1136
+
1137
+ # later history (down)
1138
+ sub cmd_j {
1139
+ my ($count, $pos, $repeat) = @_;
1140
+
1141
+ if (Irssi::version < 20090117) {
1142
+ # simulate a down-arrow
1143
+ _emulate_keystrokes(0x1b, 0x5b, 0x42);
1144
+ return (undef, undef);
1145
+ }
1146
+
1147
+ my @history = Irssi::active_win->get_history_lines();
1148
+
1149
+ if (defined $history_index) {
1150
+ $history_index += $count;
1151
+ print "History Index: $history_index" if DEBUG;
1152
+ # Prevent destroying the current input when pressing j after entering
1153
+ # command mode. Not exactly like in default irssi, but simplest solution
1154
+ # (and S can be used to clear the input line fast, which is what <down>
1155
+ # does in plain irssi).
1156
+ } else {
1157
+ return (undef, undef);
1158
+ }
1159
+
1160
+ if ($history_index > $#history) {
1161
+ # Restore the input line.
1162
+ _input($history_input);
1163
+ _input_pos($history_pos);
1164
+ $history_index = $#history + 1;
1165
+ } elsif ($history_index >= 0) {
1166
+ my $history = $history[$history_index];
1167
+ # History is not in UTF-8!
1168
+ if ($settings->{utf8}->{value}) {
1169
+ $history = decode_utf8($history);
1170
+ }
1171
+ _input($history);
1172
+ _input_pos(0);
1173
+ }
1174
+ return (undef, undef);
1175
+ }
1176
+
1177
+ # earlier history (up)
1178
+ sub cmd_k {
1179
+ my ($count, $pos, $repeat) = @_;
1180
+
1181
+ if (Irssi::version < 20090117) {
1182
+ # simulate an up-arrow
1183
+ _emulate_keystrokes(0x1b, 0x5b, 0x41);
1184
+ return (undef, undef);
1185
+ }
1186
+
1187
+ my @history = Irssi::active_win->get_history_lines();
1188
+
1189
+ if (defined $history_index) {
1190
+ $history_index -= $count;
1191
+ $history_index = 0 if $history_index < 0;
1192
+ } else {
1193
+ $history_index = $#history;
1194
+ $history_input = _input();
1195
+ $history_pos = _input_pos();
1196
+ }
1197
+ print "History Index: $history_index" if DEBUG;
1198
+ if ($history_index >= 0) {
1199
+ my $history = $history[$history_index];
1200
+ # History is not in UTF-8!
1201
+ if ($settings->{utf8}->{value}) {
1202
+ $history = decode_utf8($history);
1203
+ }
1204
+ _input($history);
1205
+ _input_pos(0);
1206
+ }
1207
+ return (undef, undef);
1208
+ }
1209
+
1210
+ sub cmd_G {
1211
+ my ($count, $pos, $repeat) = @_;
1212
+
1213
+ if (Irssi::version < 20090117) {
1214
+ _warn("G and gg not supported in irssi < 0.8.13");
1215
+ return;
1216
+ }
1217
+
1218
+ my @history = Irssi::active_win->get_history_lines();
1219
+
1220
+ # Go to the current input line if no count was given or it's too big.
1221
+ if (not $count or $count - 1 >= scalar @history) {
1222
+ if (defined $history_input and defined $history_pos) {
1223
+ _input($history_input);
1224
+ _input_pos($history_pos);
1225
+ $history_index = undef;
1226
+ }
1227
+ return;
1228
+ } else {
1229
+ # Save input line so it doesn't get lost.
1230
+ if (not defined $history_index) {
1231
+ $history_input = _input();
1232
+ $history_pos = _input_pos();
1233
+ }
1234
+ $history_index = $count - 1;
1235
+ }
1236
+
1237
+ my $history = $history[$history_index];
1238
+ # History is not in UTF-8!
1239
+ if ($settings->{utf8}->{value}) {
1240
+ $history = decode_utf8($history);
1241
+ }
1242
+ _input($history);
1243
+ _input_pos(0);
1244
+
1245
+ return (undef, undef);
1246
+ }
1247
+
1248
+ sub cmd_gg {
1249
+ my ($count, $pos, $repeat) = @_;
1250
+
1251
+ return cmd_G(1, $pos, $repeat);
1252
+ }
1253
+
1254
+ sub cmd_f {
1255
+ my ($count, $pos, $repeat, $char) = @_;
1256
+
1257
+ $pos = _next_occurrence(_input(), $char, $count, $pos);
1258
+
1259
+ $last_ftFT = { type => 'f', char => $char };
1260
+ return (undef, $pos);
1261
+ }
1262
+
1263
+ sub cmd_t {
1264
+ my ($count, $pos, $repeat, $char) = @_;
1265
+
1266
+ $pos = _next_occurrence(_input(), $char, $count, $pos);
1267
+ if (defined $pos) {
1268
+ $pos--;
1269
+ }
1270
+
1271
+ $last_ftFT = { type => 't', char => $char };
1272
+ return (undef, $pos);
1273
+ }
1274
+
1275
+ sub cmd_F {
1276
+ my ($count, $pos, $repeat, $char) = @_;
1277
+
1278
+ my $input = reverse _input();
1279
+ $pos = _next_occurrence($input, $char, $count, length($input) - $pos - 1);
1280
+ if (defined $pos) {
1281
+ $pos = length($input) - $pos - 1;
1282
+ }
1283
+
1284
+ $last_ftFT = { type => 'F', char => $char };
1285
+ return (undef, $pos);
1286
+ }
1287
+
1288
+ sub cmd_T {
1289
+ my ($count, $pos, $repeat, $char) = @_;
1290
+
1291
+ my $input = reverse _input();
1292
+ $pos = _next_occurrence($input, $char, $count, length($input) - $pos - 1);
1293
+ if (defined $pos) {
1294
+ $pos = length($input) - $pos - 1 + 1;
1295
+ }
1296
+
1297
+ $last_ftFT = { type => 'T', char => $char };
1298
+ return (undef, $pos);
1299
+ }
1300
+
1301
+ # Find $count-th next occurrence of $char.
1302
+ sub _next_occurrence {
1303
+ my ($input, $char, $count, $pos) = @_;
1304
+
1305
+ while ($count-- > 0) {
1306
+ $pos = index $input, $char, $pos + 1;
1307
+ if ($pos == -1) {
1308
+ return undef;
1309
+ }
1310
+ }
1311
+ return $pos;
1312
+ }
1313
+
1314
+ sub cmd_semicolon {
1315
+ my ($count, $pos, $repeat) = @_;
1316
+
1317
+ return (undef, undef) if not defined $last_ftFT->{type};
1318
+
1319
+ (undef, $pos)
1320
+ = $commands->{$last_ftFT->{type}}
1321
+ ->{func}($count, $pos, $repeat, $last_ftFT->{char});
1322
+ return (undef, $pos);
1323
+ }
1324
+
1325
+ sub cmd_comma {
1326
+ my ($count, $pos, $repeat) = @_;
1327
+
1328
+ return (undef, undef) if not defined $last_ftFT->{type};
1329
+
1330
+ # Change direction.
1331
+ my $save = $last_ftFT->{type};
1332
+ my $type = $save;
1333
+ $type =~ tr/ftFT/FTft/;
1334
+
1335
+ (undef, $pos)
1336
+ = $commands->{$type}
1337
+ ->{func}($count, $pos, $repeat, $last_ftFT->{char});
1338
+ # Restore type as the move functions overwrites it.
1339
+ $last_ftFT->{type} = $save;
1340
+ return (undef, $pos);
1341
+ }
1342
+
1343
+ sub cmd_w {
1344
+ my ($count, $pos, $repeat) = @_;
1345
+
1346
+ my $input = _input();
1347
+ $pos = _beginning_of_word($input, $count, $pos);
1348
+ $pos = _fix_input_pos($pos, length $input);
1349
+ return (undef, $pos);
1350
+ }
1351
+
1352
+ sub cmd_b {
1353
+ my ($count, $pos, $repeat) = @_;
1354
+
1355
+ my $input = reverse _input();
1356
+ $pos = length($input) - $pos - 1;
1357
+ $pos = 0 if ($pos < 0);
1358
+
1359
+ $pos = _end_of_word($input, $count, $pos);
1360
+ $pos = length($input) - $pos - 1;
1361
+ $pos = 0 if ($pos < 0);
1362
+ return (undef, $pos);
1363
+ }
1364
+
1365
+ sub cmd_e {
1366
+ my ($count, $pos, $repeat) = @_;
1367
+
1368
+ my $input = _input();
1369
+ $pos = _end_of_word($input, $count, $pos);
1370
+ $pos = _fix_input_pos($pos, length $input);
1371
+ return (undef, $pos);
1372
+ }
1373
+
1374
+ sub cmd_ge {
1375
+ my ($count, $pos, $repeat, $char) = @_;
1376
+
1377
+ my $input = reverse _input();
1378
+ $pos = length($input) - $pos - 1;
1379
+ $pos = 0 if ($pos < 0);
1380
+
1381
+ $pos = _beginning_of_word($input, $count, $pos);
1382
+ $pos = length($input) - $pos - 1;
1383
+ $pos = 0 if ($pos < 0);
1384
+
1385
+ return (undef, $pos);
1386
+ }
1387
+
1388
+ # Go to the beginning of $count-th word, like vi's w.
1389
+ sub _beginning_of_word {
1390
+ my ($input, $count, $pos) = @_;
1391
+
1392
+ while ($count-- > 0) {
1393
+ # Go to end of next word/non-word.
1394
+ if (substr($input, $pos) =~ /^$word+/ or
1395
+ substr($input, $pos) =~ /^$non_word+/) {
1396
+ $pos += $+[0];
1397
+ }
1398
+ # And skip over any whitespace if necessary. This also happens when
1399
+ # we're inside whitespace.
1400
+ if (substr($input, $pos) =~ /^\s+/) {
1401
+ $pos += $+[0];
1402
+ }
1403
+ }
1404
+
1405
+ return $pos;
1406
+ }
1407
+
1408
+ # Go to the end of $count-th word, like vi's e.
1409
+ sub _end_of_word {
1410
+ my ($input, $count, $pos) = @_;
1411
+
1412
+ while ($count-- > 0 and length($input) > $pos) {
1413
+ my $skipped = 0;
1414
+ # Skip over whitespace if in the middle of it or directly after the
1415
+ # current word/non-word.
1416
+ if (substr($input, $pos + 1) =~ /^\s+/) {
1417
+ $pos += $+[0] + 1;
1418
+ $skipped = 1;
1419
+ }
1420
+ elsif (substr($input, $pos) =~ /^\s+/) {
1421
+ $pos += $+[0];
1422
+ $skipped = 1;
1423
+ }
1424
+ # We are inside a word/non-word, skip to the end of it.
1425
+ if (substr($input, $pos) =~ /^$word{2,}/ or
1426
+ substr($input, $pos) =~ /^$non_word{2,}/) {
1427
+ $pos += $+[0] - 1;
1428
+ # We are the border of word/non-word. Skip to the end of the next one.
1429
+ # But not if we've already jumped over whitespace because there could
1430
+ # be only one word/non-word char after the whitespace.
1431
+ } elsif (!$skipped and (substr($input, $pos) =~ /^$word($non_word+)/ or
1432
+ substr($input, $pos) =~ /^$non_word($word+)/)) {
1433
+ $pos += $+[0] - 1;
1434
+ }
1435
+ }
1436
+
1437
+ # Necessary for correct deletion at the end of the line.
1438
+ if (length $input == $pos + 1) {
1439
+ $pos++;
1440
+ }
1441
+
1442
+ return $pos;
1443
+ }
1444
+
1445
+ sub cmd_W {
1446
+ my ($count, $pos, $repeat) = @_;
1447
+
1448
+ my $input = _input();
1449
+ $pos = _beginning_of_WORD($input, $count, $pos);
1450
+ $pos = _fix_input_pos($pos, length $input);
1451
+ return (undef, $pos);
1452
+ }
1453
+
1454
+ sub cmd_B {
1455
+ my ($count, $pos, $repeat) = @_;
1456
+
1457
+ my $input = reverse _input();
1458
+ $pos = _end_of_WORD($input, $count, length($input) - $pos - 1);
1459
+ if ($pos == -1) {
1460
+ return cmd_0();
1461
+ } else {
1462
+ return (undef, length($input) - $pos - 1);
1463
+ }
1464
+ }
1465
+
1466
+ sub cmd_E {
1467
+ my ($count, $pos, $repeat) = @_;
1468
+
1469
+ $pos = _end_of_WORD(_input(), $count, $pos);
1470
+ if ($pos == -1) {
1471
+ return cmd_dollar();
1472
+ } else {
1473
+ return (undef, $pos);
1474
+ }
1475
+ }
1476
+
1477
+ sub cmd_gE {
1478
+ my ($count, $pos, $repeat, $char) = @_;
1479
+
1480
+ my $input = reverse _input();
1481
+ $pos = _beginning_of_WORD($input, $count, length($input) - $pos - 1);
1482
+ if ($pos == -1 or length($input) - $pos - 1 == -1) {
1483
+ return cmd_0();
1484
+ } else {
1485
+ $pos = length($input) - $pos - 1;
1486
+ }
1487
+
1488
+ return (undef, $pos);
1489
+ }
1490
+
1491
+ # Go to beginning of $count-th WORD, like vi's W.
1492
+ sub _beginning_of_WORD {
1493
+ my ($input, $count, $pos) = @_;
1494
+
1495
+ # Necessary for correct movement between two words with only one
1496
+ # whitespace.
1497
+ if (substr($input, $pos) =~ /^\s\S/) {
1498
+ $pos++;
1499
+ $count--;
1500
+ }
1501
+
1502
+ while ($count-- > 0 and length($input) > $pos) {
1503
+ if (substr($input, $pos + 1) !~ /\s+/) {
1504
+ return length($input);
1505
+ }
1506
+ $pos += $+[0] + 1;
1507
+ }
1508
+
1509
+ return $pos;
1510
+ }
1511
+
1512
+ # Go to end of $count-th WORD, like vi's E.
1513
+ sub _end_of_WORD {
1514
+ my ($input, $count, $pos) = @_;
1515
+
1516
+ return $pos if $pos >= length($input);
1517
+
1518
+ # We are inside a WORD, skip to the end of it.
1519
+ if (substr($input, $pos + 1) =~ /^\S+(\s)/) {
1520
+ $pos += $-[1];
1521
+ $count--;
1522
+ }
1523
+
1524
+ while ($count-- > 0) {
1525
+ if (substr($input, $pos) !~ /\s+\S+(\s+)/) {
1526
+ return -1;
1527
+ }
1528
+ $pos += $-[1] - 1;
1529
+ }
1530
+ return $pos;
1531
+ }
1532
+
1533
+ sub cmd__i {
1534
+ my ($count, $pos, $repeat, $char) = @_;
1535
+
1536
+ _warn("i_ not implemented yet");
1537
+ return (undef, undef);
1538
+ }
1539
+
1540
+ sub cmd__a {
1541
+ my ($count, $pos, $repeat, $char) = @_;
1542
+
1543
+ my $cur_pos;
1544
+ my $input = _input();
1545
+
1546
+ # aw and aW
1547
+ if ($char eq 'w' or $char eq 'W') {
1548
+ while ($count-- > 0 and length($input) > $pos) {
1549
+ if (substr($input, $pos, 1) =~ /\s/) {
1550
+ # Any whitespace before the word/WORD must be removed.
1551
+ if (not defined $cur_pos) {
1552
+ $cur_pos = _find_regex_before($input, '\S', $pos, 0);
1553
+ if ($cur_pos < 0) {
1554
+ $cur_pos = 0;
1555
+ } else {
1556
+ $cur_pos++;
1557
+ }
1558
+ }
1559
+ # Move before the word/WORD.
1560
+ if (substr($input, $pos + 1) =~ /^\s+/) {
1561
+ $pos += $+[0];
1562
+ }
1563
+ # And delete the word.
1564
+ if ($char eq 'w') {
1565
+ if (substr($input, $pos) =~ /^\s($word+|$non_word+)/) {
1566
+ $pos += $+[0];
1567
+ } else {
1568
+ $pos = length($input);
1569
+ }
1570
+ # WORD
1571
+ } else {
1572
+ if (substr($input, $pos + 1) =~ /\s/) {
1573
+ $pos += $-[0] + 1;
1574
+ } else {
1575
+ $pos = length($input);
1576
+ }
1577
+ }
1578
+
1579
+ # word
1580
+ } elsif ($char eq 'w') {
1581
+ # Start at the beginning of this WORD.
1582
+ if (not defined $cur_pos and $pos > 0
1583
+ and substr($input, $pos - 1, 2)
1584
+ !~ /(\s.|$word$non_word|$non_word$word)/) {
1585
+
1586
+ $cur_pos = _find_regex_before
1587
+ (
1588
+ $input,
1589
+ "^($word+$non_word|$non_word+$word|$word+\\s|$non_word+\\s)",
1590
+ $pos, 1
1591
+ );
1592
+
1593
+ if ($cur_pos < 0) {
1594
+ $cur_pos = 0;
1595
+ } else {
1596
+ $cur_pos += 2;
1597
+ }
1598
+ }
1599
+ # Delete to the end of the word.
1600
+ if (substr($input, $pos) =~
1601
+ /^($word+$non_word|$non_word+$word|$word+\s+\S|$non_word+\s+\S)/) {
1602
+
1603
+ $pos += $+[0] - 1;
1604
+ } else {
1605
+ $pos = length($input);
1606
+ # If we are at the end of the line, whitespace before
1607
+ # the word is also deleted.
1608
+ my $new_pos = _find_regex_before
1609
+ ($input,
1610
+ "^($word+\\s+|$non_word+\\s+)",
1611
+ $pos, 1);
1612
+
1613
+ if ($new_pos != -1 and
1614
+ (not defined $cur_pos or
1615
+ $cur_pos > $new_pos + 1)) {
1616
+
1617
+ $cur_pos = $new_pos + 1;
1618
+ }
1619
+ }
1620
+
1621
+ # WORD
1622
+ } else {
1623
+ # Start at the beginning of this WORD.
1624
+ if (not defined $cur_pos and $pos > 0 and
1625
+ substr($input, $pos - 1, 1) !~ /\s/) {
1626
+ $cur_pos = _find_regex_before($input, '\s', $pos - 1, 0);
1627
+ if ($cur_pos < 0) {
1628
+ $cur_pos = 0;
1629
+ } else {
1630
+ $cur_pos++;
1631
+ }
1632
+ }
1633
+ # Delete to the end of the word.
1634
+ if (substr($input, $pos + 1) =~ /^\S*\s+\S/) {
1635
+ $pos += $+[0];
1636
+ } else {
1637
+ $pos = length($input);
1638
+ # If we are at the end of the line, whitespace before
1639
+ # the WORD is also deleted.
1640
+ my $new_pos = _find_regex_before($input, '\s+', $pos, 1);
1641
+ if (not defined $cur_pos or $cur_pos > $new_pos + 1) {
1642
+ $cur_pos = $new_pos + 1;
1643
+ }
1644
+ }
1645
+ }
1646
+ }
1647
+ }
1648
+
1649
+ return ($cur_pos, $pos);
1650
+ }
1651
+
1652
+ # Find regex as close as possible before the current position. If $end is true
1653
+ # the end of the match is returned, otherwise the beginning.
1654
+ sub _find_regex_before {
1655
+ my ($input, $regex, $pos, $end) = @_;
1656
+
1657
+ $input = reverse $input;
1658
+ $pos = length($input) - $pos - 1;
1659
+ $pos = 0 if $pos < 0;
1660
+
1661
+ if (substr($input, $pos) =~ /$regex/) {
1662
+ if (!$end) {
1663
+ $pos += $-[0];
1664
+ } else {
1665
+ $pos += $+[0];
1666
+ }
1667
+ return length($input) - $pos - 1;
1668
+ } else {
1669
+ return -1;
1670
+ }
1671
+ }
1672
+
1673
+ sub cmd_0 {
1674
+ return (undef, 0);
1675
+ }
1676
+
1677
+ sub cmd_caret {
1678
+ my $input = _input();
1679
+ my $pos;
1680
+ # No whitespace at all.
1681
+ if ($input !~ m/^\s/) {
1682
+ $pos = 0;
1683
+ # Some non-whitespace, go to first one.
1684
+ } elsif ($input =~ m/[^\s]/) {
1685
+ $pos = $-[0];
1686
+ # Only whitespace, go to the end.
1687
+ } else {
1688
+ $pos = _fix_input_pos(length $input, length $input);
1689
+ }
1690
+ return (undef, $pos);
1691
+ }
1692
+
1693
+ sub cmd_dollar {
1694
+ my $length = _input_len();
1695
+ return (undef, _fix_input_pos($length, $length));
1696
+ }
1697
+
1698
+ sub cmd_x {
1699
+ my ($count, $pos, $repeat) = @_;
1700
+
1701
+ cmd_operator_d($pos, $pos + $count, $commands->{x}, $repeat);
1702
+ return (undef, undef);
1703
+ }
1704
+
1705
+ sub cmd_X {
1706
+ my ($count, $pos, $repeat) = @_;
1707
+
1708
+ return (undef, undef) if $pos == 0;
1709
+
1710
+ my $new = $pos - $count;
1711
+ $new = 0 if $new < 0;
1712
+ cmd_operator_d($pos, $new, $commands->{X}, $repeat);
1713
+ return (undef, undef);
1714
+ }
1715
+
1716
+ sub cmd_s {
1717
+ my ($count, $pos, $repeat) = @_;
1718
+
1719
+ $operator = $commands->{c};
1720
+ return (undef, $pos + $count);
1721
+ }
1722
+
1723
+ sub cmd_S {
1724
+ my ($count, $pos, $repeat) = @_;
1725
+
1726
+ $operator = $commands->{c};
1727
+ return (0, _input_len());
1728
+ }
1729
+
1730
+ sub cmd_i {
1731
+ my ($count, $pos, $repeat) = @_;
1732
+
1733
+ if (!$repeat) {
1734
+ _update_mode(M_INS);
1735
+ } else {
1736
+ $pos = _insert_buffer($count, $pos);
1737
+ }
1738
+ return (undef, $pos);
1739
+ }
1740
+
1741
+ sub cmd_I {
1742
+ my ($count, $pos, $repeat) = @_;
1743
+
1744
+ $pos = cmd_caret();
1745
+ if (!$repeat) {
1746
+ _update_mode(M_INS);
1747
+ } else {
1748
+ $pos = _insert_buffer($count, $pos);
1749
+ }
1750
+ return (undef, $pos);
1751
+ }
1752
+
1753
+ sub cmd_a {
1754
+ my ($count, $pos, $repeat) = @_;
1755
+
1756
+ # Move after current character. Can't use cmd_l() because we need to mover
1757
+ # after last character at the end of the line.
1758
+ my $length = _input_len();
1759
+ $pos += 1;
1760
+ $pos = $length if $pos > $length;
1761
+
1762
+ if (!$repeat) {
1763
+ _update_mode(M_INS);
1764
+ } else {
1765
+ $pos = _insert_buffer($count, $pos);
1766
+ }
1767
+ return (undef, $pos);
1768
+ }
1769
+
1770
+ sub cmd_A {
1771
+ my ($count, $pos, $repeat) = @_;
1772
+
1773
+ $pos = _input_len();
1774
+
1775
+ if (!$repeat) {
1776
+ _update_mode(M_INS);
1777
+ } else {
1778
+ $pos = _insert_buffer($count, $pos);
1779
+ }
1780
+ return (undef, $pos);
1781
+ }
1782
+
1783
+ # Add @insert_buf to _input() at the given position.
1784
+ sub _insert_buffer {
1785
+ my ($count, $pos) = @_;
1786
+ return _insert_at_position(join('', @insert_buf), $count, $pos);
1787
+ }
1788
+
1789
+ sub _insert_at_position {
1790
+ my ($string, $count, $pos) = @_;
1791
+
1792
+ $string = $string x $count;
1793
+
1794
+ my $input = _input();
1795
+ # Check if we are not at the end of the line to prevent substr outside of
1796
+ # string error.
1797
+ if (length $input > $pos) {
1798
+ substr($input, $pos, 0) = $string;
1799
+ } else {
1800
+ $input .= $string;
1801
+ }
1802
+ _input($input);
1803
+
1804
+ return $pos - 1 + length $string;
1805
+ }
1806
+
1807
+ sub cmd_r {
1808
+ my ($count, $pos, $repeat, $char) = @_;
1809
+
1810
+ my $input = _input();
1811
+
1812
+ # Abort if at end of the line.
1813
+ return (undef, undef) if length($input) < $pos + $count;
1814
+
1815
+ substr $input, $pos, $count, $char x $count;
1816
+ _input($input);
1817
+ return (undef, $pos + $count - 1);
1818
+ }
1819
+
1820
+ sub cmd_p {
1821
+ my ($count, $pos, $repeat) = @_;
1822
+ $pos = _paste_at_position($count, $pos + 1);
1823
+ return (undef, $pos);
1824
+ }
1825
+
1826
+ sub cmd_P {
1827
+ my ($count, $pos, $repeat) = @_;
1828
+ $pos = _paste_at_position($count, $pos);
1829
+ return (undef, $pos);
1830
+ }
1831
+
1832
+ sub _paste_at_position {
1833
+ my ($count, $pos) = @_;
1834
+
1835
+ return if $registers->{$register} eq '';
1836
+ return _insert_at_position($registers->{$register}, $count, $pos);
1837
+ }
1838
+
1839
+ sub cmd_C {
1840
+ my ($count, $pos, $repeat) = @_;
1841
+
1842
+ $operator = $commands->{c};
1843
+ return (undef, _input_len());
1844
+ }
1845
+
1846
+ sub cmd_D {
1847
+ my ($count, $pos, $repeat) = @_;
1848
+
1849
+ $operator = $commands->{d};
1850
+ return (undef, _input_len());
1851
+ }
1852
+
1853
+ sub cmd_ctrl_d {
1854
+ my ($count, $pos, $repeat) = @_;
1855
+
1856
+ my $window = Irssi::active_win();
1857
+ # no count = half of screen
1858
+ if (not defined $count) {
1859
+ $count = $window->{height} / 2;
1860
+ }
1861
+ $window->view()->scroll($count);
1862
+
1863
+ Irssi::statusbar_items_redraw('more');
1864
+ return (undef, undef);
1865
+ }
1866
+
1867
+ sub cmd_ctrl_u {
1868
+ my ($count, $pos, $repeat) = @_;
1869
+
1870
+ my $window = Irssi::active_win();
1871
+ # no count = half of screen
1872
+ if (not defined $count) {
1873
+ $count = $window->{height} / 2;
1874
+ }
1875
+ $window->view()->scroll($count * -1);
1876
+
1877
+ Irssi::statusbar_items_redraw('more');
1878
+ return (undef, undef);
1879
+ }
1880
+
1881
+ sub cmd_ctrl_f {
1882
+ my ($count, $pos, $repeat) = @_;
1883
+
1884
+ my $window = Irssi::active_win();
1885
+ $window->view()->scroll($count * $window->{height});
1886
+
1887
+ Irssi::statusbar_items_redraw('more');
1888
+ return (undef, undef);
1889
+ }
1890
+
1891
+ sub cmd_ctrl_b {
1892
+ my ($count, $pos, $repeat) = @_;
1893
+
1894
+ return cmd_ctrl_f($count * -1, $pos, $repeat);
1895
+ }
1896
+
1897
+ sub cmd_ctrl_wj {
1898
+ my ($count, $pos, $repeat) = @_;
1899
+
1900
+ while ($count-- > 0) {
1901
+ Irssi::command('window down');
1902
+ }
1903
+
1904
+ return (undef, undef);
1905
+ }
1906
+
1907
+ sub cmd_ctrl_wk {
1908
+ my ($count, $pos, $repeat) = @_;
1909
+
1910
+ while ($count-- > 0) {
1911
+ Irssi::command('window up');
1912
+ }
1913
+
1914
+ return (undef, undef);
1915
+ }
1916
+
1917
+ sub cmd_ctrl_6 {
1918
+ # like :b#
1919
+ Irssi::command('window last');
1920
+ return (undef, undef);
1921
+ }
1922
+
1923
+ sub cmd_tilde {
1924
+ my ($count, $pos, $repeat) = @_;
1925
+
1926
+ my $input = _input();
1927
+ my $string = substr $input, $pos, $count;
1928
+ $string =~ s/(.)/(uc($1) eq $1) ? lc($1) : uc($1)/ge;
1929
+ substr $input, $pos, $count, $string;
1930
+
1931
+ _input($input);
1932
+ return (undef, _fix_input_pos($pos + $count, length $input));
1933
+ }
1934
+
1935
+ sub cmd_register {
1936
+ my ($count, $pos, $repeat, $char) = @_;
1937
+
1938
+ if (not exists $registers->{$char} and not exists $registers->{lc $char}) {
1939
+ print "Wrong register $char, ignoring." if DEBUG;
1940
+ return (undef, undef);
1941
+ }
1942
+
1943
+ # make sure black hole register is always empty
1944
+ if ($char eq '_') {
1945
+ $registers->{_} = '';
1946
+ }
1947
+
1948
+ # + and * contain both irssi's cut-buffer
1949
+ if ($char eq '+' or $char eq '*') {
1950
+ $registers->{'+'} = Irssi::parse_special('$U');
1951
+ $registers->{'*'} = $registers->{'+'};
1952
+ }
1953
+
1954
+ $register = $char;
1955
+ print "Changing register to $register" if DEBUG;
1956
+ return (undef, undef);
1957
+ }
1958
+
1959
+ sub cmd_undo {
1960
+ print "Undo!" if DEBUG;
1961
+
1962
+ if ($undo_index != $#undo_buffer) {
1963
+ $undo_index++;
1964
+ _restore_undo_entry($undo_index);
1965
+ print "Undoing entry index: $undo_index of " . scalar(@undo_buffer)
1966
+ if DEBUG;
1967
+ } else {
1968
+ print "No further undo." if DEBUG;
1969
+ }
1970
+ return (undef, undef);
1971
+ }
1972
+
1973
+ sub cmd_redo {
1974
+ print "Redo!" if DEBUG;
1975
+
1976
+ if ($undo_index != 0) {
1977
+ $undo_index--;
1978
+ print "Undoing entry index: $undo_index of " . scalar(@undo_buffer)
1979
+ if DEBUG;
1980
+ _restore_undo_entry($undo_index);
1981
+ } else {
1982
+ print "No further Redo." if DEBUG;
1983
+ }
1984
+ return (undef, undef);
1985
+ }
1986
+
1987
+ # Adapt the input position depending if an operator is active or not.
1988
+ sub _fix_input_pos {
1989
+ my ($pos, $length) = @_;
1990
+
1991
+ # Allow moving past the last character when an operator is active to allow
1992
+ # correct handling of last character in line.
1993
+ if ($operator) {
1994
+ $pos = $length if $pos > $length;
1995
+ # Otherwise forbid it.
1996
+ } else {
1997
+ $pos = $length - 1 if $pos > $length - 1;
1998
+ }
1999
+
2000
+ return $pos;
2001
+ }
2002
+
2003
+
2004
+ # EX MODE COMMANDS
2005
+
2006
+ sub cmd_ex_command {
2007
+ my $arg_str = join '', @ex_buf;
2008
+
2009
+ if ($arg_str !~ /^(\d*)?([a-z]+)/) {
2010
+ return _warn("Invalid Ex-mode command!");
2011
+ }
2012
+
2013
+ # Abort if command doesn't exist or used with count for unsupported
2014
+ # commands.
2015
+ if (not exists $commands_ex->{$2} or
2016
+ ($1 ne '' and not $commands_ex->{$2}->{uses_count})) {
2017
+ return _warn("Ex-mode $1$2 doesn't exist!");
2018
+ }
2019
+
2020
+ # add this item to the ex mode history
2021
+ ex_history_add($arg_str);
2022
+ $ex_history_index = 0; # and reset the history position.
2023
+
2024
+ my $count = $1;
2025
+ if ($count eq '') {
2026
+ $count = undef;
2027
+ }
2028
+ $commands_ex->{$2}->{func}($arg_str, $count);
2029
+ }
2030
+
2031
+ sub ex_substitute {
2032
+ my ($arg_str, $count) = @_;
2033
+
2034
+ # :s///
2035
+ if ($arg_str =~ m|^s/(.+)/(.*)/([ig]*)|) {
2036
+ my ($search, $replace, $flags) = ($1, $2, $3);
2037
+ print "Searching for $search, replace: $replace, flags; $flags"
2038
+ if DEBUG;
2039
+
2040
+ my $rep_fun = sub { $replace };
2041
+
2042
+ my $line = _input();
2043
+ my @re_flags = split '', defined $flags?$flags:'';
2044
+
2045
+ if (scalar grep { $_ eq 'i' } @re_flags) {
2046
+ $search = '(?i)' . $search;
2047
+ }
2048
+
2049
+ print "Search is $search" if DEBUG;
2050
+
2051
+ my $re_pattern = qr/$search/;
2052
+
2053
+ if (scalar grep { $_ eq 'g' } @re_flags) {
2054
+ $line =~ s/$re_pattern/$rep_fun->()/eg;
2055
+ } else {
2056
+ print "Single replace: $replace" if DEBUG;
2057
+ $line =~ s/$re_pattern/$rep_fun->()/e;
2058
+ }
2059
+
2060
+ print "New line is: $line" if DEBUG;
2061
+ _input($line);
2062
+ } else {
2063
+ _warn_ex('s');
2064
+ }
2065
+ }
2066
+
2067
+ sub ex_bnext {
2068
+ my ($arg_str, $count) = @_;
2069
+
2070
+ if (not defined $count) {
2071
+ if ($arg_str =~ /^bn(?:ext)?\s(\d+)$/) {
2072
+ $count = $1;
2073
+ } else {
2074
+ $count = 1;
2075
+ }
2076
+ }
2077
+
2078
+ while ($count-- > 0) {
2079
+ Irssi::command('window next');
2080
+ }
2081
+ }
2082
+
2083
+ sub ex_bprev {
2084
+ my ($arg_str, $count) = @_;
2085
+
2086
+ if (not defined $count) {
2087
+ if ($arg_str =~ /^bp(?:rev)?\s(\d+)$/) {
2088
+ $count = $1;
2089
+ } else {
2090
+ $count = 1;
2091
+ }
2092
+ }
2093
+
2094
+ while ($count-- > 0) {
2095
+ Irssi::command('window previous');
2096
+ }
2097
+ }
2098
+
2099
+ sub ex_bdelete {
2100
+ my ($arg_str, $count) = @_;
2101
+
2102
+ if (not defined $count) {
2103
+ if ($arg_str =~ /^bd(?:elete)?\s(\d+)$/) {
2104
+ $count = $1;
2105
+ }
2106
+ }
2107
+
2108
+ if (defined $count) {
2109
+ my $window = Irssi::window_find_refnum($count);
2110
+ if (not $window) {
2111
+ return;
2112
+ }
2113
+ $window->set_active();
2114
+ }
2115
+ Irssi::command('window close');
2116
+ }
2117
+
2118
+ sub ex_buffer {
2119
+ my ($arg_str, $count) = @_;
2120
+
2121
+ # :b[buffer] {args}
2122
+ if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$| or defined $count) {
2123
+ my $window;
2124
+ my $item;
2125
+ my $buffer = $1;
2126
+
2127
+ # :[N]:b[uffer]
2128
+ if (defined $count) {
2129
+ $window = Irssi::window_find_refnum($count);
2130
+ # Go to window number.
2131
+ } elsif ($buffer =~ /^[0-9]+$/) {
2132
+ $window = Irssi::window_find_refnum($buffer);
2133
+ # Go to previous window.
2134
+ } elsif ($buffer eq '#') {
2135
+ Irssi::command('window last');
2136
+ # Go to best regex matching window.
2137
+ } else {
2138
+ eval {
2139
+ my $matches = _matching_windows($buffer);
2140
+ if (scalar @$matches > 0) {
2141
+ $window = @$matches[0]->{window};
2142
+ $item = @$matches[0]->{item};
2143
+ }
2144
+ };
2145
+ # Catch errors in /$buffer/ regex.
2146
+ if ($@) {
2147
+ _warn($@);
2148
+ }
2149
+ }
2150
+
2151
+ if ($window) {
2152
+ $window->set_active();
2153
+ if ($item) {
2154
+ $item->set_active();
2155
+ }
2156
+ }
2157
+ } else {
2158
+ _warn_ex('buffer');
2159
+ }
2160
+ }
2161
+
2162
+ sub ex_item_next {
2163
+ my ($arg_str, $count) = @_;
2164
+ my $win = Irssi::active_win;
2165
+ $count = 1 unless defined $count;
2166
+
2167
+ $win->item_next for (1..$count);
2168
+ }
2169
+
2170
+ sub ex_item_prev {
2171
+ my ($arg_str, $count) = @_;
2172
+ my $win = Irssi::active_win;
2173
+ $count = 1 unless defined $count;
2174
+
2175
+ $win->item_prev for (1..$count);
2176
+ }
2177
+
2178
+ # TODO: factor out the shared search code for server next/prev.
2179
+ sub ex_server_next {
2180
+ my ($arg_str, $count) = @_;
2181
+
2182
+ my @server_ids = map { $_->{tag} . "\x1d" . $_->{nick} } Irssi::servers;
2183
+ my $server = Irssi::active_server;
2184
+
2185
+ return unless $server;
2186
+
2187
+ my $current_id = $server->{tag} . "\x1d" . $server->{nick};
2188
+ my $next = 0;
2189
+ my $server_count = scalar @server_ids;
2190
+ for my $i (0..$server_count - 1) {
2191
+ my $s_id = $server_ids[$i];
2192
+ if (defined($s_id) and ($s_id eq $current_id)) {
2193
+ print "Found match at $i" if DEBUG;
2194
+ $next = ($i + 1) % $server_count;
2195
+ last;
2196
+ }
2197
+ }
2198
+
2199
+ my $next_server = $server_ids[$next];
2200
+ $next_server =~ s|^([^\x1d]+)\x1d.*$|$1|;
2201
+
2202
+ print "Changing to server: $next: $next_server" if DEBUG;
2203
+ Irssi::command("window server $next_server");
2204
+ }
2205
+
2206
+ sub ex_server_prev {
2207
+ my ($arg_str, $count) = @_;
2208
+
2209
+ my @server_ids = map { $_->{tag} . "\x1d" . $_->{nick} } Irssi::servers;
2210
+ my $server = Irssi::active_server;
2211
+
2212
+ return unless $server;
2213
+
2214
+ my $current_id = $server->{tag} . "\x1d" . $server->{nick};
2215
+ my $prev = 0;
2216
+ my $server_count = scalar @server_ids;
2217
+
2218
+ for my $i (0..$server_count - 1) {
2219
+ my $s_id = $server_ids[$i];
2220
+ if (defined($s_id) and ($s_id eq $current_id)) {
2221
+ print "Found match at $i" if DEBUG;
2222
+ $prev = ($i - 1) % $server_count;
2223
+ last;
2224
+ }
2225
+ }
2226
+
2227
+ my $prev_server = $server_ids[$prev];
2228
+ $prev_server =~ s|^([^\x1d]+)\x1d.*$|$1|;
2229
+
2230
+ print "Changing to server: $prev: $prev_server" if DEBUG;
2231
+ Irssi::command("window server $prev_server");
2232
+
2233
+ }
2234
+
2235
+ sub ex_registers {
2236
+ my ($arg_str, $count) = @_;
2237
+
2238
+ # :reg[isters] {arg} and :di[splay] {arg}
2239
+ if ($arg_str =~ /^(?:reg(?:isters)?|di(?:splay)?)(?:\s+(.+)$)?/) {
2240
+ my @regs;
2241
+ if ($1) {
2242
+ my $regs = $1;
2243
+ $regs =~ s/\s+//g;
2244
+ @regs = split //, $regs;
2245
+ } else {
2246
+ @regs = keys %$registers;
2247
+ }
2248
+
2249
+ # Update "+ and "* registers so correct values are displayed.
2250
+ $registers->{'+'} = Irssi::parse_special('$U');
2251
+ $registers->{'*'} = $registers->{'+'};
2252
+
2253
+ my @empty_regs;
2254
+ my $special_regs = { '+' => 1, '*' => 1, '_' => 1, '"' => 1, '0' => 1 };
2255
+
2256
+ my $active_window = Irssi::active_win;
2257
+ foreach my $key (sort @regs) {
2258
+ next if $key eq '_'; # skip black hole
2259
+ if (defined $registers->{$key}) {
2260
+ my $register_val = $registers->{$key};
2261
+ if (length $register_val or exists $special_regs->{$key}) {
2262
+ $register_val =~ s/%/%%/g;
2263
+ $active_window->print("register $key: $register_val");
2264
+ } else {
2265
+ push @empty_regs, $key;
2266
+ }
2267
+ }
2268
+ }
2269
+
2270
+ # coalesce empty registers into a single line.
2271
+ if (@empty_regs) {
2272
+ my @runs;
2273
+ my $run_start;
2274
+ foreach my $i (0..$#empty_regs) {
2275
+ my $cur = $empty_regs[$i];
2276
+ my $next = $empty_regs[$i+1];
2277
+
2278
+ $run_start = $cur unless $run_start;
2279
+ if (defined $next and ord($cur) + 1 == ord($next)) {
2280
+ # extend range.
2281
+ } else {
2282
+ # terminate range and restart
2283
+ my $run_str = $run_start;
2284
+
2285
+ if ($cur ne $run_start) {
2286
+ $run_str .= "-$cur";
2287
+ }
2288
+ push @runs, $run_str;
2289
+ $run_start = undef;
2290
+ }
2291
+ }
2292
+ $active_window->print("Empty registers: " . join(', ', @runs));
2293
+ }
2294
+ } else {
2295
+ _warn_ex(':registers');
2296
+ }
2297
+ }
2298
+
2299
+ sub ex_buffers {
2300
+ my ($arg_str, $count) = @_;
2301
+
2302
+ Irssi::command('window list');
2303
+ }
2304
+
2305
+ sub ex_undolist {
2306
+ my ($arg_str, $count) = @_;
2307
+
2308
+ _print_undo_buffer();
2309
+ }
2310
+
2311
+ sub ex_map {
2312
+ my ($arg_str, $count) = @_;
2313
+
2314
+ # :map {lhs} {rhs}
2315
+ if ($arg_str =~ /^map (\S+) (\S.*)$/) {
2316
+ my $lhs = _parse_mapping($1);
2317
+ my $rhs = $2;
2318
+
2319
+ if (not defined $lhs) {
2320
+ return _warn_ex('map', 'invalid {lhs}');
2321
+ }
2322
+
2323
+ # Add new mapping.
2324
+ my $command;
2325
+ # Ex-mode command
2326
+ if (index($rhs, ':') == 0) {
2327
+ $rhs =~ /^:(\S+)(\s.+)?$/;
2328
+ if (not exists $commands_ex->{$1}) {
2329
+ return _warn_ex('map', "$rhs not found");
2330
+ } else {
2331
+ $command = { char => $rhs,
2332
+ func => $commands_ex->{$1}->{func},
2333
+ type => C_EX,
2334
+ };
2335
+ }
2336
+ # Irssi command
2337
+ } elsif (index($rhs, '/') == 0) {
2338
+ $command = { char => $rhs,
2339
+ func => substr($rhs, 1),
2340
+ type => C_IRSSI,
2341
+ };
2342
+ # <Nop> does nothing
2343
+ } elsif (lc $rhs eq '<nop>') {
2344
+ $command = { char => '<Nop>',
2345
+ func => undef,
2346
+ type => C_NOP,
2347
+ };
2348
+ # command-mode command
2349
+ } else {
2350
+ $rhs = _parse_mapping($2);
2351
+ if (not defined $rhs) {
2352
+ return _warn_ex('map', 'invalid {rhs}');
2353
+ } elsif (not exists $commands->{$rhs}) {
2354
+ return _warn_ex('map', "$2 not found");
2355
+ } else {
2356
+ $command = $commands->{$rhs};
2357
+ }
2358
+ }
2359
+ add_map($lhs, $command);
2360
+
2361
+ # :map [lhs]
2362
+ } elsif ($arg_str =~ m/^map\s*$/ or $arg_str =~ m/^map (\S+)$/) {
2363
+ # Necessary for case insensitive matchings. lc alone won't work.
2364
+ my $search = $1;
2365
+ $search = '' if not defined $search;
2366
+ $search = _parse_mapping_reverse(_parse_mapping($search));
2367
+
2368
+ my $active_window = Irssi::active_win();
2369
+ foreach my $key (sort keys %$maps) {
2370
+ my $map = $maps->{$key};
2371
+ my $cmd = $map->{cmd};
2372
+ if (defined $cmd) {
2373
+ next if $map->{char} eq $cmd->{char}; # skip default mappings
2374
+ # FIXME: Hack so <C-H> doesn't show up as mapped to <BS>.
2375
+ next if $map->{char} eq '<C-H>' and $cmd->{char} eq '<BS>';
2376
+ next if $map->{char} !~ /^\Q$search\E/; # skip non-matches
2377
+ $active_window->print(sprintf "%-15s %s", $map->{char},
2378
+ $cmd->{char});
2379
+ }
2380
+ }
2381
+ } else {
2382
+ _warn_ex('map');
2383
+ }
2384
+ }
2385
+ sub ex_unmap {
2386
+ my ($arg_str, $count) = @_;
2387
+
2388
+ # :unm[ap] {lhs}
2389
+ if ($arg_str !~ /^unm(?:ap)? (\S+)$/) {
2390
+ return _warn_ex('unmap');
2391
+ }
2392
+
2393
+ my $lhs = _parse_mapping($1);
2394
+ if (not defined $lhs) {
2395
+ return _warn_ex('unmap', 'invalid {lhs}');
2396
+ # Prevent unmapping of unknown or default mappings.
2397
+ } elsif (not exists $maps->{$lhs} or not defined $maps->{$lhs}->{cmd} or
2398
+ ($commands->{$lhs} and $maps->{$lhs}->{cmd} == $commands->{$lhs})) {
2399
+ return _warn_ex('unmap', "$1 not found");
2400
+ }
2401
+
2402
+ delete_map($lhs);
2403
+ }
2404
+ sub _parse_mapping {
2405
+ my ($string) = @_;
2406
+
2407
+ $string =~ s/<([^>]+)>/_parse_mapping_bracket($1)/ge;
2408
+ _debug("Parse mapping: $string");
2409
+ if (index($string, '<invalid>') != -1) {
2410
+ return undef;
2411
+ }
2412
+ return $string;
2413
+ }
2414
+ sub _parse_mapping_bracket {
2415
+ my ($string) = @_;
2416
+
2417
+ $string = lc $string;
2418
+
2419
+ # <C-X>, get corresponding CTRL char.
2420
+ if ($string =~ /^c-([a-z])$/i) {
2421
+ $string = chr(ord($1) - 96);
2422
+ # <C-6> and <C-^>
2423
+ } elsif ($string =~ /^c-[6^]$/i) {
2424
+ $string = chr(30);
2425
+ # <Space>
2426
+ } elsif ($string eq 'space') {
2427
+ $string = ' ';
2428
+ # <CR>
2429
+ } elsif ($string eq 'cr') {
2430
+ $string = "\n";
2431
+ # <BS>
2432
+ } elsif ($string eq 'bs') {
2433
+ $string = chr(127);
2434
+ } elsif ($string eq 'leader') {
2435
+ $string = $settings->{map_leader}->{value};
2436
+ # Invalid char, return special string to recognize the error.
2437
+ } else {
2438
+ $string = '<invalid>';
2439
+ }
2440
+ return $string;
2441
+ }
2442
+ sub _parse_mapping_reverse {
2443
+ my ($string) = @_;
2444
+
2445
+ if (not defined $string) {
2446
+ _warn("Unable to reverse-map command: " . join('', @ex_buf));
2447
+ return;
2448
+ }
2449
+
2450
+ my $escaped_leader = quotemeta($settings->{map_leader}->{value});
2451
+ $string =~ s/$escaped_leader/<Leader>/g;
2452
+
2453
+ # Convert char to <char-name>.
2454
+ $string =~ s/ /<Space>/g;
2455
+ $string =~ s/\n/<CR>/g;
2456
+ $string =~ s/\x7F/<BS>/g;
2457
+ # Convert Ctrl-X to <C-X>.
2458
+ $string =~ s/([\x01-\x1A])/"<C-" . chr(ord($1) + 64) . ">"/ge;
2459
+ # Convert Ctrl-6 and Ctrl-^ to <C-^>.
2460
+ $string =~ s/\x1E/<C-^>/g;
2461
+
2462
+ return $string;
2463
+ }
2464
+ sub _parse_partial_command_reverse {
2465
+ my ($string) = @_;
2466
+
2467
+ my $escaped_leader = quotemeta($settings->{map_leader}->{value});
2468
+ $string =~ s/$escaped_leader/<Leader>/g;
2469
+
2470
+ # Convert Ctrl-X to ^X.
2471
+ $string =~ s/([\x01-\x1A])/"^" . chr(ord($1) + 64)/ge;
2472
+ # Convert Ctrl-6 and Ctrl-^ to <C-^>.
2473
+ $string =~ s/\x1E/^^/g;
2474
+
2475
+ return $string;
2476
+ }
2477
+
2478
+ sub ex_source {
2479
+ my ($arg_str, $count) = @_;
2480
+
2481
+ # :so[urce], but only loads the vim_moderc file at the moment
2482
+
2483
+ open my $file, '<', Irssi::get_irssi_dir() . '/vim_moderc' or return;
2484
+
2485
+ while (my $line = <$file>) {
2486
+ next if $line =~ /^\s*$/ or $line =~ /^\s*"/;
2487
+
2488
+ chomp $line;
2489
+ # :map {lhs} {rhs}, keep in sync with ex_map()
2490
+ if ($line =~ /^\s*map (\S+) (\S.*)$/) {
2491
+ ex_map($line);
2492
+ } else {
2493
+ _warn_ex('source', "command not supported: $line");
2494
+ }
2495
+ }
2496
+ }
2497
+
2498
+ sub ex_mkvimrc {
2499
+ my ($arg_str, $count) = @_;
2500
+
2501
+ # :mkv[imrc][!], [file] not supported
2502
+
2503
+ my $vim_moderc = Irssi::get_irssi_dir(). '/vim_moderc';
2504
+ if (-f $vim_moderc and $arg_str !~ /^mkv(?:imrc)?!$/) {
2505
+ return _warn_ex('mkvimrc', "$vim_moderc already exists");
2506
+ }
2507
+
2508
+ open my $file, '>', $vim_moderc or return;
2509
+
2510
+ # copied from ex_map()
2511
+ foreach my $key (sort keys %$maps) {
2512
+ my $map = $maps->{$key};
2513
+ my $cmd = $map->{cmd};
2514
+ if (defined $cmd) {
2515
+ next if $map->{char} eq $cmd->{char}; # skip default mappings
2516
+ print $file "map $map->{char} $cmd->{char}\n";
2517
+ }
2518
+ }
2519
+
2520
+ close $file;
2521
+ }
2522
+
2523
+ sub ex_set {
2524
+ my ($arg_str, $count) = @_;
2525
+
2526
+ # :se[t] [option] [value]
2527
+ if ($arg_str =~ /^se(?:t)?(?:\s([^=]+)(?:=(.*)$)?)?/) {
2528
+ # :se[t] {option} {value}
2529
+ if (defined $1 and defined $2) {
2530
+ if (not exists $settings->{$1}) {
2531
+ return _warn_ex('map', "setting '$1' not found");
2532
+ }
2533
+ my $name = $1;
2534
+ my $value = $2;
2535
+ # Also accept numeric values for boolean options.
2536
+ if ($settings->{$name}->{type} == S_BOOL) {
2537
+ if ($value =~ /^(on|off)$/i) {
2538
+ $value = lc $value eq 'on' ? 1 : 0;
2539
+ } elsif ($value eq '') {
2540
+ $value = 0;
2541
+ }
2542
+ }
2543
+ _setting_set($name, $value);
2544
+ setup_changed();
2545
+
2546
+ # :se[t] [option]
2547
+ } else {
2548
+ my $search = defined $1 ? $1 : '';
2549
+ my $active_window = Irssi::active_win();
2550
+ foreach my $setting (sort keys %$settings) {
2551
+ next if $setting !~ /^\Q$search\E/; # skip non-matches
2552
+ my $value = $settings->{$setting}->{value};
2553
+ # Irssi only accepts 'on' and 'off' as values for boolean
2554
+ # options.
2555
+ if ($settings->{$setting}->{type} == S_BOOL) {
2556
+ $value = $value ? 'on' : 'off';
2557
+ }
2558
+ $active_window->print($setting . '=' . $value);
2559
+ }
2560
+ }
2561
+ } else {
2562
+ _warn_ex('map');
2563
+ }
2564
+ }
2565
+
2566
+ sub _warn_ex {
2567
+ my ($command, $description) = @_;
2568
+ my $message = "Error in ex-mode command $command";
2569
+ if (defined $description) {
2570
+ $message .= ": $description";
2571
+ }
2572
+ _warn($message);
2573
+ }
2574
+
2575
+ sub _matching_windows {
2576
+ my ($buffer) = @_;
2577
+
2578
+ my $server;
2579
+
2580
+ if ($buffer =~ m{^(.+)/(.+)}) {
2581
+ $server = $1;
2582
+ $buffer = $2;
2583
+ }
2584
+
2585
+ print ":b searching for channel $buffer" if DEBUG;
2586
+ print ":b on server $server" if $server and DEBUG;
2587
+
2588
+ my @matches;
2589
+ foreach my $window (Irssi::windows()) {
2590
+ # Matching window names.
2591
+ if ($window->{name} =~ /$buffer/i) {
2592
+ my $win_ratio = ($+[0] - $-[0]) / length($window->{name});
2593
+ push @matches, { window => $window,
2594
+ item => undef,
2595
+ ratio => $win_ratio,
2596
+ text => $window->{name} };
2597
+ print ":b $window->{name}: $win_ratio" if DEBUG;
2598
+ }
2599
+ # Matching Window item names (= channels).
2600
+ foreach my $item ($window->items()) {
2601
+ # Wrong server.
2602
+ if ($server and (!$item->{server} or
2603
+ $item->{server}->{chatnet} !~ /^$server/i)) {
2604
+ next;
2605
+ }
2606
+ if ($item->{name} =~ /$buffer/i) {
2607
+ my $length = length($item->{name});
2608
+ $length-- if index($item->{name}, '#') == 0;
2609
+ my $item_ratio = ($+[0] - $-[0]) / $length;
2610
+ push @matches, { window => $window,
2611
+ item => $item,
2612
+ ratio => $item_ratio,
2613
+ text => $item->{name} };
2614
+ print ":b $window->{name} $item->{name}: $item_ratio" if DEBUG;
2615
+ }
2616
+ }
2617
+ }
2618
+
2619
+ @matches = sort {$b->{ratio} <=> $a->{ratio}} @matches;
2620
+
2621
+ return \@matches;
2622
+ }
2623
+
2624
+
2625
+ # STATUS ITEMS
2626
+
2627
+ #TODO: give these things better names.
2628
+ sub vim_mode_cmd {
2629
+
2630
+ my $mode_str = '';
2631
+ if ($mode == M_INS) {
2632
+ $mode_str = 'Insert';
2633
+ } elsif ($mode == M_EX) {
2634
+ $mode_str = '%_Ex%_';
2635
+ } else {
2636
+ $mode_str = '%_Command%_';
2637
+ if ($register ne '"' or $numeric_prefix or $operator or $movement or
2638
+ $pending_map) {
2639
+ my $partial = '';
2640
+ if ($register ne '"') {
2641
+ $partial .= '"' . $register;
2642
+ }
2643
+ if ($numeric_prefix) {
2644
+ $partial .= $numeric_prefix;
2645
+ }
2646
+ if ($operator) {
2647
+ $partial .= $operator->{char};
2648
+ }
2649
+ if ($movement) {
2650
+ $partial .= $movement->{char};
2651
+ }
2652
+ if (defined $pending_map) {
2653
+ $partial .= $pending_map;
2654
+ }
2655
+ $partial = _parse_partial_command_reverse($partial);
2656
+ $partial =~ s/\\/\\\\\\\\/g;
2657
+ $mode_str .= " ($partial)";
2658
+ }
2659
+ }
2660
+ return $mode_str;
2661
+ }
2662
+
2663
+ sub vim_wins_data {
2664
+ my $windows = '';
2665
+
2666
+ # A little code duplication of cmd_ex_command(), but \s+ instead of \s* so
2667
+ # :bd doesn't display buffers matching d.
2668
+ my $arg_str = join '', @ex_buf;
2669
+ if ($arg_str =~ m|^b(?:uffer)?\s+(.+)$|) {
2670
+ my $buffer = $1;
2671
+ if ($buffer !~ /^[0-9]$/ and $buffer ne '#') {
2672
+ # Display matching windows.
2673
+ eval {
2674
+ my $matches = _matching_windows($buffer);
2675
+ $windows = join ',', map { $_->{text} } @$matches;
2676
+ };
2677
+ # Catch errors in /$buffer/ regex.
2678
+ if ($@) {
2679
+ _warn($@);
2680
+ }
2681
+ }
2682
+ }
2683
+ return $windows;
2684
+ }
2685
+
2686
+ sub vim_exp_mode {
2687
+ my ($server, $witem, $arg) = @_;
2688
+ return vim_mode_cmd();
2689
+ }
2690
+
2691
+ sub vim_exp_wins {
2692
+ my ($server, $witem, $arg) = @_;
2693
+ return vim_wins_data();
2694
+ }
2695
+
2696
+ # vi mode status item.
2697
+ sub vim_mode_cb {
2698
+ my ($sb_item, $get_size_only) = @_;
2699
+ my $mode_str = vim_mode_cmd();
2700
+ $sb_item->default_handler($get_size_only, "{sb $mode_str}", '', 0);
2701
+ }
2702
+
2703
+ # :b window list item.
2704
+ sub b_windows_cb {
2705
+ my ($sb_item, $get_size_only) = @_;
2706
+
2707
+ my $windows = vim_wins_data();
2708
+
2709
+ $sb_item->default_handler($get_size_only, "{sb $windows}", '', 0);
2710
+ }
2711
+
2712
+
2713
+ # INPUT HANDLING
2714
+
2715
+ sub got_key {
2716
+ my ($key) = @_;
2717
+
2718
+ return if ($should_ignore);
2719
+
2720
+ # Esc key
2721
+ if ($key == 27) {
2722
+ print "Esc seen, starting buffer" if DEBUG;
2723
+ $input_buf_enabled = 1;
2724
+
2725
+ # NOTE: this timeout might be too low on laggy systems, but
2726
+ # it comes at the cost of keystroke latency for things that
2727
+ # contain escape sequences (arrow keys, etc)
2728
+ my $esc_buf_timeout = $settings->{esc_buf_timeout}->{value};
2729
+
2730
+ $input_buf_timer
2731
+ = Irssi::timeout_add_once($esc_buf_timeout,
2732
+ \&handle_input_buffer, undef);
2733
+
2734
+ print "Buffer Timer tag: $input_buf_timer" if DEBUG;
2735
+
2736
+ } elsif ($mode == M_INS) {
2737
+
2738
+ if ($key == 3) { # Ctrl-C enters command mode
2739
+ _update_mode(M_CMD);
2740
+ _stop();
2741
+ return;
2742
+
2743
+ } elsif ($key == 10) { # enter.
2744
+ _commit_line();
2745
+
2746
+ } elsif ($input_buf_enabled and $imap) {
2747
+ print "Imap $imap active" if DEBUG;
2748
+ my $map = $imaps->{$imap};
2749
+ if (not defined $map->{map} or chr($key) eq $map->{map}) {
2750
+ $map->{func}($key);
2751
+ # Clear the buffer so the imap is not printed.
2752
+ @input_buf = ();
2753
+ } else {
2754
+ push @input_buf, $key;
2755
+ }
2756
+ flush_input_buffer();
2757
+ _stop();
2758
+ $imap = undef;
2759
+ return;
2760
+
2761
+ } elsif (exists $imaps->{chr($key)}) {
2762
+ print "Imap " . chr($key) . " seen, starting buffer" if DEBUG;
2763
+
2764
+ # start imap pending mode
2765
+ $imap = chr($key);
2766
+
2767
+ $input_buf_enabled = 1;
2768
+ push @input_buf, $key;
2769
+ $input_buf_timer
2770
+ = Irssi::timeout_add_once(1000, \&flush_input_buffer, undef);
2771
+
2772
+ _stop();
2773
+ return;
2774
+
2775
+ # Pressing delete resets insert mode repetition (8 = BS, 127 = DEL).
2776
+ # TODO: maybe allow it
2777
+ } elsif ($key == 8 || $key == 127) {
2778
+ @insert_buf = ();
2779
+ # All other entered characters need to be stored to allow repeat of
2780
+ # insert mode. Ignore delete and control characters.
2781
+ } elsif ($key > 31) {
2782
+ push @insert_buf, chr($key);
2783
+ }
2784
+ }
2785
+
2786
+ if ($input_buf_enabled) {
2787
+ push @input_buf, $key;
2788
+ _stop();
2789
+ return;
2790
+ }
2791
+
2792
+ if ($mode == M_CMD) {
2793
+ my $should_stop = handle_command_cmd($key);
2794
+ _stop() if $should_stop;
2795
+ Irssi::statusbar_items_redraw("vim_mode");
2796
+
2797
+ } elsif ($mode == M_EX) {
2798
+
2799
+ if ($key == 3) { # C-c cancels Ex mdoe as well.
2800
+ _update_mode(M_CMD);
2801
+ _stop();
2802
+ return;
2803
+ }
2804
+
2805
+ handle_command_ex($key);
2806
+ }
2807
+ }
2808
+
2809
+ # TODO: merge this with 'flush_input_buffer' below.
2810
+
2811
+ sub handle_input_buffer {
2812
+
2813
+ #Irssi::timeout_remove($input_buf_timer);
2814
+ $input_buf_timer = undef;
2815
+ # see what we've collected.
2816
+ print "Input buffer contains: ", join(", ", @input_buf) if DEBUG;
2817
+
2818
+ if (@input_buf == 1 && $input_buf[0] == 27) {
2819
+
2820
+ print "Enter Command Mode" if DEBUG;
2821
+ _update_mode(M_CMD);
2822
+
2823
+ } else {
2824
+ # we have more than a single esc, implying an escape sequence
2825
+ # (meta-* or esc-*)
2826
+
2827
+ # currently, we only extract escape sequences if:
2828
+ # a) we're in ex mode
2829
+ # b) they're arrow keys (for history control)
2830
+
2831
+ if ($mode == M_EX) {
2832
+ # ex mode
2833
+ my $key_str = join '', map { chr } @input_buf;
2834
+ if ($key_str =~ m/^\e\[([ABCD])/) {
2835
+ my $arrow = $1;
2836
+ _debug( "Arrow key: $arrow");
2837
+ if ($arrow eq 'A') { # up
2838
+ ex_history_back();
2839
+ } elsif ($arrow eq 'B') { # down
2840
+ ex_history_fwd();
2841
+ } else {
2842
+ $arrow =~ s/C/right/;
2843
+ $arrow =~ s/D/left/;
2844
+ _debug("Arrow key $arrow not supported");
2845
+ }
2846
+ }
2847
+ } else {
2848
+ # otherwise, we just forward them to irssi.
2849
+ _emulate_keystrokes(@input_buf);
2850
+ }
2851
+
2852
+ # Clear insert buffer, pressing "special" keys (like arrow keys)
2853
+ # resets it.
2854
+ @insert_buf = ();
2855
+ }
2856
+
2857
+ @input_buf = ();
2858
+ $input_buf_enabled = 0;
2859
+ }
2860
+
2861
+ sub flush_input_buffer {
2862
+ Irssi::timeout_remove($input_buf_timer) if defined $input_buf_timer;
2863
+ $input_buf_timer = undef;
2864
+ # see what we've collected.
2865
+ print "Input buffer flushed" if DEBUG;
2866
+
2867
+ # Add the characters to @insert_buf so they can be repeated.
2868
+ push @insert_buf, map chr, @input_buf;
2869
+
2870
+ _emulate_keystrokes(@input_buf);
2871
+
2872
+ @input_buf = ();
2873
+ $input_buf_enabled = 0;
2874
+
2875
+ $imap = undef;
2876
+ }
2877
+
2878
+ sub flush_pending_map {
2879
+ my ($old_pending_map) = @_;
2880
+
2881
+ print "flush_pending_map(): ", $pending_map, ' ', $old_pending_map
2882
+ if DEBUG;
2883
+
2884
+ return if not defined $pending_map or
2885
+ $pending_map ne $old_pending_map;
2886
+
2887
+ handle_command_cmd(undef);
2888
+ Irssi::statusbar_items_redraw("vim_mode");
2889
+ }
2890
+
2891
+ sub handle_numeric_prefix {
2892
+ my ($char) = @_;
2893
+ my $num = 0+$char;
2894
+
2895
+ if (defined $numeric_prefix) {
2896
+ $numeric_prefix *= 10;
2897
+ $numeric_prefix += $num;
2898
+ } else {
2899
+ $numeric_prefix = $num;
2900
+ }
2901
+ }
2902
+
2903
+ sub handle_command_cmd {
2904
+ my ($key) = @_;
2905
+
2906
+ my $pending_map_flushed = 0;
2907
+
2908
+ my $char;
2909
+ if (defined $key) {
2910
+ $char = chr($key);
2911
+ # We were called from flush_pending_map().
2912
+ } else {
2913
+ $char = $pending_map;
2914
+ $key = 0;
2915
+ $pending_map_flushed = 1;
2916
+ }
2917
+
2918
+ # Counts
2919
+ if (!$movement and !$pending_map and
2920
+ ($char =~ m/[1-9]/ or ($numeric_prefix && $char =~ m/[0-9]/))) {
2921
+ print "Processing numeric prefix: $char" if DEBUG;
2922
+ handle_numeric_prefix($char);
2923
+ return 1; # call _stop()
2924
+ }
2925
+
2926
+ if (defined $pending_map and not $pending_map_flushed) {
2927
+ $pending_map = $pending_map . $char;
2928
+ $char = $pending_map;
2929
+ }
2930
+
2931
+ my $map;
2932
+ if ($movement) {
2933
+ $map = { char => $movement->{char},
2934
+ cmd => $movement,
2935
+ maps => {},
2936
+ };
2937
+
2938
+ } elsif (exists $maps->{$char}) {
2939
+ $map = $maps->{$char};
2940
+
2941
+ # We have multiple mappings starting with this key sequence.
2942
+ if (!$pending_map_flushed and scalar keys %{$map->{maps}} > 0) {
2943
+ if (not defined $pending_map) {
2944
+ $pending_map = $char;
2945
+ }
2946
+
2947
+ # The current key sequence has a command mapped to it, run if
2948
+ # after a timeout.
2949
+ if (defined $map->{cmd}) {
2950
+ Irssi::timeout_add_once(1000, \&flush_pending_map,
2951
+ $pending_map);
2952
+ }
2953
+ return 1; # call _stop()
2954
+ }
2955
+
2956
+ } else {
2957
+ print "No mapping found for $char" if DEBUG;
2958
+ $pending_map = undef;
2959
+ $numeric_prefix = undef;
2960
+ return 1; # call _stop()
2961
+ }
2962
+
2963
+ $pending_map = undef;
2964
+
2965
+ my $cmd = $map->{cmd};
2966
+
2967
+ # Make sure we have a valid $cmd.
2968
+ if (not defined $cmd) {
2969
+ print "Bug in pending_map_flushed() $map->{char}" if DEBUG;
2970
+ return 1; # call _stop()
2971
+ }
2972
+
2973
+ # Ex-mode commands can also be bound in command mode.
2974
+ if ($cmd->{type} == C_EX) {
2975
+ print "Processing ex-command: $map->{char} ($cmd->{char})" if DEBUG;
2976
+
2977
+ $cmd->{func}->(substr($cmd->{char}, 1), $numeric_prefix);
2978
+ $numeric_prefix = undef;
2979
+
2980
+ return 1; # call _stop()
2981
+ # As can irssi commands.
2982
+ } elsif ($cmd->{type} == C_IRSSI) {
2983
+ print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG;
2984
+
2985
+ _command_with_context($cmd->{func});
2986
+
2987
+ $numeric_prefix = undef;
2988
+ return 1; # call _stop();
2989
+ # <Nop> does nothing.
2990
+ } elsif ($cmd->{type} == C_NOP) {
2991
+ print "Processing <Nop>: $map->{char}" if DEBUG;
2992
+
2993
+ $numeric_prefix = undef;
2994
+ return 1; # call _stop();
2995
+ }
2996
+
2997
+ # text-objects (i a) are simulated with $movement
2998
+ if (!$movement and ($cmd->{type} == C_NEEDSKEY or
2999
+ ($operator and ($char eq 'i' or $char eq 'a')))) {
3000
+ print "Processing movement: $map->{char} ($cmd->{char})" if DEBUG;
3001
+ if ($char eq 'i') {
3002
+ $movement = $commands->{_i};
3003
+ } elsif ($char eq 'a') {
3004
+ $movement = $commands->{_a};
3005
+ } else {
3006
+ $movement = $cmd;
3007
+ }
3008
+
3009
+ } elsif (!$movement and $cmd->{type} == C_OPERATOR) {
3010
+ print "Processing operator: $map->{char} ($cmd->{char})" if DEBUG;
3011
+ # Abort operator if we already have one pending.
3012
+ if ($operator) {
3013
+ # But allow cc/dd/yy.
3014
+ if ($operator == $cmd) {
3015
+ print "Processing line operator: ",
3016
+ $map->{char}, " (",
3017
+ $cmd->{char} ,")"
3018
+ if DEBUG;
3019
+
3020
+ my $pos = _input_pos();
3021
+ $cmd->{func}->(0, _input_len(), undef, 0);
3022
+ # Restore position for yy.
3023
+ if ($cmd == $commands->{y}) {
3024
+ _input_pos($pos);
3025
+ # And save undo for other operators.
3026
+ } else {
3027
+ _add_undo_entry(_input(), _input_pos());
3028
+ }
3029
+ if ($register ne '"') {
3030
+ print 'Changing register to "' if DEBUG;
3031
+ $register = '"';
3032
+ }
3033
+ }
3034
+ $numeric_prefix = undef;
3035
+ $operator = undef;
3036
+ $movement = undef;
3037
+ # Set new operator.
3038
+ } else {
3039
+ $operator = $cmd;
3040
+ }
3041
+
3042
+ # Start Ex mode.
3043
+ } elsif ($cmd == $commands->{':'}) {
3044
+
3045
+ if (not script_is_loaded('uberprompt')) {
3046
+ _warn("Warning: Ex mode requires the 'uberprompt' script. " .
3047
+ "Please load it and try again.");
3048
+ } else {
3049
+ _update_mode(M_EX);
3050
+ _set_prompt(':');
3051
+ }
3052
+
3053
+ # Enter key sends the current input line in command mode as well.
3054
+ } elsif ($key == 10) {
3055
+ _commit_line();
3056
+ return 0; # don't call _stop()
3057
+
3058
+ } else {
3059
+ print "Processing command: $map->{char} ($cmd->{char})" if DEBUG;
3060
+
3061
+ my $skip = 0;
3062
+ my $repeat = 0;
3063
+
3064
+ if (!$movement) {
3065
+ # . repeats the last command.
3066
+ if ($cmd == $commands->{'.'} and defined $last->{cmd}) {
3067
+ $cmd = $last->{cmd};
3068
+ $char = $last->{char};
3069
+ # If . is given a count then it replaces original count.
3070
+ if (not defined $numeric_prefix) {
3071
+ $numeric_prefix = $last->{numeric_prefix};
3072
+ }
3073
+ $operator = $last->{operator};
3074
+ $movement = $last->{movement};
3075
+ $register = $last->{register};
3076
+ $repeat = 1;
3077
+ } elsif ($cmd == $commands->{'.'}) {
3078
+ print '. pressed but $last->{char} not set' if DEBUG;
3079
+ $skip = 1;
3080
+ }
3081
+ }
3082
+
3083
+ # Ignore invalid operator/command combinations.
3084
+ if ($operator and $cmd->{no_operator}) {
3085
+ print "Invalid operator/command: $operator->{char} $cmd->{char}"
3086
+ if DEBUG;
3087
+ $skip = 1;
3088
+ }
3089
+
3090
+ if ($skip) {
3091
+ print "Skipping movement and operator." if DEBUG;
3092
+ } else {
3093
+ # Make sure count is at least 1 except for functions which need to
3094
+ # know if no count was used.
3095
+ if (not $numeric_prefix and not $cmd->{needs_count}) {
3096
+ $numeric_prefix = 1;
3097
+ }
3098
+
3099
+ my $cur_pos = _input_pos();
3100
+
3101
+ # If defined $cur_pos will be changed to this.
3102
+ my $old_pos;
3103
+ # Position after the move.
3104
+ my $new_pos;
3105
+ # Execute the movement (multiple times).
3106
+ if (not $movement) {
3107
+ ($old_pos, $new_pos)
3108
+ = $cmd->{func}->($numeric_prefix, $cur_pos, $repeat);
3109
+ } else {
3110
+ ($old_pos, $new_pos)
3111
+ = $cmd->{func}->($numeric_prefix, $cur_pos, $repeat,
3112
+ $char);
3113
+ }
3114
+ if (defined $old_pos) {
3115
+ print "Changing \$cur_pos from $cur_pos to $old_pos" if DEBUG;
3116
+ $cur_pos = $old_pos;
3117
+ }
3118
+ if (defined $new_pos) {
3119
+ _input_pos($new_pos);
3120
+ } else {
3121
+ $new_pos = _input_pos();
3122
+ }
3123
+
3124
+ # Update input position of last undo entry so that undo/redo
3125
+ # restores correct position.
3126
+ if (@undo_buffer and _input() eq $undo_buffer[0]->[0] and
3127
+ ((defined $operator and $operator == $commands->{d}) or
3128
+ $cmd->{repeatable})) {
3129
+ print "Updating history position: $undo_buffer[0]->[0]"
3130
+ if DEBUG;
3131
+ $undo_buffer[0]->[1] = $cur_pos;
3132
+ }
3133
+
3134
+ # If we have an operator pending then run it on the handled text.
3135
+ # But only if the movement changed the position (this prevents
3136
+ # problems with e.g. f when the search string doesn't exist).
3137
+ if ($operator and $cur_pos != $new_pos) {
3138
+ print "Processing operator: ", $operator->{char} if DEBUG;
3139
+ $operator->{func}->($cur_pos, $new_pos, $cmd, $repeat);
3140
+ }
3141
+
3142
+ # Save an undo checkpoint here for operators, all repeatable
3143
+ # movements, operators and repetition.
3144
+ if ((defined $operator and $operator == $commands->{d}) or
3145
+ $cmd->{repeatable}) {
3146
+ # TODO: why do history entries still show up in undo
3147
+ # buffer? Is avoiding the commands here insufficient?
3148
+
3149
+ _add_undo_entry(_input(), _input_pos());
3150
+ }
3151
+
3152
+ # Store command, necessary for .
3153
+ if ($operator or $cmd->{repeatable}) {
3154
+ $last->{cmd} = $cmd;
3155
+ $last->{char} = $char;
3156
+ $last->{numeric_prefix} = $numeric_prefix;
3157
+ $last->{operator} = $operator;
3158
+ $last->{movement} = $movement;
3159
+ $last->{register} = $register;
3160
+ }
3161
+ }
3162
+
3163
+ # Reset the count unless we go into insert mode, _update_mode() needs
3164
+ # to know it when leaving insert mode to support insert with counts
3165
+ # (like 3i).
3166
+ if ($repeat or $cmd->{type} != C_INSERT) {
3167
+ $numeric_prefix = undef;
3168
+ }
3169
+ $operator = undef;
3170
+ $movement = undef;
3171
+
3172
+ if ($cmd != $commands->{'"'} and $register ne '"') {
3173
+ print 'Changing register to "' if DEBUG;
3174
+ $register = '"';
3175
+ }
3176
+
3177
+ }
3178
+
3179
+ return 1; # call _stop()
3180
+ }
3181
+
3182
+ sub handle_command_ex {
3183
+ my ($key) = @_;
3184
+
3185
+ # BS key (8) or DEL key (127) - remove last character.
3186
+ if ($key == 8 || $key == 127) {
3187
+ print "Delete" if DEBUG;
3188
+ if (@ex_buf > 0) {
3189
+ pop @ex_buf;
3190
+ _set_prompt(':' . join '', @ex_buf);
3191
+ # Backspacing over : exits ex-mode.
3192
+ } else {
3193
+ _update_mode(M_CMD);
3194
+ }
3195
+
3196
+ # Return key - execute command
3197
+ } elsif ($key == 10) {
3198
+ print "Run ex-mode command" if DEBUG;
3199
+ cmd_ex_command();
3200
+ _update_mode(M_CMD);
3201
+
3202
+ } elsif ($key == 9) { # TAB
3203
+ print "Tab pressed" if DEBUG;
3204
+ print "Ex buf contains: " . join('', @ex_buf) if DEBUG;
3205
+ @tab_candidates = _tab_complete(join('', @ex_buf), [keys %$commands_ex]);
3206
+ _debug("Candidates: " . join(", ", @tab_candidates));
3207
+ if (@tab_candidates == 1) {
3208
+ @ex_buf = ( split('', $tab_candidates[0]), ' ');
3209
+ _set_prompt(':' . join '', @ex_buf);
3210
+ }
3211
+ # Ignore control characters for now.
3212
+ } elsif ($key > 0 && $key < 32) {
3213
+ # TODO: use them later, e.g. completion
3214
+
3215
+ # Append entered key
3216
+ } else {
3217
+ if ($key != -1) {
3218
+ # check we're not called from an ex_history_* function
3219
+ push @ex_buf, chr $key;
3220
+ }
3221
+ _set_prompt(':' . join '', @ex_buf);
3222
+ }
3223
+
3224
+ Irssi::statusbar_items_redraw("vim_windows");
3225
+
3226
+ _stop();
3227
+ }
3228
+
3229
+ sub _tab_complete {
3230
+ my ($input, $source) = @_;
3231
+ my @out;
3232
+ foreach my $item (@$source) {
3233
+ if ($item =~ m/^\Q$input\E/) {
3234
+ push @out, $item;
3235
+ }
3236
+ }
3237
+
3238
+ return sort { $a cmp $b } @out;
3239
+ }
3240
+
3241
+ sub vim_mode_init {
3242
+ Irssi::signal_add_first 'gui key pressed' => \&got_key;
3243
+ Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb');
3244
+ Irssi::statusbar_item_register ('vim_windows', 0, 'b_windows_cb');
3245
+
3246
+ Irssi::expando_create('vim_cmd_mode' => \&vim_exp_mode, {});
3247
+ Irssi::expando_create('vim_wins' => \&vim_exp_wins, {});
3248
+
3249
+
3250
+ # Register all available settings.
3251
+ foreach my $name (keys %$settings) {
3252
+ _setting_register($name);
3253
+ }
3254
+
3255
+ foreach my $char ('a' .. 'z') {
3256
+ $registers->{$char} = '';
3257
+ }
3258
+
3259
+ setup_changed();
3260
+
3261
+ Irssi::signal_add 'setup changed' => \&setup_changed;
3262
+
3263
+ # Add all default mappings.
3264
+ foreach my $char (keys %$commands) {
3265
+ next if $char =~ /^_/; # skip private commands (text-objects for now)
3266
+ add_map($char, $commands->{$char});
3267
+ }
3268
+
3269
+ # Load the vim_moderc file if it exists.
3270
+ ex_source('source');
3271
+
3272
+ setup_changed();
3273
+ _reset_undo_buffer();
3274
+
3275
+ if ($settings->{start_cmd}->{value}) {
3276
+ _update_mode(M_CMD);
3277
+ } else {
3278
+ _update_mode(M_INS);
3279
+ }
3280
+ }
3281
+
3282
+ sub setup_changed {
3283
+ my $value;
3284
+
3285
+ if ($settings->{cmd_seq}->{value} ne '') {
3286
+ delete $imaps->{$settings->{cmd_seq}->{value}};
3287
+ }
3288
+ $value = _setting_get('cmd_seq');
3289
+ if ($value eq '') {
3290
+ $settings->{cmd_seq}->{value} = $value;
3291
+ } else {
3292
+ if (length $value == 1) {
3293
+ $imaps->{$value} = { 'map' => $value,
3294
+ 'func' => sub { _update_mode(M_CMD) }
3295
+ };
3296
+ $settings->{cmd_seq}->{value} = $value;
3297
+ } else {
3298
+ _warn("Error: vim_mode_cmd_seq must be a single character");
3299
+ # Restore the value so $settings and irssi settings are
3300
+ # consistent.
3301
+ _setting_set('cmd_seq', $settings->{cmd_seq}->{value});
3302
+ }
3303
+ }
3304
+
3305
+ my $new_utf8 = _setting_get('utf8');
3306
+ if ($new_utf8 != $settings->{utf8}->{value}) {
3307
+ # recompile the patterns when switching to/from utf-8
3308
+ $word = qr/[\w_]/o;
3309
+ $non_word = qr/[^\w_\s]/o;
3310
+
3311
+ $settings->{utf8}->{value} = $new_utf8;
3312
+ }
3313
+ if ($new_utf8 and (!$^V or $^V lt v5.8.1)) {
3314
+ _warn("Warning: UTF-8 isn't supported very well in perl < 5.8.1! " .
3315
+ "Please disable the vim_mode_utf8 setting.");
3316
+ }
3317
+
3318
+ # Sync $settings with current irssi values.
3319
+ foreach my $name (keys %$settings) {
3320
+ # These were already handled above.
3321
+ next if $name eq 'cmd_seq' or $name eq 'cmd_seq';
3322
+
3323
+ $settings->{$name}->{value} = _setting_get($name);
3324
+ }
3325
+ }
3326
+
3327
+ sub UNLOAD {
3328
+ Irssi::signal_remove('gui key pressed' => \&got_key);
3329
+ Irssi::signal_remove('setup changed' => \&setup_changed);
3330
+ Irssi::statusbar_item_unregister ('vim_mode');
3331
+ Irssi::statusbar_item_unregister ('vim_windows');
3332
+ }
3333
+
3334
+ sub _add_undo_entry {
3335
+ my ($line, $pos) = @_;
3336
+
3337
+ # If we aren't at the top of the history stack, then drop newer entries as
3338
+ # we can't branch (yet).
3339
+ while ($undo_index > 0) {
3340
+ shift @undo_buffer;
3341
+ $undo_index--;
3342
+ }
3343
+
3344
+ # check it's not a dupe of the list head
3345
+ my $current = $undo_buffer[$undo_index];
3346
+ if ($line eq $current->[0] && $pos == $current->[1]) {
3347
+ print "Not adding duplicate to undo list" if DEBUG;
3348
+ } elsif ($line eq $current->[0]) {
3349
+ print "Updating position of undo list at $undo_index" if DEBUG;
3350
+ $undo_buffer[$undo_index]->[1] = $pos;
3351
+ } else {
3352
+ print "adding $line ($pos) to undo list" if DEBUG;
3353
+ # add to the front of the buffer
3354
+ unshift @undo_buffer, [$line, $pos];
3355
+ $undo_index = 0;
3356
+ }
3357
+ my $max = $settings->{max_undo_lines}->{value};
3358
+ }
3359
+
3360
+ sub _restore_undo_entry {
3361
+ my $entry = $undo_buffer[$undo_index];
3362
+ _input($entry->[0]);
3363
+ _input_pos($entry->[1]);
3364
+ }
3365
+
3366
+ sub _print_undo_buffer {
3367
+
3368
+ my $i = 0;
3369
+ my @buf;
3370
+ foreach my $entry (@undo_buffer) {
3371
+ my $str = '';
3372
+ if ($i == $undo_index) {
3373
+ $str .= '* ';
3374
+ } else {
3375
+ $str .= ' ';
3376
+ }
3377
+ my ($line, $pos) = @$entry;
3378
+ substr($line, $pos, 0) = '*';
3379
+ # substr($line, $pos+3, 0) = '%_';
3380
+
3381
+ $str .= sprintf('%02d %s [%d]', $i, $line, $pos);
3382
+ push @buf, $str;
3383
+ $i++;
3384
+ }
3385
+ print "------ undo buffer ------";
3386
+ print join("\n", @buf);
3387
+ print "------------------ ------";
3388
+
3389
+ }
3390
+
3391
+ sub _reset_undo_buffer {
3392
+ my ($line, $pos) = @_;
3393
+ $line = _input() unless defined $line;
3394
+ $pos = _input_pos() unless defined $pos;
3395
+
3396
+ print "Clearing undo buffer" if DEBUG;
3397
+ @undo_buffer = ([$line, $pos]);
3398
+ $undo_index = 0;
3399
+ }
3400
+
3401
+ sub add_map {
3402
+ my ($keys, $command) = @_;
3403
+
3404
+ # To allow multiple mappings starting with the same key (like gg, ge, gE)
3405
+ # also create maps for the keys "leading" to this key (g in this case, but
3406
+ # can be longer for this like ,ls). When looking for the mapping these
3407
+ # "leading" maps are followed.
3408
+ my $tmp = $keys;
3409
+ while (length $tmp > 1) {
3410
+ my $map = substr $tmp, -1, 1, '';
3411
+ if (not exists $maps->{$tmp}) {
3412
+ $maps->{$tmp} = { char => _parse_mapping_reverse($tmp),
3413
+ cmd => undef,
3414
+ maps => {}
3415
+ };
3416
+ }
3417
+ if (not exists $maps->{$tmp}->{maps}->{$tmp . $map}) {
3418
+ $maps->{$tmp}->{maps}->{$tmp . $map} = undef;
3419
+ }
3420
+ }
3421
+
3422
+ if (not exists $maps->{$keys}) {
3423
+ $maps->{$keys} = { char => undef,
3424
+ cmd => undef,
3425
+ maps => {}
3426
+ };
3427
+ }
3428
+ $maps->{$keys}->{char} = _parse_mapping_reverse($keys);
3429
+ $maps->{$keys}->{cmd} = $command;
3430
+ }
3431
+
3432
+ sub delete_map {
3433
+ my ($keys) = @_;
3434
+
3435
+ # Abort for non-existent mappings or placeholder mappings.
3436
+ return if not exists $maps->{$keys} or not defined $maps->{$keys}->{cmd};
3437
+
3438
+ my @add = ();
3439
+
3440
+ # If no maps need the current key, then remove it and all other
3441
+ # unnecessary keys in the "tree".
3442
+ if (keys %{$maps->{$keys}->{maps}} == 0) {
3443
+ my $tmp = $keys;
3444
+ while (length $tmp > 1) {
3445
+ my $map = substr $tmp, -1, 1, '';
3446
+ delete $maps->{$tmp}->{maps}->{$tmp . $map};
3447
+ if (not $maps->{$tmp}->{cmd} and keys %{$maps->{$tmp}->{maps}} == 0) {
3448
+ push @add, $tmp;
3449
+ delete $maps->{$tmp};
3450
+ } else {
3451
+ last;
3452
+ }
3453
+ }
3454
+ }
3455
+
3456
+ if (keys %{$maps->{$keys}->{maps}} > 0) {
3457
+ $maps->{$keys}->{cmd} = undef;
3458
+ } else {
3459
+ delete $maps->{$keys};
3460
+ }
3461
+ push @add, $keys;
3462
+
3463
+ # Restore default keybindings in case we :unmapped a <Nop> or a remapped
3464
+ # key.
3465
+ foreach my $key (@add) {
3466
+ if (exists $commands->{$key}) {
3467
+ add_map($key, $commands->{$key});
3468
+ }
3469
+ }
3470
+ }
3471
+
3472
+
3473
+ sub _commit_line {
3474
+ _update_mode(M_INS);
3475
+
3476
+ # separate from call above as _update_mode() does additional internal work
3477
+ # and we need to make sure it gets correctly called.
3478
+ _update_mode(M_CMD) if $settings->{start_cmd}->{value};
3479
+
3480
+ _reset_undo_buffer('', 0);
3481
+ }
3482
+
3483
+ sub _input {
3484
+ my ($data) = @_;
3485
+
3486
+ my $current_data = Irssi::parse_special('$L', 0, 0);
3487
+
3488
+ if ($settings->{utf8}->{value}) {
3489
+ $current_data = decode_utf8($current_data);
3490
+ }
3491
+
3492
+ if (defined $data) {
3493
+ if ($settings->{utf8}->{value}) {
3494
+ Irssi::gui_input_set(encode_utf8($data));
3495
+ } else {
3496
+ Irssi::gui_input_set($data);
3497
+ }
3498
+ } else {
3499
+ $data = $current_data;
3500
+ }
3501
+
3502
+ return $data;
3503
+ }
3504
+
3505
+ sub _input_len {
3506
+ return length _input();
3507
+ }
3508
+
3509
+ sub _input_pos {
3510
+ my ($pos) = @_;
3511
+ my $cur_pos = Irssi::gui_input_get_pos();
3512
+ # my $dpos = defined $pos?$pos:'undef';
3513
+ # my @call = caller(1);
3514
+ # my $cfunc = $call[3];
3515
+ # $cfunc =~ s/^.*?::([^:]+)$/$1/;
3516
+ # print "pos called from line: $call[2] sub: $cfunc pos: $dpos, cur_pos: $cur_pos"
3517
+ # if DEBUG;
3518
+
3519
+ if (defined $pos) {
3520
+ #print "Input pos being set from $cur_pos to $pos" if DEBUG;
3521
+ Irssi::gui_input_set_pos($pos) if $pos != $cur_pos;
3522
+ } else {
3523
+ $pos = $cur_pos;
3524
+ #print "Input pos retrieved as $pos" if DEBUG;
3525
+ }
3526
+
3527
+ return $pos;
3528
+ }
3529
+
3530
+ sub _emulate_keystrokes {
3531
+ my @keys = @_;
3532
+ $should_ignore = 1;
3533
+ for my $key (@keys) {
3534
+ Irssi::signal_emit('gui key pressed', $key);
3535
+ }
3536
+ $should_ignore = 0;
3537
+ }
3538
+
3539
+ sub _stop() {
3540
+ Irssi::signal_stop_by_name('gui key pressed');
3541
+ }
3542
+
3543
+ sub _update_mode {
3544
+ my ($new_mode) = @_;
3545
+
3546
+ my $pos;
3547
+
3548
+ if ($mode == M_INS and $new_mode == M_CMD) {
3549
+ # Support counts with insert modes, like 3i.
3550
+ if ($numeric_prefix and $numeric_prefix > 1) {
3551
+ $pos = _insert_buffer($numeric_prefix - 1, _input_pos());
3552
+ _input_pos($pos);
3553
+ $numeric_prefix = undef;
3554
+
3555
+ # In insert mode we are "between" characters, in command mode "on top"
3556
+ # of keys. When leaving insert mode we have to move on key left to
3557
+ # accomplish that.
3558
+ } else {
3559
+ $pos = _input_pos();
3560
+ if ($pos != 0) {
3561
+ _input_pos($pos - 1);
3562
+ }
3563
+ }
3564
+ # Store current line to allow undo of i/a/I/A.
3565
+ _add_undo_entry(_input(), _input_pos());
3566
+
3567
+ # Change mode to i to support insert mode repetition. This doesn't affect
3568
+ # commands like i/a/I/A because handle_command_cmd() sets $last->{cmd}.
3569
+ # It's necessary when pressing enter so the next line can be repeated.
3570
+ } elsif ($mode == M_CMD and $new_mode == M_INS) {
3571
+ $last->{cmd} = $commands->{i};
3572
+ # Make sure prompt is cleared when leaving ex mode.
3573
+ } elsif ($mode == M_EX and $new_mode != M_EX) {
3574
+ _set_prompt('');
3575
+ }
3576
+
3577
+ $mode = $new_mode;
3578
+ if ($mode == M_INS) {
3579
+ $history_index = undef;
3580
+ $register = '"';
3581
+ @insert_buf = ();
3582
+ # Reset every command mode related status as a fallback in case something
3583
+ # goes wrong.
3584
+ } elsif ($mode == M_CMD) {
3585
+ $numeric_prefix = undef;
3586
+ $operator = undef;
3587
+ $movement = undef;
3588
+ $register = '"';
3589
+
3590
+ $pending_map = undef;
3591
+
3592
+ # Also clear ex-mode buffer.
3593
+ @ex_buf = ();
3594
+ }
3595
+
3596
+ Irssi::statusbar_items_redraw("vim_mode");
3597
+ Irssi::statusbar_items_redraw ('uberprompt');
3598
+
3599
+ }
3600
+
3601
+ sub _set_prompt {
3602
+ my $msg = shift;
3603
+
3604
+ # add a leading space unless we're trying to clear it entirely.
3605
+ if (length($msg) and $settings->{prompt_leading_space}->{value}) {
3606
+ $msg = ' ' . $msg;
3607
+ }
3608
+
3609
+ # escape % symbols. This prevents any _set_prompt calls from using
3610
+ # colouring sequences.
3611
+ $msg =~ s/%/%%/g;
3612
+
3613
+ Irssi::signal_emit('change prompt', $msg, 'UP_INNER');
3614
+ }
3615
+
3616
+ sub _setting_get {
3617
+ my ($name) = @_;
3618
+
3619
+ my $type = $settings->{$name}->{type};
3620
+ $name = "vim_mode_$name";
3621
+
3622
+ my $ret = undef;
3623
+
3624
+ if ($type == S_BOOL) {
3625
+ $ret = Irssi::settings_get_bool($name);
3626
+ } elsif ($type == S_INT) {
3627
+ $ret = Irssi::settings_get_int($name);
3628
+ } elsif ($type == S_STR) {
3629
+ $ret = Irssi::settings_get_str($name);
3630
+ } elsif ($type == S_TIME) {
3631
+ $ret = Irssi::settings_get_time($name);
3632
+ } else {
3633
+ _warn("Unknown setting type '$type', please report.");
3634
+ }
3635
+
3636
+ return $ret;
3637
+ }
3638
+
3639
+ sub _setting_set {
3640
+ my ($name, $value) = @_;
3641
+
3642
+ my $type = $settings->{$name}->{type};
3643
+ $name = "vim_mode_$name";
3644
+
3645
+ if ($type == S_BOOL) {
3646
+ Irssi::settings_set_bool($name, $value);
3647
+ } elsif ($type == S_INT) {
3648
+ Irssi::settings_set_int($name, $value);
3649
+ } elsif ($type == S_STR) {
3650
+ Irssi::settings_set_str($name, $value);
3651
+ } elsif ($type == S_TIME) {
3652
+ Irssi::settings_set_time($name, $value);
3653
+ } else {
3654
+ _warn("Unknown setting type '$type', please report.");
3655
+ }
3656
+ }
3657
+ sub _setting_register {
3658
+ my ($name) = @_;
3659
+
3660
+ my $value = $settings->{$name}->{value};
3661
+ my $type = $settings->{$name}->{type};
3662
+ $name = "vim_mode_$name";
3663
+
3664
+ if ($type == S_BOOL) {
3665
+ Irssi::settings_add_bool('vim_mode', $name, $value);
3666
+ } elsif ($type == S_INT) {
3667
+ Irssi::settings_add_int('vim_mode', $name, $value);
3668
+ } elsif ($type == S_STR) {
3669
+ Irssi::settings_add_str('vim_mode', $name, $value);
3670
+ } elsif ($type == S_TIME) {
3671
+ Irssi::settings_add_time('vim_mode', $name, $value);
3672
+ } else {
3673
+ _warn("Unknown setting type '$type', please report.");
3674
+ }
3675
+ }
3676
+
3677
+ sub _warn {
3678
+ my ($warning) = @_;
3679
+
3680
+ print '%_vim_mode: ', $warning, '%_';
3681
+ }
3682
+
3683
+ sub _debug {
3684
+ return unless DEBUG;
3685
+
3686
+ my ($format, @args) = @_;
3687
+ my $str = sprintf($format, @args);
3688
+ print $str;
3689
+ }
3690
+
3691
+ sub _command_with_context {
3692
+ my ($command) = @_;
3693
+ my $context;
3694
+ my $window = Irssi::active_win;
3695
+ if (defined $window) {
3696
+ my $witem = $window->{active};
3697
+ if (defined $witem and ref($witem) eq 'Irssi::Windowitem') {
3698
+ $context = $witem;
3699
+ } else {
3700
+ $context = $window;
3701
+ }
3702
+ } else {
3703
+ my $server = Irssi::active_server;
3704
+ if (defined $server) {
3705
+ $context = $server;
3706
+ }
3707
+ }
3708
+ if (defined $context) {
3709
+ print "Command $command Using context: " . ref($context) if DEBUG;
3710
+ $context->command($command);
3711
+ } else {
3712
+ print "Command $command has no context" if DEBUG;
3713
+ Irssi::command($command);
3714
+ }
3715
+ }
3716
+
3717
+ sub ex_history_add {
3718
+ my ($line) = @_;
3719
+
3720
+ # check it's not an exact dupe of the previous history line
3721
+
3722
+ my $last_hist = $ex_history[$ex_history_index];
3723
+ $last_hist = '' unless defined $last_hist;
3724
+
3725
+ return if $last_hist eq $line;
3726
+
3727
+ _debug("Adding $line to ex command history");
3728
+
3729
+ # add it to the history
3730
+ unshift @ex_history, $line;
3731
+
3732
+ if ($settings->{ex_history_size}->{value} < @ex_history) {
3733
+ pop @ex_history; # junk the last entry if we've hit the max.
3734
+ }
3735
+ }
3736
+
3737
+ sub ex_history_fwd {
3738
+
3739
+ my $hist_max = $#ex_history;
3740
+ $ex_history_index++;
3741
+ if ($ex_history_index > $hist_max) {
3742
+ $ex_history_index = 0;
3743
+ _debug("ex history hit top, wrapping to 0");
3744
+ }
3745
+
3746
+ my $line = $ex_history[$ex_history_index];
3747
+ $line = '' if not defined $line;
3748
+
3749
+ _debug("Ex history line: $line");
3750
+
3751
+ @ex_buf = split '', $line;
3752
+ handle_command_ex(-1);
3753
+ }
3754
+
3755
+ sub ex_history_back {
3756
+ my $hist_max = $#ex_history;
3757
+ $ex_history_index--;
3758
+ if ($ex_history_index == -1) {
3759
+ $ex_history_index = $hist_max;
3760
+ _debug("ex history hit bottom, wrapping to $hist_max");
3761
+
3762
+ }
3763
+
3764
+ my $line = $ex_history[$ex_history_index];
3765
+ $line = '' if not defined $line;
3766
+
3767
+ _debug("Ex history line: $line");
3768
+ @ex_buf = split '', $line;
3769
+ handle_command_ex(-1);
3770
+
3771
+ }
3772
+
3773
+ sub ex_history_show {
3774
+ my $win = Irssi::active_win();
3775
+ $win->print("Ex command history:");
3776
+ for my $i (0 .. $#ex_history) {
3777
+ my $flag = $i == $ex_history_index
3778
+ ? ' <'
3779
+ : '';
3780
+ $win->print("$i " . $ex_history[$i] . $flag);
3781
+ }
3782
+ }
3783
+ vim_mode_init();