File Coverage

blib/lib/Term/ReadLine/Zoid/Base.pm
Criterion Covered Total %
statement 48 218 22.0
branch 25 98 25.5
condition 1 41 2.4
subroutine 11 27 40.7
pod 16 21 76.1
total 101 405 24.9


line stmt bran cond sub pod time code
1             package Term::ReadLine::Zoid::Base;
2              
3 3     3   23678 use strict;
  3         26  
  3         115  
4 3     3   15 no warnings;
  3         15  
  3         126  
5 3     3   3054 use Term::ReadKey qw/ReadMode ReadKey GetTerminalSize/;
  3         12067  
  3         281  
6             #use encoding 'utf8';
7 3     3   27 no warnings; # undef == '' down here
  3         8  
  3         12322  
8              
9             our $VERSION = '0.06';
10              
11             $| = 1;
12              
13             our @_key_buffer;
14              
15             our %chr_map = ( # partial sequences
16             "\e" => '',
17             "\e[" => '',
18             "\eO" => '',
19             "\e[[" => '',
20             ( map {("\e[$_" => '')} (1 .. 24) ),
21              
22             "\e[2" => '', "\e[5" => '',
23             "\eO2" => '', "\eO5" => '',
24             "\e[1;2" => '', "\e[1;5" => '',
25             );
26              
27             our %chr_names = ( # named keys
28             "\e" => 'escape',
29             "\cH" => 'backspace',
30             "\cI" => 'tab',
31             "\cJ" => 'return', # line feed
32             "\cM" => 'return', # carriage return
33             "\c?" => 'backspace', # traditionally known as DEL
34              
35             "\e[A" => 'up', "\eOA" => 'up',
36             "\e[B" => 'down', "\eOB" => 'down',
37             "\e[C" => 'right', "\eOC" => 'right',
38             "\e[D" => 'left', "\eOD" => 'left',
39             "\e[F" => 'end', "\eOF" => 'end',
40             "\e[H" => 'home', "\eOH" => 'home',
41              
42             "\e[1~" => 'home',
43             "\e[2~" => 'insert',
44             "\e[3~" => 'delete',
45             "\e[4~" => 'end',
46             "\e[5~" => 'page_up',
47             "\e[6~" => 'page_down',
48             "\e[11~" => 'f1', "\eOP" => 'f1', "\e[[A" => 'f1',
49             "\e[12~" => 'f2', "\eOQ" => 'f2', "\e[[B" => 'f2',
50             "\e[13~" => 'f3', "\eOR" => 'f3', "\e[[C" => 'f3',
51             "\e[14~" => 'f4', "\eOS" => 'f4', "\e[[D" => 'f4',
52             "\e[15~" => 'f5', "\e[[E" => 'f5',
53             "\e[17~" => 'f6',
54             "\e[18~" => 'f7',
55             "\e[19~" => 'f8',
56             "\e[20~" => 'f9',
57             "\e[21~" => 'f10',
58             "\e[23~" => 'f11',
59             "\e[24~" => 'f12',
60              
61             "\e[2A" => 'shift_up', "\eO2A" => 'shift_up', "\e[1;2A" => 'shift_up',
62             "\e[2B" => 'shift_down', "\eO2B" => 'shift_down', "\e[1;2B" => 'shift_down',
63             "\e[2C" => 'shift_right', "\eO2C" => 'shift_right', "\e[1;2C" => 'shift_right',
64             "\e[2D" => 'shift_left', "\eO2D" => 'shift_left', "\e[1;2D" => 'shift_left',
65             "\e[2F" => 'shift_end', "\eO2F" => 'shift_end', "\e[1;2F" => 'shift_end',
66             "\e[2H" => 'shift_home', "\eO2H" => 'shift_home', "\e[1;2H" => 'shift_home',
67              
68             "\e[5A" => 'ctrl_up', "\eO5A" => 'ctrl_up', "\e[1;5A" => 'ctrl_up',
69             "\e[5B" => 'ctrl_down', "\eO5B" => 'ctrl_down', "\e[1;5B" => 'ctrl_down',
70             "\e[5C" => 'ctrl_right', "\eO5C" => 'ctrl_right', "\e[1;5C" => 'ctrl_right',
71             "\e[5D" => 'ctrl_left', "\eO5D" => 'ctrl_left', "\e[1;5D" => 'ctrl_left',
72             "\e[5F" => 'ctrl_end', "\eO5F" => 'ctrl_end', "\e[1;5F" => 'ctrl_end',
73             "\e[5H" => 'ctrl_home', "\eO5H" => 'ctrl_home', "\e[1;5H" => 'ctrl_home',
74             );
75              
76             # '[6A' => 'ctrl_shift_up', 'O6A' => 'ctrl_shift_up', '[1;6A' => 'ctrl_shift_up',
77             # '[6B' => 'ctrl_shift_down', 'O6B' => 'ctrl_shift_down', '[1;6B' => 'ctrl_shift_down',
78             # '[6C' => 'ctrl_shift_right', 'O6C' => 'ctrl_shift_right', '[1;6C' => 'ctrl_shift_right',
79             # '[6D' => 'ctrl_shift_left', 'O6D' => 'ctrl_shift_left', '[1;6D' => 'ctrl_shift_left',
80             # '[6F' => 'ctrl_shift_end', 'O6F' => 'ctrl_shift_end', '[1;6F' => 'ctrl_shift_end',
81             # '[6H' => 'ctrl_shift_home', 'O6H' => 'ctrl_shift_home', '[1;6H' => 'ctrl_shift_home',
82              
83             # '[7A' => 'ctrl_alt_up', 'O7A' => 'ctrl_alt_up', '[1;7A' => 'ctrl_alt_up',
84             # '[7B' => 'ctrl_alt_down', 'O7B' => 'ctrl_alt_down', '[1;7B' => 'ctrl_alt_down',
85             # '[7C' => 'ctrl_alt_right', 'O7C' => 'ctrl_alt_right', '[1;7C' => 'ctrl_alt_right',
86             # '[7D' => 'ctrl_alt_left', 'O7D' => 'ctrl_alt_left', '[1;7D' => 'ctrl_alt_left',
87             # '[7F' => 'ctrl_alt_end', 'O7F' => 'ctrl_alt_end', '[1;7F' => 'ctrl_alt_end',
88             # '[7H' => 'ctrl_alt_home', 'O7H' => 'ctrl_alt_home', '[1;7H' => 'ctrl_alt_home',
89              
90             # ############## #
91             # base functions #
92             # ############## #
93              
94             sub bell {
95             #print STDERR 'bell called by: ',join(', ', caller)."\n";
96 0         0 exists( $_[0]{config}{bell} )
97             ? $_[0]{config}{bell}->()
98 6 50   6 1 35 : print { $_[0]{OUT} } "\cG" ; # ^G == \007 == BELL
99 6         32 return 0;
100             }
101              
102             sub loop {
103 0     0 1 0 my $self = shift;
104 0 0       0 $$self{lines} = [''] unless @{$$self{lines}};
  0         0  
105 0         0 $$self{term_size} = [ (GetTerminalSize($$self{IN}))[0,1] ];
106 0 0       0 @ENV{'COLUMNS', 'LINES'} = @{$$self{term_size}} if $$self{config}{autoenv};
  0         0  
107 0         0 $self->draw();
108 0         0 $$self{_loop} = 1;
109 0         0 while ($$self{_loop}) {
110 0         0 $self->do_key();
111 0         0 while (@_key_buffer) { $self->do_key() }
  0         0  
112 0         0 $self->draw();
113             }
114 0         0 $self->cursor_at(@{$$self{_buffer_end}});
  0         0  
115             }
116              
117 0 0   0 1 0 sub beat { $_[0]{config}{beat}->() if exists $_[0]{config}{beat} }
118              
119 125 50   125 1 275 sub read_key { die "deprecated warning" if $_[1];
120 125         146 my $self = shift;
121 125 50       422 return shift @_key_buffer if scalar @_key_buffer;
122              
123 0         0 my $chr;
124 0         0 ReadMode('raw', $$self{IN});
125             {
126 0     0   0 local $SIG{WINCH} = sub { $$self{_SIGWINCH} = 1 };
  0         0  
  0         0  
127              
128 0         0 while ( not defined ($chr = ReadKey(1, $$self{IN})) ) { $self->beat() }
  0         0  
129              
130 0         0 my $n_chr;
131 0 0 0     0 if (
      0        
132             exists $chr_map{$chr} and
133             ( $$self{config}{low_latency} or ($n_chr = ReadKey(0.05, $$self{IN})) )
134             ) {
135 0         0 $chr .= $n_chr;
136 0         0 while (exists $chr_map{$chr}) {
137 0         0 while ( not defined ($n_chr = ReadKey(1, $$self{IN})) ) { $self->beat() }
  0         0  
138 0         0 $chr .= $n_chr;
139             }
140 0 0       0 unless (exists $chr_names{$chr}) {
141 0         0 $chr =~ s/^(.)(.*)/$1/s;
142 0         0 push @_key_buffer, split '', $2;
143             }
144             }
145             }
146 0         0 ReadMode('normal', $$self{IN});
147              
148 0         0 return $chr;
149             }
150              
151             sub do_key {
152 119     119 1 182 my ($self, $key) = (shift, shift);
153 119 100       349 $key = $self->read_key() unless length $key;
154              
155             # $self->key_name()
156 119 100       701 if (exists $chr_names{$key}) { $key = $chr_names{$key} }
  16 50       32  
157             elsif (length $key < 2) {
158 103         129 my $ord = ord $key;
159 103 50       289 $key = ($ord < 32) ? 'ctrl_' . (chr $ord + 64)
    100          
160             : ($ord == 127) ? 'ctrl_?' : $key ;
161             }
162              
163             # $self->key_binding
164 119         342 my $map = $$self{keymaps}{$$self{mode}};
165 119         127 my $sub;
166             DO_KEY:
167 295 100       813 if (exists $$map{$key}) { $sub = $$map{$key} }
  25 100       45  
    50          
168             elsif (exists $$map{_isa}) {
169 176   50     515 $map = $$self{keymaps}{ $$map{_isa} }
170             || return warn "$$map{_isa}: no such keymap\n\n";
171 176         911 goto DO_KEY;
172             }
173 94         215 elsif (exists $$map{_default}) { $sub = $$map{_default} }
174 0         0 else { $sub = 'bell' }
175              
176             #print STDERR "# key: $key sub: $sub\n";
177 119 100       545 my $re = ref($sub) ? $sub->($self, $key, @_) : $self->$sub($key, @_) ;
178 119         213 $$self{last_key} = $key;
179 119         1161 return $re;
180             }
181              
182             sub print {
183             # The idea is to let the terminal render the line wrap
184             # but calculate what it will do in order to get the cursor position right.
185 0     0 1 0 my ($self, $lines, $pos) = @_;
186             # use Data::Dumper;
187             # print STDERR Dumper $lines, $pos;
188 0 0       0 if ($$self{_SIGWINCH}) { # GetTerminalSize is kind of heavy
189 0         0 $$self{term_size} = [ (GetTerminalSize($$self{IN}))[0,1] ];
190 0 0       0 @ENV{'COLUMNS', 'LINES'} = @{$$self{term_size}} if $$self{config}{autoenv};
  0         0  
191 0         0 $$self{_SIGWINCH} = 0;
192             }
193              
194 0         0 my ($width, $higth) = @{$$self{term_size}};
  0         0  
195              
196             # calculate how line wrap will work out
197 0         0 my @nlines = map { int(print_length($_) / $width) } @$lines;
  0         0  
198 0         0 $$pos[1] += $nlines[$_] for 0 .. $$pos[1] - 1;
199 0         0 $$pos[1] += int($$pos[0] / $width);
200 0         0 $$pos[0] %= $width;
201             # print STDERR Dumper \@nlines, $pos;
202              
203             # get the lines at the right position
204 0         0 my $buffer = -1; # always 1 lines minimum
205 0         0 $buffer += 1 + $_ for @nlines;
206 0         0 my $null = 1;
207 0 0       0 if ($buffer > $higth) { # big buffer or small screen :$
208             # FIXME does not yet reckon with line wrap
209             # FIXME some +1 or -1 offsets not right
210 0         0 my $offset = $$pos[1] - $$self{scroll_pos};
211 0 0       0 if ($offset < 0) { $$self{scroll_pos} = $$pos[1] }
  0 0       0  
212 0         0 elsif ($offset > $higth) { $$self{scroll_pos} += $offset - $higth }
213 0         0 @$lines = splice @$lines, $$self{scroll_pos}, $higth;
214 0         0 $$pos[1] -= $$self{scroll_pos};
215 0         0 $$self{_buffer_end} = [$width, $higth];
216 0         0 $$self{_buffer} = $higth;
217             }
218             else { # normal readline buffer
219 0 0       0 if ($buffer > $$self{_buffer}) { # clear screen area
220 0         0 $self->cursor_at(@{$$self{term_size}});
  0         0  
221 0         0 print { $$self{OUT} } "\n" x ($buffer - $$self{_buffer});
  0         0  
222 0         0 $$self{_buffer} = $buffer;
223             }
224 0         0 $null = $$self{term_size}[1] - $$self{_buffer};
225 0         0 $$self{_buffer_end} = [print_length($$lines[-1]), $null + $buffer]; # save real cursor
226             }
227 0         0 $self->cursor_at(1, $null);
228 0         0 print { $$self{OUT} } $$lines[$_], "\e[K\n" for 0 .. $#$lines - 1;
  0         0  
229 0         0 print { $$self{OUT} } $$lines[-1], "\e[J";
  0         0  
230              
231 0         0 $self->cursor_at($$pos[0]+1, $$pos[1]+$null); # set cursor
232             }
233              
234             # ######### #
235             # utilities #
236             # ######### #
237              
238 0     0 0 0 sub TermSize { (GetTerminalSize($_[0]{IN}))[0,1] }
239              
240             sub key_name {
241 26 50   26 1 96 if (exists $chr_names{$_[1]}) { return $chr_names{$_[1]} }
  0 50       0  
242             elsif (length $_[1] < 2) {
243 26         38 my $ord = ord $_[1];
244 26 50       902 return ($ord < 32) ? 'ctrl_' . (chr $ord + 64)
    50          
245             : ($ord == 127) ? 'ctrl_?' : $_[1] ;
246             }
247 0         0 else { return $_[1] }
248             }
249              
250             sub key_binding {
251 0     0 1 0 my ($self, $key, $mode) = @_;
252 0   0     0 $mode ||= $$self{mode};
253              
254 0         0 my $map = $$self{keymaps}{$mode};
255             FIND_KEY:
256 0 0       0 if (exists $$map{$key}) { return $$map{$key} }
  0 0       0  
257             elsif (exists $$map{_isa}) {
258 0   0     0 $map = $$self{keymaps}{ $$map{_isa} }
259             || return warn "$$map{_isa}: no such keymap\n\n";
260 0         0 goto FIND_KEY;
261             }
262 0         0 else { return undef }
263             }
264              
265             sub press {
266 44     44 1 1083 my $self = shift;
267 44 50       202 push @_key_buffer, (@_>1) ? (@_) : (split '', $_[0]);
268 44         132 while (scalar @_key_buffer) { $self->do_key() }
  97         239  
269             }
270              
271             sub unread_key {
272 3     3 1 4 my $self = shift;
273 3 50       90 unshift @_key_buffer, (@_>1) ? (@_) : (split '', $_[0]);
274             }
275              
276             sub pos2off {
277 0     0 0 0 my ($self, $pos) = @_;
278 0   0     0 $pos ||= $$self{pos};
279 0         0 my $off = $$pos[0];
280 0         0 $off += 1 + length $$self{lines}[$_] for 0 .. $$pos[1] - 1;
281 0         0 return $off;
282             }
283              
284             sub output {
285 0     0 0 0 my ($self, @items) = @_;
286              
287 0         0 $self->cursor_at(@{$$self{_buffer_end}});
  0         0  
288 0         0 print { $$self{OUT} } "\n";
  0         0  
289              
290 0         0 my ($max, $cnt) = ($$self{config}{maxcomplete}, scalar @items);
291 0 0 0     0 $self->_ask($cnt) or return if $max and $max =~ /^\d+$/ and $cnt > $max;
      0        
      0        
292              
293 0 0       0 @items = ($cnt > 1) ? ($self->col_format(@items)) : (split /\n/, $items[0]);
294              
295 0 0       0 $$self{_buffer} = (@items < $$self{_buffer}) ? ($$self{_buffer} - @items) : 0;
296 0 0       0 if (@items > $$self{term_size}[1]) {
297 0 0 0     0 $self->_ask($cnt) or return if $max and $max eq 'pager';
      0        
298 0   0     0 my $pager = $ENV{PAGER} || 'more';
299 0         0 eval {
300 0         0 local $SIG{PIPE} = 'IGNORE';
301 0   0     0 open PAGER, "| $pager" || die;
302 0         0 print PAGER join("\n", @items), "\n";
303 0         0 close PAGER;
304             } ;
305             }
306 0         0 else { print { $$self{OUT} } join("\n", @items), "\n" }
  0         0  
307             }
308              
309             sub _ask {
310 0     0   0 my ($self, $cnt) = @_;
311 0         0 print { $$self{OUT} } "Display all $cnt possibilities? [yN]";
  0         0  
312 0         0 my $answ = $self->read_key();
313 0         0 print { $$self{OUT} } "\n";
  0         0  
314 0 0       0 return( ($answ =~ /y/i) ? 1 : 0 );
315             }
316              
317             sub col_format { # takes minimum number of rows, but fills cols first
318 0     0 0 0 my ($self, @items) = @_;
319              
320 0         0 my $len = 0;
321 0   0     0 $_ > $len and $len = $_ for map {length $_} @items;
  0         0  
322 0         0 $len += 2; # spacing
323 0         0 my ($width) = $self->TermSize();
324 0 0       0 return @items if $width < (2 * $len); # rows == items
325 0 0       0 return join ' ', @items if $width > (@items * $len); # 1 row
326              
327 0         0 my $cols = int($width / $len ) - 1; # 0 based
328 0         0 my $rows = int(@items / ($cols+1)); # 0 based ceil
329 0 0       0 $rows -= 1 unless @items % ($cols+1); # tune ceil
330 0         0 my @rows;
331 0         0 for my $r (0 .. $rows) {
332 0         0 my @row = map { $items[ ($_ * ($rows+1)) + $r] } 0 .. $cols;
  0         0  
333 0         0 push @rows, join '', map { $_ .= ' 'x($len - length $_) } @row;
  0         0  
334             }
335             #print STDERR scalar(@items)." items, $len long, $width width, $cols+1 cols, $rows+1 rows\n";
336 0         0 return @rows;
337             }
338              
339             # ################# #
340             # Key binding stuff #
341             # ################# #
342              
343             sub bindchr {
344 1     1 1 10 my ($self, $chr, $key) = @_;
345 1 50       23 if ($chr =~ /^\^(.)$/) { $chr = eval qq/"\\c$1"/ }
  1         106  
346 1         11 $chr_names{$chr} = $key;
347 1         4 chop $chr;
348 1         10 while (length $chr) {
349 0           $chr_map{$chr} = '';
350 0           chop $chr;
351             }
352             }
353              
354             sub recalc_chr_map {
355 0     0 1   my $self = shift;
356 0           %chr_map = ();
357 0           while (my ($k,$v) = each %chr_names) {
358 0           $self->bindchr($k, $v);
359             }
360             }
361              
362             # ########## #
363             # ANSI stuff #
364             # ########## #
365              
366 0     0 1   sub cursor_at { print { $_[0]{OUT} } "\e[$_[2];$_[1]H" } # ($x, $y) 1-based !
  0            
367              
368             sub new_line {
369 0     0 0   my $self = shift;
370 0 0 0       return unless -t $$self{OUT} and -t $$self{IN};
371              
372 0           ReadMode 'raw';
373 0           my $r;
374 0           print { $$self{OUT} } "\e[6n";
  0            
375 0   0       $r = ReadKey( -1, $$self{IN}) || return print { $$self{OUT} } "\n";
376 0           while ($r =~ /^(\e|\e\[\d*|\e\[\d+;\d*)$/) { $r .= ReadKey -1, $$self{IN} }
  0            
377             # in this case timed read doesn't work :(
378 0           ReadMode 'normal';
379              
380 0 0         if ($r =~ /^\e\[\d+;(\d+)\D$/) {
381 0 0         print { $$self{OUT} } "\n" if $1 > 1;
  0            
382             }
383             else {
384 0           $self->unread_key($r);
385 0           print { $$self{OUT} } "\n";
  0            
386             }
387             }
388              
389 0     0 1   sub clear_screen { print { $_[0]{OUT} } "\e[2J" }
  0            
390              
391             sub print_length {
392 0     0 1   my $string = pop;
393 0           $string =~ s{\e\[[\d;]*\w}{}g; # strip ansi codes
394 0           return length $string;
395             }
396              
397             ## Sequences from the "How to change the title of an xterm" howto
398             ##
399             sub title {
400 0     0 1   my ($self, $title) = @_;
401 0 0         return unless $ENV{TERM};
402 0 0         my $string =
    0          
    0          
403             ($ENV{TERM} =~ /^((ai)?xterm.*|dtterm|screen)$/) ? "\e]0;$title\cG" :
404             ($ENV{TERM} eq 'iris-ansi') ? "\eP1.y$title\e\\" :
405             ($ENV{TERM} eq 'sun-cmd') ? "\e]l$title\e\\" : undef ;
406 0 0         print { $$self{OUT} } $string if $string;
  0            
407             }
408              
409             1;
410              
411             __END__