zsh_dots 0.5.9 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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();