File Coverage

blib/lib/Termbox/PP.pm
Criterion Covered Total %
statement 1188 1587 74.8
branch 373 796 46.8
condition 124 311 39.8
subroutine 188 199 94.4
pod 0 103 0.0
total 1873 2996 62.5


line stmt bran cond sub pod time code
1             # ------------------------------------------------------------------------
2             #
3             # Termbox (Perl port)
4             #
5             # Implementation based on termbox2 v2.7.0-dev, 8. Feb 2026
6             #
7             # Copyright (C) 2015-2026 Adam Saponara
8             # 2010-2020 nsf
9             #
10             # ------------------------------------------------------------------------
11             # Author: 2024-2026 J. Schneider
12             # ------------------------------------------------------------------------
13            
14             package Termbox;
15            
16             # ------------------------------------------------------------------------
17             # Boilerplate ------------------------------------------------------------
18             # ------------------------------------------------------------------------
19            
20 37     37   3150732 use 5.010;
  37         120  
21 37     37   164 use strict;
  37         56  
  37         783  
22 37     37   134 use warnings;
  37         55  
  37         1665  
23            
24             # version '...'
25 37     37   12457 use version;
  37         54888  
  37         204  
26             our $version = version->declare('v2.7.0_0');
27             our $VERSION = version->declare('v0.4.3');
28            
29             # authority '...'
30             our $authority = 'github:adsr';
31             our $AUTHORITY = 'github:brickpool';
32            
33             # ------------------------------------------------------------------------
34             # Imports ----------------------------------------------------------------
35             # ------------------------------------------------------------------------
36            
37             require bytes;
38 37     37   4295 use Carp ();
  37         56  
  37         767  
39 37     37   124 use Config;
  37         46  
  37         1315  
40 37     37   17193 use IO::File ();
  37         272771  
  37         1087  
41 37     37   15296 use Params::Check qw( check );
  37         137305  
  37         2437  
42 37         181 use POSIX qw(
43             :errno_h
44             :termios_h
45 37     37   15017 );
  37         258898  
46 37         1948 use Scalar::Util qw(
47             looks_like_number
48             blessed
49 37     37   62603 );
  37         55  
50 37     37   12419 use utf8;
  37         7396  
  37         182  
51            
52             # ------------------------------------------------------------------------
53             # Compile-time options ---------------------------------------------------
54             # ------------------------------------------------------------------------
55            
56             # use constant TB_VERSION_STR => $version->normal;
57 2     2 0 361 BEGIN { sub TB_VERSION_STR () { state $qv = $version->normal } }
58            
59             # STRICT is a global flag that enables strict argument checking.
60 37 100       71 use constant STRICT => !!grep { exists $ENV{$_} && $ENV{$_} } qw(
  148         4555  
61             PERL_STRICT
62             EXTENDED_TESTING
63             AUTHOR_TESTING
64             RELEASE_TESTING
65 37     37   4204 );
  37         49  
66            
67             # The following compile-time options are available for conditional code.
68             # They are set to the default values used in the C implementation, but can be
69             # overridden by setting the corresponding environment variables before loading
70             # this module.
71            
72 37 50   37   214 use constant TB_OPT_EGC => exists $ENV{TB_OPT_EGC} ? !!$ENV{TB_OPT_EGC} : 1;
  37         49  
  37         2417  
73            
74             # Define this to set the string length used in 'tb_printf' and 'tb_sendf'.
75             use constant TB_OPT_PRINTF_BUF => exists $ENV{TB_OPT_PRINTF_BUF}
76             ? 0+$ENV{TB_OPT_PRINTF_BUF}
77 37 50   37   169 : 4096;
  37         53  
  37         2658  
78            
79             # Define this to set the size of the buffer used when reading from the tty.
80             use constant TB_OPT_READ_BUF => exists $ENV{TB_OPT_READ_BUF}
81             ? 0+$ENV{TB_OPT_READ_BUF}
82 37 50   37   225 : 64;
  37         118  
  37         2243  
83            
84             # In this port, TB_OPT_LIBC_WCHAR selects the Unicode::UCD-based wcwidth backend
85             use constant TB_OPT_LIBC_WCHAR => exists $ENV{TB_OPT_LIBC_WCHAR}
86             ? !!$ENV{TB_OPT_LIBC_WCHAR}
87 37 50   37   149 : 1;
  37         67  
  37         2231  
88            
89 37 50   37   214 use constant TB_PATH_MAX => exists $ENV{PATH_MAX} ? $ENV{PATH_MAX} : 4096;
  37         58  
  37         2766  
90            
91             use constant TB_TERMINFO_DIR => exists $ENV{TB_TERMINFO_DIR}
92             ? ''.$ENV{TB_TERMINFO_DIR}
93 37 50   37   159 : undef;
  37         50  
  37         2193  
94            
95             use constant TB_RESIZE_FALLBACK_MS => exists $ENV{TB_RESIZE_FALLBACK_MS}
96             ? 0+$ENV{TB_RESIZE_FALLBACK_MS}
97 37 50   37   142 : 1000;
  37         48  
  37         2328  
98            
99             # Deprecated. Back-compat for old flag.
100             use constant TB_OPT_TRUECOLOR => exists $ENV{TB_OPT_TRUECOLOR}
101             ? !!$ENV{TB_OPT_TRUECOLOR}
102 37 50   37   164 : undef;
  37         66  
  37         4751  
103            
104             # Ensure sane 'TB_OPT_ATTR_W' (16, 32, or 64)
105             use constant TB_OPT_ATTR_W =>
106             ( exists $ENV{TB_OPT_ATTR_W}
107             && $ENV{TB_OPT_ATTR_W} =~ /^(16|32|64)$/ ) ? $ENV{TB_OPT_ATTR_W} :
108 37 50 33     4919 ( !defined TB_OPT_TRUECOLOR ) ? $Config{ivsize} * 8 :
    50          
109             ( TB_OPT_TRUECOLOR ) ? 32 :
110 37     37   203 16 ;
  37         66  
111            
112             # ------------------------------------------------------------------------
113             # Exports ----------------------------------------------------------------
114             # ------------------------------------------------------------------------
115            
116 37     37   192 use Exporter qw( import );
  37         83  
  37         15401  
117            
118             our @EXPORT_OK = qw(
119             );
120            
121             our %EXPORT_TAGS = (
122            
123             api => [qw(
124             tb_init
125             tb_init_file
126             tb_init_fd
127             tb_init_rwfd
128             tb_shutdown
129            
130             tb_width
131             tb_height
132            
133             tb_clear
134             tb_set_clear_attrs
135            
136             tb_present
137             tb_invalidate
138            
139             tb_set_cursor
140             tb_hide_cursor
141            
142             tb_set_cell
143             tb_set_cell_ex
144             tb_extend_cell
145            
146             tb_get_cell
147            
148             tb_set_input_mode
149             tb_set_output_mode
150            
151             tb_peek_event
152             tb_poll_event
153            
154             tb_get_fds
155            
156             tb_print
157             tb_printf
158             tb_print_ex
159             tb_printf_ex
160            
161             tb_send
162             tb_sendf
163            
164             tb_set_func
165            
166             tb_utf8_char_length
167             tb_utf8_char_to_unicode
168             tb_utf8_unicode_to_char
169            
170             tb_last_errno
171             tb_strerror
172             tb_cell_buffer
173             tb_has_truecolor
174             tb_has_egc
175             tb_attr_width
176             tb_version
177             )],
178            
179             keys => [qw(
180             TB_KEY_CTRL_TILDE
181             TB_KEY_CTRL_2
182             TB_KEY_CTRL_A
183             TB_KEY_CTRL_B
184             TB_KEY_CTRL_C
185             TB_KEY_CTRL_D
186             TB_KEY_CTRL_E
187             TB_KEY_CTRL_F
188             TB_KEY_CTRL_G
189             TB_KEY_BACKSPACE
190             TB_KEY_CTRL_H
191             TB_KEY_TAB
192             TB_KEY_CTRL_I
193             TB_KEY_CTRL_J
194             TB_KEY_CTRL_K
195             TB_KEY_CTRL_L
196             TB_KEY_ENTER
197             TB_KEY_CTRL_M
198             TB_KEY_CTRL_N
199             TB_KEY_CTRL_O
200             TB_KEY_CTRL_P
201             TB_KEY_CTRL_Q
202             TB_KEY_CTRL_R
203             TB_KEY_CTRL_S
204             TB_KEY_CTRL_T
205             TB_KEY_CTRL_U
206             TB_KEY_CTRL_V
207             TB_KEY_CTRL_W
208             TB_KEY_CTRL_X
209             TB_KEY_CTRL_Y
210             TB_KEY_CTRL_Z
211             TB_KEY_ESC
212             TB_KEY_CTRL_LSQ_BRACKET
213             TB_KEY_CTRL_3
214             TB_KEY_CTRL_4
215             TB_KEY_CTRL_BACKSLASH
216             TB_KEY_CTRL_5
217             TB_KEY_CTRL_RSQ_BRACKET
218             TB_KEY_CTRL_6
219             TB_KEY_CTRL_7
220             TB_KEY_CTRL_SLASH
221             TB_KEY_CTRL_UNDERSCORE
222             TB_KEY_SPACE
223             TB_KEY_BACKSPACE2
224             TB_KEY_CTRL_8
225            
226             TB_KEY_F1
227             TB_KEY_F2
228             TB_KEY_F3
229             TB_KEY_F4
230             TB_KEY_F5
231             TB_KEY_F6
232             TB_KEY_F7
233             TB_KEY_F8
234             TB_KEY_F9
235             TB_KEY_F10
236             TB_KEY_F11
237             TB_KEY_F12
238             TB_KEY_INSERT
239             TB_KEY_DELETE
240             TB_KEY_HOME
241             TB_KEY_END
242             TB_KEY_PGUP
243             TB_KEY_PGDN
244             TB_KEY_ARROW_UP
245             TB_KEY_ARROW_DOWN
246             TB_KEY_ARROW_LEFT
247             TB_KEY_ARROW_RIGHT
248             TB_KEY_BACK_TAB
249             TB_KEY_MOUSE_LEFT
250             TB_KEY_MOUSE_RIGHT
251             TB_KEY_MOUSE_MIDDLE
252             TB_KEY_MOUSE_RELEASE
253             TB_KEY_MOUSE_WHEEL_UP
254             TB_KEY_MOUSE_WHEEL_DOWN
255            
256             TB_CAP_F1
257             TB_CAP_F2
258             TB_CAP_F3
259             TB_CAP_F4
260             TB_CAP_F5
261             TB_CAP_F6
262             TB_CAP_F7
263             TB_CAP_F8
264             TB_CAP_F9
265             TB_CAP_F10
266             TB_CAP_F11
267             TB_CAP_F12
268             TB_CAP_INSERT
269             TB_CAP_DELETE
270             TB_CAP_HOME
271             TB_CAP_END
272             TB_CAP_PGUP
273             TB_CAP_PGDN
274             TB_CAP_ARROW_UP
275             TB_CAP_ARROW_DOWN
276             TB_CAP_ARROW_LEFT
277             TB_CAP_ARROW_RIGHT
278             TB_CAP_BACK_TAB
279             TB_CAP__COUNT_KEYS
280             TB_CAP_ENTER_CA
281             TB_CAP_EXIT_CA
282             TB_CAP_SHOW_CURSOR
283             TB_CAP_HIDE_CURSOR
284             TB_CAP_CLEAR_SCREEN
285             TB_CAP_SGR0
286             TB_CAP_UNDERLINE
287             TB_CAP_BOLD
288             TB_CAP_BLINK
289             TB_CAP_ITALIC
290             TB_CAP_REVERSE
291             TB_CAP_ENTER_KEYPAD
292             TB_CAP_EXIT_KEYPAD
293             TB_CAP_DIM
294             TB_CAP_INVISIBLE
295             TB_CAP__COUNT
296             )],
297            
298             color => [qw(
299             TB_DEFAULT
300             TB_BLACK
301             TB_RED
302             TB_GREEN
303             TB_YELLOW
304             TB_BLUE
305             TB_MAGENTA
306             TB_CYAN
307             TB_WHITE
308            
309             TB_BOLD
310             TB_UNDERLINE
311             TB_REVERSE
312             TB_ITALIC
313             TB_BLINK
314             TB_HI_BLACK
315             TB_BRIGHT
316             TB_DIM
317             ),
318             TB_OPT_ATTR_W == 32
319             ? qw( TB_256_BLACK )
320             : qw( TB_TRUECOLOR_BOLD
321             TB_TRUECOLOR_UNDERLINE
322             TB_TRUECOLOR_REVERSE
323             TB_TRUECOLOR_ITALIC
324             TB_TRUECOLOR_BLINK
325             TB_TRUECOLOR_BLACK ),
326             TB_OPT_ATTR_W == 64
327             ? qw( TB_STRIKEOUT
328             TB_UNDERLINE_2
329             TB_OVERLINE
330             TB_INVISIBLE )
331             : ()
332             ],
333            
334             event => [qw(
335             TB_EVENT_KEY
336             TB_EVENT_RESIZE
337             TB_EVENT_MOUSE
338            
339             TB_MOD_ALT
340             TB_MOD_CTRL
341             TB_MOD_SHIFT
342             TB_MOD_MOTION
343            
344             TB_INPUT_CURRENT
345             TB_INPUT_ESC
346             TB_INPUT_ALT
347             TB_INPUT_MOUSE
348            
349             TB_OUTPUT_CURRENT
350             TB_OUTPUT_NORMAL
351             TB_OUTPUT_256
352             TB_OUTPUT_216
353             TB_OUTPUT_GRAYSCALE
354             ),
355             TB_OPT_ATTR_W >= 32
356             ? qw( TB_OUTPUT_TRUECOLOR )
357             : ()
358             ],
359            
360             return => [qw(
361             TB_OK
362             TB_ERR
363             TB_ERR_NEED_MORE
364             TB_ERR_INIT_ALREADY
365             TB_ERR_INIT_OPEN
366             TB_ERR_MEM
367             TB_ERR_NO_EVENT
368             TB_ERR_NO_TERM
369             TB_ERR_NOT_INIT
370             TB_ERR_OUT_OF_BOUNDS
371             TB_ERR_READ
372             TB_ERR_RESIZE_IOCTL
373             TB_ERR_RESIZE_PIPE
374             TB_ERR_RESIZE_SIGACTION
375             TB_ERR_POLL
376             TB_ERR_TCGETATTR
377             TB_ERR_TCSETATTR
378             TB_ERR_UNSUPPORTED_TERM
379             TB_ERR_RESIZE_WRITE
380             TB_ERR_RESIZE_POLL
381             TB_ERR_RESIZE_READ
382             TB_ERR_RESIZE_SSCANF
383             TB_ERR_CAP_COLLISION
384            
385             TB_ERR_SELECT
386             TB_ERR_RESIZE_SELECT
387             )],
388            
389             func => [qw(
390             TB_FUNC_EXTRACT_PRE
391             TB_FUNC_EXTRACT_POST
392             )],
393            
394             );
395            
396             # add all the other %EXPORT_TAGS ":class" tags to the ":all" class and
397             # @EXPORT_OK, deleting duplicates
398             {
399             my %seen;
400             push
401             @EXPORT_OK,
402             grep {!$seen{$_}++} @{$EXPORT_TAGS{$_}}
403             foreach keys %EXPORT_TAGS;
404             push
405             @{$EXPORT_TAGS{all}},
406             @EXPORT_OK;
407             }
408            
409             # ------------------------------------------------------------------------
410             # Constants --------------------------------------------------------------
411             # ------------------------------------------------------------------------
412            
413 37     37   231 use constant INT16_SIZE => $Config{shortsize};
  37         44  
  37         6957  
414            
415             # ASCII key constants ($tb_event->key)
416             use constant {
417 37         13529 TB_KEY_CTRL_TILDE => 0x00,
418             TB_KEY_CTRL_2 => 0x00, # clash with 'CTRL_TILDE'
419             TB_KEY_CTRL_A => 0x01,
420             TB_KEY_CTRL_B => 0x02,
421             TB_KEY_CTRL_C => 0x03,
422             TB_KEY_CTRL_D => 0x04,
423             TB_KEY_CTRL_E => 0x05,
424             TB_KEY_CTRL_F => 0x06,
425             TB_KEY_CTRL_G => 0x07,
426             TB_KEY_BACKSPACE => 0x08,
427             TB_KEY_CTRL_H => 0x08, # clash with 'BACKSPACE'
428             TB_KEY_TAB => 0x09,
429             TB_KEY_CTRL_I => 0x09, # clash with 'TAB'
430             TB_KEY_CTRL_J => 0x0a,
431             TB_KEY_CTRL_K => 0x0b,
432             TB_KEY_CTRL_L => 0x0c,
433             TB_KEY_ENTER => 0x0d,
434             TB_KEY_CTRL_M => 0x0d, # clash with 'ENTER'
435             TB_KEY_CTRL_N => 0x0e,
436             TB_KEY_CTRL_O => 0x0f,
437             TB_KEY_CTRL_P => 0x10,
438             TB_KEY_CTRL_Q => 0x11,
439             TB_KEY_CTRL_R => 0x12,
440             TB_KEY_CTRL_S => 0x13,
441             TB_KEY_CTRL_T => 0x14,
442             TB_KEY_CTRL_U => 0x15,
443             TB_KEY_CTRL_V => 0x16,
444             TB_KEY_CTRL_W => 0x17,
445             TB_KEY_CTRL_X => 0x18,
446             TB_KEY_CTRL_Y => 0x19,
447             TB_KEY_CTRL_Z => 0x1a,
448             TB_KEY_ESC => 0x1b,
449             TB_KEY_CTRL_LSQ_BRACKET => 0x1b, # clash with 'ESC'
450             TB_KEY_CTRL_3 => 0x1b, # clash with 'ESC'
451             TB_KEY_CTRL_4 => 0x1c,
452             TB_KEY_CTRL_BACKSLASH => 0x1c, # clash with 'CTRL_4'
453             TB_KEY_CTRL_5 => 0x1d,
454             TB_KEY_CTRL_RSQ_BRACKET => 0x1d, # clash with 'CTRL_5'
455             TB_KEY_CTRL_6 => 0x1e,
456             TB_KEY_CTRL_7 => 0x1f,
457             TB_KEY_CTRL_SLASH => 0x1f, # clash with 'CTRL_7'
458             TB_KEY_CTRL_UNDERSCORE => 0x1f, # clash with 'CTRL_7'
459             TB_KEY_SPACE => 0x20,
460             TB_KEY_BACKSPACE2 => 0x7f,
461             TB_KEY_CTRL_8 => 0x7f, # clash with 'BACKSPACE2'
462 37     37   217 };
  37         44  
463            
464 0     0 0 0 sub tb_key_i($) { 0xFFFF - $_[0] }
465            
466             # Terminal-dependent key constants ($tb_event->key) and terminfo caps
467             use constant {
468 37         8523 TB_KEY_F1 => 0xFFFF - 0,
469             TB_KEY_F2 => 0xFFFF - 1,
470             TB_KEY_F3 => 0xFFFF - 2,
471             TB_KEY_F4 => 0xFFFF - 3,
472             TB_KEY_F5 => 0xFFFF - 4,
473             TB_KEY_F6 => 0xFFFF - 5,
474             TB_KEY_F7 => 0xFFFF - 6,
475             TB_KEY_F8 => 0xFFFF - 7,
476             TB_KEY_F9 => 0xFFFF - 8,
477             TB_KEY_F10 => 0xFFFF - 9,
478             TB_KEY_F11 => 0xFFFF - 10,
479             TB_KEY_F12 => 0xFFFF - 11,
480             TB_KEY_INSERT => 0xFFFF - 12,
481             TB_KEY_DELETE => 0xFFFF - 13,
482             TB_KEY_HOME => 0xFFFF - 14,
483             TB_KEY_END => 0xFFFF - 15,
484             TB_KEY_PGUP => 0xFFFF - 16,
485             TB_KEY_PGDN => 0xFFFF - 17,
486             TB_KEY_ARROW_UP => 0xFFFF - 18,
487             TB_KEY_ARROW_DOWN => 0xFFFF - 19,
488             TB_KEY_ARROW_LEFT => 0xFFFF - 20,
489             TB_KEY_ARROW_RIGHT => 0xFFFF - 21,
490             TB_KEY_BACK_TAB => 0xFFFF - 22,
491             TB_KEY_MOUSE_LEFT => 0xFFFF - 23,
492             TB_KEY_MOUSE_RIGHT => 0xFFFF - 24,
493             TB_KEY_MOUSE_MIDDLE => 0xFFFF - 25,
494             TB_KEY_MOUSE_RELEASE => 0xFFFF - 26,
495             TB_KEY_MOUSE_WHEEL_UP => 0xFFFF - 27,
496             TB_KEY_MOUSE_WHEEL_DOWN => 0xFFFF - 28,
497 37     37   195 };
  37         50  
498            
499             use constant {
500 37         9746 TB_CAP_F1 => 0,
501             TB_CAP_F2 => 1,
502             TB_CAP_F3 => 2,
503             TB_CAP_F4 => 3,
504             TB_CAP_F5 => 4,
505             TB_CAP_F6 => 5,
506             TB_CAP_F7 => 6,
507             TB_CAP_F8 => 7,
508             TB_CAP_F9 => 8,
509             TB_CAP_F10 => 9,
510             TB_CAP_F11 => 10,
511             TB_CAP_F12 => 11,
512             TB_CAP_INSERT => 12,
513             TB_CAP_DELETE => 13,
514             TB_CAP_HOME => 14,
515             TB_CAP_END => 15,
516             TB_CAP_PGUP => 16,
517             TB_CAP_PGDN => 17,
518             TB_CAP_ARROW_UP => 18,
519             TB_CAP_ARROW_DOWN => 19,
520             TB_CAP_ARROW_LEFT => 20,
521             TB_CAP_ARROW_RIGHT => 21,
522             TB_CAP_BACK_TAB => 22,
523             TB_CAP__COUNT_KEYS => 23,
524             TB_CAP_ENTER_CA => 23,
525             TB_CAP_EXIT_CA => 24,
526             TB_CAP_SHOW_CURSOR => 25,
527             TB_CAP_HIDE_CURSOR => 26,
528             TB_CAP_CLEAR_SCREEN => 27,
529             TB_CAP_SGR0 => 28,
530             TB_CAP_UNDERLINE => 29,
531             TB_CAP_BOLD => 30,
532             TB_CAP_BLINK => 31,
533             TB_CAP_ITALIC => 32,
534             TB_CAP_REVERSE => 33,
535             TB_CAP_ENTER_KEYPAD => 34,
536             TB_CAP_EXIT_KEYPAD => 35,
537             TB_CAP_DIM => 36,
538             TB_CAP_INVISIBLE => 37,
539             TB_CAP__COUNT => 38,
540 37     37   190 };
  37         63  
541            
542             # Some hard-coded caps
543             use constant {
544 37         2927 TB_HARDCAP_ENTER_MOUSE => "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h",
545             TB_HARDCAP_EXIT_MOUSE => "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l",
546             TB_HARDCAP_STRIKEOUT => "\x1b[9m",
547             TB_HARDCAP_UNDERLINE_2 => "\x1b[21m",
548             TB_HARDCAP_OVERLINE => "\x1b[53m",
549 37     37   225 };
  37         48  
550            
551             # Colors (numeric) and attributes (bitwise) ($tb_cell->fg, $tb_cell->bg)
552             use constant {
553 37         3333 TB_DEFAULT => 0x0000,
554             TB_BLACK => 0x0001,
555             TB_RED => 0x0002,
556             TB_GREEN => 0x0003,
557             TB_YELLOW => 0x0004,
558             TB_BLUE => 0x0005,
559             TB_MAGENTA => 0x0006,
560             TB_CYAN => 0x0007,
561             TB_WHITE => 0x0008,
562 37     37   155 };
  37         64  
563            
564 37     37   118 BEGIN { if (TB_OPT_ATTR_W == 16) {
565 37     37   449 no warnings 'once';
  37         222  
  37         7142  
566             *TB_BOLD = sub () { 0x0100 };
567             *TB_UNDERLINE = sub () { 0x0200 };
568             *TB_REVERSE = sub () { 0x0400 };
569             *TB_ITALIC = sub () { 0x0800 };
570             *TB_BLINK = sub () { 0x1000 };
571             *TB_HI_BLACK = sub () { 0x2000 };
572             *TB_BRIGHT = sub () { 0x4000 };
573             *TB_DIM = sub () { 0x8000 };
574             *TB_256_BLACK = &TB_HI_BLACK; # TB_256_BLACK is deprecated
575             }
576             else {
577 37     37   224 no warnings 'once';
  37         70  
  37         6503  
578 37         72 *TB_BOLD = sub () { 0x00010000 };
579 37         59 *TB_UNDERLINE = sub () { 0x00020000 };
580 37         55 *TB_REVERSE = sub () { 0x00040000 };
581 37         63 *TB_ITALIC = sub () { 0x00080000 };
582 37         53 *TB_BLINK = sub () { 0x00100000 };
583 37         48 *TB_HI_BLACK = sub () { 0x00200000 };
584 37         47 *TB_BRIGHT = sub () { 0x00400000 };
585 37         85 *TB_DIM = sub () { 0x00800000 };
586 37         228 *TB_TRUECOLOR_BOLD = &TB_BOLD; # TB_TRUECOLOR_BOLD is deprecated
587 37         124 *TB_TRUECOLOR_UNDERLINE = &TB_UNDERLINE;
588 37         94 *TB_TRUECOLOR_REVERSE = &TB_REVERSE;
589 37         99 *TB_TRUECOLOR_ITALIC = &TB_ITALIC;
590 37         121 *TB_TRUECOLOR_BLINK = &TB_BLINK;
591 37         1355 *TB_TRUECOLOR_BLACK = &TB_HI_BLACK;
592             }}
593            
594 37     37   104 BEGIN { if (TB_OPT_ATTR_W == 64) {
595 37     37   151 no warnings;
  37         40  
  37         3518  
596 37         65 *TB_STRIKEOUT = sub () { 0x010000000000 };
597 37         67 *TB_UNDERLINE_2 = sub () { 0x020000000000 };
598 37         77 *TB_OVERLINE = sub () { 0x040000000000 };
599 37         1057 *TB_INVISIBLE = sub () { 0x080000000000 };
600             }}
601            
602             # Event types ($tb_event->type)
603             use constant {
604 37         3763 TB_EVENT_KEY => 1,
605             TB_EVENT_RESIZE => 2,
606             TB_EVENT_MOUSE => 3,
607 37     37   145 };
  37         67  
608            
609             # Key modifiers (bitwise) ($tb_event->mod)
610             use constant {
611 37         2269 TB_MOD_ALT => 1,
612             TB_MOD_CTRL => 2,
613             TB_MOD_SHIFT => 4,
614             TB_MOD_MOTION => 8,
615 37     37   163 };
  37         41  
616            
617             # Input modes (bitwise) (tb_set_input_mode)
618             use constant {
619 37         1886 TB_INPUT_CURRENT => 0,
620             TB_INPUT_ESC => 1,
621             TB_INPUT_ALT => 2,
622             TB_INPUT_MOUSE => 4,
623 37     37   140 };
  37         40  
624            
625             # Output modes (bitwise) (tb_set_output_mode)
626             use constant {
627 37         2669 TB_OUTPUT_CURRENT => 0,
628             TB_OUTPUT_NORMAL => 1,
629             TB_OUTPUT_256 => 2,
630             TB_OUTPUT_216 => 4,
631             TB_OUTPUT_GRAYSCALE => 8,
632 37     37   141 };
  37         43  
633 37     37   108 BEGIN { if (TB_OPT_ATTR_W >= 32) {
634 37     37   148 no warnings 'once';
  37         61  
  37         2130  
635 37         2747 *TB_OUTPUT_TRUECOLOR = sub () { 5 };
636             }}
637            
638             # Common function return values unless otherwise noted.
639             use constant {
640 37         5830 TB_OK => 0,
641             TB_ERR => -1,
642             TB_ERR_NEED_MORE => -2,
643             TB_ERR_INIT_ALREADY => -3,
644             TB_ERR_INIT_OPEN => -4,
645             TB_ERR_MEM => -5,
646             TB_ERR_NO_EVENT => -6,
647             TB_ERR_NO_TERM => -7,
648             TB_ERR_NOT_INIT => -8,
649             TB_ERR_OUT_OF_BOUNDS => -9,
650             TB_ERR_READ => -10,
651             TB_ERR_RESIZE_IOCTL => -11,
652             TB_ERR_RESIZE_PIPE => -12,
653             TB_ERR_RESIZE_SIGACTION => -13,
654             TB_ERR_POLL => -14,
655             TB_ERR_TCGETATTR => -15,
656             TB_ERR_TCSETATTR => -16,
657             TB_ERR_UNSUPPORTED_TERM => -17,
658             TB_ERR_RESIZE_WRITE => -18,
659             TB_ERR_RESIZE_POLL => -19,
660             TB_ERR_RESIZE_READ => -20,
661             TB_ERR_RESIZE_SSCANF => -21,
662             TB_ERR_CAP_COLLISION => -22,
663 37     37   144 };
  37         50  
664 37     37   168 use constant TB_ERR_SELECT => TB_ERR_POLL;
  37         39  
  37         1670  
665 37     37   159 use constant TB_ERR_RESIZE_SELECT => TB_ERR_RESIZE_POLL;
  37         62  
  37         1608  
666            
667             # Deprecated. Function types to be used with 'tb_set_func'.
668             use constant {
669 37         1900 TB_FUNC_EXTRACT_PRE => 0,
670             TB_FUNC_EXTRACT_POST => 1,
671 37     37   185 };
  37         64  
672            
673             # Mouse escape parser type tags (enum equivalent)
674             use constant {
675 37         2005 TYPE_VT200 => 0,
676             TYPE_1006 => 1,
677             TYPE_1015 => 2,
678             TYPE_MAX => 3,
679 37     37   150 };
  37         47  
680            
681             # ------------------------------------------------------------------------
682             # Globals ----------------------------------------------------------------
683             # ------------------------------------------------------------------------
684            
685             # Holds the current state of the terminal
686 37     37   149 use vars qw( $global );
  37         40  
  37         2617  
687            
688             # ------------------------------------------------------------------------
689             # Utility functions ------------------------------------------------------
690             # ------------------------------------------------------------------------
691            
692 37     37   86 BEGIN { if ( TB_OPT_LIBC_WCHAR ) {
693 37         36821 require Unicode::UCD;
694             } else {
695             require Termbox::PP::WCWidth;
696             }}
697            
698             # Determine if the current locale is CJK, which affects the width of
699             # 'A' (Ambiguous) characters.
700             our $is_cjk = eval {
701             require Unicode::EastAsianWidth::Detect;
702             Unicode::EastAsianWidth::Detect::is_cjk_lang() ? 1 : 0;
703             } || 0;
704            
705             sub wcwidth {
706 60     60 0 154611 my ($cp) = @_;
707            
708 60 50 33     167 return -1 if $cp < 0 || $cp > 0x10FFFF;
709 60 50 66     109 return -1 if $cp >= 0xD800 && $cp <= 0xDFFF;
710 60 100       82 return 0 if $cp == 0;
711 58 100 66     215 return -1 if $cp < 0x20 || ($cp >= 0x7F && $cp < 0xA0);
      66        
712 54 100 66     184 return 1 if $cp <= 0x7E || ($cp >= 0xA0 && $cp <= 0xFF);
      100        
713            
714 21         32 state $cat = [ Unicode::UCD::prop_invmap('General_Category') ];
715 21         271632 state $eaw = [ Unicode::UCD::prop_invmap('East_Asian_Width') ];
716 21         16285 state $dig = [ Unicode::UCD::prop_invmap('Default_Ignorable_Code_Point') ];
717            
718 21         2155 my $i = Unicode::UCD::search_invlist($cat->[0], $cp);
719 21 50       1236 if (defined $i) {
720 21   50     43 my $v = $cat->[1]->[$i] // '';
721 21 50 33     85 return -1
      33        
722             if $v eq 'Cc' || $v eq 'Cs' || $v eq 'Cn';
723 21 50 100     102 return 0
      100        
      66        
      66        
724             if $v eq 'Mn' || $v eq 'Me' || $v eq 'Cf' || $v eq 'Zl' || $v eq 'Zp';
725             }
726            
727 17         26 $i = Unicode::UCD::search_invlist($dig->[0], $cp);
728 17 50       463 if (defined $i) {
729 17   50     27 my $v = $dig->[1]->[$i] // '';
730 17 50 33     105 return 0
      33        
      33        
      33        
731             if $v eq 'Y' || $v eq 'Yes' || $v eq 'T' || $v eq 'True' || $v eq '1';
732             }
733            
734 17         27 $i = Unicode::UCD::search_invlist($eaw->[0], $cp);
735 17 50       769 if (defined $i) {
736 17   50     29 my $v = $eaw->[1]->[$i] // '';
737 17 100 66     48 return 2 if $v eq 'W' || $v eq 'F';
738 5 50       11 return $is_cjk ? 2 : 1 if $v eq 'A';
    100          
739             }
740            
741 4         8 return 1;
742             }
743            
744             # Use NCCS (if supported); otherwise, read at least 32 fields.
745 37 50   37   1221887 BEGIN { if (eval { &POSIX::NCCS }) {
  37         211  
746 37         27938 *NCCS = \&POSIX::NCCS;
747             } else {
748 0         0 *NCCS = sub () { 32 };
749             }}
750            
751             sub cfmakeraw { # void (\$tios)
752 0     0 0 0 my ($tios) = @_;
753 0 0 0     0 return unless blessed($tios) && $tios->isa('POSIX::Termios');
754 0         0 $tios->setiflag(
755             $tios->getiflag()
756             & ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON)
757             );
758 0         0 $tios->setoflag(
759             $tios->getoflag()
760             & ~OPOST
761             );
762 0         0 $tios->setlflag(
763             $tios->getlflag()
764             & ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN)
765             );
766 0         0 $tios->setcflag(
767             ($tios->getcflag() & ~(CSIZE | PARENB)) | CS8
768             );
769 0         0 return;
770             }
771            
772             my $clone = sub { # $clone ()
773             my ($src) = @_;
774             return undef unless blessed($src) && $src->isa('POSIX::Termios');
775             my $dst = POSIX::Termios->new();
776             $dst->setiflag($src->getiflag());
777             $dst->setoflag($src->getoflag());
778             $dst->setcflag($src->getcflag());
779             $dst->setlflag($src->getlflag());
780             $dst->setispeed($src->getispeed());
781             $dst->setospeed($src->getospeed());
782             for my $i (0 .. NCCS - 1) {
783             $dst->setcc($i, $src->getcc($i));
784             }
785             return $dst;
786             };
787            
788             # ------------------------------------------------------------------------
789             # Params Check -----------------------------------------------------------
790             # ------------------------------------------------------------------------
791            
792             # This sub is a wrapper around Params::Check::check that provides positional
793             # argument checking. It returns a coderef that can be used to check the
794             # arguments of a function. The coderef will return the original arguments if
795             # they pass the checks, or will die with an error message if they fail.
796             # See Type::Params::compile for the inspiration for this function.
797             sub compile {
798 0     0 0 0 return sub { @_ } unless STRICT;
  261     261   399  
799            
800 261 100       658 unless (@_) {
801             return sub {
802 56 50   56   183 Carp::croak "Expected no arguments" if @_;
803 56         73 return @_;
804 39         190 };
805             }
806            
807 222         314 my %tmpl;
808 222         928 @tmpl{1 .. @_} = @_;
809            
810             return sub {
811 31702     31702   27251 my %in;
812 31702         51466 @in{1 .. @_} = @_;
813            
814 31702         32187 local $Params::Check::STRIP_LEADING_DASHES = 0;
815 31702         27784 local $Params::Check::PRESERVE_CASE = 1;
816 31702         28337 local $Params::Check::CALLER_DEPTH
817             = $Params::Check::CALLER_DEPTH + 1;
818            
819 31702 100       40281 Params::Check::check(\%tmpl, \%in, 0) or do {
820 16         1762 local $_ = Params::Check::last_error();
821 16         215 s/\bKey '(\d+)'/Argument #$1/g;
822 16         1944 Carp::croak $_;
823             };
824            
825 31686         1952309 return @_;
826 222         1133 };
827             }
828            
829             # Types::Standard like templates for Params::Check.
830             use constant {
831             _Bool => { required => 1, allow => qr/\A[01]?\z/ },
832             _Str => {
833             required => 1,
834             defined => 1, default => '', strict_type => 1
835             },
836             _Int => { required => 1, defined => 1, allow => qr/\A[-]?\d+\z/ },
837             _ClassName => { required => 1, defined => 1, allow => qr/\A\w+(::\w+)*\z/ },
838             _Ref => {
839             required => 1,
840 0         0 allow => ($] >= 5.016) ? \&CORE::ref : sub { ref($_[0]) }
841             },
842             _ScalarRef => {
843             required => 1, defined => 1, default => \undef,
844             strict_type => 1
845             },
846             _ArrayRef => { required => 1, defined => 1, default => [], strict_type => 1 },
847             _CodeRef => {
848             required => 1, defined => 1, default => sub { },
849 37 50       14137 strict_type => 1
850             },
851             _Object => { required => 1, allow => \&blessed },
852 37     37   345 };
  37         59  
853            
854             sub _Maybe ($) {
855 7     7   14 my ($aref) = @_;
856            
857 7 50 33     64 Carp::croak("_Maybe['a] expects exactly one template hashref")
      33        
858             unless ref($aref) eq 'ARRAY' && @$aref == 1 && ref($aref->[0]) eq 'HASH';
859            
860 7         14 my ($t) = @$aref;
861 7         20 my %u = %$t;
862            
863 7         16 my $allow = delete $u{allow};
864 7         23 my $strict_type = delete $u{strict_type};
865 7         11 my $default = $u{default};
866            
867 7         10 $u{defined} = 0;
868             $u{allow} = sub {
869 35     35   3052 my ($v) = @_;
870 35 100       71 return 1 unless defined $v;
871 18 50 33     58 return 0 if $strict_type && ref($v) ne ref($default);
872 18 50       37 return 1 unless defined $allow;
873 0 0       0 return Params::Check::allow(
874             $v,
875             ref($allow) eq 'ARRAY' ? $allow : [ $allow ],
876             );
877 7         30 };
878 7         27 return \%u;
879             }
880            
881             # Types::Common::Numeric like templates for Params::Check.
882             use constant {
883 37         7855 _PositiveInt => { required => 1, defined => 1, allow => qr/\A[1-9]\d*\z/ },
884             _PositiveOrZeroInt => { required => 1, defined => 1, allow => qr/\A\d+\z/ },
885 37     37   638 };
  37         206  
886            
887             # ------------------------------------------------------------------------
888             # Helper classes ---------------------------------------------------------
889             # ------------------------------------------------------------------------
890            
891             #
892             # The 'Termbox::Cell' class represents a single cell in the terminal.
893             #
894            
895             sub Termbox::Cell::new { # $cell ()
896 11610     11610   369457 state $sig = compile(
897             _ClassName,
898             );
899 11610         11901 my ($class) = $sig->(@_);
900 11610         23407 return bless {
901             ch => '', # UFT8 string
902             fg => 0, # bitwise foreground attributes
903             bg => 0, # bitwise background attributes
904             }, $class;
905             }
906            
907 1     1   2868 sub Termbox::Cell::ch { ord $_[0]->{ch} }
908 1     1   5 sub Termbox::Cell::fg { 0+ $_[0]->{fg} }
909 1     1   4 sub Termbox::Cell::bg { 0+ $_[0]->{bg} }
910             if (TB_OPT_EGC) {
911 37     37   291 no warnings 'once';
  37         87  
  37         210169  
912             *Termbox::Cell::ech = sub {
913 2     2   6 my $ch = $_[0]->{ch};
914 2 50       16 length($ch) > 1 ? [ unpack('U*', $ch) ] : undef;
915             };
916             *Termbox::Cell::nech = sub {
917 1     1   1366 my $ch = $_[0]->{ch};
918 1         4 my $n = length($ch);
919 1 50 33     14 return ($ch =~ /\A\X\z/ && $n > 1) ? $n : 0;
920             };
921             *Termbox::Cell::cech = sub {
922 1     1   3 my $ech = $_[0]->ech;
923 1 50       6 $ech ? scalar(@$ech) : 0;
924             };
925             }
926            
927             sub Termbox::Cell::set { # $int ($ch, $fg, $bg)
928 11584     11584   9870 state $sig = compile(
929             _Object,
930             _Str,
931             _PositiveOrZeroInt,
932             _PositiveOrZeroInt,
933             );
934 11584         11863 my ($cell, $ch, $fg, $bg) = $sig->(@_);
935            
936 11584 100       15310 $ch = "\0" unless length($ch);
937 11584         13331 $cell->{ch} = TB_OPT_EGC ? $ch : substr($ch, 0, 1);
938 11584         10691 $cell->{fg} = $fg;
939 11584         10156 $cell->{bg} = $bg;
940 11584         15836 return TB_OK;
941             }
942            
943             sub Termbox::Cell::equal { # $bool ($other)
944 6     6   19 state $sig = compile(
945             _Object,
946             _Object,
947             );
948 6         10 my ($a, $b) = $sig->(@_);
949             return 0
950             if !(ref $a && ref $b)
951             || $a->{ch} ne $b->{ch}
952             || $a->{fg} != $b->{fg}
953 6 100 33     67 || $a->{bg} != $b->{bg};
      66        
      100        
      100        
954 3         14 return 1;
955             }
956            
957             sub Termbox::Cell::copy { # $int ($src)
958 21     21   50 state $sig = compile(
959             _Object,
960             _Object,
961             );
962 21         43 my ($dst, $src) = $sig->(@_);
963 21         85 %$dst = %$src;
964 21         111 return TB_OK;
965             }
966            
967             #
968             # The 'Termbox::Event' class represents an input event from the terminal.
969             #
970            
971             sub Termbox::Event::new { # $event ()
972 17     17   528801 state $sig = compile(
973             _ClassName,
974             );
975 17         65 my ($class) = $sig->(@_);
976 17         138 return bless {
977             type => 0, # one of 'TB_EVENT_*' constants
978             mod => 0, # bitwise 'TB_MOD_*' constants
979             key => 0, # one of 'TB_KEY_*' constants
980             ch => 0, # a Unicode codepoint
981             w => 0, # resize height
982             h => 0, # resize height
983             x => 0, # mouse x
984             y => 0, # mouse y
985             }, $class;
986             };
987            
988 1     1   14 sub Termbox::Event::type { $_[0]->{type} }
989 1     1   4 sub Termbox::Event::mod { $_[0]->{mod} }
990 1     1   4 sub Termbox::Event::key { $_[0]->{key} }
991 1     1   4 sub Termbox::Event::ch { $_[0]->{ch} }
992 1     1   4 sub Termbox::Event::w { $_[0]->{w} }
993 1     1   4 sub Termbox::Event::h { $_[0]->{h} }
994 1     1   4 sub Termbox::Event::x { $_[0]->{x} }
995 1     1   3 sub Termbox::Event::y { $_[0]->{y} }
996            
997             #
998             # The 'cellbuf' class represents the cell buffer of the terminal,
999             # which holds the state of each cell on the screen.
1000             #
1001            
1002             sub cellbuf::new { # $cellbuf ()
1003 106     106   542149 state $sig = compile(
1004             _ClassName,
1005             );
1006 106         199 my ($class) = $sig->(@_);
1007 106         1062 return bless {
1008             width => 0,
1009             height => 0,
1010             cells => [],
1011             }, $class;
1012             };
1013            
1014             sub cellbuf::init { # $int ($width, $height)
1015 21     21   6395 state $sig = compile(
1016             _Object,
1017             _PositiveInt,
1018             _PositiveInt,
1019             );
1020 21         45 my ($c, $w, $h) = $sig->(@_);
1021 21         53 $c->{width} = $w;
1022 21         32 $c->{height} = $h;
1023 21         508 $c->{cells} = [ map { Termbox::Cell->new() } 0 .. $w*$h-1 ];
  11585         13533  
1024 21         471 return TB_OK;
1025             }
1026            
1027             sub cellbuf::clear { # $int ()
1028 14     14   4102 state $sig = compile(
1029             _Object,
1030             );
1031 14         37 my ($c) = $sig->(@_);
1032 14         42 my $rv;
1033 14         70 for my $i (0 .. $c->{width}*$c->{height}-1) {
1034 11557 50       17880 return $rv if $rv = $c->{cells}[$i]->set(' ', $global->{fg}, $global->{bg});
1035             }
1036 14         119 return TB_OK;
1037             }
1038            
1039             sub cellbuf::get { # $cell ($x, $y)
1040 32     32   95 state $sig = compile(
1041             _Object,
1042             _PositiveOrZeroInt,
1043             _PositiveOrZeroInt,
1044             );
1045 32         58 my ($c, $x, $y) = $sig->(@_);
1046 32         118 return $c->{cells}[ $x + $y * $c->{width} ];
1047             }
1048            
1049             sub cellbuf::in_bounds { # $bool ($x, $y)
1050 46     46   86 state $sig = compile(
1051             _Object,
1052             _PositiveOrZeroInt,
1053             _PositiveOrZeroInt,
1054             );
1055 46         81 my ($c, $x, $y) = $sig->(@_);
1056 46 50 66     356 if ($x < 0 || $x >= $c->{width} || $y < 0 || $y >= $c->{height}) {
      66        
      66        
1057 3         16 return 0;
1058             }
1059 43         96 return 1;
1060             }
1061            
1062             sub cellbuf::resize { # $int ($width, $height)
1063 5     5   22487 state $sig = compile(
1064             _Object,
1065             _PositiveInt,
1066             _PositiveInt,
1067             );
1068 5         17 my ($c, $w, $h) = $sig->(@_);
1069 5         10 my $rv;
1070            
1071 5         9 my $ow = $c->{width};
1072 5         8 my $oh = $c->{height};
1073            
1074 5 50 33     19 if ($ow == $w && $oh == $h) {
1075 0         0 return TB_OK;
1076             }
1077            
1078 5 50       56 $w = $w < 1 ? 1 : $w;
1079 5 50       10 $h = $h < 1 ? 1 : $h;
1080            
1081 5 100       11 my $minw = ($w < $ow) ? $w : $ow;
1082 5 100       10 my $minh = ($h < $oh) ? $h : $oh;
1083            
1084 5         8 my $prev = $c->{cells};
1085            
1086 5         16 $rv = $c->init($w, $h);
1087 5 50       14 return $rv if $rv != TB_OK;
1088 5         15 $rv = $c->clear();
1089 5 50       16 return $rv if $rv != TB_OK;
1090            
1091 5         9 my ($x, $y);
1092 5         20 for ($x = 0; $x < $minw; $x++) {
1093 8         18 for ($y = 0; $y < $minh; $y++) {
1094 19         29 my ($src, $dst);
1095 19         37 $src = $prev->[($y * $ow) + $x];
1096 19 50       64 return TB_ERR_OUT_OF_BOUNDS unless $c->in_bounds($x, $y);
1097 19         53 $dst = $c->get($x, $y);
1098 19 50       40 return TB_ERR unless defined $dst;
1099 19         50 $rv = $dst->copy($src);
1100 19 50       70 return $rv if $rv != TB_OK;
1101             }
1102             }
1103            
1104 5         40 return TB_OK;
1105             }
1106            
1107             #
1108             # captrie class for terminal capability matching
1109             #
1110            
1111             sub captrie::new { # $captrie ()
1112 51     51   189582 state $sig = compile(
1113             _ClassName,
1114             );
1115 51         136 my ($class) = $sig->(@_);
1116 51         392 return bless {
1117             exact => {},
1118             prefixes => {},
1119             alt => undef,
1120             }, $class;
1121             }
1122            
1123             sub captrie::clear { # $int ()
1124 5     5   11 state $sig = compile(
1125             _Object,
1126             );
1127 5         15 my ($self) = $sig->(@_);
1128 5         34 $self->{exact} = {};
1129 5         10 $self->{prefixes} = {};
1130 5         10 $self->{alt} = undef;
1131 5         7 return TB_OK;
1132             }
1133            
1134             sub captrie::rebuild_alt { # $int ()
1135 3     3   4 state $sig = compile(
1136             _Object,
1137             );
1138 3         5 my ($self) = $sig->(@_);
1139             my @ordered = sort {
1140 0 0       0 length($b) <=> length($a)
1141             || $a cmp $b
1142 3         6 } keys %{ $self->{exact} };
  3         10  
1143 3 50       10 $self->{alt} = @ordered ? join('|', map { quotemeta($_) } @ordered) : undef;
  3         11  
1144 3         14 return TB_OK;
1145             }
1146            
1147             sub captrie::has_prefix { # $bool ($s)
1148 3     3   5 state $sig = compile(
1149             _Object,
1150             _Str,
1151             );
1152 3         6 my ($self, $s) = $sig->(@_);
1153 3 50       10 return 0 unless length($s);
1154 3 100       17 return exists $self->{prefixes}{$s} ? 1 : 0;
1155             }
1156            
1157             sub captrie::best_match { # $s|undef ($buf)
1158 3     3   5 state $sig = compile(
1159             _Object,
1160             _Str,
1161             );
1162 3         10 my ($self, $buf) = $sig->(@_);
1163 3 50       8 return undef unless length($buf);
1164 3         4 my $alt = $self->{alt};
1165 3 50       7 return undef unless defined $alt;
1166 3 100       76 return $1 if $buf =~ /\A($alt)/s;
1167 1         5 return undef;
1168             }
1169            
1170             sub captrie::add { # $int ($cap, $key, $mod)
1171 3     3   29 state $sig = compile(
1172             _Object,
1173             _Str,
1174             _PositiveOrZeroInt,
1175             _PositiveOrZeroInt,
1176             );
1177 3         8 my ($self, $cap, $key, $mod) = $sig->(@_);
1178             # Nothing to do for empty caps
1179 3 50       7 return TB_OK unless length($cap);
1180 3 50       13 return TB_ERR_CAP_COLLISION if exists $self->{exact}{$cap};
1181            
1182 3         13 $self->{exact}{$cap} = {
1183             key => 0 + $key,
1184             mod => 0 + $mod,
1185             };
1186            
1187 3         9 for my $i (1 .. length($cap) - 1) {
1188 8         13 my $p = substr($cap, 0, $i);
1189 8         14 $self->{prefixes}{$p} = 1;
1190             }
1191            
1192 3         10 return $self->rebuild_alt();
1193             }
1194            
1195             sub captrie::find { # $int ($buf, \$last, \$depth)
1196 3     3   4 state $sig = compile(
1197             _Object,
1198             _Str,
1199             _Ref,
1200             _Ref,
1201             );
1202 3         6 my ($self, $buf, $last, $depth) = $sig->(@_);
1203             my $node = {
1204             is_leaf => 0,
1205             key => 0,
1206             mod => 0,
1207 3 50       7 nchildren => scalar(keys %{ $self->{exact} }) ? 1 : 0,
  3         15  
1208             cap => '',
1209             };
1210            
1211 3 100       8 if (defined(my $m = $self->best_match($buf))) {
    50          
1212 2         18 my $leaf = $self->{exact}{$m};
1213             $node = {
1214             is_leaf => 1,
1215             key => $leaf->{key},
1216             mod => $leaf->{mod},
1217             # Keep C-shape node contract; derive child-ness from prefix table.
1218 2 50       9 nchildren => ($self->has_prefix($m) ? 1 : 0),
1219             cap => $m,
1220             };
1221 2         5 $$depth = length($m);
1222             }
1223             elsif ($self->has_prefix($buf)) {
1224 1         2 $node->{nchildren} = 1;
1225 1         3 $node->{cap} = $buf;
1226 1         2 $$depth = length($buf);
1227             }
1228             else {
1229 0         0 $$depth = 0;
1230             }
1231            
1232 3         5 $$last = $node;
1233 3         6 return TB_OK;
1234             }
1235            
1236             # ------------------------------------------------------------------------
1237             # Global state and initialization ----------------------------------------
1238             # ------------------------------------------------------------------------
1239            
1240             #
1241             # The 'tb_global' struct holds the global state of the terminal, including
1242             # filehandles, terminal state, colors, modes, parser state, buffers,
1243             #
1244            
1245             $global = {
1246             # Filehandles
1247             ttyfd => -1,
1248             rfd => -1,
1249             wfd => -1,
1250             ttyfd_open => -1,
1251             resize_pipefd => [-1, -1],
1252            
1253             # Terminal state
1254             width => -1,
1255             height => -1,
1256             cursor_x => -1,
1257             cursor_y => -1,
1258             last_x => -1,
1259             last_y => -1,
1260            
1261             # Colors
1262             fg => TB_DEFAULT,
1263             bg => TB_DEFAULT,
1264             last_fg => ~TB_DEFAULT,
1265             last_bg => ~TB_DEFAULT,
1266            
1267             # Modes
1268             input_mode => TB_INPUT_ESC,
1269             output_mode => TB_OUTPUT_NORMAL,
1270            
1271             # Terminfo
1272             terminfo => '',
1273             # ->{nterminfo} is not needed
1274            
1275             # Parser
1276             caps => [ (undef) x TB_CAP__COUNT ],
1277             cap_trie => captrie->new(),
1278            
1279             # Buffers
1280             inbuf => '',
1281             outbuf => '',
1282             back => cellbuf->new(),
1283             front => cellbuf->new(),
1284            
1285             # Termios
1286             orig_tios => undef,
1287             has_orig_tios => 0,
1288            
1289             # (Error) state
1290             last_errno => 0,
1291             errbuf => "",
1292             initialized => 0,
1293             # ->{errbuf} is not needed
1294            
1295             # Custom callbacks for escape sequence parsing
1296             fn_extract_esc_pre => undef,
1297             fn_extract_esc_post => undef,
1298             };
1299            
1300             # ------------------------------------------------------------------------
1301             # Implementation of the termbox2 API incl. helpers ----------------------
1302             # ------------------------------------------------------------------------
1303            
1304             #
1305             # Forwarding declarations of the public termbox API.
1306             #
1307            
1308             # Initialize the termbox library
1309             sub tb_init;
1310             sub tb_init_file;
1311             sub tb_init_fd;
1312             sub tb_init_rwfd;
1313             sub tb_shutdown;
1314            
1315             # Return the size of the internal back buffer
1316             sub tb_width;
1317             sub tb_height;
1318            
1319             # Clear the internal back buffer
1320             sub tb_clear;
1321             sub tb_set_clear_attrs;
1322            
1323             # Synchronize the internal back buffer with the terminal by writing to tty
1324             sub tb_present;
1325            
1326             # Clear the internal front buffer and force a complete re-render
1327             sub tb_invalidate;
1328            
1329             # Set the position of the cursor
1330             sub tb_set_cursor;
1331             sub tb_hide_cursor;
1332            
1333             # Set cell contents in the internal back buffer at the specified position
1334             sub tb_set_cell;
1335             sub tb_set_cell_ex;
1336             sub tb_extend_cell;
1337            
1338             # Return a reference to the cell at the specified position
1339             sub tb_get_cell;
1340            
1341             # Set the input and output mode
1342             sub tb_set_input_mode;
1343             sub tb_set_output_mode;
1344            
1345             # Wait for an event
1346             sub tb_peek_event;
1347             sub tb_poll_event;
1348            
1349             # Internal termbox fds that can be used with 'poll', 'select'
1350             sub tb_get_fds;
1351            
1352             # Print and printf functions
1353             sub tb_print;
1354             sub tb_printf;
1355             sub tb_print_ex;
1356             sub tb_printf_ex;
1357            
1358             # Send raw bytes to terminal
1359             sub tb_send;
1360             sub tb_sendf;
1361            
1362             # Set custom callbacks for escape sequence parsing
1363             sub tb_set_func; # Deprecated
1364            
1365             # UTF-8 utility functions
1366             sub tb_utf8_char_length;
1367             sub tb_utf8_char_to_unicode;
1368             sub tb_utf8_unicode_to_char;
1369            
1370             # Library utility functions
1371             sub tb_last_errno;
1372             sub tb_strerror;
1373             sub tb_cell_buffer; # Deprecated
1374             sub tb_has_truecolor;
1375             sub tb_has_egc;
1376             sub tb_attr_width;
1377             sub tb_version;
1378             sub tb_iswprint;
1379             sub tb_wcwidth;
1380            
1381             #
1382             # Forwarding declarations of internal helper functions.
1383             #
1384            
1385             # Process management helpers
1386             sub tb_reset;
1387             sub tb_printf_inner;
1388             sub tb_deinit;
1389             sub tb_iswprint_ex;
1390             sub tb_cluster_width;
1391            
1392             # Terminal initialization helpers
1393             sub init_term_attrs;
1394             sub init_term_caps;
1395            
1396             # Terminfo parsing helpers
1397             sub load_terminfo;
1398             sub load_terminfo_from_path;
1399             sub read_terminfo_path;
1400             sub parse_terminfo_caps;
1401             sub load_builtin_caps;
1402             sub get_terminfo_string;
1403             sub get_terminfo_int16;
1404            
1405             # Resize handling helpers
1406             sub init_resize_handler;
1407             sub resize_cellbufs;
1408             sub handle_resize;
1409             sub update_term_size;
1410             sub update_term_size_via_esc;
1411            
1412             # Escape-cap parser helpers
1413             sub init_cap_trie;
1414             sub cap_trie_add;
1415             sub cap_trie_find;
1416             sub cap_trie_deinit;
1417            
1418             # Event extraction helpers
1419             sub wait_event;
1420             sub extract_event;
1421             sub extract_esc;
1422             sub extract_esc_user;
1423             sub extract_esc_cap;
1424             sub extract_esc_mouse;
1425            
1426             # Cell related helpers
1427             sub cell_cmp;
1428             sub cell_copy;
1429             sub cell_set;
1430             sub cell_reserve_ech;
1431             sub cell_free;
1432            
1433             # Cell buffer related helpers
1434             sub init_cellbuf;
1435             sub cellbuf_init;
1436             sub cellbuf_free;
1437             sub cellbuf_clear;
1438             sub cellbuf_get;
1439             sub cellbuf_in_bounds;
1440             sub cellbuf_resize;
1441            
1442             # Sending output helpers
1443             sub send_literal;
1444             sub send_num;
1445             sub send_init_escape_codes;
1446             sub send_clear;
1447             sub send_attr;
1448             sub send_sgr;
1449             sub send_cursor_if;
1450             sub send_char;
1451             sub send_cluster;
1452             sub convert_num;
1453            
1454             # Byte buffer related helpers
1455             sub bytebuf_puts;
1456             sub bytebuf_nputs;
1457             sub bytebuf_shift;
1458             sub bytebuf_flush;
1459             sub bytebuf_reserve;
1460             sub bytebuf_free;
1461            
1462             # ------------------------------------------------------------------------
1463             # Public API implementation ----------------------------------------------
1464             # ------------------------------------------------------------------------
1465            
1466             #
1467             # Initialize the termbox library
1468             #
1469            
1470             sub tb_init { # $int ()
1471 0     0 0 0 state $sig = compile();
1472 0         0 $sig->(@_);
1473 0         0 return tb_init_file('/dev/tty');
1474             }
1475            
1476             sub tb_init_file { # $int ($path)
1477 1     1 0 3931 state $sig = compile(
1478             _Str,
1479             );
1480 1         3 my ($path) =$sig->(@_);
1481 1 50       6 return TB_ERR_INIT_ALREADY if $global->{initialized};
1482            
1483 0         0 my $ttyfd = POSIX::open($path, POSIX::O_RDWR);
1484 0 0 0     0 if (!defined $ttyfd || $ttyfd < 0) {
1485 0         0 $global->{last_errno} = 0+ $!;
1486 0         0 return TB_ERR_INIT_OPEN;
1487             }
1488            
1489 0         0 $global->{ttyfd_open} = 1;
1490 0         0 return tb_init_fd($ttyfd);
1491             }
1492            
1493             sub tb_init_fd { # $int ($ttyfd)
1494 1     1 0 3972 state $sig = compile(
1495             _PositiveOrZeroInt,
1496             );
1497 1         11 my ($ttyfd) = $sig->(@_);
1498 1         4 return tb_init_rwfd($ttyfd, $ttyfd);
1499             }
1500            
1501             sub tb_init_rwfd { # $int ($rfd, $wfd)
1502 2     2 0 150489 state $sig = compile(
1503             _PositiveOrZeroInt,
1504             _PositiveOrZeroInt,
1505             );
1506 2         4 my ($rfd, $wfd) = $sig->(@_);
1507            
1508 2         7 tb_reset();
1509             $global->{ttyfd} =
1510 2 0       6 POSIX::isatty($rfd) ? $rfd :
    50          
1511             POSIX::isatty($wfd) ? $wfd :
1512             -1;
1513 2         8 $global->{rfd} = $rfd;
1514 2         3 $global->{wfd} = $wfd;
1515            
1516 2         3 my $rv;
1517 2         4 for (1) {
1518 2 50       4 $rv = init_term_attrs(); last if $rv != TB_OK;
  2         7  
1519 2 50       2 $rv = init_term_caps(); last if $rv != TB_OK;
  2         6  
1520 2 50       4 $rv = init_cap_trie(); last if $rv != TB_OK;
  2         6  
1521 2 50       5 $rv = init_resize_handler(); last if $rv != TB_OK;
  2         5  
1522 2 50       5 $rv = send_init_escape_codes(); last if $rv != TB_OK;
  2         5  
1523 2 50       14 $rv = send_clear(); last if $rv != TB_OK;
  2         6  
1524 2 50       2 $rv = update_term_size(); last if $rv != TB_OK;
  2         7  
1525 2 50       3 $rv = init_cellbuf(); last if $rv != TB_OK;
  2         5  
1526            
1527 2         4 $global->{initialized} = 1;
1528             };
1529            
1530 2 50       4 tb_deinit() if $rv != TB_OK;
1531            
1532 2         7 return $rv;
1533             }
1534            
1535             sub tb_shutdown { # $int ()
1536 2     2 0 6755 state $sig = compile();
1537 2         5 $sig->(@_);
1538 2 100       9 return TB_ERR_NOT_INIT unless $global->{initialized};
1539 1         4 tb_deinit();
1540 1         5 return TB_OK;
1541             }
1542            
1543             #
1544             # Return the size of the internal back buffer
1545             #
1546            
1547             sub tb_width { # $int ()
1548 1     1 0 139313 state $sig = compile();
1549 1         4 $sig->(@_);
1550 1 50       3 return TB_ERR_NOT_INIT unless $global->{initialized};
1551 1         2 return $global->{width};
1552             }
1553            
1554             sub tb_height { # $int ()
1555 1     1 0 356 state $sig = compile();
1556 1         2 $sig->(@_);
1557 1 50       3 return TB_ERR_NOT_INIT unless $global->{initialized};
1558 1         3 return $global->{height};
1559             }
1560            
1561             #
1562             # Clear the internal back buffer
1563             #
1564            
1565             sub tb_clear { # $int ()
1566 2     2 0 146035 state $sig = compile();
1567 2         5 $sig->(@_);
1568 2 100       7 return TB_ERR_NOT_INIT unless $global->{initialized};
1569 1         5 return cellbuf_clear($global->{back});
1570             }
1571            
1572             sub tb_set_clear_attrs { # $int ($fg, $bg)
1573 2     2 0 3192 state $sig = compile(
1574             _PositiveOrZeroInt,
1575             _PositiveOrZeroInt,
1576             );
1577 2         5 my ($fg, $bg) = $sig->(@_);
1578 2 100       9 return TB_ERR_NOT_INIT unless $global->{initialized};
1579 1         4 $global->{fg} = $fg;
1580 1         3 $global->{bg} = $bg;
1581 1         4 return TB_OK;
1582             }
1583            
1584             #
1585             # Synchronize the internal back buffer with the terminal by writing to tty
1586             #
1587            
1588             sub tb_present { # $int ()
1589 1     1 0 5 state $sig = compile();
1590 1         2 $sig->(@_);
1591 1 50       4 return TB_ERR_NOT_INIT unless $global->{initialized};
1592            
1593 1         1 my $rv;
1594            
1595             # TODO: assert $global->{back}[width,height] == global->{front}[width,height]
1596            
1597 1         2 $global->{last_x} = -1;
1598 1         2 $global->{last_y} = -1;
1599            
1600 1         2 my ($x, $y, $i);
1601 1         4 for ($y = 0; $y < $global->{front}{height}; $y++) {
1602 1         10 for ($x = 0; $x < $global->{front}{width}; ) {
1603 1         1 my ($back, $front);
1604 1         4 $rv = cellbuf_get($global->{back}, $x, $y, \$back);
1605 1 50       9 return $rv if $rv != TB_OK;
1606 1         3 $rv = cellbuf_get($global->{front}, $x, $y, \$front);
1607 1 50       6 return $rv if $rv != TB_OK;
1608            
1609 1         1 my $w;
1610 1 50 33     12 if (TB_OPT_EGC and
1611             $back->{ch} =~ /\A\X\z/ && length($back->{ch}) > 1
1612             ) {
1613 1         11 my $ech = [ unpack 'U*', $back->{ch} ];
1614 1         4 $w = tb_cluster_width($ech, scalar @$ech);
1615             } else {
1616 0         0 $w = tb_wcwidth(ord($back->{ch}));
1617             }
1618 1 50       4 $w = 1 if $w < 1; # wcwidth returns -1 for invalid codepoints
1619            
1620 1 50       3 if (cell_cmp($back, $front) != 0) {
1621 1         5 %$front = %$back;
1622            
1623 1         4 send_attr($back->{fg}, $back->{bg});
1624 1 50 33     7 if ($w > 1 && $x >= $global->{front}{width} - ($w - 1)) {
1625             # Not enough room for wide char, send spaces
1626 0         0 for ($i = $x; $i < $global->{front}{width}; $i++) {
1627 0         0 send_char($i, $y, ord(' '));
1628             }
1629             } else {
1630 1 50 33     7 if (TB_OPT_EGC and
1631             $back->{ch} =~ /\A\X\z/ && length($back->{ch}) > 1
1632             ) {
1633 1         3 my $ech = [ unpack 'U*', $back->{ch} ];
1634 1         3 send_cluster($x, $y, $ech, scalar @$ech);
1635             } else {
1636 0         0 send_char($x, $y, ord($back->{ch}));
1637             }
1638            
1639             # When wcwidth>1, we need to advance the cursor by more
1640             # than 1, thereby skipping some cells. Set these skipped
1641             # cells to an invalid codepoint in the front buffer, so
1642             # that if this cell is later replaced by a wcwidth==1
1643             # char, we'll get a cell_cmp diff for the skipped cells
1644             # and properly re-render.
1645 1         5 for ($i = 1; $i < $w; $i++) {
1646 0         0 my $front_wide;
1647 0         0 $rv = cellbuf_get($global->{front}, $x + $i, $y, \$front_wide);
1648 0 0       0 return $rv if $rv != TB_OK;
1649 0         0 my $invalid = "\x{fffd}";
1650 0         0 $rv = $front_wide->set($invalid, -1, -1);
1651 0 0       0 return $rv if $rv != TB_OK;
1652             }
1653             }
1654             }
1655 1         5 $x += $w;
1656             }
1657             }
1658            
1659 1         3 $rv = send_cursor_if($global->{cursor_x}, $global->{cursor_y});
1660 1 50       4 return $rv if $rv != TB_OK;
1661 1         13 $rv = bytebuf_flush(\$global->{out}, $global->{wfd});
1662 1 50       5 return $rv if $rv != TB_OK;
1663            
1664 1         33 return TB_OK;
1665             }
1666            
1667             #
1668             # Clear the internal front buffer and force a complete re-render
1669             #
1670            
1671             sub tb_invalidate { # $int ()
1672 3     3 0 2750 state $sig = compile();
1673 3         27 $sig->(@_);
1674 3 100       9 return TB_ERR_NOT_INIT unless $global->{initialized};
1675 2         3 my $rv = resize_cellbufs();
1676 2 100       11 return $rv if $rv != TB_OK;
1677 1         3 return TB_OK;
1678             }
1679            
1680             #
1681             # Set the position of the cursor
1682             #
1683            
1684             sub tb_set_cursor { # $int ($cx, $cy)
1685 2     2 0 161717 state $sig = compile(
1686             _PositiveOrZeroInt,
1687             _PositiveOrZeroInt,
1688             );
1689 2         5 my ($cx, $cy) = $sig->(@_);
1690 2 100       11 return TB_ERR_NOT_INIT unless $global->{initialized};
1691            
1692 1 50       4 $cx = 0 if $cx < 0;
1693 1 50       30 $cy = 0 if $cy < 0;
1694            
1695 1         3 my $rv;
1696 1 50       3 if ($global->{cursor_x} == -1) {
1697 1         3 $rv = bytebuf_puts(\$global->{out}, $global->{caps}[TB_CAP_SHOW_CURSOR]);
1698 1 50       5 return $rv if $rv != TB_OK;
1699             }
1700 1         3 $rv = send_cursor_if($cx, $cy);
1701 1 50       4 return $rv if $rv != TB_OK;
1702            
1703 1         2 $global->{cursor_x} = $cx;
1704 1         2 $global->{cursor_y} = $cy;
1705            
1706 1         4 return TB_OK;
1707             }
1708            
1709             sub tb_hide_cursor { # $int ()
1710 1     1 0 3234 state $sig = compile();
1711 1         3 $sig->(@_);
1712 1 50       3 return TB_ERR_NOT_INIT unless $global->{initialized};
1713            
1714 1 50       3 if ($global->{cursor_x} >= 0) {
1715 1         3 my $rv = bytebuf_puts(\$global->{out}, $global->{caps}[TB_CAP_HIDE_CURSOR]);
1716 1 50       6 return $rv if $rv != TB_OK;
1717             }
1718            
1719 1         1 $global->{cursor_x} = -1;
1720 1         1 $global->{cursor_y} = -1;
1721            
1722 1         4 return TB_OK;
1723             }
1724            
1725             #
1726             # Set cell contents in the internal back buffer at the specified position
1727             #
1728            
1729             sub tb_set_cell { # $int ($x, $y, $ch, $fg, $bg)
1730 8     8 0 143814 state $sig = compile(
1731             _PositiveOrZeroInt,
1732             _PositiveOrZeroInt,
1733             _Str,
1734             _PositiveOrZeroInt,
1735             _PositiveOrZeroInt,
1736             );
1737 8         16 my ($x, $y, $ch, $fg, $bg) = $sig->(@_);
1738             # Note: tb_set_cell stores only the first Perl character
1739 8         30 return tb_set_cell_ex($x, $y, substr($ch, 0, 1), 1, $fg, $bg);
1740             }
1741            
1742             sub tb_set_cell_ex { # $int ($x, $y, $ch, $nch, $fg, $bg)
1743 12     12 0 2948 state $sig = compile(
1744             _PositiveOrZeroInt,
1745             _PositiveOrZeroInt,
1746             _Str,
1747             _PositiveOrZeroInt,
1748             _PositiveOrZeroInt,
1749             _PositiveOrZeroInt,
1750             );
1751 12         22 my ($x, $y, $ch, $nch, $fg, $bg) = $sig->(@_);
1752             # Note: ch is a Perl string, not an array of codepoints
1753             # Note: nch is accepted for API compatibility but ignored in the Perl port
1754 12 100       52 return TB_ERR_NOT_INIT unless $global->{initialized};
1755 9         11 my $rv;
1756             my $cell;
1757 9         27 $rv = cellbuf_get($global->{back}, $x, $y, \$cell);
1758 9 50       26 return $rv if $rv != TB_OK;
1759 9         24 $rv = $cell->set($ch, $fg, $bg);
1760 9 50       17 return $rv if $rv != TB_OK;
1761 9         18 return TB_OK;
1762             }
1763            
1764             sub tb_extend_cell { # $int ($x, $y, $ch)
1765 3     3 0 1719 state $sig = compile(
1766             _PositiveOrZeroInt,
1767             _PositiveOrZeroInt,
1768             _Str,
1769             );
1770 3         9 my ($x, $y, $ch) = $sig->(@_);
1771 3 100       12 return TB_ERR_NOT_INIT unless $global->{initialized};
1772 2         2 return TB_ERR unless TB_OPT_EGC;
1773             # TODO: iswprint ch?
1774 2         5 my $rv;
1775             my $cell;
1776 2         10 $rv = cellbuf_get($global->{back}, $x, $y, \$cell);
1777 2 50       6 return $rv if $rv != TB_OK;
1778             # Note: tb_extend_cell appends only the first Perl character
1779 2         13 $cell->{ch} .= substr($ch, 0, 1);
1780 2         5 return TB_OK;
1781             }
1782            
1783             #
1784             # Return a reference to the cell at the specified position
1785             #
1786            
1787             sub tb_get_cell { # $cell ($x, $y, $back, $cell)
1788 3     3 0 4133 state $sig = compile(
1789             _PositiveOrZeroInt,
1790             _PositiveOrZeroInt,
1791             _Bool,
1792             _Ref,
1793             );
1794 3         5 my ($x, $y, $back, $cell) = $sig->(@_);
1795 3 100       13 return TB_ERR_NOT_INIT unless $global->{initialized};
1796 2 50       9 return cellbuf_get($back ? $global->{back} : $global->{front}, $x, $y, $cell);
1797             }
1798            
1799             #
1800             # Set the input and output mode
1801             #
1802            
1803             sub tb_set_input_mode { # $int ($mode)
1804 2     2 0 139471 state $sig = compile(
1805             _PositiveOrZeroInt,
1806             );
1807 2         4 my ($mode) = $sig->(@_);
1808 2 50       7 return TB_ERR_NOT_INIT unless $global->{initialized};
1809            
1810 2 100       4 if ($mode == TB_INPUT_CURRENT) {
1811 1         5 return $global->{input_mode};
1812             }
1813            
1814 1         2 my $esc_or_alt = TB_INPUT_ESC | TB_INPUT_ALT;
1815 1 50       5 if (($mode & $esc_or_alt) == 0) {
    0          
1816             # neither specified; flip on ESC
1817 1         3 $mode |= TB_INPUT_ESC;
1818             } elsif (($mode & $esc_or_alt) == $esc_or_alt) {
1819             # both specified; flip off ALT
1820 0         0 $mode &= ~TB_INPUT_ALT;
1821             }
1822            
1823 1 50       3 if ($mode & TB_INPUT_MOUSE) {
1824 1         4 bytebuf_puts(\$global->{outbuf}, TB_HARDCAP_ENTER_MOUSE);
1825 1         3 bytebuf_flush(\$global->{outbuf}, $global->{wfd});
1826             } else {
1827 0         0 bytebuf_puts(\$global->{outbuf}, TB_HARDCAP_EXIT_MOUSE);
1828 0         0 bytebuf_flush(\$global->{outbuf}, $global->{wfd});
1829             }
1830            
1831 1         3 $global->{input_mode} = $mode;
1832 1         4 return TB_OK;
1833             }
1834            
1835             sub tb_set_output_mode { # $int ($mode)
1836 3     3 0 3387 state $sig = compile(
1837             _PositiveOrZeroInt,
1838             );
1839 3         7 my ($mode) = $sig->(@_);
1840 2 50       6 return TB_ERR_NOT_INIT unless $global->{initialized};
1841 2         4 switch: for ($mode) {
1842             case: TB_OUTPUT_CURRENT == $_ and
1843 2 100       9 return $global->{output_mode};
1844             case: TB_OUTPUT_NORMAL == $_ ||
1845             TB_OUTPUT_256 == $_ ||
1846             TB_OUTPUT_216 == $_ ||
1847             TB_OUTPUT_GRAYSCALE == $_ ||
1848             (TB_OPT_ATTR_W >= 32 && TB_OUTPUT_TRUECOLOR == $_) and do
1849 1 0 33     9 {
      33        
      33        
      0        
1850 1         3 $global->{last_fg} = ~$global->{fg};
1851 1         2 $global->{last_bg} = ~$global->{bg};
1852 1         2 $global->{output_mode} = $mode;
1853 1         4 return TB_OK;
1854             }
1855             }
1856 0         0 return TB_ERR;
1857             }
1858            
1859             #
1860             # Wait for an event
1861             #
1862            
1863             sub tb_peek_event { # $int ($event, $timeout_ms)
1864 2     2 0 10 state $sig = compile(
1865             _Object,
1866             _Int,
1867             );
1868 2         4 my ($event, $timeout_ms) = $sig->(@_);
1869 2 100       11 return TB_ERR_NOT_INIT unless $global->{initialized};
1870 1         3 return wait_event($event, $timeout_ms);
1871             }
1872            
1873             sub tb_poll_event { # $int ($event)
1874 2     2 0 9 state $sig = compile(
1875             _Object,
1876             );
1877 2         5 my ($event) = $sig->(@_);
1878 2 100       8 return TB_ERR_NOT_INIT unless $global->{initialized};
1879 1         2 return wait_event($event, -1);
1880             }
1881            
1882             #
1883             # Internal termbox fds that can be used with 'poll', 'select'
1884             #
1885            
1886             sub tb_get_fds { # $int (\$ttyfd, \$resizefd)
1887 2     2 0 162723 state $sig = compile(
1888             _ScalarRef,
1889             _ScalarRef,
1890             );
1891 2         6 my ($ttyfd, $resizefd) = $sig->(@_);
1892 2 100       10 return TB_ERR_NOT_INIT unless $global->{initialized};
1893            
1894 1         2 $$ttyfd = $global->{rfd};
1895 1         3 $$resizefd = $global->{resize_pipefd}[0];
1896            
1897 1         3 return TB_OK;
1898             }
1899            
1900             #
1901             # Print and printf functions
1902             #
1903            
1904             sub tb_print { # $int ($x, $y, $fg, $bg, $str)
1905 1     1 0 141591 state $sig = compile(
1906             _PositiveOrZeroInt,
1907             _PositiveOrZeroInt,
1908             _PositiveOrZeroInt,
1909             _PositiveOrZeroInt,
1910             _Str,
1911             );
1912 1         3 my ($x, $y, $fg, $bg, $str) = $sig->(@_);
1913 1         5 return tb_print_ex($x, $y, $fg, $bg, undef, $str);
1914             }
1915            
1916             sub tb_print_ex { # $int ($x, $y, $fg, $bg, \$out_w|undef, $str)
1917 11     11 0 2557 state $sig = compile(
1918             _PositiveOrZeroInt,
1919             _PositiveOrZeroInt,
1920             _PositiveOrZeroInt,
1921             _PositiveOrZeroInt,
1922             _Maybe[_ScalarRef],
1923             _Str,
1924             );
1925 11         23 my ($x, $y, $fg, $bg, $out_w, $str) = $sig->(@_);
1926            
1927 10 100       37 return TB_ERR_NOT_INIT unless $global->{initialized};
1928            
1929 3         5 my $back = $global->{back};
1930 3 50       13 return TB_ERR_OUT_OF_BOUNDS unless $back->in_bounds($x, $y);
1931            
1932 3 50       9 $$out_w = 0 if defined $out_w;
1933            
1934 3         5 my $rv;
1935             my $w;
1936 3         4 my $ix = $x;
1937 3         4 my $x_prev = $x;
1938 3         3 my $uni;
1939            
1940 3         9 while ($str) {
1941 7         22 $rv = tb_utf8_char_to_unicode(\$uni, $str);
1942 7 50       17 if ($rv < 0) {
    50          
1943 0         0 $uni = 0xfffd; # replace invalid UTF-8 char with U+FFFD
1944 0         0 bytes::substr($str, 0, -$rv, '');
1945             } elsif ($rv > 0) {
1946 7         16 bytes::substr($str, 0, $rv, '');
1947             } else {
1948 0         0 last; # shouldn't get here
1949             }
1950            
1951 7 50       34 if ($uni eq ord("\n")) { # TODO: \r, \t, \v, \f, etc?
1952 0         0 $x = $ix;
1953 0         0 $x_prev = $x;
1954 0         0 $y++;
1955 0         0 next;
1956             }
1957            
1958 7 50       17 if (!tb_iswprint_ex($uni, \$w)) {
1959 0         0 $uni = 0xfffd; # replace non-printable with U+FFFD
1960 0         0 $w = 1;
1961             }
1962            
1963 7 50       21 if ($w < 0) {
    100          
1964 0         0 return TB_ERR; # shouldn't happen if iswprint
1965             }
1966             elsif ($w == 0) { # combining character
1967 1 50       7 if ($back->in_bounds($x_prev, $y)) {
1968 1         7 $rv = tb_extend_cell($x_prev, $y, chr $uni);
1969 1 50       6 return $rv if $rv != TB_OK;
1970             }
1971             }
1972             else {
1973 6 50       12 if ($back->in_bounds($x, $y)) {
1974 6         51 $rv = tb_set_cell($x, $y, chr $uni, $fg, $bg);
1975 6 50       14 return $rv if $rv != TB_OK;
1976             }
1977 6         7 $x_prev = $x;
1978 6         7 $x += $w;
1979 6 50       14 $$out_w += $w if defined $out_w;
1980             }
1981             }
1982            
1983 3         10 return TB_OK;
1984             }
1985            
1986             sub tb_printf { # $int ($x, $y, $fg, $bg, $fmt, @args)
1987 1     1 0 5266 state $sig = compile(
1988             _PositiveOrZeroInt,
1989             _PositiveOrZeroInt,
1990             _PositiveOrZeroInt,
1991             _PositiveOrZeroInt,
1992             _Str,
1993             );
1994 1         4 my ($x, $y, $fg, $bg, $fmt, @args) = ($sig->(@_[0..4]), @_[5..$#_]);
1995 1         5 return tb_print_ex($x, $y, $fg, $bg, undef, $fmt, @args);
1996             }
1997            
1998             sub tb_printf_ex { # $int ($x, $y, $fg, $bg, \$out_w, $fmt, @args)
1999 2     2 0 1046 goto &tb_printf_inner;
2000             }
2001            
2002             #
2003             # Send raw bytes to terminal
2004             #
2005            
2006             sub tb_send { # $int ($buf, $nbuf)
2007 2     2 0 138384 state $sig = compile(
2008             _Str,
2009             _PositiveOrZeroInt,
2010             );
2011 2         6 my ($buf, $nbuf) = $sig->(@_);
2012 2         7 return bytebuf_nputs(\$global->{outbuf}, $buf, $nbuf);
2013             }
2014            
2015             sub tb_sendf { # $int ($fmt, @args)
2016 1     1 0 1287 state $sig = compile(
2017             _Str,
2018             );
2019 1         3 my ($fmt, @args) = ($sig->(shift), @_);
2020 1 50       6 my $buf = @args ? sprintf($fmt, @args) : $fmt;
2021 1         4 my $len = bytes::length($buf);
2022 1 50       7 return TB_ERR if $len >= TB_OPT_PRINTF_BUF;
2023 1         3 return tb_send($buf, $len);
2024             }
2025            
2026             #
2027             # Set custom callbacks for escape sequence parsing
2028             #
2029            
2030             sub tb_set_func {
2031 3     3 0 137260 state $sig = compile(
2032             _PositiveOrZeroInt,
2033             _CodeRef,
2034             );
2035 3         6 my ($fn_type, $fn) = $sig->(@_);
2036            
2037 2         4 state $warned = 0;
2038 2 100       14 warn "tb_set_func() is deprecated and may be removed in a future release\n"
2039             unless $warned++;
2040            
2041 2         8 switch: for ($fn_type) {
2042 2 100       5 case: TB_FUNC_EXTRACT_PRE == $_ and do {
2043 1         3 $global->{fn_extract_esc_pre} = $fn;
2044 1         5 return TB_OK;
2045             };
2046 1 50       3 case: TB_FUNC_EXTRACT_POST == $_ and do {
2047 1         2 $global->{fn_extract_esc_post} = $fn;
2048 1         5 return TB_OK;
2049             };
2050             }
2051            
2052 0         0 return TB_ERR;
2053             }
2054            
2055            
2056             #
2057             # UTF-8 utility functions
2058             #
2059            
2060             sub tb_utf8_char_length { # $length ($c)
2061 24     24 0 153486 state $sig = compile(
2062             _Str,
2063             );
2064 24         35 my ($c) = $sig->(@_);
2065 24 50       42 return 0 if $c eq '';
2066 24         74 $c = bytes::substr($c, 0, 1);
2067 24         93 state $utf8_length = {};
2068 24   66     81 $utf8_length->{$c} //= do {
2069 17         43 my $b = unpack('C', $c);
2070 17 50       89 ($b < 0xC0) ? 1
    50          
    100          
    100          
    100          
    100          
2071             : ($b < 0xE0) ? 2
2072             : ($b < 0xF0) ? 3
2073             : ($b < 0xF8) ? 4
2074             : ($b < 0xFC) ? 5
2075             : ($b < 0xFE) ? 6
2076             : 1;
2077             };
2078             }
2079            
2080             sub tb_utf8_char_to_unicode { # $length (\$out, $c)
2081 19     19 0 16037 state $sig = compile(
2082             _ScalarRef,
2083             _Str,
2084             );
2085 19         37 my ($out, $c) = $sig->(@_);
2086 37     37   304 use bytes;
  37         77  
  37         189  
2087            
2088 19 50       38 return 0 if $c eq '';
2089 19         35 my $b0 = substr($c, 0, 1);
2090 19 100       31 return 0 if $b0 eq "\0";
2091 18         38 my $len = tb_utf8_char_length($b0);
2092            
2093 18         45 my @c = unpack('C*', substr($c, 0, $len));
2094            
2095 18         22 state $utf8_mask = [0x7F, 0x1F, 0x0F, 0x07, 0x03, 0x01];
2096 18   50     52 my $mask = $utf8_mask->[$len - 1] // 0x7F;
2097            
2098 18         22 my $result = $c[0] & $mask;
2099            
2100 18         22 my $i;
2101 18   100     64 for ($i = 1; $i < @c && $c[$i] != 0; $i++) {
2102 14         15 $result <<= 6;
2103 14         31 $result |= $c[$i] & 0x3F;
2104             }
2105            
2106 18 100       37 return -$i if $i != $len;
2107            
2108 12         15 $$out = $result;
2109 12         21 return $len;
2110             }
2111            
2112             sub tb_utf8_unicode_to_char { # $length (\$out, $c)
2113 5     5 0 11238 state $sig = compile(
2114             _ScalarRef,
2115             _PositiveOrZeroInt,
2116             );
2117 5         13 my ($out, $c) = $sig->(@_);
2118            
2119             # Fast path for real Unicode scalar values (<= 0x10FFFF)
2120 5 100       12 if ($c <= 0x10FFFF) {
2121 4         8 my $s = chr($c);
2122 4         10 utf8::encode($s);
2123 4         4 $$out = $s;
2124 4         15 return bytes::length($s);
2125             }
2126            
2127             # Fallback for "extended UTF-8" (5/6-byte), C-compatible behavior
2128 1         2 my ($first, $len);
2129            
2130 1 50       8 if ($c < 0x80) { $first = 0x00; $len = 1; }
  0 50       0  
  0 50       0  
    50          
    0          
2131 0         0 elsif ($c < 0x800) { $first = 0xC0; $len = 2; }
  0         0  
2132 0         0 elsif ($c < 0x10000) { $first = 0xE0; $len = 3; }
  0         0  
2133 1         2 elsif ($c < 0x200000) { $first = 0xF0; $len = 4; }
  1         1  
2134 0         0 elsif ($c < 0x4000000) { $first = 0xF8; $len = 5; }
  0         0  
2135 0         0 else { $first = 0xFC; $len = 6; }
  0         0  
2136            
2137 1         3 my @out = (0) x $len;
2138 1         4 for (my $i = $len - 1; $i > 0; --$i) {
2139 3         6 $out[$i] = ($c & 0x3F) | 0x80;
2140 3         4 $c >>= 6;
2141             }
2142 1         2 $out[0] = $c | $first;
2143 1         5 $$out = pack('C*', @out);
2144            
2145 1         3 return $len;
2146             }
2147            
2148             #
2149             # Library utility functions
2150             #
2151            
2152             sub tb_last_errno { # $int ()
2153 2     2 0 2738 state $sig = compile();
2154 2         7 $sig->(@_);
2155 2         9 return 0+ $global->{last_errno};
2156             }
2157            
2158             sub tb_strerror { # $str ($err)
2159 4     4 0 292 state $sig = compile(
2160             _Int,
2161             );
2162 4         9 my ($err) = $sig->(@_);
2163 4         8 switch: for (int($err)) {
2164 4 100       10 case: TB_OK == $_ and do {
2165 1         5 return "Success"
2166             };
2167 3 100       6 case: TB_ERR_NEED_MORE == $_ and do {
2168 1         4 return "Not enough input"
2169             };
2170 2 50       3 case: TB_ERR_INIT_ALREADY == $_ and do {
2171 0         0 return "Termbox initialized already"
2172             };
2173 2 50       5 case: TB_ERR_MEM == $_ and do {
2174 0         0 return "Out of memory"
2175             };
2176 2 50       5 case: TB_ERR_NO_EVENT == $_ and do {
2177 0         0 return "No event"
2178             };
2179 2 50       3 case: TB_ERR_NO_TERM == $_ and do {
2180 0         0 return "No TERM in environment"
2181             };
2182 2 50       4 case: TB_ERR_NOT_INIT == $_ and do {
2183 0         0 return "Termbox not initialized"
2184             };
2185 2 50       5 case: TB_ERR_OUT_OF_BOUNDS == $_ and do {
2186 0         0 return "Out of bounds"
2187             };
2188 2 50       4 case: TB_ERR_UNSUPPORTED_TERM == $_ and do {
2189 0         0 return "Unsupported terminal"
2190             };
2191 2 50       4 case: TB_ERR_CAP_COLLISION == $_ and do {
2192 0         0 return "Termcaps collision"
2193             };
2194 2 50       3 case: TB_ERR_RESIZE_SSCANF == $_ and do {
2195 0         0 return "Terminal width/height not received by sscanf() after resize"
2196             };
2197             case:
2198             TB_ERR == $_ ||
2199             TB_ERR_INIT_OPEN == $_ ||
2200             TB_ERR_READ == $_ ||
2201             TB_ERR_RESIZE_IOCTL == $_ ||
2202             TB_ERR_RESIZE_PIPE == $_ ||
2203             TB_ERR_RESIZE_SIGACTION == $_ ||
2204             TB_ERR_POLL == $_ ||
2205             TB_ERR_TCGETATTR == $_ ||
2206             TB_ERR_TCSETATTR == $_ ||
2207             TB_ERR_RESIZE_WRITE == $_ ||
2208             TB_ERR_RESIZE_POLL == $_ ||
2209             TB_ERR_RESIZE_READ == $_ and do
2210 2 50 66     24 {
      66        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
2211 1         2 $! = $global->{last_errno};
2212 1         4 return "$!";
2213             };
2214             default: {
2215 1         2 $! = $global->{last_errno};
  1         2  
2216 1         3 return "$!";
2217             }
2218             }
2219             }
2220            
2221             sub tb_cell_buffer { # \@ ()
2222 4     4 0 5889 state $sig = compile();
2223 4         11 $sig->(@_);
2224            
2225 4         6 state $warned = 0;
2226 4 100       35 warn "tb_cell_buffer() is deprecated; ".
2227             "use tb_get_cell() and related APIs instead\n"
2228             unless $warned++;
2229            
2230 4         16 my $back = $global->{back};
2231 4 50 33     30 return [] unless ref($back) eq 'cellbuf' && ref($back->{cells}) eq 'ARRAY';
2232 4         15 return $back->{cells};
2233             }
2234            
2235             sub tb_has_truecolor { # $int ()
2236 1     1 0 3 state $sig = compile();
2237 1         3 $sig->(@_);
2238 1         2 return TB_OPT_ATTR_W >= 32 ? 1 : 0;
2239             }
2240            
2241             sub tb_has_egc { # $int ()
2242 1     1 0 315 state $sig = compile();
2243 1         3 $sig->(@_);
2244 1         2 return TB_OPT_EGC ? 1 : 0;
2245             }
2246            
2247             sub tb_attr_width { # $int ()
2248 1     1 0 301 state $sig = compile();
2249 1         3 $sig->(@_);
2250 1         2 return TB_OPT_ATTR_W;
2251             }
2252            
2253             sub tb_version { # $str ()
2254 1     1 0 138619 state $sig = compile();
2255 1         3 $sig->(@_);
2256 1         5 return TB_VERSION_STR;
2257             }
2258            
2259             sub tb_iswprint { # $int ($codepoint)
2260 12     12 0 146155 state $sig = compile(
2261             _PositiveOrZeroInt,
2262             );
2263 12         20 my ($codepoint) = $sig->(@_);
2264 12         29 return tb_iswprint_ex($codepoint, undef);
2265             }
2266            
2267             sub tb_wcwidth { # $int ($codepoint)
2268 8     8 0 9 state $sig = compile(
2269             _PositiveOrZeroInt,
2270             );
2271 8         15 my ($codepoint) = $sig->(@_);
2272 8         10 if (TB_OPT_LIBC_WCHAR) {
2273 8         16 return wcwidth($codepoint);
2274             } else {
2275             return Termbox::PP::WCWidth::wcwidth($codepoint);
2276             }
2277             }
2278            
2279             # ------------------------------------------------------------------------
2280             # Internal helper implementation -----------------------------------------
2281             # ------------------------------------------------------------------------
2282            
2283             #
2284             # Process Management Helpers
2285             #
2286            
2287             sub tb_reset { # $int ()
2288 11     11 0 211728 state $sig = compile();
2289 11         29 $sig->(@_);
2290            
2291             # Keep tty ownership flag, reset everything else like termbox2.
2292 11   50     36 my $ttyfd_open = $global->{ttyfd_open} // -1;
2293 11         118 $global = {
2294             ttyfd => -1,
2295             rfd => -1,
2296             wfd => -1,
2297             ttyfd_open => $ttyfd_open,
2298             resize_pipefd => [-1, -1],
2299            
2300             width => -1,
2301             height => -1,
2302             cursor_x => -1,
2303             cursor_y => -1,
2304             last_x => -1,
2305             last_y => -1,
2306            
2307             fg => TB_DEFAULT,
2308             bg => TB_DEFAULT,
2309             last_fg => ~TB_DEFAULT,
2310             last_bg => ~TB_DEFAULT,
2311            
2312             input_mode => TB_INPUT_ESC,
2313             output_mode => TB_OUTPUT_NORMAL,
2314            
2315             terminfo => '',
2316            
2317             caps => [ (undef) x TB_CAP__COUNT ],
2318             cap_trie => captrie->new(),
2319            
2320             inbuf => '',
2321             outbuf => '',
2322             back => cellbuf->new(),
2323             front => cellbuf->new(),
2324            
2325             orig_tios => undef,
2326             has_orig_tios => 0,
2327            
2328             last_errno => 0,
2329             errbuf => '',
2330             initialized => 0,
2331            
2332             fn_extract_esc_pre => undef,
2333             fn_extract_esc_post => undef,
2334             };
2335            
2336 11         56 return TB_OK;
2337             }
2338            
2339             sub tb_printf_inner { # $int ($x, $y, $fg, $bg, \$out_w, $fmt, @args)
2340 5     5 0 1973 state $sig = compile(
2341             _PositiveOrZeroInt,
2342             _PositiveOrZeroInt,
2343             _PositiveOrZeroInt,
2344             _PositiveOrZeroInt,
2345             _Maybe[_ScalarRef],
2346             _Str,
2347             );
2348 5         17 my ($x, $y, $fg, $bg, $out_w, $fmt, @args) = ($sig->(@_[0..5]), @_[6..$#_]);
2349            
2350 4 100       15 my $str = @args ? sprintf($fmt, @args) : $fmt;
2351 4 50 33     118 return TB_ERR
2352             if !defined($str)
2353             || length($str) >= TB_OPT_PRINTF_BUF;
2354            
2355 4         13 return tb_print_ex($x, $y, $fg, $bg, $out_w, $str);
2356             }
2357            
2358             sub tb_deinit { # $int ()
2359 5     5 0 6461 state $sig = compile();
2360 5         16 $sig->(@_);
2361            
2362 5 50 33     21 if (defined($global->{caps}[0]) && $global->{wfd} >= 0) {
2363 0         0 bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_SHOW_CURSOR]);
2364 0         0 bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_SGR0]);
2365 0         0 bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_CLEAR_SCREEN]);
2366 0         0 bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_EXIT_CA]);
2367 0         0 bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_EXIT_KEYPAD]);
2368 0         0 bytebuf_puts(\$global->{outbuf}, TB_HARDCAP_EXIT_MOUSE);
2369 0         0 bytebuf_flush(\$global->{outbuf}, $global->{wfd});
2370             }
2371            
2372 5 50       15 if ($global->{ttyfd} >= 0) {
2373 0 0       0 if ($global->{has_orig_tios}) {
2374 0         0 $global->{orig_tios}->setattr($global->{ttyfd}, TCSAFLUSH);
2375             }
2376 0 0       0 if ($global->{ttyfd_open}) {
2377 0         0 POSIX::close($global->{ttyfd});
2378 0         0 $global->{ttyfd_open} = 0;
2379 0         0 $global->{ttyfd} = -1;
2380             }
2381             }
2382            
2383 5 50       61 $SIG{WINCH} = 'DEFAULT' if exists $SIG{WINCH};
2384 5         8 foreach my $fd (@{ $global->{resize_pipefd}}) {
  5         16  
2385 10 50       20 POSIX::close($fd) if $fd >= 0;
2386             }
2387            
2388 5         18 cellbuf_free($global->{back});
2389 5         18 cellbuf_free($global->{front});
2390 5         23 bytebuf_free(\$global->{inbuf});
2391 5         11 bytebuf_free(\$global->{outbuf});
2392            
2393 5 50       13 $global->{terminfo} = '' if $global->{terminfo};
2394            
2395 5 50       49 my $rv = $global->{cap_trie}->clear() if $global->{cap_trie};
2396 5 50       13 return $rv if $rv != TB_OK;
2397            
2398 5         18 tb_reset();
2399 5         20 return TB_OK;
2400             }
2401            
2402             sub tb_iswprint_ex { # $bool ($ch, \$width|undef)
2403 19     19 0 28 state $sig = compile(
2404             _PositiveOrZeroInt,
2405             _Maybe[_ScalarRef],
2406             );
2407 19         28 my ($ch, $width) = $sig->(@_);
2408            
2409 19         21 if (TB_OPT_LIBC_WCHAR) {
2410 19         43 my $w = wcwidth($ch);
2411 19 100       36 $$width = $w if defined $width;
2412            
2413             # NUL is not printable even though width is 0.
2414 19 100 100     133 return $ch != 0 && $w >= 0 ? 1 : 0;
2415             } else {
2416            
2417             # Fast path for 1-byte codepoints
2418             if (($ch >= 0x20 && $ch <= 0x7e) || ($ch >= 0xa0 && $ch <= 0xff)) {
2419             $$width = 1 if defined $width;
2420             return 1;
2421             }
2422             if ($ch <= 0xff) {
2423             $$width = ($ch == 0 ? 0 : -1) if defined $width;
2424             return 0;
2425             }
2426            
2427             my $w = Termbox::PP::WCWidth::wcwidth($ch);
2428             $$width = $w if defined $width;
2429             return $w >= 0 ? 1 : 0;
2430             }
2431             }
2432            
2433             sub tb_cluster_width { # $int (\@cluster, $nch)
2434 9     9 0 7057 state $sig = compile(
2435             _ArrayRef,
2436             _PositiveOrZeroInt,
2437             );
2438 9         19 my ($cluster, $nch) = $sig->(@_);
2439            
2440 9         13 my $wmax = -1;
2441 9         13 my ($vs15, $vs16, $ri, $zwj) = (0, 0, 0, 0);
2442            
2443 9         22 for my $i (0 .. $nch-1) {
2444 10   50     21 my $c = $cluster->[$i] // return -1;
2445 10 50 33     49 if ($c == 0xfe0e) { $vs15++ }
  0 50       0  
    50          
    50          
2446 0         0 elsif ($c == 0xfe0f) { $vs16++ }
2447 0         0 elsif ($c == 0x200d) { $zwj++ }
2448 0         0 elsif ($c >= 0x1f1e6 && $c <= 0x1f1ff) { $ri++ }
2449            
2450 10         51 my $w = tb_wcwidth($c);
2451 10 50       19 $w = -1 unless defined $w;
2452 10 100       23 $wmax = $w if $w > $wmax;
2453             }
2454            
2455 9 100       15 if ($wmax >= 1) {
2456 8 50       15 return 1 if $vs15;
2457 8 50 33     36 return 2 if $vs16 || $zwj || $ri >= 2;
      33        
2458             }
2459            
2460 9         25 return $wmax;
2461             }
2462            
2463             #
2464             # Terminal initialization helpers
2465             #
2466            
2467             sub init_term_attrs { # $int ()
2468 1     1 0 141422 state $sig = compile();
2469 1         4 $sig->(@_);
2470            
2471 1 50       9 return TB_OK if $global->{ttyfd} < 0;
2472            
2473 0   0     0 $global->{orig_tios} = POSIX::Termios->new() // return TB_ERR_TCGETATTR;
2474            
2475 0 0       0 if (!defined $global->{orig_tios}->getattr($global->{ttyfd})) {
2476 0         0 $global->{last_errno} = 0+ $!;
2477 0         0 return TB_ERR_TCGETATTR;
2478             }
2479            
2480 0   0     0 my $tios = $global->{orig_tios}->$clone() // return TB_ERR_TCSETATTR;
2481 0         0 $global->{has_orig_tios} = 1;
2482            
2483 0         0 cfmakeraw($tios);
2484 0         0 $tios->setcc(VMIN, 1);
2485 0         0 $tios->setcc(VTIME, 0);
2486            
2487 0 0       0 if (!defined $tios->setattr($global->{ttyfd}, TCSAFLUSH)) {
2488 0         0 $global->{last_errno} = 0+ $!;
2489 0         0 return TB_ERR_TCSETATTR;
2490             }
2491            
2492 0         0 return TB_OK;
2493             }
2494            
2495             sub init_term_caps { # $int ()
2496 1     1 0 3105 state $sig = compile();
2497 1         3 $sig->(@_);
2498 1 50       3 if (load_terminfo() == TB_OK) {
2499 1         3 return parse_terminfo_caps();
2500             }
2501 0         0 return load_builtin_caps();
2502             }
2503            
2504             #
2505             # Terminfo parsing helpers
2506             #
2507            
2508             sub load_terminfo { # $int ()
2509 3     3 0 6479 state $sig = compile();
2510 3         9 $sig->(@_);
2511            
2512 3         6 my $rv;
2513            
2514             # See terminfo(5) "Fetching Compiled Descriptions" for a description of
2515             # this behavior. Some of these paths are compile-time ncurses options, so
2516             # best guesses are used here.
2517 3         6 my $term = $ENV{TERM};
2518 3 50       9 return TB_ERR unless $term;
2519            
2520             # If TERMINFO is set, try that directory first
2521 3 50       82 if (defined(my $terminfo = $ENV{"TERMINFO"})) {
2522 0         0 $rv = load_terminfo_from_path($terminfo, $term);
2523 0 0       0 return $rv if $rv == TB_OK;
2524             }
2525            
2526             # Next try ~/.terminfo
2527 3 50       13 if (defined(my $home = $ENV{"HOME"})) {
2528 3         7 my $tmp = "$home/.terminfo";
2529            
2530 3 50       8 return TB_ERR if length($tmp) >= TB_PATH_MAX;
2531            
2532 3         9 $rv = load_terminfo_from_path($tmp, $term);
2533 3 50       8 return $rv if $rv == TB_OK;
2534             }
2535            
2536             # Next try TERMINFO_DIRS
2537             #
2538             # Note, empty entries are supposed to be interpretted as the "compiled-in
2539             # default", which is of course system-dependent. Previously /etc/terminfo
2540             # was used here. Let's skip empty entries altogether rather than give
2541             # precedence to a guess, and check common paths after this loop.
2542 3 50       19 if (defined(my $dirs = $ENV{"TERMINFO_DIRS"})) {
2543 0 0       0 return TB_ERR if length($dirs) >= TB_PATH_MAX;
2544 0         0 foreach my $dir (split /:/, $dirs, -1) {
2545 0 0       0 next if $dir eq '';
2546 0         0 $rv = load_terminfo_from_path($dir, $term);
2547 0 0       0 return $rv if $rv == TB_OK;
2548             }
2549             }
2550            
2551             # Try compile-time terminfo directory if available
2552 3 50       39 if (defined TB_TERMINFO_DIR) {
2553 0         0 $rv = load_terminfo_from_path(TB_TERMINFO_DIR, $term);
2554 0 0       0 return $rv if $rv == TB_OK;
2555             }
2556            
2557 3         10 foreach my $dir (qw(
2558             /usr/local/etc/terminfo
2559             /usr/local/share/terminfo
2560             /usr/local/lib/terminfo
2561             /etc/terminfo
2562             /usr/share/terminfo
2563             /usr/lib/terminfo
2564             /usr/share/lib/terminfo
2565             /lib/terminfo
2566             )) {
2567 15         20 $rv = load_terminfo_from_path($dir, $term);
2568 15 100       37 return $rv if $rv == TB_OK;
2569             }
2570            
2571 0         0 return TB_ERR;
2572             }
2573            
2574             sub load_terminfo_from_path { # $int ($path, $term)
2575 19     19 0 3893 state $sig = compile(
2576             _Str,
2577             _Str,
2578             );
2579 19         31 my ($path, $term) = $sig->(@_);
2580 19         24 my $rv;
2581             my $tmp;
2582            
2583             # Look for term at this terminfo location, e.g., /x/xterm
2584 19         35 $tmp = "$path/" . substr($term, 0, 1) . "/$term";
2585 19 50       27 return TB_ERR if length($tmp) >= TB_PATH_MAX;
2586            
2587 19         27 $rv = read_terminfo_path($tmp);
2588 19 100       53 return $rv if $rv == TB_OK;
2589            
2590 15 50       39 if ($^O eq 'darwin') {
2591             # Try the Darwin equivalent path, e.g., /78/xterm
2592 0         0 $tmp = "$path/" . sprintf('%x', ord(substr($term, 0, 1))) . "/$term";
2593 0 0       0 return TB_ERR if length($tmp) >= TB_PATH_MAX;
2594            
2595 0         0 return read_terminfo_path($tmp);
2596             }
2597            
2598 15         18 return TB_ERR;
2599             }
2600            
2601             sub read_terminfo_path { # $int ($path)
2602 19     19 0 23 state $sig = compile(
2603             _Str,
2604             );
2605 19         26 my ($path) = $sig->(@_);
2606            
2607 19 100       1042 open(my $fp, '<:raw', $path)
2608             or return TB_ERR;
2609            
2610 4         28 my @st = stat($fp);
2611 4 50       10 if (!@st) {
2612 0         0 close($fp);
2613 0         0 return TB_ERR;
2614             }
2615            
2616 4         6 my $fsize = $st[7];
2617            
2618 4         6 my $data = '';
2619 4         98 my $nread = sysread($fp, $data, $fsize);
2620            
2621 4 50 33     23 if (!defined($nread) || $nread != $fsize) {
2622 0         0 close($fp);
2623 0         0 return TB_ERR;
2624             }
2625            
2626 4         11 $global->{terminfo} = $data;
2627            
2628 4         38 close($fp);
2629 4         25 return TB_OK;
2630             }
2631            
2632             sub parse_terminfo_caps { # $int ()
2633 2     2 0 22 state $sig = compile();
2634 2         5 $sig->(@_);
2635            
2636 2         4 my $terminfo = $global->{terminfo};
2637 2   50     7 my $nterminfo = length($terminfo) // 0;
2638            
2639             # Ensure there's at least a header's worth of data
2640 2 50       7 return TB_ERR if $nterminfo < 6 * INT16_SIZE;
2641            
2642 2 50       2 my $terminfo_cap_indexes = eval {
2643 37     37   108977 no warnings 'once';
  37         60  
  37         12521  
2644 2         1141 require Termbox::PP::Terminfo::Builtin;
2645 2         12 \@Termbox::PP::Terminfo::Builtin::terminfo_cap_indexes;
2646             } or return TB_ERR;
2647            
2648 2         5 my ($magic_number, $nbytes_names, $nbytes_bools, $num_ints, $num_offsets,
2649             $nbytes_strings);
2650            
2651             # header[0] the magic number (octal 0432 or 01036)
2652             # header[1] the size, in bytes, of the names section
2653             # header[2] the number of bytes in the boolean section
2654             # header[3] the number of short integers in the numbers section
2655             # header[4] the number of offsets (short integers) in the strings section
2656             # header[5] the size, in bytes, of the string table
2657 2         46 get_terminfo_int16(0 * INT16_SIZE, \$magic_number);
2658 2         5 get_terminfo_int16(1 * INT16_SIZE, \$nbytes_names);
2659 2         5 get_terminfo_int16(2 * INT16_SIZE, \$nbytes_bools);
2660 2         6 get_terminfo_int16(3 * INT16_SIZE, \$num_ints);
2661 2         4 get_terminfo_int16(4 * INT16_SIZE, \$num_offsets);
2662 2         5 get_terminfo_int16(5 * INT16_SIZE, \$nbytes_strings);
2663            
2664             # Legacy ints are 16-bit, extended ints are 32-bit
2665 2 50       6 my $bytes_per_int = $magic_number == 01036 ? 4 # 32-bit
2666             : 2; # 16-bit
2667            
2668             # Between the boolean section and the number section, a null byte will be
2669             # inserted, if necessary, to ensure that the number section begins on
2670             # an even byte
2671 2 50       7 my $align_offset = (($nbytes_names + $nbytes_bools) % 2 != 0) ? 1 : 0;
2672            
2673 2         3 my $nbytes_header = 6 * INT16_SIZE;
2674 2         5 my $pos_str_offsets =
2675             $nbytes_header # header (12 bytes)
2676             + $nbytes_names # length of names section
2677             + $nbytes_bools # length of boolean section
2678             + $align_offset
2679             + ($num_ints * $bytes_per_int); # length of string offsets section
2680            
2681 2         3 my $pos_str_table =
2682             $pos_str_offsets
2683             + ($num_offsets * INT16_SIZE); # length of string offsets table
2684            
2685 2   50     6 $global->{caps} ||= [];
2686            
2687             # Load caps
2688 2         7 for (my $i = 0; $i < TB_CAP__COUNT; $i++) {
2689 76         113 my $cap = get_terminfo_string($pos_str_offsets, $num_offsets,
2690             $pos_str_table, $nbytes_strings, $terminfo_cap_indexes->[$i]);
2691             # Something is not right
2692 76 50       103 return TB_ERR unless defined $cap;
2693 76         136 $global->{caps}->[$i] = $cap;
2694             }
2695            
2696 2         11 return TB_OK;
2697             }
2698            
2699             sub load_builtin_caps { # $int ()
2700 1     1 0 157651 state $sig = compile();
2701 1         4 $sig->(@_);
2702 1         2 my $term = $ENV{"TERM"};
2703            
2704 1 50       3 return TB_ERR_NO_TERM unless $term;
2705            
2706 1 50       2 my $builtin_terms = do {
2707 1         7 require Termbox::PP::Terminfo::Builtin;
2708 37     37   243 no warnings 'once';
  37         223  
  37         3028  
2709 1         3 \%Termbox::PP::Terminfo::Builtin::builtin_terms;
2710             } or return TB_ERR;
2711 1 50       1 my $builtin_terms_orders = do {
2712 1         3 require Termbox::PP::Terminfo::Builtin;
2713 37     37   375 no warnings 'once';
  37         78  
  37         49866  
2714 1         3 \@Termbox::PP::Terminfo::Builtin::builtin_terms_orders;
2715             } or return TB_ERR;
2716            
2717             # Check for exact TERM match
2718 1 50       2 if (exists $builtin_terms->{$term}) {
2719 1         2 my $caps = $builtin_terms->{$term};
2720 1         3 @{ $global->{caps} } = @$caps[0 .. TB_CAP__COUNT - 1];
  1         10  
2721 1         2 return TB_OK;
2722             }
2723            
2724             # Check for partial TERM or alias match
2725 0         0 foreach my $name (@$builtin_terms_orders) {
2726 0 0       0 next if index($term, $name) < 0;
2727 0 0       0 next unless exists $builtin_terms->{$name};
2728            
2729 0         0 my $caps = $builtin_terms->{$name};
2730 0         0 @{ $global->{caps} } = @$caps[0 .. TB_CAP__COUNT - 1];
  0         0  
2731 0         0 return TB_OK;
2732             }
2733            
2734 0         0 return TB_ERR_UNSUPPORTED_TERM;
2735             }
2736            
2737             sub get_terminfo_string { # $str|undef ($offsets_pos, $offsets_len, $table_pos, $table_size, $index)
2738 80     80 0 148640 state $sig = compile(
2739             _PositiveOrZeroInt,
2740             _PositiveOrZeroInt,
2741             _PositiveOrZeroInt,
2742             _PositiveOrZeroInt,
2743             _PositiveOrZeroInt,
2744             );
2745 80         98 my ($offsets_pos, $offsets_len, $table_pos, $table_size, $index) = $sig->(@_);
2746            
2747 80 100       139 if ($index >= $offsets_len) {
2748             # An index beyond the offset table indicates absent
2749             # See 'convert_strings' in tinfo 'read_entry.c'
2750 1         4 return '';
2751             }
2752            
2753 79         75 my $table_offset;
2754 79         78 my $table_offset_offset = $offsets_pos + ($index * INT16_SIZE);
2755 79 50       101 if (get_terminfo_int16($table_offset_offset, \$table_offset) != TB_OK) {
2756             # offset beyond end of terminfo entry
2757             # Truncated/corrupt terminfo entry?
2758 0         0 return undef;
2759             }
2760            
2761 79 50 33     239 if ($table_offset < 0 || $table_offset >= $table_size) {
2762             # A negative offset indicates absent
2763             # An offset beyond the string table indicates absent
2764             # See 'convert_strings' in tinfo 'read_entry.c'
2765 0         0 return '';
2766             }
2767            
2768 79         79 my $str_offset = $table_pos + $table_offset;
2769 79 50       113 if ($str_offset >= length($global->{terminfo})) {
2770             # string beyond end of terminfo entry
2771             # Truncated/corrupt terminfo entry?
2772 0         0 return undef;
2773             }
2774            
2775 79         217 my $str = substr($global->{terminfo}, $str_offset);
2776 79         87 my $len = index($str, "\0");
2777 79 50       152 return $len >= 0 ? substr($str, 0, $len) : undef;
2778             }
2779            
2780             sub get_terminfo_int16 { # $int ($offset, \$val)
2781 95     95 0 94 state $sig = compile(
2782             _Int,
2783             _ScalarRef,
2784             );
2785 95         109 my ($offset, $val) = $sig->(@_);
2786 95 100 66     275 if ($offset < 0 || $offset + INT16_SIZE > length($global->{terminfo})) {
2787 1         2 $$val = -1;
2788 1         3 return TB_ERR;
2789             }
2790 94         186 $$val = unpack('s<', substr($global->{terminfo}, $offset, INT16_SIZE));
2791 94         132 return TB_OK;
2792             }
2793            
2794             #
2795             # Resize handling helpers
2796             #
2797            
2798             sub init_resize_handler { # $int ()
2799 1     1 0 3278 state $sig = compile();
2800 1         3 $sig->(@_);
2801 1         2 my ($rfd, $wfd);
2802 1 50       24 unless (($rfd, $wfd) = POSIX::pipe()) {
2803 0         0 $global->{last_errno} = 0+ $!;
2804 0         0 return TB_ERR_RESIZE_PIPE;
2805             }
2806            
2807 1         4 $global->{resize_pipefd} = [$rfd, $wfd];
2808            
2809 1 50       11 $SIG{WINCH} = \&handle_resize if exists $SIG{WINCH};
2810 1         3 return TB_OK;
2811             }
2812            
2813             sub resize_cellbufs { # $int ()
2814 2     2 0 7125 state $sig = compile();
2815 2         6 $sig->(@_);
2816 2         2 my $rv;
2817 2         9 $rv = cellbuf_resize($global->{back}, $global->{width}, $global->{height});
2818 2 100       12 return $rv if $rv != TB_OK;
2819 1         4 $rv = cellbuf_resize($global->{front}, $global->{width}, $global->{height});
2820 1 50       4 return $rv if $rv != TB_OK;
2821 1         4 $rv = cellbuf_clear($global->{front});
2822 1 50       4 return $rv if $rv != TB_OK;
2823 1         2 $rv = send_clear();
2824 1 50       3 return $rv if $rv != TB_OK;
2825 1         4 return TB_OK;
2826             }
2827            
2828             sub handle_resize { # void ($sig)
2829 1     1 0 5250 state $sig = compile(
2830             _Int,
2831             );
2832 1         2 my ($signo) = $sig->(@_);
2833 1         4 local $! = $!;
2834 1         4 my $payload = pack('i', $signo);
2835 1         24 POSIX::write($global->{resize_pipefd}[1], $payload, length($payload));
2836 1         4 return;
2837             }
2838            
2839             sub update_term_size { # $int ()
2840 1     1 0 4590 state $sig = compile();
2841 1         2 $sig->(@_);
2842 1         2 my ($rv, $ioctl_errno);
2843            
2844 1 50       5 return TB_OK if $global->{ttyfd} < 0;
2845            
2846 0         0 my $fh = IO::File->new_from_fd($global->{ttyfd}, 'w');
2847 0 0       0 return TB_OK if !$fh;
2848            
2849 0         0 my $sz = pack('S4', 0, 0, 0, 0);
2850            
2851             # Try ioctl TIOCGWINSZ
2852 0 0 0     0 if (eval { require 'sys/ioctl.ph'; 1 } && ioctl($fh, &TIOCGWINSZ, $sz)) {
  0         0  
  0         0  
2853 0         0 my ($row, $col) = unpack('S4', $sz);
2854 0         0 $global->{width} = $col;
2855 0         0 $global->{height} = $row;
2856 0         0 return TB_OK;
2857             }
2858 0         0 $ioctl_errno = 0+ $!;
2859            
2860             # Try >cursor(9999,9999), >u7,
2861 0         0 $rv = update_term_size_via_esc();
2862 0 0       0 return TB_OK if $rv == TB_OK;
2863            
2864 0         0 $global->{last_errno} = $ioctl_errno;
2865 0         0 return TB_ERR_RESIZE_IOCTL;
2866             }
2867            
2868             sub update_term_size_via_esc { # $int ()
2869 1     1 0 3301 state $sig = compile();
2870 1         12 $sig->(@_);
2871            
2872 1         2 my $move_and_report = "\e[9999;9999H\e[6n";
2873            
2874             my $write_rv = POSIX::write(
2875             $global->{wfd},
2876 1         5 $move_and_report,
2877             length($move_and_report),
2878             );
2879 1 50 33     8 return TB_ERR_RESIZE_WRITE
2880             if !defined($write_rv)
2881             || $write_rv != length($move_and_report);
2882            
2883 1         2 my $rin = '';
2884 1         5 vec($rin, $global->{rfd}, 1) = 1;
2885            
2886 1         2 my $timeout = TB_RESIZE_FALLBACK_MS / 1000;
2887 1         7 my $select_rv = select($rin, undef, undef, $timeout);
2888 1 50       4 if ($select_rv != 1) {
2889 0         0 $global->{last_errno} = 0+ $!;
2890 0         0 return TB_ERR_RESIZE_POLL;
2891             }
2892            
2893 1         2 my $buf = '';
2894 1         7 my $read_rv = POSIX::read($global->{rfd}, $buf, TB_OPT_READ_BUF);
2895 1 50 33     4 if (!defined($read_rv) || $read_rv < 1) {
2896 0         0 $global->{last_errno} = 0+ $!;
2897 0         0 return TB_ERR_RESIZE_READ;
2898             }
2899            
2900 1 50       7 if ($buf !~ /\e\[(\d+);(\d+)R/) {
2901 0         0 return TB_ERR_RESIZE_SSCANF;
2902             }
2903 1         4 my ($rh, $rw) = ($1, $2);
2904            
2905 1         3 $global->{width} = $rw;
2906 1         2 $global->{height} = $rh;
2907 1         3 return TB_OK;
2908             }
2909            
2910             #
2911             # Escape-cap parser helpers
2912             #
2913            
2914             sub init_cap_trie { # $int ()
2915 0     0 0 0 state $sig = compile();
2916 0         0 $sig->(@_);
2917            
2918 0   0     0 my $trie = $global->{cap_trie} //= captrie->new();
2919 0         0 my $rv = $trie->clear();
2920 0 0       0 return $rv if $rv != TB_OK;
2921            
2922 0 0       0 my $builtin_mod_caps = eval {
2923 37     37   271 no warnings 'once';
  37         48  
  37         32450  
2924 0         0 require Termbox::PP::Terminfo::Builtin;
2925 0         0 \%Termbox::PP::Terminfo::Builtin::builtin_mod_caps;
2926             } or return TB_ERR;
2927            
2928             # Add caps from terminfo or built-in
2929             # Collisions are expected as some terminfo entries have dupes. (For
2930             # example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap
2931             # in TB_CAP_* index order will win.
2932             #
2933             # TODO: Reorder TB_CAP_* so more critical caps come first.
2934 0         0 for my $i (0 .. TB_CAP__COUNT_KEYS - 1) {
2935 0         0 $rv = $trie->add($global->{caps}[$i], tb_key_i($i), 0);
2936 0 0 0     0 return $rv if $rv != TB_OK && $rv != TB_ERR_CAP_COLLISION;
2937             }
2938            
2939             # Add built-in mod caps
2940             #
2941             # Collisions are OK here as well. This can happen if $global->{caps} collides
2942             # with builtin_mod_caps. It is desirable to give precedence to $global->{caps}
2943             # here.
2944 0         0 while (my ($cap, $entry) = each %$builtin_mod_caps) {
2945 0   0     0 my $key = $entry->{key} // next;
2946 0   0     0 my $mod = $entry->{mod} // next;
2947 0         0 $rv = $trie->add($cap, $key, $mod);
2948 0 0 0     0 return $rv if $rv != TB_OK && $rv != TB_ERR_CAP_COLLISION;
2949             }
2950            
2951 0         0 return TB_OK;
2952             }
2953            
2954             # Regex/prefix equivalent of termbox2's cap_trie_add.
2955             sub cap_trie_add { # $int ($cap|undef, $key, $mod)
2956 0     0 0 0 state $sig = compile(
2957             _Maybe[_Str],
2958             _PositiveOrZeroInt,
2959             _PositiveOrZeroInt,
2960             );
2961 0         0 my ($cap, $key, $mod) = $sig->(@_);
2962 0 0       0 my $trie = $global->{cap_trie} or return TB_ERR;
2963 0         0 return $trie->add($cap, $key, $mod);
2964             }
2965            
2966             # Regex/prefix equivalent of termbox2's cap_trie_find.
2967             # Returns a hash-ref node in $$last with {is_leaf,key,mod,nchildren,cap}.
2968             sub cap_trie_find { # $int ($buf|undef, $nbuf, \$last, \$depth)
2969 0     0 0 0 state $sig = compile(
2970             _Maybe[_Str],
2971             _PositiveOrZeroInt,
2972             _ScalarRef,
2973             _ScalarRef,
2974             );
2975 0         0 my ($buf, $nbuf, $last, $depth) = $sig->(@_);
2976 0 0       0 my $trie = $global->{cap_trie} or return TB_ERR;
2977 0         0 return $trie->find($buf, $last, $depth); # Perl does not need nbuf
2978             }
2979            
2980             sub cap_trie_deinit { # $int ($node)
2981 0     0 0 0 state $sig = compile(
2982             _Object,
2983             );
2984 0         0 my ($node) = $sig->(@_);
2985 0         0 return $node->clear();
2986             }
2987            
2988             #
2989             # Event extraction helpers
2990             #
2991            
2992             sub wait_event {
2993 2     2 0 9 state $sig = compile(
2994             _Object,
2995             _Int,
2996             );
2997 2         5 my ($event, $timeout) = $sig->(@_);
2998            
2999 2         3 my $rv;
3000 2         4 my $buf = '';
3001 2         5 state $empty_event = Termbox::Event->new();
3002            
3003 2         3 alias: for ($event) {
3004 2         8 %$event = %$empty_event;
3005            
3006             # Fast path: buffered input already yields a full event
3007 2         6 $rv = extract_event($event);
3008 2 100       7 return TB_OK if $rv == TB_OK;
3009            
3010 1         2 my $rfd = $global->{rfd};
3011 1         2 my $resizefd = $global->{resize_pipefd}[0];
3012            
3013             # Perl select timeout is seconds as float; undef means block forever
3014 1 50       6 my $timeout_sec = ($timeout < 0) ? undef : ($timeout / 1000);
3015            
3016 1         1 do {
3017 1         2 my $rin = '';
3018 1         4 vec($rin, $rfd, 1) = 1;
3019 1         3 vec($rin, $resizefd, 1) = 1;
3020            
3021 1         2 my $rout = $rin;
3022            
3023 1         10 my $select_rv = select($rout, undef, undef, $timeout_sec);
3024            
3025 1 50       3 if ($select_rv < 0) {
3026             # Let EINTR/EAGAIN bubble up
3027 0         0 $global->{last_errno} = 0+ $!;
3028 0         0 return TB_ERR_POLL;
3029             }
3030 1 50       4 if ($select_rv == 0) {
3031 0         0 return TB_ERR_NO_EVENT;
3032             }
3033            
3034 1         3 my $tty_has_events = vec($rout, $rfd, 1);
3035 1         2 my $resize_has_events = vec($rout, $resizefd, 1);
3036            
3037 1 50       2 if ($tty_has_events) {
3038 0         0 my $buf = '';
3039 0         0 my $read_rv = POSIX::read($rfd, $buf, TB_OPT_READ_BUF);
3040 0 0 0     0 if (!defined($read_rv) || $read_rv < 0) {
    0          
3041 0         0 $global->{last_errno} = 0+ $!;
3042 0         0 return TB_ERR_READ;
3043             } elsif ($read_rv > 0) {
3044 0         0 bytebuf_nputs(\$global->{inbuf}, $buf, $read_rv);
3045             }
3046             }
3047            
3048 1 50       3 if ($resize_has_events) {
3049 1         1 my $ignore = pack('i', 0);
3050 1         14 POSIX::read($resizefd, $ignore, length($ignore));
3051             # TODO: Harden against errors encountered mid-resize
3052 1         4 $rv = update_term_size();
3053 1 50       4 return $rv if $rv != TB_OK;
3054 1         2 $rv = resize_cellbufs();
3055 1 50       4 return $rv if $rv != TB_OK;
3056            
3057 1         2 $event->{type} = TB_EVENT_RESIZE;
3058 1         3 $event->{w} = $global->{width};
3059 1         2 $event->{h} = $global->{height};
3060 1         4 return TB_OK;
3061             }
3062            
3063             # Try to extract an event after consuming input / handling resize
3064 0         0 %$event = %$empty_event;
3065 0         0 $rv = extract_event($event);
3066 0 0       0 return TB_OK if $rv == TB_OK;
3067            
3068             } while ($timeout < 0);
3069            
3070 0         0 return $rv;
3071             } #/ alias:
3072             }
3073            
3074             sub extract_event { # $int ($event)
3075 3     3 0 8 state $sig = compile(
3076             _Object,
3077             );
3078 3         5 my ($event) = $sig->(@_);
3079 3         4 my $trie = $global->{cap_trie};
3080 3         5 alias: for my $in ($global->{inbuf}) {
3081            
3082 3 100       7 return TB_ERR unless length($in);
3083            
3084 37     37   271 use bytes;
  37         65  
  37         171  
3085 2         5 my $b0 = substr($in, 0, 1);
3086 2 50       5 if ($b0 eq "\e") {
3087             # Escape sequence?
3088             # In TB_INPUT_ESC, skip if the buffer is a single escape char
3089 0 0 0     0 unless (($global->{input_mode} & TB_INPUT_ESC) && length($in) == 1) {
3090 0         0 my $rv = extract_esc($event);
3091 0 0 0     0 return $rv if $rv != TB_OK && $rv != TB_ERR_NEED_MORE;
3092             }
3093            
3094             # Escape key?
3095 0 0       0 if ($global->{input_mode} & TB_INPUT_ESC) {
3096 0         0 $event->{type} = TB_EVENT_KEY;
3097 0         0 $event->{ch} = 0;
3098 0         0 $event->{key} = TB_KEY_ESC;
3099 0         0 $event->{mod} = 0;
3100 0         0 substr($in, 0, 1, '');
3101 0         0 return TB_OK;
3102             }
3103            
3104             # Recurse for alt key
3105 0         0 $event->{mod} |= TB_MOD_ALT;
3106 0         0 substr($in, 0, 1, '');
3107 0         0 return extract_event($event);
3108             }
3109            
3110             # ASCII control key?
3111 2   33     8 my $is_ctrl = ord($b0) < TB_KEY_SPACE || ord($b0) == TB_KEY_BACKSPACE2;
3112 2 50       5 if ($is_ctrl) {
3113 0         0 $event->{type} = TB_EVENT_KEY;
3114 0         0 $event->{ch} = 0;
3115 0         0 $event->{key} = ord($in);
3116 0         0 $event->{mod} |= TB_MOD_CTRL;
3117 0         0 substr($in, 0, 1, '');
3118 0         0 return TB_OK;
3119             }
3120            
3121             # UTF-8?
3122 2         4 my $need = tb_utf8_char_length($b0);
3123 2 50       20 if (length($in) >= $need) {
3124 2         7 $event->{type} = TB_EVENT_KEY;
3125 2         7 tb_utf8_char_to_unicode(\$event->{ch}, $in);
3126 2         9 $event->{key} = 0;
3127 2         5 substr($in, 0, $need, '');
3128 2         6 return TB_OK;
3129             }
3130            
3131             # Need more input
3132 0         0 return TB_ERR
3133             } #/ alias:
3134             }
3135            
3136             sub extract_esc { # $int ($event)
3137 2     2 0 12 state $sig = compile(
3138             _Object,
3139             );
3140 2         5 my ($event) = $sig->(@_);
3141            
3142 2         4 my $rv;
3143            
3144 2         6 $rv = extract_esc_user($event, 0);
3145 2 100 66     11 return $rv if $rv == TB_OK || $rv == TB_ERR_NEED_MORE;
3146            
3147 1         3 $rv = extract_esc_cap($event);
3148 1 50 33     7 return $rv if $rv == TB_OK || $rv == TB_ERR_NEED_MORE;
3149            
3150 0         0 $rv = extract_esc_mouse($event);
3151 0 0 0     0 return $rv if $rv == TB_OK || $rv == TB_ERR_NEED_MORE;
3152            
3153 0         0 $rv = extract_esc_user($event, 1);
3154 0 0 0     0 return $rv if $rv == TB_OK || $rv == TB_ERR_NEED_MORE;
3155            
3156 0         0 return TB_ERR;
3157             }
3158            
3159             sub extract_esc_user { # $int ($event, $is_post)
3160 3     3 0 5 state $sig = compile(
3161             _Object,
3162             _Bool,
3163             );
3164 3         6 my ($event, $is_post) = $sig->(@_);
3165            
3166             my $fn = $is_post ? $global->{fn_extract_esc_post}
3167 3 100       9 : $global->{fn_extract_esc_pre};
3168 3 100       15 return TB_ERR unless ref($fn) eq 'CODE';
3169            
3170 1         2 my $consumed = 0;
3171 1         3 my $rv = $fn->($event, \$consumed);
3172            
3173 1 50       9 if ($rv == TB_OK) {
3174 1         5 bytebuf_shift(\$global->{inbuf}, $consumed);
3175             }
3176            
3177 1 50 33     3 return $rv if $rv == TB_OK || $rv == TB_ERR_NEED_MORE;
3178 0         0 return TB_ERR;
3179             }
3180            
3181             sub extract_esc_mouse { # $int ($event)
3182 3     3 0 19 state $sig = compile(
3183             _Object,
3184             );
3185 3         10 my ($event) = $sig->(@_);
3186            
3187 37     37   22750 use bytes;
  37         60  
  37         127  
3188 3         9 alias: for my $in ($global->{inbuf}) {
3189 3         19 my @in = unpack('C*', $in);
3190            
3191             # Bail if not enough to determine type
3192 3 50       18 if (@in < 2) {
    50          
    50          
3193 0         0 return TB_ERR_NEED_MORE;
3194             } elsif ($in[1] != ord('[')) {
3195 0         0 return TB_ERR;
3196             } elsif (@in < 3) {
3197 0         0 return TB_ERR_NEED_MORE;
3198             }
3199            
3200             # Discern type of mouse event from 3rd byte
3201 3         7 my $type = TYPE_VT200;
3202 3 100       12 if ($in[2] == ord('M')) {
    100          
3203             # X10 mouse encoding, the simplest one: \x1b [ M Cb Cx Cy
3204 1         3 $type = TYPE_VT200;
3205             }
3206             elsif ($in[2] == ord('<')) {
3207             # xterm 1006 extended mode or urxvt 1015 extended mode
3208             # xterm: \x1b [ < Cb ; Cx ; Cy (M or m)
3209 1         5 $type = TYPE_1006;
3210             }
3211             else {
3212             # urxvt: \x1b [ Cb ; Cx ; Cy M
3213 1         2 $type = TYPE_1015;
3214             }
3215            
3216 3         5 my $buf_shift = 0;
3217            
3218 3         21 switch_parse: for ($type) {
3219 3 100       6 case: TYPE_VT200 == $_ and do {
3220             # In this mode, we need 6 bytes
3221 1 50       4 return TB_ERR_NEED_MORE if @in < 6;
3222            
3223 1         3 my $b = $in[3] - 0x20;
3224            
3225 1         2 local $_;
3226 1         4 switch: for ($b & 3) {
3227 1 50       3 case: 0 == $_ and do {
3228 1 50       5 $event->{key} = ($b & 64) ? TB_KEY_MOUSE_WHEEL_UP
3229             : TB_KEY_MOUSE_LEFT;
3230 1         4 last;
3231             };
3232 0 0       0 case: 1 == $_ and do {
3233 0 0       0 $event->{key} = ($b & 64) ? TB_KEY_MOUSE_WHEEL_DOWN
3234             : TB_KEY_MOUSE_MIDDLE;
3235 0         0 last;
3236             };
3237 0 0       0 case: 2 == $_ and do {
3238 0         0 $event->{key} = TB_KEY_MOUSE_RIGHT;
3239 0         0 last;
3240             };
3241 0 0       0 case: 3 == $_ and do {
3242 0         0 $event->{key} = TB_KEY_MOUSE_RELEASE;
3243 0         0 last;
3244             };
3245             default: {
3246 0         0 return TB_ERR;
  0         0  
3247             };
3248             }
3249            
3250 1 50       4 if (($b & 32) != 0) {
3251 0         0 $event->{mod} |= TB_MOD_MOTION;
3252             }
3253            
3254             # The coord is 1,1 for upper left
3255 1         2 $event->{x} = $in[4] - 0x21;
3256 1         3 $event->{y} = $in[5] - 0x21;
3257            
3258             # Eat 6 bytes
3259 1         2 $buf_shift = 6;
3260 1         3 last;
3261             };
3262            
3263             case: TYPE_1006 == $_ || # fallthrough
3264             TYPE_1015 == $_ and do
3265 2 50 66     11 {
3266 2         5 my @num = (-1, -1, -1);
3267 2         42 my $num_i = 0;
3268 2         24 my $cur_num = -1;
3269 2         4 my $trail = ord(' ');
3270            
3271 2         5 my $i = 2;
3272 2 100       7 ++$i if $type == TYPE_1006; # skip '<'
3273            
3274             # Parse %d;%d;%d[mM] into @num
3275 2   66     13 while ($i < @in && $num_i < 3) {
3276 19         27 my $c = $in[$i];
3277            
3278 19 100 66     50 if ($c >= ord('0') && $c <= ord('9')) {
3279             # Digit
3280 13 100       24 $cur_num = 0 if $cur_num == -1;
3281 13         16 $cur_num *= 10;
3282 13         15 $cur_num += $c - ord('0');
3283 13         17 ++$i;
3284 13         93 next;
3285             }
3286            
3287 6 50 66     97 if ($cur_num != -1
      33        
3288             && (($num_i < 2 && $c == ord(';'))
3289             || ($num_i == 2 && ($c == ord('m') || $c == ord('M')))))
3290             {
3291             # We're at a semi-colon, 'm', or 'M' and we have a number
3292 6         10 $num[$num_i] = $cur_num;
3293 6         8 ++$num_i;
3294 6         8 $cur_num = -1;
3295 6         6 $trail = $c;
3296 6         9 ++$i;
3297 6         18 next;
3298             }
3299            
3300             # Something else; not a mouse event
3301 0         0 return TB_ERR;
3302             }
3303            
3304             # If we didn't get to the 3rd number, we need more
3305 2 50       5 return TB_ERR_NEED_MORE if $num[2] == -1;
3306            
3307             # We have a valid mouse event, eat i bytes from the buffer
3308 2         3 $buf_shift = $i;
3309            
3310 2 100       6 $num[0] -= 0x20 if $type == TYPE_1015;
3311            
3312 2         4 local $_;
3313 2         7 switch: for ($num[0] & 3) {
3314 2 50       5 case: 0 == $_ and do {
3315 0 0       0 $event->{key} = ($num[0] & 64) ? TB_KEY_MOUSE_WHEEL_UP
3316             : TB_KEY_MOUSE_LEFT;
3317 0         0 last;
3318             };
3319 2 100       4 case: 1 == $_ and do {
3320 1 50       5 $event->{key} = ($num[0] & 64) ? TB_KEY_MOUSE_WHEEL_DOWN
3321             : TB_KEY_MOUSE_MIDDLE;
3322 1         2 last;
3323             };
3324 1 50       4 case: 2 == $_ and do {
3325 0         0 $event->{key} = TB_KEY_MOUSE_RIGHT;
3326 0         0 last;
3327             };
3328 1 50       4 case: 3 == $_ and do {
3329 1         2 $event->{key} = TB_KEY_MOUSE_RELEASE;
3330 1         3 last;
3331             };
3332             default: {
3333 0         0 return TB_ERR;
  0         0  
3334             };
3335             }
3336            
3337             # On xterm mouse release is signaled by lowercase m
3338 2 50       5 if ($trail == ord('m')) {
3339 0         0 $event->{key} = TB_KEY_MOUSE_RELEASE;
3340             }
3341            
3342 2 100       6 if (($num[0] & 32) != 0) {
3343 1         4 $event->{mod} |= TB_MOD_MOTION;
3344             }
3345            
3346 2 50       8 $event->{x} = ($num[1] - 1 < 0) ? 0 : $num[1] - 1;
3347 2 50       5 $event->{y} = ($num[2] - 1 < 0) ? 0 : $num[2] - 1;
3348 2         4 last;
3349             };
3350             }
3351            
3352 3 50       11 substr($in, 0, $buf_shift, '') if $buf_shift > 0;
3353            
3354 3         7 $event->{type} = TB_EVENT_MOUSE;
3355            
3356 3         47 return TB_OK;
3357             } #/ alias:
3358             }
3359            
3360             sub extract_esc_cap {
3361 3     3 0 11 state $sig = compile(
3362             _Object,
3363             );
3364 3         8 my ($event) = $sig->(@_);
3365 3         5 my $trie = $global->{cap_trie};
3366 3         7 alias: for my $in ($global->{inbuf}) {
3367 3         5 my $node;
3368             my $depth;
3369            
3370 3         10 my $rv = $trie->find($in, \$node, \$depth);
3371 3 50       6 return $rv if $rv != TB_OK;
3372            
3373 3 100 33     12 if ($node->{is_leaf}) {
    50          
3374             # Found a leaf node
3375 2         9 $event->{type} = TB_EVENT_KEY;
3376 2         3 $event->{ch} = 0;
3377 2         7 $event->{key} = $node->{key};
3378 2         4 $event->{mod} = $node->{mod};
3379 2         4 substr($in, 0, $depth, '');
3380 2         8 return TB_OK;
3381             }
3382             elsif ($node->{nchildren} > 0 && length($in) <= $depth) {
3383             # Found a branch node (not enough input)
3384 1         5 return TB_ERR_NEED_MORE;
3385             }
3386            
3387 0         0 return TB_ERR;
3388             } #/ alias:
3389             }
3390            
3391             #
3392             # Cell related helpers
3393             #
3394            
3395             sub cell_cmp { # $int ($a, $b)
3396 2     2 0 11 state $sig = compile(
3397             _Object,
3398             _Object,
3399             );
3400 2         8 my ($a, $b) = $sig->(@_);
3401 2 50       9 return $a->equal($b) ? 0 : 1;
3402             }
3403            
3404             sub cell_copy { # $int ($dst, $src)
3405 1     1 0 6 goto &Termbox::Cell::copy;
3406             }
3407            
3408             sub cell_set { # $int ($cell, \@ch, $nch, $fg, $bg)
3409 10     10 0 872 state $sig = compile(
3410             _Object,
3411             _ArrayRef,
3412             _PositiveOrZeroInt,
3413             _PositiveOrZeroInt,
3414             _PositiveOrZeroInt,
3415             );
3416 10         25 my ($cell, $ch, $nch, $fg, $bg) = $sig->(@_);
3417 8         88 return $cell->set(pack('U*', @$ch), $fg, $bg);
3418             }
3419            
3420             sub cell_reserve_ech { # $int ($cell, $n)
3421 1     1 0 6 state $sig = compile(
3422             _Object,
3423             _PositiveOrZeroInt,
3424             );
3425 1         4 my ($cell, $n) = $sig->(@_);
3426 1         3 return TB_OPT_EGC ? TB_OK : TB_ERR;
3427             }
3428            
3429             sub cell_free { # $int ($cell)
3430 7685     7685 0 6159 state $sig = compile(
3431             _Object,
3432             );
3433 7685         7649 my ($cell) = $sig->(@_);
3434 7685         9003 $cell->{ch} = "\0";
3435 7685         7332 $cell->{fg} = 0;
3436 7685         6977 $cell->{bg} = 0;
3437 7685         7099 return TB_OK;
3438             }
3439            
3440             #
3441             # Cell buffer related helpers
3442             #
3443            
3444             sub init_cellbuf { # $int ()
3445 3     3 0 146897 state $sig = compile();
3446 3         8 $sig->(@_);
3447 3         3 my $rv;
3448            
3449 3   33     23 $global->{back} ||= cellbuf->new();
3450 3   33     9 $global->{front} ||= cellbuf->new();
3451            
3452 3         15 $rv = $global->{back}->init($global->{width}, $global->{height});
3453 3 50       18 return $rv if $rv != TB_OK;
3454 3         20 $rv = $global->{front}->init($global->{width}, $global->{height});
3455 3 50       25 return $rv if $rv != TB_OK;
3456 3         44 $rv = $global->{back}->clear();
3457 3 50       35 return $rv if $rv != TB_OK;
3458 3         20 $rv = $global->{front}->clear();
3459 3 50       19 return $rv if $rv != TB_OK;
3460            
3461 3         17 return TB_OK;
3462             }
3463            
3464             sub cellbuf_init { # $int ($c, $w, $h)
3465 5     5 0 38 goto &cellbuf::init;
3466             }
3467            
3468             sub cellbuf_free { # $int ($c)
3469 11     11 0 21 state $sig = compile(
3470             _Object,
3471             );
3472 11         23 my ($c) = $sig->(@_);
3473 11 50       14 for my $cell (@{ $c->{cells} || [] }) {
  11         37  
3474 7684         7718 my $rv = cell_free($cell);
3475 7684 50       9384 return $rv if $rv != TB_OK;
3476             }
3477            
3478 11         23 $c->{width} = 0;
3479 11         20 $c->{height} = 0;
3480 11         1013 $c->{cells} = [];
3481 11         23 return TB_OK;
3482             }
3483            
3484             sub cellbuf_clear { # $int ($c)
3485 2     2 0 10 goto &cellbuf::clear;
3486             }
3487            
3488             sub cellbuf_get { # $int ($c, $x, $y, \$out)
3489 15     15 0 29 state $sig = compile(
3490             _Object,
3491             _PositiveOrZeroInt,
3492             _PositiveOrZeroInt,
3493             _Ref,
3494             );
3495 15         29 my ($c, $x, $y, $out) = $sig->(@_);
3496 15 100       43 return TB_ERR_OUT_OF_BOUNDS unless $c->in_bounds($x, $y);
3497 13         42 $$out = $c->get($x, $y);
3498 13 50       40 return defined $$out ? TB_OK : TB_ERR;
3499             }
3500            
3501             sub cellbuf_in_bounds { # $int ($c, $x, $y)
3502 2     2 0 30 goto &cellbuf::in_bounds;
3503             }
3504            
3505             sub cellbuf_resize { # $int ($c, $w, $h)
3506 1     1 0 7 goto &cellbuf::resize;
3507             }
3508            
3509             #
3510             # Sending output helpers
3511             #
3512            
3513             sub send_literal { # $int ($rv, $a)
3514 6     6 0 140514 state $sig = compile(
3515             _PositiveOrZeroInt,
3516             _Str,
3517             );
3518 6         9 my ($rv, $a) = $sig->(@_);
3519 2         5 $global->{outbuf} .= $a;
3520 2         5 return TB_OK;
3521             }
3522            
3523             sub send_num { # $int ($rv, \$buf, $n)
3524 2     2 0 1593 state $sig = compile(
3525             _PositiveOrZeroInt,
3526             _ScalarRef,
3527             _PositiveOrZeroInt,
3528             );
3529 2         4 my ($rv, $buf, $n) = $sig->(@_);
3530 2         9 $global->{outbuf} .= sprintf('%u', $n);
3531 2         4 return TB_OK;
3532             }
3533            
3534             sub send_init_escape_codes { # $int ()
3535 2     2 0 1439 state $sig = compile();
3536 2         4 $sig->(@_);
3537 2         3 my $rv;
3538 2         9 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_ENTER_CA]);
3539 1 50       3 return $rv if $rv != TB_OK;
3540 1         3 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_ENTER_KEYPAD]);
3541 1 50       2 return $rv if $rv != TB_OK;
3542 1         3 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_HIDE_CURSOR]);
3543 1 50       3 return $rv if $rv != TB_OK;
3544 1         3 return TB_OK;
3545             }
3546            
3547             sub send_clear { # $int ()
3548 1     1 0 358 state $sig = compile();
3549 1         2 $sig->(@_);
3550 1         1 my $rv;
3551            
3552 1         3 $rv = send_attr($global->{fg}, $global->{bg});
3553 0 0       0 return $rv if $rv != TB_OK;
3554 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_CLEAR_SCREEN]);
3555 0 0       0 return $rv if $rv != TB_OK;
3556            
3557 0         0 $rv = send_cursor_if($global->{cursor_x}, $global->{cursor_y});
3558 0 0       0 return $rv if $rv != TB_OK;
3559 0         0 $rv = bytebuf_flush(\$global->{outbuf}, $global->{wfd});
3560 0 0       0 return $rv if $rv != TB_OK;
3561            
3562 0         0 $global->{last_x} = -1;
3563 0         0 $global->{last_y} = -1;
3564            
3565 0         0 return TB_OK;
3566             }
3567            
3568             sub send_attr { # $int ($fg, $bg)
3569 3     3 0 943 state $sig = compile(
3570             _PositiveOrZeroInt,
3571             _PositiveOrZeroInt,
3572             );
3573 3         8 my ($fg, $bg) = $sig->(@_);
3574 3         5 my $rv;
3575            
3576 3 100 66     32 if ($fg == $global->{last_fg} && $bg == $global->{last_bg}) {
3577 1         3 return TB_OK;
3578             }
3579            
3580 2         5 $rv = bytebuf_puts($global->{outbuf}, $global->{caps}[TB_CAP_SGR0]);
3581 0 0       0 return $rv if $rv != TB_OK;
3582            
3583 0         0 my ($cfg, $cbg);
3584 0         0 switch: for ($global->{output_mode}) {
3585             DEFAULT:
3586 0 0       0 case: TB_OUTPUT_NORMAL == $_ and do {
3587             # The minus 1 below is because our colors are 1-indexed starting
3588             # from black. Black is represented by a 30, 40, 90, or 100 for fg,
3589             # bg, bright fg, or bright bg respectively. Red is 31, 41, 91,
3590             # 101, etc.
3591 0 0       0 $cfg = ($fg & TB_BRIGHT ? 90 : 30) + ($fg & 0x0f) - 1;
3592 0 0       0 $cbg = ($bg & TB_BRIGHT ? 100 : 40) + ($bg & 0x0f) - 1;
3593 0         0 last;
3594 0         0 };
3595 0 0       0 case: TB_OUTPUT_256 == $_ and do {
3596 0         0 $cfg = $fg & 0xff;
3597 0         0 $cbg = $bg & 0xff;
3598 0 0       0 $cfg = 0 if $fg & TB_HI_BLACK;
3599 0 0       0 $cbg = 0 if $bg & TB_HI_BLACK;
3600 0         0 last;
3601             };
3602 0 0       0 case: TB_OUTPUT_216 == $_ and do {
3603 0         0 $cfg = $fg & 0xff;
3604 0         0 $cbg = $bg & 0xff;
3605 0 0       0 $cfg = 216 if $cfg > 216;
3606 0 0       0 $cbg = 216 if $cbg > 216;
3607 0         0 $cfg += 0x0f;
3608 0         0 $cbg += 0x0f;
3609 0         0 last;
3610             };
3611 0 0       0 case: TB_OUTPUT_GRAYSCALE == $_ and do {
3612 0         0 $cfg = $fg & 0xff;
3613 0         0 $cbg = $bg & 0xff;
3614 0 0       0 $cfg = 24 if $cfg > 24;
3615 0 0       0 $cbg = 24 if $cbg > 24;
3616 0         0 $cfg += 0xe7;
3617 0         0 $cbg += 0xe7;
3618 0         0 last;
3619             };
3620 0 0       0 case: TB_OPT_ATTR_W >= 32 && TB_OUTPUT_TRUECOLOR == $_ and do {
3621 0         0 $cfg = $fg & 0xffffff;
3622 0         0 $cbg = $bg & 0xffffff;
3623 0 0       0 $cfg = 0 if $fg & TB_HI_BLACK;
3624 0 0       0 $cbg = 0 if $bg & TB_HI_BLACK;
3625 0         0 last;
3626             };
3627             default: {
3628 0         0 $_ = TB_OUTPUT_NORMAL;
  0         0  
3629 0         0 goto DEFAULT;
3630             };
3631             }
3632            
3633 0 0       0 if ($fg & TB_BOLD) {
3634 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_BOLD]);
3635 0 0       0 return $rv if $rv != TB_OK;
3636             }
3637 0 0       0 if ($fg & TB_BLINK) {
3638 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_BLINK]);
3639 0 0       0 return $rv if $rv != TB_OK;
3640             }
3641 0 0       0 if ($fg & TB_UNDERLINE) {
3642 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_UNDERLINE]);
3643 0 0       0 return $rv if $rv != TB_OK;
3644             }
3645 0 0       0 if ($fg & TB_ITALIC) {
3646 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_ITALIC]);
3647 0 0       0 return $rv if $rv != TB_OK;
3648             }
3649 0 0       0 if ($fg & TB_DIM) {
3650 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_DIM]);
3651 0 0       0 return $rv if $rv != TB_OK;
3652             }
3653 0         0 if (TB_OPT_ATTR_W == 64) {
3654 0 0       0 if ($fg & TB_STRIKEOUT) {
3655             $rv = bytebuf_puts(\$global->{outbuf},
3656 0         0 $global->{caps}[TB_HARDCAP_STRIKEOUT]);
3657 0 0       0 return $rv if $rv != TB_OK;
3658             }
3659 0 0       0 if ($fg & TB_UNDERLINE_2) {
3660             $rv = bytebuf_puts(\$global->{outbuf},
3661 0         0 $global->{caps}[TB_HARDCAP_UNDERLINE_2]);
3662 0 0       0 return $rv if $rv != TB_OK;
3663             }
3664 0 0       0 if ($fg & TB_OVERLINE) {
3665             $rv = bytebuf_puts(\$global->{outbuf},
3666 0         0 $global->{caps}[TB_HARDCAP_OVERLINE]);
3667 0 0       0 return $rv if $rv != TB_OK;
3668             }
3669 0 0       0 if ($fg & TB_INVISIBLE) {
3670 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_INVISIBLE]);
3671 0 0       0 return $rv if $rv != TB_OK;
3672             }
3673             }
3674            
3675 0 0 0     0 if (($fg & TB_REVERSE) || ($bg & TB_REVERSE)) {
3676 0         0 $rv = bytebuf_puts(\$global->{outbuf}, $global->{caps}[TB_CAP_REVERSE]);
3677 0 0       0 return $rv if $rv != TB_OK;
3678             }
3679 0         0 my $fg_is_default = ($fg & 0xff) == 0;
3680 0         0 my $bg_is_default = ($bg & 0xff) == 0;
3681 0 0       0 if ($global->{output_mode} == TB_OUTPUT_256) {
3682 0 0       0 $fg_is_default = 0 if $fg & TB_HI_BLACK;
3683 0 0       0 $bg_is_default = 0 if $bg & TB_HI_BLACK;
3684             }
3685 0         0 if (TB_OPT_ATTR_W >= 32) {
3686 0 0       0 if ($global->{output_mode} == TB_OUTPUT_TRUECOLOR) {
3687 0   0     0 $fg_is_default = (($fg & 0xffffff) == 0) && (($fg & TB_HI_BLACK) == 0);
3688 0   0     0 $bg_is_default = (($bg & 0xffffff) == 0) && (($bg & TB_HI_BLACK) == 0);
3689             }
3690             }
3691            
3692 0         0 $rv = send_sgr($cfg, $cbg, $fg_is_default, $bg_is_default);
3693 0 0       0 return $rv if $rv != TB_OK;
3694            
3695 0         0 $global->{last_fg} = $fg;
3696 0         0 $global->{last_bg} = $bg;
3697            
3698 0         0 return TB_OK;
3699             }
3700            
3701             sub send_sgr { # $int ($cfg, $cbg, $fg_is_default, $bg_is_default)
3702 2     2 0 944 state $sig = compile(
3703             _PositiveOrZeroInt,
3704             _PositiveOrZeroInt,
3705             _Bool,
3706             _Bool,
3707             );
3708 2         6 my ($cfg, $cbg, $fg_is_default, $bg_is_default) = $sig->(@_);
3709 2         3 my $rv;
3710 2         3 my $nbuf = '';
3711            
3712 2 50 33     8 return TB_OK if $fg_is_default && $bg_is_default;
3713            
3714 2         5 switch: for ($global->{output_mode}) {
3715             DEFAULT:
3716 3 100       34 case: TB_OUTPUT_NORMAL == $_ and do {
3717 2         5 send_literal($rv, "\x1b[");
3718 0 0       0 if (!$fg_is_default) {
3719 0         0 send_num($rv, \$nbuf, $cfg);
3720 0 0       0 if (!$bg_is_default) {
3721 0         0 send_literal($rv, ";");
3722             }
3723             }
3724 0 0       0 if (!$bg_is_default) {
3725 0         0 send_num($rv, \$nbuf, $cbg);
3726             }
3727 0         0 send_literal($rv, "m");
3728 0         0 last;
3729 3         3 };
3730             case: TB_OUTPUT_256 == $_ ||
3731             TB_OUTPUT_216 == $_ ||
3732             TB_OUTPUT_GRAYSCALE == $_ and do
3733 1 50 33     7 {
      33        
3734 0         0 send_literal($rv, "\x1b[");
3735 0 0       0 if (!$fg_is_default) {
3736 0         0 send_literal($rv, "38;5;");
3737 0         0 send_num($rv, \$nbuf, $cfg);
3738 0 0       0 if (!$bg_is_default) {
3739 0         0 send_literal($rv, ";");
3740             }
3741             }
3742 0 0       0 if (!$bg_is_default) {
3743 0         0 send_literal($rv, "48;5;");
3744 0         0 send_num($rv, \$nbuf, $cbg);
3745             }
3746 0         0 send_literal($rv, "m");
3747 0         0 last;
3748             };
3749 1 50       3 case: TB_OPT_ATTR_W >= 32 && TB_OUTPUT_TRUECOLOR == $_ and do {
3750 0         0 send_literal($rv, "\x1b[");
3751 0 0       0 if (!$fg_is_default) {
3752 0         0 send_literal($rv, "38;2;");
3753 0         0 send_num($rv, \$nbuf, ($cfg >> 16) & 0xff);
3754 0         0 send_literal($rv, ";");
3755 0         0 send_num($rv, \$nbuf, ($cfg >> 8) & 0xff);
3756 0         0 send_literal($rv, ";");
3757 0         0 send_num($rv, \$nbuf, $cfg & 0xff);
3758 0 0       0 if (!$bg_is_default) {
3759 0         0 send_literal($rv, ";");
3760             }
3761             }
3762 0 0       0 if (!$bg_is_default) {
3763 0         0 send_literal($rv, "48;2;");
3764 0         0 send_num($rv, \$nbuf, ($cbg >> 16) & 0xff);
3765 0         0 send_literal($rv, ";");
3766 0         0 send_num($rv, \$nbuf, ($cbg >> 8) & 0xff);
3767 0         0 send_literal($rv, ";");
3768 0         0 send_num($rv, \$nbuf, $cbg & 0xff);
3769             }
3770 0         0 send_literal($rv, "m");
3771 0         0 last;
3772             };
3773             default: {
3774 1         2 $_ = TB_OUTPUT_NORMAL;
  1         1  
3775 1         46 goto DEFAULT;
3776             };
3777             }
3778 0         0 return TB_OK;
3779             }
3780            
3781             sub send_cursor_if { # $int ($x, $y)
3782 2     2 0 975 state $sig = compile(
3783             _Int,
3784             _Int,
3785             );
3786 2         6 my ($x, $y) = $sig->(@_);
3787 2         3 my $rv;
3788 2         4 my $nbuf = '';
3789 2 50 33     10 return TB_OK if $x < 0 || $y < 0;
3790 2         4 send_literal($rv, "\x1b[");
3791 0         0 send_num($rv, \$nbuf, $y + 1);
3792 0         0 send_literal($rv, ";");
3793 0         0 send_num($rv, \$nbuf, $x + 1);
3794 0         0 send_literal($rv, "H");
3795 0         0 return TB_OK;
3796             }
3797            
3798             sub send_char { # $int ($x, $y, $ch)
3799 0     0 0 0 state $sig = compile(
3800             _Int,
3801             _Int,
3802             _PositiveOrZeroInt,
3803             );
3804 0         0 my ($x, $y, $ch) = $sig->(@_);
3805 0         0 return send_cluster($x, $y, [$ch], 1);
3806             }
3807            
3808             sub send_cluster { # $int ($x, $y, \@ch, $nch)
3809 0     0 0 0 state $sig = compile(
3810             _Int,
3811             _Int,
3812             _ArrayRef,
3813             _PositiveOrZeroInt,
3814             );
3815 0         0 my ($x, $y, $ch, $nch) = $sig->(@_);
3816            
3817 0 0 0     0 if ($global->{last_x} != $x - 1 || $global->{last_y} != $y) {
3818 0         0 my $rv = send_cursor_if($x, $y);
3819 0 0       0 return $rv if $rv != TB_OK;
3820             }
3821 0         0 $global->{last_x} = $x;
3822 0         0 $global->{last_y} = $y;
3823            
3824 0         0 foreach my $ch32 (@$ch) {
3825 0 0       0 if (!tb_iswprint_ex($ch32, undef)) {
3826 0         0 $ch32 = 0xfffd; # replace non-printable codepoints with U+FFFD
3827             }
3828 0         0 my $cu8_len = tb_utf8_unicode_to_char(\my $chu8, $ch32);
3829 0         0 my $rv = bytebuf_nputs(\$global->{outbuf}, $chu8, $cu8_len);
3830 0 0       0 return $rv if $rv != TB_OK;
3831             }
3832            
3833 0         0 return TB_OK;
3834             }
3835            
3836             sub convert_num { # $len ($num, \$buf)
3837 0     0 0 0 state $sig = compile(
3838             _PositiveOrZeroInt,
3839             _ScalarRef,
3840             );
3841 0         0 my ($num, $buf) = $sig->(@_);
3842 0         0 $$buf = sprintf('%u', $num);
3843 0         0 return bytes::length($$buf);
3844             }
3845            
3846             #
3847             # Byte buffer related helpers
3848             #
3849            
3850             sub bytebuf_puts { # $int (\$buf, $str)
3851 10     10 0 153731 state $sig = compile(
3852             _ScalarRef,
3853             _Str,
3854             );
3855 10         18 my ($b, $str) = $sig->(@_);
3856             # Nothing to do for empty caps
3857 5 100       17 $$b .= $str if bytes::length($str);
3858 5         45 return TB_OK
3859             }
3860            
3861             sub bytebuf_nputs { # $int (\$buf, $str, $nstr)
3862 1     1 0 4 state $sig = compile(
3863             _ScalarRef,
3864             _Str,
3865             _PositiveOrZeroInt,
3866             );
3867 1         4 my ($b, $str, $nstr) = $sig->(@_);
3868 1         5 $$b .= bytes::substr($str, 0, $nstr);
3869 1         8 return TB_OK;
3870             }
3871            
3872             sub bytebuf_shift { # $int (\$buf, $n)
3873 3     3 0 3491 state $sig = compile(
3874             _ScalarRef,
3875             _PositiveOrZeroInt,
3876             );
3877 3         8 my ($b, $n) = $sig->(@_);
3878 37     37   127309 use bytes;
  37         58  
  37         126  
3879 3 100       10 $n = length($$b) if $n > length($$b);
3880 3         9 substr($$b, 0, $n, '');
3881 3         9 return TB_OK;
3882             }
3883            
3884             sub bytebuf_flush { # $int (\$buf, $fd)
3885 4     4 0 11080 state $sig = compile(
3886             _ScalarRef,
3887             _Int,
3888             );
3889 4         9 my ($b, $fd) = $sig->(@_);
3890 37     37   3396 use bytes;
  37         52  
  37         106  
3891 4 100       18 return TB_OK unless length($$b);
3892 2         4 my $want = length($$b);
3893 2   50     88 my $wrote = POSIX::write($fd // -1, $$b, $want);
3894 2 100 66     16 if (!defined($wrote) || $wrote != $want) {
3895 1         7 $global->{last_errno} = 0+ $!;
3896 1         4 return TB_ERR;
3897             }
3898 1         3 $$b = '';
3899 1         6 return TB_OK;
3900             }
3901            
3902             sub bytebuf_reserve { # $int (\$buf, $sz)
3903 1     1 0 3099 state $sig = compile(
3904             _ScalarRef,
3905             _PositiveOrZeroInt,
3906             );
3907 1         3 $sig->(@_);
3908 1         4 return TB_OK;
3909             }
3910            
3911             sub bytebuf_free { # $int (\$buf)
3912 12     12 0 760 state $sig = compile(
3913             _ScalarRef,
3914             );
3915 12         24 my ($b) = $sig->(@_);
3916 11         21 $$b = '';
3917 11         115 return TB_OK;
3918             }
3919            
3920             # Hack to make 'use Termbox' work without an actual Termbox.pm file.
3921             $INC{"Termbox.pm"} = __FILE__;