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; |