File Coverage

blib/lib/Games/Roguelike/Console/ANSI.pm
Criterion Covered Total %
statement 63 310 20.3
branch 12 138 8.7
condition 2 30 6.6
subroutine 14 32 43.7
pod 13 22 59.0
total 104 532 19.5


line stmt bran cond sub pod time code
1             package Games::Roguelike::Console::ANSI;
2            
3             =head1 NAME
4            
5             Games::Roguelike::Console::ANSI - socket-friendly, object oriented curses-like support for an ansi screen buffer
6            
7             =head1 SYNOPSIS
8            
9             use Games::Roguelike::Console::ANSI;
10            
11             $con = Games::Roguelike::Console::ANSI->new();
12             $con->attron('bold yellow');
13             $con->addstr('test');
14             $con->attroff();
15             $con->refresh();
16            
17             =head1 DESCRIPTION
18            
19             Allows a curses-like ansi screen buffer that works on win32, and doesn't crash when used with
20             sockets like the perl ncurses does.
21            
22             Inherits from Games::Roguelike::Console. See Games::Roguelike::Console for list of methods.
23            
24             Uses Term::ANSIColor for colors.
25            
26             =head1 SEE ALSO
27            
28             L
29            
30             =head1 AUTHOR
31            
32             Erik Aronesty C
33            
34             =head1 LICENSE
35            
36             This program is free software; you can redistribute it and/or
37             modify it under the same terms as Perl itself.
38            
39             See L or the included LICENSE file.
40            
41             =cut
42            
43 5     5   40 use strict;
  5         10  
  5         952  
44 5     5   9179 use IO::File;
  5         97496  
  5         863  
45 5     5   5805 use Term::ReadKey;
  5         37282  
  5         775  
46 5     5   13427 use Term::ANSIColor;
  5         67156  
  5         13459  
47 5     5   11247 use POSIX;
  5         61117  
  5         41  
48 5     5   17753 use Carp qw(confess croak);
  5         13  
  5         330  
49            
50 5     5   37 use base 'Games::Roguelike::Console';
  5         12  
  5         19260  
51            
52             our $VERSION = '0.4.' . [qw$Revision: 258 $]->[1];
53            
54             our $KEY_ESCAPE = chr(27);
55             our $KEY_NOOP = chr(241);
56             our ($KEY_LEFT, $KEY_UP, $KEY_RIGHT, $KEY_DOWN) = ('[D','[A','[C','[B');
57            
58             my %TELKEY = (
59             "\xfb" => 'WILL',
60             "\xfc" => 'WONT',
61             "\xfd" => 'DO',
62             "\xfe" => 'DONT',
63             );
64            
65             sub new {
66 2     2 1 6 my $pkg = shift;
67 2 50       10 croak "usage: Games::Roguelike::Console::ANSI->new()" unless $pkg;
68            
69 2         8 my $self = bless {}, $pkg;
70 2         13 $self->init(@_);
71 2         8 return $self;
72             }
73            
74             my $STD;
75             sub init {
76 2     2 0 20 my $self = shift;
77            
78 2         9 my %opt = @_;
79            
80 2 100       32 $self->{in} = *STDIN{IO} unless $self->{in} = $opt{in};
81 2 100       14 $self->{out} = *STDOUT{IO} unless $self->{out} = $opt{out};
82 2         6 $self->{cursor} = 1;
83 2         6 $self->{cx} = 0;
84 2         5 $self->{cy} = 0;
85 2         6 $self->{cattr} = '';
86 2         7 $self->{cbuf} = '';
87 2         15 $self->{reset} = color('reset');
88            
89 2         81 $self->SUPER::init(%opt);
90            
91             # initialize ansi terminal
92 2 50       25 if (!$opt{noinit}) {
93            
94 0         0 my $out = $self->{out};
95            
96 0         0 $self->{usereadkey} = (($self->{out}->fileno() == 1));
97            
98             # i think get away from readkey
99             # and just send the ansi sequence
100             # for determining terminal size
101            
102 0 0       0 if ($self->{usereadkey}) {
103             # this will (wrongly) close the output handle if it fails
104 0         0 eval {
105 0         0 ($self->{winx}, $self->{winy}) = GetTerminalSize($self->{out});
106 0         0 ReadMode 'cbreak', $self->{in};
107             };
108             }
109            
110 0 0       0 if (!$self->{winx}) {
111             # todo: negotiate using telnet stuff
112 0         0 ($self->{winx}, $self->{winy}) = (80,40);
113             }
114            
115 0         0 $self->{invl}=$self->{winx}+1;
116 0         0 $self->{invr}=-1;
117 0         0 $self->{invt}=$self->{winy}+1;
118 0         0 $self->{invb}=-1;
119            
120            
121 0         0 print $out ("\033[2J"); #clear the screen
122 0         0 print $out ("\033[0;0H"); #jump to 0,0
123 0         0 print $out ("\033[=0c"); #hide cursor
124            
125 0         0 $self->{cursor} = 0;
126 0 0       0 if ($self->{out}->fileno() == 1) {
127             # there's probably a tty
128 0         0 $STD = $self;
129 0         0 $SIG{INT} = \&sig_int_handler;
130 0         0 $SIG{__DIE__} = \&sig_die_handler;
131 0 0       0 $self->{speed} = `stty speed` unless $self->{speed};
132             }
133             }
134             }
135            
136             sub clear {
137 0     0 0 0 my $self = shift;
138 0         0 my $out = $self->{out};
139 0         0 @{$self->{buf}} = [];
  0         0  
140 0         0 @{$self->{cur}} = [];
  0         0  
141 0         0 print $out ("\033[2J"); #clear the screen
142 0         0 print $out ("\033[0;0H"); #jump to 0,0
143             }
144            
145             sub redraw {
146 0     0 1 0 my $self = shift;
147 0         0 my $out = $self->{out};
148 0         0 @{$self->{cur}} = [];
  0         0  
149 0         0 print $out "\033c"; # reset
150 0 0       0 print $out ("\033[=0c") if !$self->{cursor}; # hide cursor
151 0         0 $self->clear();
152 0         0 refresh();
153             }
154            
155             sub reset_fh {
156 1     1 0 2 my $out = shift;
157             #print $out "\033[c"; # reset
158 1         20 print $out "\033[=1c"; # show cursor
159 1         8 print $out "\033[30;0H"; # jump to col 0
160 1         3 eval {ReadMode 0, $out}; # normal input
  1         8  
161             }
162            
163             sub sig_int_handler {
164 0     0 0 0 reset_fh(*STDOUT{IO});
165 0         0 exit;
166             }
167            
168             sub sig_die_handler {
169 0 0   0 0 0 die @_ if $^S;
170 0         0 reset_fh(*STDOUT{IO});
171 0         0 die @_;
172             }
173            
174             sub END {
175             # this is only done because DESTROY is never called for some reason on Win32
176 5 50   5   2184 if ($STD) {
177 0         0 reset_fh(*STDOUT{IO});
178 0         0 $STD = undef;
179             }
180            
181 5 50       66 if ($^O =~ /linux|darwin/) {
182 5 50       47 if (my $tty = POSIX::ttyname(1)) {
183 0         0 system("stty -F $tty sane");
184             }
185             }
186             }
187            
188             sub DESTROY {
189 1     1   329 my $self = shift;
190 1 50 33     26 if ($self->{out} && fileno($self->{out})) {
191 1         6 reset_fh($self->{out});
192 1 50 33     65 if ($self->{out}->fileno()
193             && $self->{out}->fileno() == 1) {
194 1         20 $STD = undef;
195 1         14 $SIG{INT} = undef;
196 1         114 $SIG{__DIE__} = undef;
197             }
198             }
199             }
200            
201             sub tagstr {
202 0     0 1 0 my $self = shift;
203 0         0 my ($y, $x, $str);
204 0 0       0 if (@_ == 1) {
205 0         0 ($y, $x, $str) = ($self->{cy}, $self->{cx}, @_);
206             } else {
207 0         0 ($y, $x, $str) = @_;
208             }
209 0         0 my $attr;
210 0         0 my $r = $x;
211 0         0 my $c;
212 0         0 for (my $i = 0; $i < length($str); ++$i) {
213 0         0 $c = substr($str,$i,1);
214 0 0       0 if ($c eq '<') {
215 0         0 substr($str,$i) =~ s/<([^>]*)>//;
216 0         0 $attr = $1;
217 0         0 $attr =~ s/(bold )?gray/bold black/i;
218 0         0 $attr =~ s/,/ /;
219 0         0 $attr =~ s/\bon /on_/;
220 0 0       0 if ($attr eq 'gt') {
    0          
221 0         0 $c = '>';
222 0         0 --$i;
223             } elsif ($attr eq 'lt') {
224 0         0 $c = '<';
225 0         0 --$i;
226             } else {
227 0         0 $c = substr($str,$i,1);
228             }
229             }
230 0 0       0 if ($c eq "\r") {
231 0         0 next;
232             }
233 0 0       0 if ($c eq "\n") {
234 0         0 $r = 0;
235 0         0 $y++;
236 0         0 next;
237             }
238 0         0 $self->{buf}->[$y][$r]->[0] = $self->parsecolor($attr);
239 0         0 $self->{buf}->[$y][$r]->[1] = $c;
240 0         0 ++$r;
241            
242             }
243 0         0 $self->invalidate($x, $y, $x+$r, $y);
244 0         0 $self->{cy}=$y;
245 0         0 $self->{cx}=$x+$r;
246             }
247            
248             sub parsecolor {
249 2     2 1 5 my $self = shift;
250 2         4 my $color = shift;
251 2 50       13 if ($color) {
252 2         13 $color =~ s/(bold )?gray/bold black/i;
253 2         5 $color =~ s/,/ /;
254 2         15 $color =~ s/\bon\s+/on_/;
255 2         15 return color($color);
256             } else {
257 0           return '';
258             }
259             }
260            
261             sub attron {
262 0     0 1   my $self = shift;
263 0           $self->{cattr} = $self->parsecolor(@_);
264             }
265            
266             sub attroff {
267 0     0 1   my $self = shift;
268 0           my ($attr) = @_;
269 0           $self->{cattr} = '';
270             }
271            
272             sub addstr {
273 0     0 1   my $self = shift;
274 0           my $str = pop @_;
275            
276 0 0         if (@_== 0) {
    0          
277 0           for (my $i = 0; $i < length($str); ++$i) {
278 0           my $c = substr($str,$i,1);
279 0 0         if ($c eq "\n") {
280 0           $self->{cx} = 0;
281 0           $self->{cy}++;
282 0           next;
283             }
284 0           $self->{buf}->[$self->{cy}][$self->{cx}+$i]->[0] = $self->{cattr};
285 0           $self->{buf}->[$self->{cy}][$self->{cx}+$i]->[1] = $c;
286             }
287 0           $self->invalidate($self->{cx}, $self->{cy}, $self->{cx} + length($str), $self->{cy});
288 0           $self->{cx} += length($str);
289             } elsif (@_==2) {
290 0           my ($y, $x) = @_;
291 0           for (my $i = 0; $i < length($str); ++$i) {
292 0           my $c = substr($str,$i,1);
293 0 0         if ($c eq "\n") {
294 0           $c = 0;
295 0           $y++;
296 0           next;
297             }
298 0           $self->{buf}->[$y][$x+$i]->[0] = $self->{cattr};
299 0           $self->{buf}->[$y][$x+$i]->[1] = $c;
300             }
301 0           $self->invalidate($x, $y, $x+length($str), $y);
302 0           $self->{cy}=$y;
303 0           $self->{cx}=$x+length($str);
304             }
305             }
306            
307             sub invalidate {
308 0     0 0   my $self = shift;
309 0           my ($l, $t, $r, $b) = @_;
310 0 0         $r = 0 if ($r < 0);
311 0 0         $t = 0 if ($t < 0);
312 0 0         $b = $self->{winy} if ($b > $self->{winy});
313 0 0         $r = $self->{winx} if ($r > $self->{winx});
314            
315 0 0         if ($r < $l) {
316 0           my $m = $r;
317 0           $r = $l;
318 0           $l = $m;
319             }
320 0 0         if ($b < $t) {
321 0           my $m = $t;
322 0           $b = $t;
323 0           $t = $m;
324             }
325 0 0         $self->{invl} = $l if $l < $self->{invl};
326 0 0         $self->{invr} = $r if $r > $self->{invr};
327 0 0         $self->{invt} = $t if $t < $self->{invt};
328 0 0         $self->{invb} = $b if $b > $self->{invb};
329             }
330            
331             sub refresh {
332 0     0 1   my $self = shift;
333 0           my $out = $self->{out};
334            
335             # it's expected that the "buf" array will frequently be uninitialized
336 5     5   57 no warnings 'uninitialized';
  5         9  
  5         17647  
337            
338 0           my $cc;
339 0           for (my $y = $self->{invt}; $y <= $self->{invb}; ++$y) {
340 0           for (my $x = $self->{invl}; $x <= $self->{invr}; ++$x) {
341 0 0 0       if (!($self->{buf}->[$y][$x]->[0] eq $self->{cur}->[$y][$x]->[0]) || !($self->{buf}->[$y][$x]->[1] eq $self->{cur}->[$y][$x]->[1])) {
342 0           print $out "\033[", ($y+1), ";", ($x+1), "H", @{$self->{buf}->[$y][$x]};
  0            
343 0           $cc += 9;
344 0           $self->{cur}->[$y][$x]->[0]=$self->{buf}->[$y][$x]->[0];
345 0           $self->{cur}->[$y][$x]->[1]=$self->{buf}->[$y][$x]->[1];
346 0           my $pattr = $self->{cur}->[$y][$x]->[0];
347             # reduce unnecessary cursor moves & color sets
348 0   0       while ($x < $self->{invr} &&
      0        
349             !( ($self->{buf}->[$y][$x+1]->[0] eq $self->{cur}->[$y][$x+1]->[0])
350             && ($self->{buf}->[$y][$x+1]->[1] eq $self->{cur}->[$y][$x+1]->[1])
351             )
352             ) {
353 0           ++$x;
354 0 0         if (!($pattr eq $self->{buf}->[$y][$x]->[0])) {
355 0           print $out $self->{reset};
356 0           print $out $self->{buf}->[$y][$x]->[0];
357 0           $pattr = $self->{buf}->[$y][$x]->[0];
358 0           $cc += 7;
359             }
360 0           print $out $self->{buf}->[$y][$x]->[1];
361 0           $self->{cur}->[$y][$x]->[0]=$self->{buf}->[$y][$x]->[0];
362 0           $self->{cur}->[$y][$x]->[1]=$self->{buf}->[$y][$x]->[1];
363 0           $cc += 1;
364             }
365 0           print $out $self->{reset};
366 0           $cc += 4;
367             }
368             }
369             }
370 0           $self->{invl}=$self->{winx}+1;
371 0           $self->{invr}=-1;
372 0           $self->{invt}=$self->{winy}+1;
373 0           $self->{invb}=-1;
374             }
375            
376             sub move {
377 0     0 1   my $self = shift;
378 0           my $out = $self->{out};
379 0           my ($y, $x) = @_;
380 0           $self->{cy}=$y;
381 0           $self->{cx}=$x;
382 0 0 0       if ($self->{cursor} && !($self->{cx}==$self->{scx} && $self->{cx}==$self->{scy})) {
      0        
383 0           print $out "\033[", ($y+1), ";", ($x+1), "H";
384 0           $self->{scx} = $self->{cx};
385 0           $self->{scy} = $self->{cy};
386             }
387             }
388            
389             sub cursor {
390 0     0 1   my $self = shift;
391 0           my ($set) = @_;
392 0           my $out = $self->{out};
393            
394 0 0 0       if ($set && !$self->{cursor}) {
    0 0        
395 0           print $out ("\033[=1c"); #show cursor
396 0           $self->{cursor} = 1;
397 0           $self->move($self->{cy}, $self->{cx});
398             } elsif (!$set && $self->{cursor}) {
399 0           print $out ("\033[=0c"); #hide cursor
400 0           $self->{cursor} = 0;
401             }
402             }
403            
404             sub addch {
405 0     0 1   my $self = shift;
406 0           $self->addstr(@_);
407             }
408            
409             sub getch_raw {
410 0     0 0   my $self = shift;
411 0           my $time = shift;
412 0 0         if ($self->{usereadkey}) {
413 0 0         return ReadKey($time ? $time : 0, $self->{in});
414             } else {
415 0           my $c;
416 0 0         $c = undef if !sysread($self->{in}, $c, 1);
417 0           return $c;
418             }
419             }
420            
421             sub trans {
422 0     0 0   my ($c) = @_;
423            
424 0 0         if ($c eq $KEY_UP) {
    0          
    0          
    0          
    0          
425 0           return 'UP'
426             } elsif ($c eq $KEY_DOWN) {
427 0           return 'DOWN'
428             } elsif ($c eq $KEY_LEFT) {
429 0           return 'LEFT'
430             } elsif ($c eq $KEY_RIGHT) {
431 0           return 'RIGHT'
432             } elsif ($c eq "\x8") {
433 0           return 'BACKSPACE'
434             }
435            
436 0           return $c;
437             }
438            
439             sub getch {
440 0     0 1   my $self = shift;
441            
442 0           my $c;
443 0 0         if ($self->{cbuf}) {
444 0           $c = substr($self->{cbuf},0,1);
445 0           $self->{cbuf} = substr($self->{cbuf},1);
446             } else {
447 0           $c = $self->getch_raw();
448             }
449            
450 0 0         if ($c eq $KEY_ESCAPE) {
451 0           $c = $self->getch_raw(1);
452 0 0         if ($c eq '[') {
    0          
    0          
453 0           $c = $self->getch_raw(1);
454 0           $c = '[' . $c;
455             } elsif ($c eq $KEY_NOOP) {
456 0           return getch();
457             } elsif ($c eq $KEY_ESCAPE) {
458 0           return 'ESC';
459             } else {
460             # unknown escape sequence
461 0           $self->{cbuf} .= $c;
462 0           return 'ESC';
463             }
464             }
465            
466 0           return trans($c);
467             }
468            
469             sub nbgetch_raw {
470 0     0 0   my $self = shift;
471 0           my $c;
472 0 0         if (length($self->{cbuf}) > 0) {
473 0           $c = substr($self->{cbuf},0,1);
474 0           $self->{cbuf} = substr($self->{cbuf},1);
475             } else {
476 0 0         if ($self->{usereadkey}) {
477 0           $c = ReadKey(-1, $self->{in});
478             } else {
479 0           sysread($self->{in}, $c, 1);
480             }
481             }
482 0           return $c;
483             }
484            
485             sub nbgetch {
486 0     0 1   my $self = shift;
487            
488 0           my $c = $self->nbgetch_raw();
489            
490 0 0         if ($c eq $KEY_ESCAPE) {
    0          
491 0           my $c2 = $self->nbgetch_raw();
492 0 0         if (!defined($c2)) {
    0          
    0          
    0          
493 0           $self->{cbuf} = $KEY_ESCAPE;
494 0           $c = '';
495             } elsif ($c2 eq '[') {
496 0           my $ct = '';
497 0           my $cs = '';
498            
499 0   0       do {
500 0           $ct = $self->nbgetch_raw();
501 0 0         $cs .= $ct if defined $ct;
502             } while (defined $ct && $ct !~ /^[a-z]$/i);
503            
504 0 0         if (!defined($ct)) {
505 0           $self->{cbuf} = $KEY_ESCAPE . '[' . $cs;
506             } else {
507 0           $c = '[' . $cs;
508             }
509             } elsif ($c2 eq $KEY_NOOP) {
510 0           $c = '';
511             } elsif ($c2 eq $KEY_ESCAPE) {
512 0           $c = 'ESC';
513             } else {
514 0           $c = $c2;
515 0 0         $c = '' if ord($c) > 240;
516             }
517             } elsif (ord($c) == 255) { # telnet esc
518 0           my $c2 = $self->nbgetch_raw();
519 0 0         if (!defined($c2)) {
    0          
520 0           $self->{cbuf} = $c;
521 0           $c = '';
522             } elsif ($TELKEY{$c2}) {
523             # telnet do/don't
524 0           my $c3 = $self->nbgetch_raw();
525 0 0         if (!defined($c3)) {
526 0           $self->{cbuf} = $c . $c2;
527             } else {
528 0           $c = $TELKEY{$c2} . ord($c3);
529             }
530             } else {
531 0           $c = $c2;
532 0 0         $c = '' if ord($c) > 240;
533             }
534             }
535            
536 0           return trans($c);
537             }
538            
539             1;