/[Term-Shelly]/trunk/lib/Term/Shelly.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/lib/Term/Shelly.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 10 - (show annotations)
Mon Jun 25 12:31:11 2007 UTC (16 years, 10 months ago) by dpavlin
File size: 26539 byte(s)
small pod tweaks (move methods to =head2), debug for handle_key
1 =pod
2
3 =head1 NAME
4
5 Term::Shelly - Yet Another Shell Kit for Perl
6
7 =head1 VERSION
8
9 $Id$
10
11 =head1 GOAL
12
13 I needed a shell kit for an aim client I was writing. All of the Term::ReadLine modules are do blocking reads in doing their readline() functions, and as such are entirely unacceptable. This module is an effort on my part to provide the advanced functionality of great ReadLine modules like Zoid into a package that's more flexible, extendable, and most importantly, allows nonblocking reads to allow other things to happen at the same time.
14
15 =head1 NEEDS
16
17 - Settable key bindings
18 - history
19 - vi mode (Yeah, I lub vi)
20
21 =head1 DONE
22
23 - Callback for 'anykey'
24 - Tab completion
25 - Support for window size changes (sigwinch)
26 - movement/in-line editing.
27 - Completion function calls
28 - Settable callbacks for when we have an end-of-line (EOL binding?)
29
30 =cut
31
32 package Term::Shelly;
33
34 use strict;
35 use warnings;
36
37 use vars qw($VERSION);
38 $VERSION = '0.3_01';
39
40 # Default perl modules...
41 use IO::Select;
42 use IO::Handle; # I need flush()... or do i?;
43
44 # Get these from CPAN
45 use Term::ReadKey;
46
47 # Useful constants we need...
48
49 # for find_word_bound()
50 use constant WORD_BEGINNING => 1; # look for the beginning of this word.
51 use constant WORD_END => 2; # look for end of the word.
52 use constant WORD_NEXT => 4; # look for beginning of next word
53 use constant WORD_ONLY => 8; # Trailing spaces are important.
54 use constant WORD_REGEX => 16; # I want to specify my own regexp
55
56 # for vi_jumpchar()
57 use constant JUMP_BACKCHARTO => 000; # 'T' in vi (backwards)
58 use constant JUMP_BACKCHAR => 001; # 'F' in vi (backwards)
59 use constant JUMP_CHARTO => 010; # 't' in vi (forwards)
60 use constant JUMP_CHAR => 011; # 'f' in vi (forwards)
61
62 # Some key constant name mappings.
63 # I definately need a function to do this and some sort of hash returned which
64 # specifies the key pressed and any modifiers too.
65 # Like... ctrl+f5 is \e[15;5~ ... and not on all systems.
66 my %KEY_CONSTANTS = (
67 "\e[A" => "UP",
68 "\e[B" => "DOWN",
69 "\e[C" => "RIGHT",
70 "\e[D" => "LEFT",
71 );
72
73 # I need to know how big the terminal is (columns, anyway)
74
75 =pod
76
77 =head1 DESCRIPTION
78
79 =head1 METHODS
80
81 =head2 new
82
83 my $sh = Term::Shelly->new();
84
85 =cut
86
87 sub new ($) {
88 my $class = shift;
89
90 my $self = {
91 "input_line" => "",
92 "input_position" => 0,
93 "input_prompt" => "",
94 "leftcol" => 0,
95 "echo" => 1,
96 "vi_mode" => 0,
97 "mode" => "insert",
98 "debug" => 0,
99 };
100
101 bless $self, $class;
102
103 ($self->{"termcols"}) = GetTerminalSize();
104 $SIG{WINCH} = sub { ($self->{"termcols"}) = GetTerminalSize(); $self->fix_inputline() };
105 $SIG{CONT} = sub { ReadMode 3; $self->fix_inputline; };
106
107 $self->{"select"} = new IO::Select(\*STDIN);
108
109 my $bindings = {
110 "ANYKEY" => "anykey",
111 "LEFT" => "backward-char",
112 "RIGHT" => "forward-char",
113 "UP" => "up-history",
114 "DOWN" => "down-history",
115
116 "BACKSPACE" => "delete-char-backward",
117 "^H" => "delete-char-backward",
118 "^?" => "delete-char-backward",
119 "^W" => "delete-word-backward",
120
121 "^U" => "kill-line",
122
123 "^J" => "newline",
124 "^M" => "newline",
125
126 "^A" => "beginning-of-line",
127 "^E" => "end-of-line",
128 "^K" => "kill-to-eol",
129 "^L" => "redraw",
130
131 "^I" => "complete-word",
132 "TAB" => "complete-word",
133
134 #"^T" => "expand-line",
135
136 #--------------------------------
137 # vi bindings
138 #
139
140 # -------- DIRECTIONS
141
142 "vi_h" => "vi-backward-char", # DONE
143 "vi_l" => "vi-forward-char", # DONE
144 "vi_k" => "vi-up-history",
145 "vi_j" => "vi-down-history",
146
147 "vi_w" => "vi-forward-word", # DONE
148 "vi_W" => "vi-forward-whole-word", # DONE
149 "vi_e" => "vi-end-word", # DONE
150 "vi_E" => "vi-end-whole-word", # DONE
151 "vi_t" => "vi-forward-charto",
152 "vi_T" => "vi-backward-charto",
153 "vi_f" => "vi-forward-charat",
154 "vi_F" => "vi-backward-charat",
155 "vi_G" => "vi-history-goto",
156 "vi_b" => "vi-beginning-word",
157 "vi_B" => "vi-beginning-whole-word",,
158 "vi_n" => "vi-search-next",
159 "vi_N" => "vi-search-prev",
160 "vi_'" => "vi-mark-goto",
161 'vi_$' => "vi-end-of-line",
162 "vi_^" => "vi-beginning-of-line",
163
164 # -------- INSERTION
165
166 "vi_i" => "vi-insert",
167 "vi_I" => "vi-insert-at-bol",
168 "vi_a" => "vi-add",
169 "vi_A" => "vi-add-at-eol",
170 "vi_r" => "vi-replace-char",
171 "vi_R" => "vi-replace-mode",
172 "vi_s" => "vi-substitute-char",
173 "vi_S" => "vi-substitute-line",
174 #"vi_o"
175 #"vi_O"
176 "vi_c" => "vi-change",
177 "vi_C" => "vi-change-to-eol",
178
179 #"vi_y" => "vi-yank-direction",
180 #"vi_Y" => "vi-yank-to-eol",
181 #"vi_u" => "vi-undo",
182 #"vi_p" => "vi-paste-at",
183 #"vi_P" => "vi-paste-before",
184 "vi_x" => "vi-delete-char-backward",
185 "vi_X" => "vi-delete-char-forward",
186 "vi_d" => "vi-delete",
187
188 # -------- OTHER COMMANDS
189
190 "vi_m" => "vi-mark",
191
192 };
193
194 my $mappings = {
195 "anykey" => [ \&anykey ],
196 "backward-char" => [ \&backward_char ],
197 "forward-char" => [ \&forward_char ],
198 "delete-char-backward" => [ \&delete_char_backward ],
199 "kill-line" => [ \&kill_line ],
200 "newline" => [ \&newline ],
201 "redraw" => [ \&fix_inputline ],
202 "beginning-of-line" => [ \&beginning_of_line ],
203 "end-of-line" => [ \&end_of_line ],
204 "delete-word-backward" => [ \&delete_word_backward ],
205
206 "complete-word" => [ \&complete_word ],
207 #"expand-line" => [ \&expand_line ],
208
209 # ----------------------------------------------------------- vi mappings
210 "vi-backward-char" => [ \&vi_backward_char ],
211 "vi-forward-char" => [ \&vi_forward_char ],
212 "vi-forward-word" => [ \&vi_forward_word ],
213 "vi-forward-whole-word" => [ \&vi_forward_whole_word ],
214 "vi-beginning-word" => [ \&vi_beginning_word ],
215 "vi-beginning-whole-word" => [ \&vi_beginning_whole_word ],
216 "vi-end-of-line" => [ \&vi_eol ],
217 "vi-beginning-of-line" => [ \&vi_bol ],
218 "vi-forward-charto" => [ \&vi_forward_charto ],
219 "vi-forward-charat" => [ \&vi_forward_charat ],
220 "vi-backward-charto" => [ \&vi_backward_charto ],
221 "vi-backward-charat" => [ \&vi_backward_charat ],
222
223 "vi-end-word" => [ \&vi_end_word ],
224 "vi-end-whole-word" => [ \&vi_end_whole_word ],
225 "vi-insert" => [ \&vi_insert ],
226 "vi-insert-at-bol" => [ \&vi_insert_at_bol ],,
227 "vi-add" => [ \&vi_add ],
228 "vi-add-at-eol" => [ \&vi_add_at_eol ],
229
230 "vi-delete-char-backward" => [ \&vi_delete_char_backward ],
231 "vi-delete-char-forward" => [ \&vi_delete_char_forward ],
232 "vi-delete" => [ \&vi_delete ],
233
234 };
235
236 $self->{"bindings"} = $bindings;
237 $self->{"mappings"} = $mappings;
238
239 # stty raw, basically
240 ReadMode 3;
241
242 return $self;
243 }
244
245 sub DESTROY {
246 my $self = shift;
247 $self->real_out("\n");
248 ReadMode 0;
249 }
250
251 =pod
252
253 =head2 $sh->do_one_loop()
254
255 Does... one... loop. Makes a pass at grabbing input and processing it. For
256 speedy pasts, this loops until there are no characters left to read.
257 It will handle event processing, etc.
258
259 =cut
260
261 # Nonblocking readline
262 sub do_one_loop ($) {
263 my $self = shift;
264 my $text;
265 my $char;
266
267 # Select for .01
268 #
269 if ($self->{"select"}->can_read(.01)) {
270 my $bytes = sysread(STDIN, $text, 4096);
271 for (my $i = 0; $i < length($text); $i++) {
272 $char = substr($text,$i,1);
273 $self->handle_key($char);
274 }
275 }
276
277 }
278
279 =pod
280
281 =head2 handle_key($key)
282
283 Handle a single character input. This is not a "key press" so much as doing all
284 the necessary things to handle key presses.
285
286 =cut
287
288 sub handle_key($$) {
289 my $self = shift;
290 my $char = shift;
291
292 $self->debug("handle_key( '$char' == ", ord($char), " )");
293
294 my $line = $self->{"input_line"} || "";
295 my $pos = $self->{"input_position"} || 0;
296
297 if (defined($self->{"input_slurper"})) {
298 &{$self->{"input_slurper"}}($self, $char);
299 return;
300 }
301
302 if ($self->{"escape"}) {
303 $self->{"escape_string"} .= $char;
304 if ($self->{"escape_expect_ansi"}) {
305 $self->{"escape_expect_ansi"} = 0 if ($char =~ m/[a-zA-Z~]/);
306 }
307
308 $self->{"escape_expect_ansi"} = 1 if ($char eq '[');
309 $self->{"escape"} = 0 unless ($self->{"escape_expect_ansi"});
310
311 unless ($self->{"escape_expect_ansi"}) {
312 my $estring = $self->{"escape_string"};
313
314 $self->{"escape_string"} = undef;
315 $self->execute_binding("\e".$estring);
316 } else {
317 return;
318 }
319 } elsif ($char eq "\e") { # Trap escapes, they're speshul.
320 if ($self->{"vi_mode"}) {
321 if ($self->{"mode"} eq 'insert') {
322 $self->{"input_position"}-- if ($self->{"input_position"} > 1);
323 $self->{"mode"} = "command";
324 }
325 } else {
326 $self->{"escape"} = 1;
327 $self->{"escape_string"} = undef;
328 return;
329 }
330 } elsif ((ord($char) < 32) || (ord($char) > 126)) { # Control character
331 $self->execute_binding($char);
332 } elsif ((defined($char)) && (ord($char) >= 32)) {
333 if (defined($self->{"mode"}) && $self->{"mode"} eq "command") {
334 if ($char =~ m/[0-9]/) {
335 $self->{"vi_count"} .= $char;
336 } else {
337 my $cmdwait = defined($self->{"vi_command_waiting"});
338
339 $self->{"vi_count"} ||= 1;
340 while ($self->{"vi_count"} > 0) {
341 $self->execute_binding("vi_$char");
342 $self->{"vi_count"}--;
343 }
344 if ($cmdwait) {
345 &{$self->{"vi_command_waiting"}}($self, 1);
346 $self->{"input_position"} = $self->{"vi_input_position"};
347 delete $self->{"vi_command_waiting"};
348 }
349 }
350 } else {
351 if (defined($self->{"bindings"}->{"$char"})) {
352 $self->execute_binding($char);
353 } else {
354 # Insert the character in our string, wherever we are.
355 #substr($line, $pos, 0) = $char;
356 #$self->{"input_position"}++;
357 $self->insert_at_cursor($char);
358 }
359 }
360
361 # If we just did a tab completion, kill the state.
362 delete($self->{"completion"}) if (defined($self->{"completion"}));
363 $self->fix_inputline();
364 }
365
366 # This is sometimes a nice feature to have...
367 # Press the any key!!!
368 $self->{"lastchar"} = $char;
369 $self->execute_binding("ANYKEY");
370
371 #$self->fix_inputline();
372 }
373
374 =pod
375
376 =head2 execute_binding(raw_key)
377
378 Guess what this does? Ok I'll explain anyway... It takes a key and prettifies
379 it, then checks the known key bindings for a mapping and checks if that mapping
380 is a coderef (a function reference). If it is, it'll call that function. If
381 not, it'll do nothing. If it finds a binding for which there is no mapped
382 function, it'll tell you that it is an unimplemented function.
383
384 =cut
385
386 sub execute_binding ($$) {
387 my $self = shift;
388 my $str = shift;
389 my $key = $self->prettify_key($str);
390
391 #$self->out("Key: $key");
392
393 my $bindings = $self->{"bindings"};
394 my $mappings = $self->{"mappings"};
395
396 if (defined($bindings->{$key})) {
397
398 # Check if we have stored completion state and the next binding is
399 # not complete-word. If it isn't, then kill the completion state.
400 if (defined($self->{"completion"}) && $key ne 'ANYKEY' &&
401 $bindings->{$key} ne 'complete-word') {
402 delete($self->{"completion"});
403 }
404
405 if (ref($mappings->{$bindings->{$key}}) =~ m/(CODE|ARRAY)/) {
406
407 # This is a hack, passing $self instead of doing:
408 # $self->function, becuase I don't want to do an eval.
409
410 if ($1 eq 'ARRAY') {
411 map { &{$_}($self) } @{$mappings->{$bindings->{$key}}};
412 } else {
413 &{$mappings->{$bindings->{$key}}}($self);
414 }
415
416 } else {
417 $self->error("Unimplemented function, " . $bindings->{$key});
418 }
419 }
420 }
421
422 =pod
423
424 =head2 prettify_key(raw_key)
425
426 This happy function lets me turn raw input into something less ugly. It turns
427 control keys into their equivalent ^X form. It does some other things to turn
428 the key into something more readable
429
430 =cut
431
432 sub prettify_key ($$) {
433 my $self = shift;
434 my $key = shift;
435
436 # Return ^X for control characters, like CTRL+A...
437 if (length($key) == 1) { # One-character keycombos should only be ctrl keys
438 if (ord($key) <= 26) { # Control codes, another check anyway...
439 return "^" . chr(65 + ord($key) - 1);
440 }
441 if (ord($key) == 127) { # Speshul backspace key
442 return "^?";
443 }
444 if (ord($key) < 32) {
445 return "^" . (split("", "\]_^"))[ord($key) - 28];
446 }
447 }
448
449 # Return ESC-X for escape shenanigans, like ESC-W
450 if (length($key) == 2) {
451 my ($p, $k) = split("", $key);
452 if ($p eq "\e") { # This should always be an escape, but.. check anyway
453 return "ESC-" . $k;
454 }
455 }
456
457 # Ok, so it's not ^X or ESC-X, it's gotta be some ansi funk or a normal char.
458 return $KEY_CONSTANTS{$key} || $key;
459 }
460
461 =pod
462
463 =head2 real_out($string)
464
465 This function allows you to bypass any sort of evil shenanigans regarding output fudging. All this does is 'print @_;'
466
467 Don't use this unless you know what you're doing.
468
469 =cut
470
471 sub real_out {
472 my $self = shift;
473 print @_;
474 }
475
476 sub out ($;$) {
477 my $self = shift;
478 $self->real_out("\r\e[2K", @_, "\n");
479 $self->fix_inputline();
480 }
481
482 sub error ($$) {
483 my $self = shift;
484 $self->real_out("\r\e[2K");
485 print STDERR "*> ", @_, "\n";
486 $self->fix_inputline();
487 }
488
489 sub debug ($;$) {
490 my $self = shift;
491 return unless $self->{'debug'};
492 $self->out( '# ', @_ );
493 }
494
495 =pod
496
497 =head2 fix_inputline
498
499 This super-happy function redraws the input line. If input_position is beyond the bounds of the terminal, it'll shuffle around so that it can display it. This function is called just about any time any key is hit.
500
501 =cut
502
503 sub fix_inputline ($) {
504 my $self = shift;
505
506 print "\r\e[2K";
507
508 if ($self->{"echo"} == 0) {
509 #print "Echo is off...\n";
510 print $self->{"input_prompt"};
511 return;
512 }
513
514 # If we're before the beginning of the terminal line, shuffle over!
515 if ($self->{"input_position"} - $self->{"leftcol"} <= 0) {
516 $self->{"leftcol"} -= 30;
517 $self->{"leftcol"} = 0 if ($self->{"leftcol"} < 0);
518 }
519
520 # If we're past the end of the terminal line, shuffle back!
521 # length = input_position - leftcol + input_prompt - leftcol
522 my $pl = length($self->{"input_prompt"}) - $self->{"leftcol"};
523 $pl = 0 if ($pl < 0);
524 if ($self->{"input_position"} - $self->{"leftcol"} + $pl > $self->{"termcols"}) {
525 $self->{"leftcol"} += 30;
526 }
527
528 # Can se show the whole line? If so, do it!
529 if (length($self->{"input_line"}) + length($self->{"input_prompt"}) < $self->{"termcols"}) {
530 $self->{"leftcol"} = 0;
531 }
532
533 # only print as much as we can in this one line.
534 my $prompt = $self->{"input_prompt"};
535 my $offset = 0;
536 if ($self->{"leftcol"} <= length($self->{"input_prompt"})) {
537 print substr($prompt,$self->{"leftcol"});
538 $offset = length(substr($prompt,$self->{"leftcol"}));
539 }
540
541 print substr($self->{"input_line"}, $self->{"leftcol"}, $self->{"termcols"} - $offset);
542 print "\r";
543 print "\e[" . ($self->{"input_position"} - $self->{"leftcol"} + $offset) .
544 "C" if (($self->{"input_position"} + $offset) > 0);
545 STDOUT->flush();
546 }
547
548 sub newline {
549 my $self = shift;
550 # Process the input line.
551
552 if ($self->{"supress_newline_echo"}) {
553 # Clear the line
554 $self->real_out("\e[2K");
555 } else {
556 $self->real_out("\n");
557 }
558
559 my $line = $self->{"input_line"};
560
561 $self->{"input_line"} = "";
562 $self->{"input_position"} = 0;
563 $self->{"leftcol"} = 0;
564
565 $self->callback("readline", $line);
566 #if (ref($self->{"readline_callback"}) eq 'CODE') {
567 #&{$self->{"readline_callback"}}($line);
568 #}
569
570 $self->fix_inputline();
571 }
572
573 sub kill_line {
574 my $self = shift;
575
576 # Ask for more data perhaps...
577 $self->callback("fardelete");# if (length($self->{"input_line"}) == 0);
578
579 $self->{"input_line"} = "";
580 $self->{"input_position"} = 0;
581 $self->{"leftcol"} = 0;
582
583 #real_out("\r\e[2K");
584
585 $self->fix_inputline();
586
587 return 0;
588 }
589
590 sub forward_char {
591 my $self = shift;
592 if ($self->{"input_position"} < length($self->{"input_line"})) {
593 $self->{"input_position"}++;
594 $self->real_out("\e[C");
595 }
596 }
597
598 sub backward_char {
599 my $self = shift;
600 if ($self->{"input_position"} > 0) {
601 $self->{"input_position"}--;
602 $self->real_out("\e[D");
603 }
604 }
605
606 sub delete_char_backward {
607 my $self = shift;
608
609 $self->callback("fardelete") if (length($self->{"input_line"}) == 0);
610
611 if ($self->{"input_position"} > 0) {
612 substr($self->{"input_line"}, $self->{"input_position"} - 1, 1) = '';
613 $self->{"input_position"}--;
614 $self->fix_inputline();
615 }
616 }
617
618 sub beginning_of_line {
619 my $self = shift;
620 $self->{"input_position"} = 0;
621 $self->{"leftcol"} = 0;
622 $self->fix_inputline();
623 }
624
625 sub end_of_line {
626 my $self = shift;
627 $self->{"input_position"} = length($self->{"input_line"});
628 $self->fix_inputline();
629 }
630
631 sub delete_word_backward {
632 my $self = shift;
633 my $pos = $self->{"input_position"};
634 my $line = $self->{"input_line"};
635 #my $regex = '[A-Za-z0-9]';
636 my $regex = '\S';
637 my $bword;
638
639 $self->callback("fardelete") if (length($self->{"input_line"}) == 0);
640 $bword = $self->find_word_bound($line, $pos, WORD_BEGINNING | WORD_REGEX, $regex);
641
642 #$self->error("Testing $bword $pos");
643 # Delete whatever word we just found.
644 substr($line, $bword, $pos - $bword) = '';
645
646 # Update stuff...
647 $self->{"input_line"} = $line;
648 $self->{"input_position"} -= ($pos - $bword);
649
650 $self->fix_inputline();
651 }
652
653 sub vi_backward_char {
654 my $self = shift;
655
656 $self->backward_char();
657 $self->{"vi_done"} = 1;
658 }
659
660 sub vi_forward_char {
661 my $self = shift;
662
663 $self->forward_char();
664 $self->{"vi_done"} = 1;
665 }
666
667 sub vi_forward_word {
668 my $self = shift;
669 my $pos = $self->{"input_position"};
670 my $line = $self->{"input_line"};
671 my $bword = $pos;
672 my $BITS = WORD_NEXT;
673 my $regex = shift;
674
675 $BITS |= WORD_REGEX if (defined($regex));
676 $bword = $self->find_word_bound($line, $pos, $BITS, $regex);
677
678 $self->{"input_position"} = $bword;
679
680 $self->{"vi_done"};
681 }
682
683 sub vi_forward_whole_word {
684 my $self = shift;
685 $self->vi_forward_word('\S');
686 $self->{"vi_done"} = 1;
687 }
688
689 sub vi_beginning_word {
690 my $self = shift;
691 my $pos = $self->{"input_position"};
692 my $line = $self->{"input_line"};
693 my $bword = $pos;
694 my $BITS = WORD_BEGINNING;
695 my $regex = shift;
696
697 $BITS |= WORD_REGEX if (defined($regex));
698 $bword = $self->find_word_bound($line, $pos, $BITS, $regex);
699
700 $self->{"input_position"} = $bword;
701
702 $self->{"vi_done"};
703 }
704
705 sub vi_beginning_whole_word {
706 my $self = shift;
707 $self->vi_beginning_word('\S');
708 $self->{"vi_done"} = 1;
709 }
710
711 sub vi_end_word {
712 my $self = shift;
713 my $pos = $self->{"input_position"};
714 my $line = $self->{"input_line"};
715 my $bword = $pos;
716 my $BITS = WORD_END;
717 my $regex = shift;
718
719 $BITS |= WORD_REGEX if (defined($regex));
720 $bword = $self->find_word_bound($line, $pos, $BITS, $regex);
721
722 $self->{"input_position"} = $bword;
723
724 $self->{"vi_done"};
725 }
726
727 sub vi_end_whole_word {
728 my $self = shift;
729 $self->vi_end_word('\S');
730 $self->{"vi_done"} = 1;
731 }
732
733 sub vi_forward_charto {
734 my $self = shift;
735
736 # We need to wait for another character input...
737 $self->{"jumpchardir"} = JUMP_CHARTO;
738 $self->{"input_slurper"} = \&vi_jumpchar;
739 }
740
741 sub vi_forward_charat {
742 my $self = shift;
743
744 # We need to wait for another character input...
745 $self->{"jumpchardir"} = JUMP_CHAR;
746 $self->{"input_slurper"} = \&vi_jumpchar;
747 }
748
749 sub vi_backward_charto {
750 my $self = shift;
751
752 $self->{"jumpchardir"} = JUMP_BACKCHARTO;
753 $self->{"input_slurper"} = \&vi_jumpchar;
754 }
755
756 sub vi_backward_charat {
757 my $self = shift;
758
759 $self->{"jumpchardir"} = JUMP_BACKCHAR;
760 $self->{"input_slurper"} = \&vi_jumpchar;
761 }
762
763 sub vi_jumpchar {
764 my $self = shift;
765 my $char = shift;
766 my $pos = $self->{"input_position"};
767 my $line = $self->{"input_line"};
768 my $newpos;
769 my $mod = 0;
770
771 delete $self->{"input_slurper"};
772
773 $mod = ($self->{"jumpchardir"} & JUMP_CHARTO ? 1 : -1);
774
775 if ($mod == 1) {
776 #$self->out("F: $line / $pos / " . $line =~ m/^(.{$pos}[^$char]*)$char/);
777 #$self->out(" " . " " x ($pos) . "^ / $1");
778 $pos = length($1) if (defined($1));
779 } else {
780 #$self->out("B: $line / $pos / " . $line =~ m/$char([^$char]*.{$pos})$/);
781 #$self->out(" " . " " x ($pos - 1) . "^ / $1");
782 $pos = length($line) - length($1) if (defined($1));
783 }
784 $self->{"input_position"} = $pos;
785
786 $self->fix_inputline();
787 }
788
789 sub vi_bol {
790 my $self = shift;
791 $self->{"input_position"} = 0;
792 $self->{"vi_done"} = 1;
793 }
794
795 sub vi_eol {
796 my $self = shift;
797 $self->{"input_position"} = length($self->{"input_line"});
798 $self->{"vi_done"} = 1;
799 }
800 sub vi_insert {
801 my $self = shift;
802
803 $self->{"mode"} = "insert";
804 $self->{"vi_done"} = 1;
805 }
806
807 sub vi_insert_at_bol {
808 my $self = shift;
809
810 $self->vi_bol();
811 $self->vi_insert();
812 $self->{"vi_done"} = 1;
813 }
814
815 sub vi_add {
816 my $self = shift;
817
818 $self->{"input_position"}++ if ($self->{"input_position"} < length($self->{"input_line"}));
819
820 $self->vi_insert();
821 $self->{"vi_done"} = 1;
822 }
823
824 sub vi_add_at_eol {
825 my $self = shift;
826
827 $self->vi_eol();
828 $self->vi_add();
829 $self->{"vi_done"} = 1;
830 }
831
832 sub vi_delete_char_forward {
833 my $self = shift;
834 unless ($self->{"input_position"} == 0) {
835 substr($self->{"input_line"}, $self->{"input_position"} - 1, 1) = '';
836 $self->{"input_position"}--;
837 }
838 }
839
840 sub vi_delete_char_backward {
841 my $self = shift;
842
843 $self->callback("fardelete") if (length($self->{"input_line"}) == 0);
844
845 substr($self->{"input_line"}, $self->{"input_position"}, 1) = '';
846 $self->{"input_position"}-- if ($self->{"input_position"} == length($self->{"input_line"}) && $self->{"input_position"} > 0);
847 }
848
849 sub vi_delete {
850 my $self = shift;
851 my $exec = shift || 0;
852
853 if ($exec == 1) {
854 my ($start, $end);
855
856 $self->callback("fardelete") if (length($self->{"input_line"}) == 0);
857
858 if ($self->{"input_position"} < $self->{"vi_input_position"}) {
859 $start = $self->{"input_position"};
860 $end = $self->{"vi_input_position"};
861 } else {
862 $start = $self->{"vi_input_position"};
863 $end = $self->{"input_position"};
864 }
865 substr($self->{"input_line"}, $start, ($end - $start)) = '';
866 } else {
867 # Mark such that we remember what command we're doing at the time
868 # and set ourselves as the call back for the end of the next valid
869 # command. soo.... something like:
870 $self->{"vi_command_waiting"} = \&vi_delete;
871 $self->{"vi_input_position"} = $self->{"input_position"};
872 }
873
874 }
875
876 =pod
877
878 =head2 $sh->complete_word
879
880 This is called whenever the complete-word binding is triggered. See the
881 COMPLETION section below for how to write your own completion function.
882
883 =cut
884
885 sub complete_word {
886 my $self = shift;
887 my $pos = $self->{"input_position"};
888 my $line = $self->{"input_line"};
889 my $regex = "[A-Za-z0-9]";
890 my $bword;
891 my $complete;
892
893 if (ref($self->{"completion_function"}) eq 'CODE') {
894 my @matches;
895
896 # Maintain some sort of state here if this is the first time we've
897 # hit complete_word() for this "scenario." What I mean is, we need to track
898 # whether or not this user is hitting tab once or twice (or more) in the
899 # same position.
900 RECHECK:
901 if (!defined($self->{"completion"})) {
902 $bword = $self->find_word_bound($line, $pos, WORD_BEGINNING | WORD_REGEX, '\S');
903 $complete = substr($line,$bword,$pos - $bword);
904 $self->debug("Complete '$complete'");
905 $self->debug(length($line) . " / $bword / $pos");
906
907 # Make sure we can actually do this ?
908
909 $self->debug("First time completing '$complete'");
910 $self->{"completion"} = {
911 "index" => 0,
912 "original" => $complete,
913 "pos" => $pos,
914 "bword" => $bword,
915 "line" => $line,
916 "endpos" => $pos,
917 };
918 } else {
919 $bword = $self->{"completion"}->{"bword"};
920 $self->debug(length($line) . " / $bword / $pos");
921 $complete = substr($line,$bword,$pos - $bword);
922 }
923
924 # If we don't have any matches to check against...
925 unless (defined($self->{"completion"}->{"matches"})) {
926 @matches =
927 &{$self->{"completion_function"}}($line, $bword, $pos, $complete);
928 @{$self->{"completion"}->{"matches"}} = @matches;
929 } else {
930 @matches = @{$self->{"completion"}->{"matches"}};
931 }
932
933 my $match = $matches[$self->{"completion"}->{"index"}];
934
935 return unless (defined($match));
936
937 $self->{"completion"}->{"index"}++;
938 $self->{"completion"}->{"index"} = 0 if ($self->{"completion"}->{"index"} == scalar(@matches));
939
940 $self->debug(length($line) . " / $bword / $pos");
941 substr($line, $bword, $pos - $bword) = $match . " ";
942
943 $self->{"completion"}->{"endpos"} = $pos;
944
945 $pos = $bword + length($match) + 1;
946 $self->{"input_position"} = $pos;
947 $self->{"input_line"} = $line;
948
949 $self->fix_inputline();
950 }
951 }
952
953 sub anykey {
954 my $self = shift;
955
956 $self->callback("anykey");
957 #if (ref($self->{"anykey_callback"}) eq 'CODE') {
958 #&{$self->{"anykey_callback"}};
959 #}
960 }
961
962
963
964 #------------------------------------------------------------------------------
965 # Useful functions to set prompt and other things.
966
967 =pod
968
969 =head2 $sh->prompt([$prompt])
970
971 Get or set the prompt
972
973 =cut
974
975 sub prompt ($;$) {
976 my $self = shift;
977
978 if (@_) {
979 $self->{"input_prompt"} = shift;
980 $self->fix_inputline();
981 }
982 return $self->{"input_prompt"};
983 }
984
985 sub echo ($;$) {
986 my $self = shift;
987
988 if (@_) {
989 $self->{"echo"} = shift;
990 }
991 return $self->{"echo"};
992 }
993
994 # --------------------------------------------------------------------
995 # Helper functions
996 #
997
998 sub callback($$;$) {
999 my $self = shift;
1000 my $callback = shift() . "_callback";
1001 if (ref($self->{$callback}) eq 'CODE') {
1002 $self->{$callback}->(@_);
1003 }
1004 }
1005
1006 # Go from a position and find the beginning of the word we're on.
1007 sub find_word_bound ($$$$;$) {
1008 my ($self, $line, $pos, $opts, $rx) = @_;
1009 my $nrx;
1010 $rx = '\\w' if (!($opts & WORD_REGEX));
1011
1012 # Mod? This is either -1 or +1 depending on if we're looking behind or
1013 # if we're looking ahead.
1014 my $mod = ($opts & WORD_BEGINNING) ? -1 : 1;
1015 $nrx = qr/[^$rx]/;
1016 $rx = qr/[$rx]/;
1017
1018 if ($opts & WORD_NEXT) {
1019 #$regex = qr/^.{$pos}(.+?)(?<!$regex)$regex/;
1020 $rx = qr/^.{$pos}(.+?)(?<!$rx)$rx/;
1021 } elsif ($opts & WORD_BEGINNING) {
1022 #$regex = qr/($regex+[^$regex]*)(?<=^.{$pos})/;
1023 $rx = qr/($rx+$nrx*)(?<=^.{$pos})/;
1024 } elsif ($opts & WORD_END) {
1025 #$regex = qr/^.{$pos}(.+?)$regex(?:[^$regex]|$)/;
1026 $rx = qr/^.{$pos}(.+?)$rx(?:$nrx|$)/;
1027 }
1028
1029 $self->debug("regex: $rx");
1030
1031 if ($line =~ $rx) {
1032 $pos += length($1) * $mod;
1033 } else {
1034 $pos = ($mod == 1 ? length($line) : 0);
1035 }
1036
1037 return $pos;
1038 }
1039
1040 # -----------------------------------------------------------------------------
1041 # Functions people might call on us...
1042 #
1043
1044 sub insert_at_cursor($$) {
1045 my $self = shift;
1046 my $string = shift;
1047
1048 substr($self->{"input_line"}, $self->{"input_position"}, 0) = $string;
1049 $self->{"input_position"} += length($string)
1050 }
1051
1052 =pod
1053
1054 =back
1055
1056 =cut
1057
1058 1;

  ViewVC Help
Powered by ViewVC 1.1.26