/[cwmp]/google/lib/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 /google/lib/Shelly.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 87 - (show annotations)
Fri Jun 22 20:05:30 2007 UTC (16 years, 11 months ago) by dpavlin
File size: 14098 byte(s)
import Term::Shelly 0.01 from CPAN
1 =pod
2
3 =head1 NAME
4
5 Term::Shelly - Yet Another Shell Kit for Perl
6
7 =head1 VERSION
8
9 $Id: Shelly.pm,v 1.5 2004/06/04 04:21:23 psionic Exp $
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 - Tab completion
19 - Support for window size changes (sigwinch)
20 - movement in-line editing.
21 - vi mode (Yeah, I lub vi)
22 - history
23 - Completion function calls
24
25 - Settable callbacks for when we have an end-of-line (EOL binding?)
26
27 =cut
28
29 package Term::Shelly;
30
31 use strict;
32 use warnings;
33
34 use vars qw($VERSION);
35 $VERSION = '0.01';
36
37 # Default perl modules...
38 use IO::Handle; # I need flush()... or do i?;
39
40 # Get these from CPAN
41 use Term::ReadKey;
42
43 # Useful constants we need...
44
45 # for find_word_bound()
46 use constant WORD_BEGINNING => 0; # I want the beginning of this word.
47 use constant WORD_END => 1; # I want the end of the word.
48 use constant WORD_ONLY => 2; # Trailing spaces are important.
49 use constant WORD_REGEX => 4; # I want to specify my own regexp
50
51 # Some key constant name mappings.
52 my %KEY_CONSTANTS = (
53 "\e[A" => "UP",
54 "\e[B" => "DOWN",
55 "\e[C" => "RIGHT",
56 "\e[D" => "LEFT",
57 );
58
59 # stty raw, basically
60 ReadMode 3;
61
62 # I need to know how big the terminal is (columns, anyway)
63
64 =pod
65
66 =head1 DESCRIPTION
67
68 =over 4
69
70 =cut
71
72 sub new ($) {
73 my $class = shift;
74
75 my $self = {
76 "input_line" => "",
77 "input_position" => 0,
78 "leftcol" => 0,
79 };
80
81 bless $self, $class;
82
83 ($self->{"termcols"}) = GetTerminalSize();
84 $SIG{WINCH} = sub { ($self->{"termcols"}) = GetTerminalSize(); $self->fix_inputline() };
85 my $bindings = {
86 "LEFT" => "backward-char",
87 "RIGHT" => "forward-char",
88 "UP" => "up-history",
89 "DOWN" => "down-history",
90
91 "BACKSPACE" => "delete-char-backward",
92 "^H" => "delete-char-backward",
93 "^?" => "delete-char-backward",
94 "^W" => "delete-word-backward",
95
96 "^U" => "kill-line",
97
98 "^J" => "newline",
99 "^M" => "newline",
100
101 "^A" => "beginning-of-line",
102 "^E" => "end-of-line",
103 "^K" => "kill-to-eol",
104 "^L" => "redraw",
105
106 "^I" => "complete-word",
107 "TAB" => "complete-word",
108
109 #"^T" => "expand-line",
110 };
111
112 my $mappings = {
113 "backward-char" => \&backward_char,
114 "forward-char" => \&forward_char,
115 "delete-char-backward" => \&delete_char_backward,
116 "kill-line" => \&kill_line,
117 "newline" => \&newline,
118 "redraw" => \&fix_inputline,
119 "beginning-of-line" => \&beginning_of_line,
120 "end-of-line" => \&end_of_line,
121 "delete-word-backward" => \&delete_word_backward,
122
123 "complete-word" => \&complete_word,
124 #"expand-line" => \&expand_line,
125 };
126
127 $self->{"bindings"} = $bindings;
128 $self->{"mappings"} = $mappings;
129 return $self;
130 }
131
132 =pod
133
134 =item $sh->do_one_loop()
135
136 Does... one... loop. Makes a pass at grabbing input and processing it. For
137 speedy pasts, this loops until there are no characters left to read.
138 It will handle event processing, etc.
139
140 =cut
141
142 # Nonblocking readline
143 sub do_one_loop ($) {
144 my $self = shift;
145 my $char;
146
147 # ReadKey(.1) means no timeout waiting for data, thus is nonblocking
148 while (defined($char = ReadKey(.1))) {
149 $self->handle_key($char);
150 }
151
152 }
153
154 =pod
155
156 =item handle_key($key)
157
158 Handle a single character input. This is not a "key press" so much as doing all
159 the necessary things to handle key presses.
160
161 =cut
162
163 sub handle_key($$) {
164 my $self = shift;
165 my $char = shift;
166
167 my $line = $self->{"input_line"} || "";
168 my $pos = $self->{"input_position"} || 0;
169
170 if ($self->{"escape"}) {
171 $self->{"escape_string"} .= $char;
172 if ($self->{"escape_expect_ansi"}) {
173 $self->{"escape_expect_ansi"} = 0 if ($char =~ m/[a-zA-Z]/);
174 }
175
176 $self->{"escape_expect_ansi"} = 1 if ($char eq '[');
177 $self->{"escape"} = 0 unless ($self->{"escape_expect_ansi"});
178
179 unless ($self->{"escape_expect_ansi"}) {
180 my $estring = $self->{"escape_string"};
181
182 $self->{"escape_string"} = undef;
183 return $self->execute_binding("\e".$estring);
184 }
185
186 return 0;
187 }
188
189 if ($char eq "\e") { # Trap escapes, they're speshul.
190 $self->{"escape"} = 1;
191 $self->{"escape_string"} = undef;
192
193 # What now?
194 return 0;
195 }
196
197 if ((ord($char) < 32) || (ord($char) > 126)) { # Control character
198 $self->execute_binding($char);
199 return 0;
200 }
201
202 if ((defined($char)) && (ord($char) >= 32)) {
203 substr($line, $pos, 0) = $char;
204 $self->{"input_position"}++;
205
206 # If we just did a tab completion, kill the state.
207 delete($self->{"completion"}) if (defined($self->{"completion"}));
208 }
209
210 $self->{"input_line"} = $line;
211 $self->fix_inputline();
212 }
213
214 =pod
215
216 =item execute_binding(raw_key)
217
218 Guess what this does? Ok I'll explain anyway... It takes a key and prettifies
219 it, then checks the known key bindings for a mapping and checks if that mapping
220 is a coderef (a function reference). If it is, it'll call that function. If
221 not, it'll do nothing. If it finds a binding for which there is no mapped
222 function, it'll tell you that it is an unimplemented function.
223
224 =cut
225
226 sub execute_binding ($$) {
227 my $self = shift;
228 my $str = shift;
229 my $key = $self->prettify_key($str);
230
231 my $bindings = $self->{"bindings"};
232 my $mappings = $self->{"mappings"};
233
234 if (defined($bindings->{$key})) {
235
236 # Check if we have stored completion state and the next binding is
237 # not complete-word. If it isn't, then kill the completion state.
238 if (defined($self->{"completion"}) &&
239 $bindings->{$key} ne 'complete-word') {
240 delete($self->{"completion"});
241 }
242
243 if (ref($mappings->{$bindings->{$key}}) eq 'CODE') {
244
245 # This is a hack, passing $self instead of doing:
246 # $self->function, becuase I don't want to do an eval.
247
248 return &{$mappings->{$bindings->{$key}}}($self);
249
250 } else {
251 error("Unimplemented function, " . $bindings->{$key});
252 }
253 }
254
255 return 0;
256 }
257
258 =pod
259
260 =item prettify_key(raw_key)
261
262 This happy function lets me turn raw input into something less ugly. It turns
263 control keys into their equivalent ^X form. It does some other things to turn
264 the key into something more readable
265
266 =cut
267
268 sub prettify_key ($$) {
269 my $self = shift;
270 my $key = shift;
271
272 # Return ^X for control characters, like CTRL+A...
273 if (length($key) == 1) { # One-character keycombos should only be ctrl keys
274 if (ord($key) <= 26) { # Control codes, another check anyway...
275 return "^" . chr(65 + ord($key) - 1);
276 }
277 if (ord($key) == 127) { # Speshul backspace key
278 return "^?";
279 }
280 if (ord($key) < 32) {
281 return "^" . (split("", "\]_^"))[ord($key) - 28];
282 }
283 }
284
285 # Return ESC-X for escape shenanigans, like ESC-W
286 if (length($key) == 2) {
287 my ($p, $k) = split("", $key);
288 if ($p eq "\e") { # This should always be an escape, but.. check anyway
289 return "ESC-" . $k;
290 }
291 }
292
293 # Ok, so it's not ^X or ESC-X, it's gotta be some ansi funk.
294 return $KEY_CONSTANTS{$key};
295 }
296
297 =pod
298
299 =item real_out($string)
300
301 This function allows you to bypass any sort of evil shenanigans regarding output fudging. All this does is 'print @_;'
302
303 Don't use this unless you know what you're doing.
304
305 =cut
306
307 sub real_out {
308 my $self = shift;
309 print @_;
310 }
311
312 sub out ($;$) {
313 my $self = shift;
314 $self->real_out("\r\e[2K", @_, "\n");
315 $self->fix_inputline();
316 }
317
318 sub error ($$) {
319 my $self = shift;
320 print STDERR "*> ", @_, "\n";
321 $self->fix_inputline();
322 }
323
324 =pod
325
326 =item fix_inputline
327
328 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.
329
330 =cut
331
332 sub fix_inputline {
333 my $self = shift;
334
335 print "\r\e[2K";
336
337 # If we're past the end of the terminal line, shuffle back!
338 if ($self->{"input_position"} - $self->{"leftcol"} <= 0) {
339 $self->{"leftcol"} -= 30;
340 $self->{"leftcol"} = 0 if ($self->{"leftcol"} < 0);
341 }
342
343 # If we're before the beginning of the terminal line, shuffle over!
344 if ($self->{"input_position"} - $self->{"leftcol"} > $self->{"termcols"}) {
345 $self->{"leftcol"} += 30;
346 }
347
348 # Can se show the whole line? If so, do it!
349 if (length($self->{"input_line"}) < $self->{"termcols"}) {
350 $self->{"leftcol"} = 0;
351 }
352
353 # only print as much as we can in this one line.
354 print substr($self->{"input_line"}, $self->{"leftcol"}, $self->{"termcols"});
355 print "\r";
356 print "\e[" . ($self->{"input_position"} - $self->{"leftcol"}) .
357 "C" if ($self->{"input_position"} > 0);
358 STDOUT->flush();
359 }
360
361 sub newline {
362 my $self = shift;
363 # Process the input line.
364
365 $self->real_out("\n");
366 print "You wrote: " . $self->{"input_line"} . "\n";
367
368 $self->{"input_line"} = "";
369 $self->{"input_position"} = 0;
370 }
371
372 sub kill_line {
373 my $self = shift;
374 $self->{"input_line"} = "";
375 $self->{"input_position"} = 0;
376 $self->{"leftcol"} = 0;
377
378 #real_out("\r\e[2K");
379
380 $self->fix_inputline();
381
382 return 0;
383 }
384
385 sub forward_char {
386 my $self = shift;
387 if ($self->{"input_position"} < length($self->{"input_line"})) {
388 $self->{"input_position"}++;
389 $self->real_out("\e[C");
390 }
391 }
392
393 sub backward_char {
394 my $self = shift;
395 if ($self->{"input_position"} > 0) {
396 $self->{"input_position"}--;
397 $self->real_out("\e[D");
398 }
399 }
400
401 sub delete_char_backward {
402 my $self = shift;
403 #"delete-char-backward" => \&delete_char_backward,
404 if ($self->{"input_position"} > 0) {
405 substr($self->{"input_line"}, $self->{"input_position"} - 1, 1) = '';
406 $self->{"input_position"}--;
407
408 $self->fix_inputline();
409 }
410 }
411
412 sub beginning_of_line {
413 my $self = shift;
414 $self->{"input_position"} = 0;
415 $self->{"leftcol"} = 0;
416 $self->fix_inputline();
417 }
418
419 sub end_of_line {
420 my $self = shift;
421 $self->{"input_position"} = length($self->{"input_line"});
422 $self->fix_inputline();
423 }
424
425 sub delete_word_backward {
426 my $self = shift;
427 my $pos = $self->{"input_position"};
428 my $line = $self->{"input_line"};
429 my $regex = "[A-Za-z0-9]";
430 my $bword;
431
432 $bword = $self->find_word_bound($line, $pos, WORD_BEGINNING);
433
434 # Delete whatever word we just found.
435 substr($line, $bword, $pos - $bword) = '';
436
437 # Update stuff...
438 $self->{"input_line"} = $line;
439 $self->{"input_position"} -= ($pos - $bword);
440
441 $self->fix_inputline();
442 }
443
444 =pod
445
446 =item $sh->complete_word
447
448 This is called whenever the complete-word binding is triggered. See the
449 COMPLETION section below for how to write your own completion function.
450
451 =cut
452
453 sub complete_word {
454 my $self = shift;
455 my $pos = $self->{"input_position"};
456 my $line = $self->{"input_line"};
457 my $regex = "[A-Za-z0-9]";
458 my $bword;
459 my $complete;
460
461 if (ref($self->{"completion_function"}) eq 'CODE') {
462 my @matches;
463
464 # Maintain some sort of state here if this is the first time we've
465 # hit complete_word() for this "scenario." What I mean is, we need to track
466 # whether or not this user is hitting tab once or twice (or more) in the
467 # same position.
468 RECHECK:
469 if (!defined($self->{"completion"})) {
470 $bword = $self->find_word_bound($line, $pos, WORD_BEGINNING | WORD_REGEX, '\S');
471 $complete = substr($line,$bword,$pos - $bword);
472 #$self->out("Complete: $complete");
473
474 #$self->out("First time completing $complete");
475 $self->{"completion"} = {
476 "index" => 0,
477 "original" => $complete,
478 "pos" => $pos,
479 "bword" => $bword,
480 "line" => $line,
481 "endpos" => $pos,
482 };
483 } else {
484 $bword = $self->{"completion"}->{"bword"};
485 $complete = substr($line,$bword,$pos - $bword);
486 }
487
488 # If we don't have any matches to check against...
489 unless (defined($self->{"completion"}->{"matches"})) {
490 @matches =
491 &{$self->{"completion_function"}}($line, $bword, $pos, $complete);
492 @{$self->{"completion"}->{"matches"}} = @matches;
493 } else {
494 @matches = @{$self->{"completion"}->{"matches"}};
495 }
496
497 my $match = $matches[$self->{"completion"}->{"index"}];
498
499 return unless (defined($match));
500
501 #$self->out("Match: $match / " . $self->{"completion"}->{"index"} . " / " . @matches);
502
503 $self->{"completion"}->{"index"}++;
504 $self->{"completion"}->{"index"} = 0 if ($self->{"completion"}->{"index"} == scalar(@matches));
505
506 substr($line, $bword, $pos - $bword) = $match;
507
508 $self->{"completion"}->{"endpos"} = $pos;
509
510 $pos = $bword + length($match);
511 $self->{"input_position"} = $pos;
512 $self->{"input_line"} = $line;
513
514 $self->fix_inputline();
515
516 }
517 }
518
519
520 # --------------------------------------------------------------------
521 # Helper functions
522
523 # Go from a position and find the beginning of the word we're on.
524 sub find_word_bound ($$$;$) {
525 my $self = shift;
526 my $line = shift;
527 my $pos = shift;
528 my $opts = shift || 0;
529 my $regex = "[A-Za-z0-9]";
530 my $bword;
531
532 $regex = shift if ($opts & WORD_REGEX);
533
534 # Mod? This is either -1 or +1 depending on if we're looking behind or
535 # if we're looking ahead.
536 my $mod = -1;
537 $mod = 1 if ($opts & WORD_END);
538
539 # What are we doing?
540 # If we're in a word, go to the beginning of the word
541 # If we're on a space, go to end of previous word.
542 # If we're on a nonspace/nonword, go to beginning of nonword chars
543
544 $bword = $pos - 1;
545
546 # If we're at the end of the string, ignore all trailing whitespace.
547 # unless WORD_ONLY is set.
548 #out("
549 if (($bword + 1 == $pos) && (! $opts & WORD_ONLY)) {
550 $bword += $mod while (substr($line,$bword,1) =~ m/^\s$/);
551 }
552
553 # If we're not on an ALPHANUM, then we want to reverse the match.
554 # that is, if we are:
555 # "testing here hello .......there"
556 # ^-- here
557 # Then we want to delete (match) all the periods (nonalphanums)
558 substr($regex, 1, 0) = "^" if (substr($line,$bword,1) !~ m/$regex/);
559
560 # Back up until we hit the end of our "word"
561 $bword += $mod while (substr($line,$bword,1) =~ m/$regex/ && $bword >= 0);
562
563 # Whoops, one too far...
564 $bword -= $mod;
565
566 return $bword;
567 }
568
569 =pod
570
571 =back
572
573 =cut
574
575 1;

  ViewVC Help
Powered by ViewVC 1.1.26