File Coverage

lib/Curses/UI/Common.pm
Criterion Covered Total %
statement 92 230 40.0
branch 25 136 18.3
condition 5 52 9.6
subroutine 14 20 70.0
pod 9 15 60.0
total 145 453 32.0


line stmt bran cond sub pod time code
1             # ----------------------------------------------------------------------
2             # Curses::UI::Common
3             #
4             # (c) 2001-2002 by Maurice Makaay. All rights reserved.
5             # (c) 2003-2005 by Marcus Thiesen et al.
6             # This file is part of Curses::UI. Curses::UI is free software.
7             # You can redistribute it and/or modify it under the same terms
8             # as perl itself.
9             #
10             # Currently maintained by Marcus Thiesen
11             # e-mail: marcus@cpan.thiesenweb.de
12             # ----------------------------------------------------------------------
13              
14             # TODO: fix dox
15              
16             package Curses::UI::Common;
17              
18 11     11   72 use strict;
  11         24  
  11         436  
19 11     11   11937 use Term::ReadKey;
  11         69068  
  11         1139  
20 11     11   8144 use Curses;
  8         10230  
  8         36779  
21             require Exporter;
22              
23 8         13823 use vars qw(
24             @ISA
25             @EXPORT_OK
26             @EXPORT
27             $VERSION
28 8     8   94 );
  8         16  
29              
30             $VERSION = '1.10';
31              
32             @ISA = qw(
33             Exporter
34             );
35            
36             @EXPORT = qw(
37             keys_to_lowercase
38             text_wrap
39             text_draw
40             text_length
41             text_chop
42             scrlength
43             split_to_lines
44             text_dimension
45             CUI_ESCAPE CUI_SPACE CUI_TAB
46             WORDWRAP NO_WORDWRAP
47             );
48              
49             # ----------------------------------------------------------------------
50             # Misc. routines
51             # ----------------------------------------------------------------------
52              
53             sub parent()
54             {
55 7     7 1 95 my $this = shift;
56 7         48 $this->{-parent};
57             }
58              
59             sub root()
60             {
61 27     27 1 649 my $this = shift;
62 27         36 my $root = $this;
63 27         92 while (defined $root->{-parent}) {
64 30         92 $root = $root->{-parent};
65             }
66 27         121 return $root;
67             }
68              
69             sub accessor($;$)
70             {
71 25     25 1 42 my $this = shift;
72 25         275 my $key = shift;
73 25         31 my $value = shift;
74              
75 25 100       122 $this->{$key} = $value if defined $value;
76 25         98 return $this->{$key};
77             }
78              
79             sub keys_to_lowercase($;)
80             {
81 70     70 1 114 my $hash = shift;
82              
83 70         448 my $copy = {%$hash};
84 70         311 while (my ($k,$v) = each %$copy) {
85 708         1818 $hash->{lc $k} = $v;
86             }
87              
88 70         259 return $hash;
89             }
90              
91             # ----------------------------------------------------------------------
92             # Text processing
93             # ----------------------------------------------------------------------
94              
95             sub split_to_lines($;)
96             {
97             # Make $this->split_to_lines() possible.
98 5 100   5 1 13 shift if ref $_[0];
99 5         7 my $text = shift;
100              
101             # Break up the text in lines. IHATEBUGS is
102             # because a split with /\n/ on "\n\n\n" would
103             # return zero result :-(
104 5         21 my @lines = split /\n/, $text . "IHATEBUGS";
105 5         22 $lines[-1] =~ s/IHATEBUGS$//g;
106            
107 5         17 return \@lines;
108             }
109              
110             sub scrlength($;)
111             {
112             # Make $this->scrlength() possible.
113 4 50   4 1 13 shift if ref $_[0];
114 4         12 my $line = shift;
115              
116 4 50       10 return 0 unless defined $line;
117              
118 4         6 my $scrlength = 0;
119 4         12 for (my $i=0; $i < length($line); $i++)
120             {
121 460         395 my $chr = substr($line, $i, 1);
122 460         395 $scrlength++;
123 460 100       937 if ($chr eq "\t") {
124 1         5 while ($scrlength%8) {
125 4         8 $scrlength++;
126             }
127             }
128             }
129 4         21 return $scrlength;
130             }
131              
132             # Contstants for text_wrap()
133             sub NO_WORDWRAP() { 1 }
134             sub WORDWRAP() { 0 }
135              
136             sub text_wrap($$;)
137             {
138             # Make $this->text_wrap() possible.
139 1 50   1 1 4 shift if ref $_[0];
140 1         2 my ($line, $maxlen, $wordwrap) = @_;
141 1 50       3 $wordwrap = WORDWRAP unless defined $wordwrap;
142 1         1 $maxlen = int $maxlen;
143            
144 1 50       3 return [""] if $line eq '';
145              
146 1         3 my @wrapped = ();
147 1         1 my $len = 0;
148 1         2 my $wrap = '';
149              
150             # Special wrapping is needed if the line contains tab
151             # characters. These should be expanded to the TAB-stops.
152 1 50       10 if ($line =~ /\t/)
153             {
154 0         0 CHAR: for (my $i = 0; $i <= length($line); $i++)
155             {
156 0         0 my $nextchar = substr($line, $i, 1);
157              
158             # Find the length of the string in case the
159             # next character is added.
160 0         0 my $newlen = $len + 1;
161 0 0       0 if ($nextchar eq "\t") { while($newlen%8) { $newlen++ } }
  0         0  
  0         0  
162              
163             # Would that go beyond the end of the available width?
164 0 0       0 if ($newlen > $maxlen)
165             {
166 0 0 0     0 if ($wordwrap == WORDWRAP
167             and $wrap =~ /^(.*)([\s])(\S+)$/)
168             {
169 0         0 push @wrapped, $1 . $2;
170 0         0 $wrap = $3;
171 0         0 $len = scrlength($wrap) + 1;
172             } else {
173 0         0 $len = 1;
174 0         0 push @wrapped, $wrap;
175 0         0 $wrap = '';
176             }
177             } else {
178 0         0 $len = $newlen;
179             }
180 0         0 $wrap .= $nextchar;
181             }
182 0 0       0 push @wrapped, $wrap if defined $wrap;
183              
184             # No tab characters in the line? Then life gets a bit easier. We can
185             # process large chunks at once.
186             } else {
187 1         1 my $idx = 0;
188              
189             # Line shorter than allowed? Then return immediately.
190 1 50       4 return [$line] if length($line) < $maxlen;
191 1 50       3 return ["internal wrap error: wraplength undefined"]
192             unless defined $maxlen;
193              
194 1         2 CHUNK: while ($idx < length($line))
195             {
196 6         8 my $next = substr($line, $idx, $maxlen);
197 6 100       25 if (length($next) < $maxlen)
    50          
198             {
199 1         2 push @wrapped, $next;
200 1         3 last CHUNK;
201             }
202             elsif ($wordwrap == WORDWRAP)
203             {
204 0         0 my $space_idx = rindex($next, " ");
205 0 0 0     0 if ($space_idx == -1 or $space_idx == 0)
206             {
207 0         0 push @wrapped, $next;
208 0         0 $idx += $maxlen;
209             } else {
210 0         0 push @wrapped, substr($next, 0, $space_idx + 1);
211 0         0 $idx += $space_idx + 1;
212             }
213             } else {
214 5         6 push @wrapped, $next;
215 5         10 $idx += $maxlen;
216             }
217             }
218             }
219            
220 1         3 return \@wrapped;
221             }
222              
223             sub text_tokenize {
224 0     0 0 0 my ($text) = @_;
225              
226 0         0 my @tokens = ();
227 0         0 while ($text ne '') {
228 0 0       0 if ($text =~ m/^<\/?[a-zA-Z0-9_]+>/s) {
    0          
229 0         0 push(@tokens, $&);
230 0         0 $text = $';
231             }
232             elsif ($text =~ m/^.+?(?=<\/?[a-zA-Z0-9_]+>)/s) {
233 0         0 push(@tokens, $&);
234 0         0 $text = $';
235             }
236             else {
237 0         0 push(@tokens, $text);
238 0         0 last;
239             }
240             }
241 0         0 return @tokens;
242             }
243              
244             sub text_draw($$;)
245             {
246 0     0 0 0 my $this = shift;
247 0         0 my ($y, $x, $text) = @_;
248              
249 0 0       0 if ($this->{-htmltext}) {
250 0         0 my @tokens = &text_tokenize($text);
251 0         0 foreach my $token (@tokens) {
252 0 0       0 if ($token =~ m/^<(standout|reverse|bold|underline|blink|dim)>$/s) {
    0          
    0          
253 0         0 my $type = $1;
254 0 0       0 if ($type eq 'standout') { $this->{-canvasscr}->attron(A_STANDOUT); }
  0 0       0  
    0          
    0          
    0          
    0          
255 0         0 elsif ($type eq 'reverse') { $this->{-canvasscr}->attron(A_REVERSE); }
256 0         0 elsif ($type eq 'bold') { $this->{-canvasscr}->attron(A_BOLD); }
257 0         0 elsif ($type eq 'underline') { $this->{-canvasscr}->attron(A_UNDERLINE); }
258 0         0 elsif ($type eq 'blink') { $this->{-canvasscr}->attron(A_BLINK); }
259 0         0 elsif ($type eq 'dim') { $this->{-canvasscr}->attron(A_DIM); }
260             } elsif ($token =~ m/^<\/(standout|reverse|bold|underline|blink|dim)>$/s) {
261 0         0 my $type = $1;
262 0 0       0 if ($type eq 'standout') { $this->{-canvasscr}->attroff(A_STANDOUT); }
  0 0       0  
    0          
    0          
    0          
    0          
263 0         0 elsif ($type eq 'reverse') { $this->{-canvasscr}->attroff(A_REVERSE); }
264 0         0 elsif ($type eq 'bold') { $this->{-canvasscr}->attroff(A_BOLD); }
265 0         0 elsif ($type eq 'underline') { $this->{-canvasscr}->attroff(A_UNDERLINE); }
266 0         0 elsif ($type eq 'blink') { $this->{-canvasscr}->attroff(A_BLINK); }
267 0         0 elsif ($type eq 'dim') { $this->{-canvasscr}->attroff(A_DIM); }
268             # Tags: (see, man 5 terminfo)
269             # | <4_ACS_VLINE> -- Vertical line (4 items).
270             # -- <5_ACS_HLINE> -- Horizontal line (5 items).
271             # ` <12_ACS_TTEE> -- Tee pointing down (12 items).
272             # ~ -- Tee pointing up (1 item).
273             # + -- Large plus or crossover (1 item).
274             # ------------------------------------------------------------------
275             } elsif ($token =~ m/^<(\d*)_?(ACS_HLINE|ACS_VLINE|ACS_TTEE|ACS_BTEE|ACS_PLUS)>$/s) {
276 8     8   62 no strict 'refs';
  8         17  
  8         14245  
277 0   0     0 my $scrlen = ($1 || 1);
278 0         0 my $type = &{ $2 };
  0         0  
279 0         0 $this->{-canvasscr}->hline( $y, $x, $type, $scrlen );
280 0         0 $x += $scrlen;
281             } else {
282 0         0 $this->{-canvasscr}->addstr($y, $x, $token);
283 0         0 $x += length($token);
284             }
285             }
286             }
287             else {
288 0         0 $this->{-canvasscr}->addstr($y, $x, $text);
289             }
290             }
291              
292             sub text_length {
293 0     0 0 0 my $this = shift;
294 0         0 my ($text) = @_;
295            
296 0         0 my $length = 0;
297 0 0       0 if ($this->{-htmltext}) {
298 0         0 my @tokens = &text_tokenize($text);
299 0         0 foreach my $token (@tokens) {
300 0 0       0 if ($token !~ m/^<\/?(reverse|bold|underline|blink|dim)>$/s) {
301 0         0 $length += length($token);
302             }
303             }
304             }
305             else {
306 0         0 $length = length($text);
307             }
308 0         0 return $length;
309             }
310              
311             sub text_chop {
312 0     0 0 0 my $this = shift;
313 0         0 my ($text, $max_length) = @_;
314              
315 0 0       0 if ($this->{-htmltext}) {
316 0         0 my @open = ();
317 0         0 my @tokens = &text_tokenize($text);
318 0         0 my $length = 0;
319 0         0 $text = '';
320 0         0 foreach my $token (@tokens) {
321 0 0       0 if ($token =~ m/^<(\/?)(reverse|bold|underline|blink|dim)>/s) {
322 0         0 my ($type, $name) = ($1, $2);
323 0 0 0     0 if (defined($type) and $type eq '/') {
324 0         0 pop(@open);
325             }
326             else {
327 0         0 push(@open, $name);
328             }
329 0         0 $text .= $token;
330             }
331             else {
332 0         0 $text .= $token;
333 0         0 $length += length($token);
334 0 0       0 if ($length > $max_length) {
335 0         0 $text = substr($text, 0, $max_length);
336 0         0 $text =~ s/.$/\$/;
337 0         0 while (defined($token = pop(@open))) {
338 0         0 $text .= "";
339             }
340 0         0 last;
341             }
342             }
343             }
344             }
345             else {
346 0 0       0 if (length($text) > $max_length) {
347 0         0 $text = substr($text, 0, $max_length);
348             }
349             }
350 0         0 return $text;
351             }
352              
353             sub text_dimension ($;)
354             {
355             # Make $this->text_wrap() possible.
356 0 0   0 1 0 shift if ref $_[0];
357 0         0 my $text = shift;
358            
359 0         0 my $lines = split_to_lines($text);
360            
361 0         0 my $height = scalar @$lines;
362            
363 0         0 my $width = 0;
364 0         0 foreach (@$lines)
365             {
366 0         0 my $l = length($_);
367 0 0       0 $width = $l if $l > $width;
368             }
369              
370 0         0 return ($width, $height);
371             }
372              
373             # ----------------------------------------------------------------------
374             # Keyboard input
375             # ----------------------------------------------------------------------
376              
377             # Constants:
378              
379             # Keys that are not defined in curses.h, but which might come in handy.
380             sub CUI_ESCAPE() { "\x1b" }
381             sub CUI_TAB() { "\t" }
382             sub CUI_SPACE() { " " }
383              
384             # Make ascii representation of a key.
385             sub key_to_ascii($;)
386             {
387 0     0 0 0 my $this = shift;
388 0         0 my $key = shift;
389              
390 0 0 0     0 if ($key eq CUI_ESCAPE()) {
    0 0        
    0          
391 0         0 $key = '';
392             }
393             # Control characters. Change them into something printable
394             # via Curses' unctrl function.
395             elsif ($key lt ' ' and $key ne "\n" and $key ne "\t") {
396 0         0 $key = '<' . uc(unctrl($key)) . '>';
397             }
398              
399             # Extended keys get translated into their names via Curses'
400             # keyname function.
401             elsif ($key =~ /^\d{2,}$/) {
402 0         0 $key = '<' . uc(keyname($key)) . '>';
403             }
404              
405 0         0 return $key;
406             }
407              
408             # For the select() syscall in char_read().
409             my $rin = '';
410             my $fno = fileno(STDIN);
411             $fno = 0 unless $fno >= 0;
412             vec($rin, $fno , 1) = 1;
413              
414             sub char_read(;$)
415             {
416 1     1 0 2 my $this = shift;
417 1         1 my $blocktime = shift;
418              
419             # Initialize the toplevel window for
420             # reading a key.
421 1         7 my $s = $this->root->{-canvasscr};
422 1         4 noecho();
423 1         4 raw();
424 1         8 $s->keypad(1);
425              
426             # Read input on STDIN.
427 1         158 my $key = '-1';
428 1 50       9 $blocktime = undef if $blocktime < 0; # Wait infinite
429 1         2 my $crin = $rin;
430 1         2 $! = 0;
431 1         13 my $found = select($crin, undef, undef, $blocktime);
432              
433 1 50       5 if ($found < 0 ) {
    50          
434 0 0       0 print STDERR "DEBUG: get_key() -> select() -> $!\n"
435             if $Curses::UI::debug;
436             } elsif ($found) {
437 1         4 $key = $s->getch();
438             }
439              
440 1         27 return $key;
441             }
442              
443             sub get_key(;$)
444             {
445 1     1 1 2 my $this = shift;
446 1   50     15 my $blocktime = shift || 0;
447              
448 1         7 my $key = $this->char_read($blocktime);
449              
450             # ------------------------------------ #
451             # Hacks for broken termcaps / curses #
452             # ------------------------------------ #
453              
454 1 50 33     14 $key = KEY_BACKSPACE if (
455             ord($key) == 127 or
456             $key eq "\cH"
457             );
458              
459 1 50 33     5 $key = KEY_DC if (
460             $key eq "\c?" or
461             $key eq "\cD"
462             );
463              
464 1 50 33     6 $key = KEY_ENTER if (
465             $key eq "\n" or
466             $key eq "\cM"
467             );
468              
469             # Catch ESCape sequences.
470 1         2 my $ESC = CUI_ESCAPE();
471 1 50       2 if ($key eq $ESC)
472             {
473 0         0 $key .= $this->char_read(0);
474              
475             # Only ESC pressed?
476 0 0 0     0 $key = $ESC if $key eq "${ESC}-1"
477             or $key eq "${ESC}${ESC}";
478 0 0       0 return $key if $key eq $ESC;
479            
480             # Not only a single ESC?
481             # Then get extra keypresses.
482 0         0 $key .= $this->char_read(0);
483 0         0 while ($key =~ /\[\d+$/) {
484 0         0 $key .= $this->char_read(0);
485             }
486              
487             # Function keys on my Sun Solaris box.
488             # I have no idea of the portability of
489             # this stuff, but it works for me...
490 0 0       0 if ($key =~ /\[(\d+)\~/)
491             {
492 0         0 my $digit = $1;
493 0 0 0     0 if ($digit >= 11 and $digit <= 15) {
    0 0        
494 0         0 $key = KEY_F($digit-10);
495             } elsif ($digit >= 17 and $digit <= 21) {
496 0         0 $key = KEY_F($digit-11);
497             }
498             }
499            
500 0 0 0     0 $key = KEY_HOME if (
      0        
501             $key eq $ESC . "OH" or
502             $key eq $ESC . "[7~" or
503             $key eq $ESC . "[1~"
504             );
505              
506 0 0 0     0 $key = KEY_BTAB if (
507             $key eq $ESC . "OI" or # My xterm under solaris
508             $key eq $ESC . "[Z" # My xterm under Redhat Linux
509             );
510            
511 0 0       0 $key = KEY_DL if (
512             $key eq $ESC . "[2K"
513             );
514              
515 0 0 0     0 $key = KEY_END if (
516             $key eq $ESC . "OF" or
517             $key eq $ESC . "[4~"
518             );
519              
520 0 0       0 $key = KEY_PPAGE if (
521             $key eq $ESC . "[5~"
522             );
523              
524 0 0       0 $key = KEY_NPAGE if (
525             $key eq $ESC . "[6~"
526             );
527             }
528              
529             # ----------#
530             # Debugging #
531             # ----------#
532              
533 1 50 33     4 if ($Curses::UI::debug and $key ne "-1")
534             {
535 0         0 my $k = '';
536 0         0 my @k = split //, $key;
537 0         0 foreach (@k) { $k .= $this->key_to_ascii($_) }
  0         0  
538 0         0 print STDERR "DEBUG: get_key() -> [$k]\n"
539             }
540              
541 1         3 return $key;
542             }
543              
544             1;
545              
546              
547             =pod
548              
549             =head1 NAME
550              
551             Curses::UI::Common - Common methods for Curses::UI
552              
553             =head1 CLASS HIERARCHY
554              
555             Curses::UI::Common - base class
556              
557              
558             =head1 SYNOPSIS
559              
560             package MyPackage;
561              
562             use Curses::UI::Common;
563             use vars qw(@ISA);
564             @ISA = qw(Curses::UI::Common);
565              
566             =head1 DESCRIPTION
567              
568             Curses::UI::Common is a collection of methods that is
569             shared between Curses::UI classes.
570              
571              
572              
573              
574             =head1 METHODS
575              
576             =head2 Various methods
577              
578             =over 4
579              
580             =item * B ( )
581              
582             Returns the data member $this->{B<-parent>}.
583              
584             =item * B ( )
585              
586             Returns the topmost B<-parent> (the Curses::UI instance).
587              
588             =item * B ( )
589              
590             This method will walk through all the data members of the
591             class intance. Each data member that is a Curses::Window
592             descendant will be removed.
593              
594             =item * B ( NAME, [VALUE] )
595              
596             If VALUE is set, the value for the data member $this->{NAME}
597             will be changed. The method will return the current value for
598             data member $this->{NAME}.
599              
600             =item * B ( HASHREF )
601              
602             All keys in the hash referred to by HASHREF will be
603             converted to lower case.
604              
605             =back
606              
607              
608             =head2 Text processing
609              
610             =over 4
611              
612             =item B ( TEXT )
613              
614             This method will split TEXT into a list of separate lines.
615             It returns a reference to this list.
616              
617             =item B ( LINE )
618              
619             Returns the screenlength of the string LINE. The difference
620             with the perl function length() is that this method will
621             expand TAB characters. It is exported by this class and it may
622             be called as a stand-alone routine.
623              
624             =item B ( TEXT )
625              
626             This method will return an array containing the width
627             (the length of the longest line) and the height (the
628             number of lines) of the TEXT.
629              
630             =item B ( LINE, LENGTH, WORDWRAP )
631              
632             =item B ( )
633              
634             =item B ( )
635              
636             This method will wrap a line of text (LINE) to a
637             given length (LENGTH). If the WORDWRAP argument is
638             true, wordwrap will be enabled (this is the default
639             for WORDWRAP). It will return a reference to a list
640             of wrapped lines. It is exported by this class and it may
641             be called as a stand-alone routine.
642              
643             The B and B routines will
644             return the correct value vor the WORDWRAP argument.
645             These routines are exported by this class.
646              
647             Example:
648              
649             $this->text_wrap($line, 50, NO_WORDWRAP);
650              
651             =back
652              
653              
654              
655             =head2 Reading key input
656              
657             =over 4
658              
659             =item B ( )
660              
661             =item B ( )
662              
663             =item B ( )
664              
665             These are a couple of routines that are not defined by the
666             L module, but which might be useful anyway.
667             These routines are exported by this class.
668              
669             =item B ( BLOCKTIME, CURSOR )
670              
671             This method will try to read a key from the keyboard.
672             It will return the key pressed or -1 if no key was
673             pressed. It is exported by this class and it may
674             be called as a stand-alone routine.
675              
676             The BLOCKTIME argument can be used to set
677             the curses halfdelay (the time to wait before the
678             routine decides that no key was pressed). BLOCKTIME is
679             given in tenths of seconds. The default is 0 (non-blocking
680             key read).
681              
682             Example:
683              
684             my $key = $this->get_key(5)
685              
686             =back
687              
688              
689              
690             =head1 SEE ALSO
691              
692             L
693              
694              
695              
696              
697             =head1 AUTHOR
698              
699             Copyright (c) 2001-2002 Maurice Makaay. All rights reserved.
700              
701             Maintained by Marcus Thiesen (marcus@cpan.thiesenweb.de)
702              
703              
704             This package is free software and is provided "as is" without express
705             or implied warranty. It may be used, redistributed and/or modified
706             under the same terms as perl itself.
707