From 5d388d765fcbb81c4600b74c468ccd1da5b2ecdc Mon Sep 17 00:00:00 2001 From: Adam Saponara Date: Mon, 6 Sep 2021 21:53:45 -0400 Subject: [PATCH] termbox rewrite (wip) --- .gitattributes | 1 + LICENSE | 19 + Makefile | 37 + codegen.sh | 172 ++ demo/keyboard.c | 762 +++++++ termbox.h | 2500 +++++++++++++++++++++ tests/Dockerfile | 14 + tests/run.sh | 126 ++ tests/test_basic/expected.ansi | 24 + tests/test_basic/test.php | 36 + tests/test_ffi.php | 97 + tests/test_non_spacing_mark/expected.ansi | 24 + tests/test_non_spacing_mark/test.php | 11 + 13 files changed, 3823 insertions(+) create mode 100644 .gitattributes create mode 100644 LICENSE create mode 100644 Makefile create mode 100755 codegen.sh create mode 100644 demo/keyboard.c create mode 100644 termbox.h create mode 100644 tests/Dockerfile create mode 100755 tests/run.sh create mode 100644 tests/test_basic/expected.ansi create mode 100755 tests/test_basic/test.php create mode 100644 tests/test_ffi.php create mode 100644 tests/test_non_spacing_mark/expected.ansi create mode 100755 tests/test_non_spacing_mark/test.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a998924 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.ansi binary diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0212b7c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 termbox developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..02b206c --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +prefix?=/usr/local + +termbox_cflags:=-std=c99 -Wall -Wextra -pedantic -Wno-unused-result -g -O0 -D_XOPEN_SOURCE -D_DEFAULT_SOURCE $(CFLAGS) +termbox_demos:=$(patsubst demo/%.c,demo/%,$(wildcard demo/*.c)) +termbox_so:=libtermbox.so +termbox_o:=termbox.o +termbox_h:=termbox.h + +all: $(termbox_demos) + +$(termbox_demos): %: %.c + $(CC) -DTB_OPT_TRUECOLOR -DTB_OPT_EGC $(termbox_cflags) $^ -o $@ + +$(termbox_o): + $(CC) -DTB_IMPL -DTB_OPT_TRUECOLOR -DTB_OPT_EGC -fPIC -xc -c $(termbox_cflags) $(termbox_h) -o $@ + +$(termbox_so): $(termbox_o) + $(CC) -shared $(termbox_o) -o $@ + +terminfo: + awk -vg=0 'g==0{print} /BEGIN codegen h/{g=1; system("./codegen.sh h")} /END codegen h/{g=0; print} g==1{next}' termbox.h >termbox.h.tmp && mv -vf termbox.h.tmp termbox.h + awk -vg=0 'g==0{print} /BEGIN codegen c/{g=1; system("./codegen.sh c")} /END codegen c/{g=0; print} g==1{next}' termbox.h >termbox.h.tmp && mv -vf termbox.h.tmp termbox.h + +test: $(termbox_so) + docker build -f tests/Dockerfile . + +test_local: $(termbox_so) + ./tests/run.sh + +install: + install -d $(DESTDIR)$(prefix)/include + install -p -m 644 $(termbox_h) $(DESTDIR)$(prefix)/include/$(termbox_h) + +clean: + rm -f $(termbox_demos) $(termbox_o) $(termbox_so) tests/**/observed.ansi + +.PHONY: all terminfo test test_local install clean diff --git a/codegen.sh b/codegen.sh new file mode 100755 index 0000000..dacedec --- /dev/null +++ b/codegen.sh @@ -0,0 +1,172 @@ +#!/bin/bash +set -uo pipefail + +read -r -d '' builtin_terms <<'EOD' + xterm + linux + screen tmux + rxvt-256color + rxvt-unicode rxvt + Eterm +EOD + +read -r -d '' terminfo_keys <<'EOD' + kf1 F1 + kf2 F2 + kf3 F3 + kf4 F4 + kf5 F5 + kf6 F6 + kf7 F7 + kf8 F8 + kf9 F9 + kf10 F10 + kf11 F11 + kf12 F12 + kich1 INSERT + kdch1 DELETE + khome HOME + kend END + kpp PGUP + knp PGDN + kcuu1 ARROW_UP + kcud1 ARROW_DOWN + kcub1 ARROW_LEFT + kcuf1 ARROW_RIGHT +EOD + +read -r -d '' terminfo_funcs <<'EOD' + smcup ENTER_CA + rmcup EXIT_CA + cnorm SHOW_CURSOR + civis HIDE_CURSOR + clear CLEAR_SCREEN + sgr0 SGR0 + smul UNDERLINE + bold BOLD + blink BLINK + sitm ITALIC + rev REVERSE + smkx ENTER_KEYPAD + rmkx EXIT_KEYPAD +EOD + +read -r -d '' extra_keys <<'EOD' + MOUSE_LEFT + MOUSE_RIGHT + MOUSE_MIDDLE + MOUSE_RELEASE + MOUSE_WHEEL_UP + MOUSE_WHEEL_DOWN +EOD + +main() { + local IFS=$'\n' + local codegen_type=$1 + + # codegen terminfo_cap_indexes + # codegen #define TB_CAP_* + # codegen #define TB_KEY_* + c_cap_indexes='static const int16_t terminfo_cap_indexes[] = {'$'\n' + c_cap_defines='' + c_cap_num=0 + c_key_defines='' + c_key_num=0 + for terminfo_cap_tuple in $terminfo_keys; do + string_name=$(awk '{print $1}' <<<"$terminfo_cap_tuple") + define_name=$(awk '{print $2}' <<<"$terminfo_cap_tuple") + string_index=$(terminfo_string_index $string_name) + c_cap_indexes+=" $string_index, // $string_name (TB_CAP_${define_name})"$'\n' + c_cap_defines+="#define TB_CAP_${define_name} $c_cap_num"$'\n' + c_key_defines+="#define TB_KEY_${define_name} (0xffff - $c_key_num)"$'\n' + let c_cap_num+=1 + let c_key_num+=1 + done + c_cap_defines+="#define TB_CAP__COUNT_KEYS $c_cap_num"$'\n' + for extra_keys_tuple in $extra_keys; do + define_name=$(awk '{print $1}' <<<"$extra_keys_tuple") + c_key_defines+="#define TB_KEY_${define_name} (0xffff - $c_key_num)"$'\n' + let c_key_num+=1 + done + for terminfo_cap_tuple in $terminfo_funcs; do + string_name=$(awk '{print $1}' <<<"$terminfo_cap_tuple") + define_name=$(awk '{print $2}' <<<"$terminfo_cap_tuple") + string_index=$(terminfo_string_index $string_name) + c_cap_indexes+=" $string_index, // $string_name (TB_CAP_${define_name})"$'\n' + c_cap_defines+="#define TB_CAP_${define_name} $c_cap_num"$'\n' + let c_cap_num+=1 + done + c_cap_defines+="#define TB_CAP__COUNT $c_cap_num"$'\n' + c_cap_indexes+='};' + + c_codegen_comment="/* Produced by $0 on $(date -uR) */" + + if [ "$codegen_type" == "c" ]; then + echo "$c_codegen_comment" + echo + echo "$c_cap_indexes" + echo + + # codegen built-in _keys and _funcs as _caps + c_term_name_cap='static struct {'$'\n' + c_term_name_cap+=' const char *name;'$'\n' + c_term_name_cap+=' const char **caps;'$'\n' + c_term_name_cap+=' const char *alias;'$'\n' + c_term_name_cap+='} builtin_terms[] = {'$'\n' + for builtin_terms_tuple in $builtin_terms; do + term_name=$(awk '{print $1}' <<<"$builtin_terms_tuple") + term_alias=$(awk '{print $2}' <<<"$builtin_terms_tuple") + c_term_name=$(tr -d '\n' <<<$term_name | tr -c 'A-Za-z0-9' '_' | tr 'A-Z' 'a-z') + + c_term_caps="static const char *${c_term_name}_caps[] = {"$'\n' + for terminfo_cap_tuple in $terminfo_keys $terminfo_funcs; do + string_name=$(awk '{print $1}' <<<"$terminfo_cap_tuple") + define_name=$(awk '{print $2}' <<<"$terminfo_cap_tuple") + c_string_literal=$(terminfo_string_literal $term_name $string_name) + c_term_caps+=" ${c_string_literal}, // $string_name (TB_CAP_${define_name})"$'\n' + done + c_term_caps+='};' + + echo "// $term_name" + echo "$c_term_caps" + echo + + c_term_name_cap+=" { \"${term_name}\", ${c_term_name}_caps, \"${term_alias}\" },"$'\n' + done + c_term_name_cap+=' { NULL, NULL, NULL },'$'\n' + c_term_name_cap+='};'$'\n' + + echo "$c_term_name_cap" + + elif [ "$codegen_type" == "h" ]; then + + echo "$c_codegen_comment" + echo -n "$c_key_defines" + echo + echo -n "$c_cap_defines" + + fi +} + +terminfo_string_index() { + local string_name=$1 + infocmp -E | grep -w $string_name | awk '{print $2}' | sed 's|:$||g' +} + +terminfo_string_literal() { + local term_name=$1 + local string_name=$2 + local var_name=$(infocmp -E $term_name | grep -w $string_name | awk '{print $5}' | sed 's|,$||g') + if [ "$var_name" == "ABSENT_STRING" ]; then + echo '""' + return 0 + fi + local c_string_literal=$(infocmp -E $term_name | grep -w "${var_name}" | grep ' = ' | awk '{print $NF}' | sed 's|;$||g') + if [ -z "$c_string_literal" ]; then + echo '""' + return 0 + fi + echo $c_string_literal +} + +main "$@" diff --git a/demo/keyboard.c b/demo/keyboard.c new file mode 100644 index 0000000..d7906d1 --- /dev/null +++ b/demo/keyboard.c @@ -0,0 +1,762 @@ +#include +#include +#include +#include +#define TB_IMPL +#include "../termbox.h" + +struct key { + unsigned char x; + unsigned char y; + uint32_t ch; +}; + +#define STOP {0,0,0} +struct key K_ESC[] = {{1,1,'E'},{2,1,'S'},{3,1,'C'},STOP}; +struct key K_F1[] = {{6,1,'F'},{7,1,'1'},STOP}; +struct key K_F2[] = {{9,1,'F'},{10,1,'2'},STOP}; +struct key K_F3[] = {{12,1,'F'},{13,1,'3'},STOP}; +struct key K_F4[] = {{15,1,'F'},{16,1,'4'},STOP}; +struct key K_F5[] = {{19,1,'F'},{20,1,'5'},STOP}; +struct key K_F6[] = {{22,1,'F'},{23,1,'6'},STOP}; +struct key K_F7[] = {{25,1,'F'},{26,1,'7'},STOP}; +struct key K_F8[] = {{28,1,'F'},{29,1,'8'},STOP}; +struct key K_F9[] = {{33,1,'F'},{34,1,'9'},STOP}; +struct key K_F10[] = {{36,1,'F'},{37,1,'1'},{38,1,'0'},STOP}; +struct key K_F11[] = {{40,1,'F'},{41,1,'1'},{42,1,'1'},STOP}; +struct key K_F12[] = {{44,1,'F'},{45,1,'1'},{46,1,'2'},STOP}; +struct key K_PRN[] = {{50,1,'P'},{51,1,'R'},{52,1,'N'},STOP}; +struct key K_SCR[] = {{54,1,'S'},{55,1,'C'},{56,1,'R'},STOP}; +struct key K_BRK[] = {{58,1,'B'},{59,1,'R'},{60,1,'K'},STOP}; +struct key K_LED1[] = {{66,1,'-'},STOP}; +struct key K_LED2[] = {{70,1,'-'},STOP}; +struct key K_LED3[] = {{74,1,'-'},STOP}; + +struct key K_TILDE[] = {{1,4,'`'},STOP}; +struct key K_TILDE_SHIFT[] = {{1,4,'~'},STOP}; +struct key K_1[] = {{4,4,'1'},STOP}; +struct key K_1_SHIFT[] = {{4,4,'!'},STOP}; +struct key K_2[] = {{7,4,'2'},STOP}; +struct key K_2_SHIFT[] = {{7,4,'@'},STOP}; +struct key K_3[] = {{10,4,'3'},STOP}; +struct key K_3_SHIFT[] = {{10,4,'#'},STOP}; +struct key K_4[] = {{13,4,'4'},STOP}; +struct key K_4_SHIFT[] = {{13,4,'$'},STOP}; +struct key K_5[] = {{16,4,'5'},STOP}; +struct key K_5_SHIFT[] = {{16,4,'%'},STOP}; +struct key K_6[] = {{19,4,'6'},STOP}; +struct key K_6_SHIFT[] = {{19,4,'^'},STOP}; +struct key K_7[] = {{22,4,'7'},STOP}; +struct key K_7_SHIFT[] = {{22,4,'&'},STOP}; +struct key K_8[] = {{25,4,'8'},STOP}; +struct key K_8_SHIFT[] = {{25,4,'*'},STOP}; +struct key K_9[] = {{28,4,'9'},STOP}; +struct key K_9_SHIFT[] = {{28,4,'('},STOP}; +struct key K_0[] = {{31,4,'0'},STOP}; +struct key K_0_SHIFT[] = {{31,4,')'},STOP}; +struct key K_MINUS[] = {{34,4,'-'},STOP}; +struct key K_MINUS_SHIFT[] = {{34,4,'_'},STOP}; +struct key K_EQUALS[] = {{37,4,'='},STOP}; +struct key K_EQUALS_SHIFT[] = {{37,4,'+'},STOP}; +struct key K_BACKSLASH[] = {{40,4,'\\'},STOP}; +struct key K_BACKSLASH_SHIFT[] = {{40,4,'|'},STOP}; +struct key K_BACKSPACE[] = {{44,4,0x2190},{45,4,0x2500},{46,4,0x2500},STOP}; +struct key K_INS[] = {{50,4,'I'},{51,4,'N'},{52,4,'S'},STOP}; +struct key K_HOM[] = {{54,4,'H'},{55,4,'O'},{56,4,'M'},STOP}; +struct key K_PGU[] = {{58,4,'P'},{59,4,'G'},{60,4,'U'},STOP}; +struct key K_K_NUMLOCK[] = {{65,4,'N'},STOP}; +struct key K_K_SLASH[] = {{68,4,'/'},STOP}; +struct key K_K_STAR[] = {{71,4,'*'},STOP}; +struct key K_K_MINUS[] = {{74,4,'-'},STOP}; + +struct key K_TAB[] = {{1,6,'T'},{2,6,'A'},{3,6,'B'},STOP}; +struct key K_q[] = {{6,6,'q'},STOP}; +struct key K_Q[] = {{6,6,'Q'},STOP}; +struct key K_w[] = {{9,6,'w'},STOP}; +struct key K_W[] = {{9,6,'W'},STOP}; +struct key K_e[] = {{12,6,'e'},STOP}; +struct key K_E[] = {{12,6,'E'},STOP}; +struct key K_r[] = {{15,6,'r'},STOP}; +struct key K_R[] = {{15,6,'R'},STOP}; +struct key K_t[] = {{18,6,'t'},STOP}; +struct key K_T[] = {{18,6,'T'},STOP}; +struct key K_y[] = {{21,6,'y'},STOP}; +struct key K_Y[] = {{21,6,'Y'},STOP}; +struct key K_u[] = {{24,6,'u'},STOP}; +struct key K_U[] = {{24,6,'U'},STOP}; +struct key K_i[] = {{27,6,'i'},STOP}; +struct key K_I[] = {{27,6,'I'},STOP}; +struct key K_o[] = {{30,6,'o'},STOP}; +struct key K_O[] = {{30,6,'O'},STOP}; +struct key K_p[] = {{33,6,'p'},STOP}; +struct key K_P[] = {{33,6,'P'},STOP}; +struct key K_LSQB[] = {{36,6,'['},STOP}; +struct key K_LCUB[] = {{36,6,'{'},STOP}; +struct key K_RSQB[] = {{39,6,']'},STOP}; +struct key K_RCUB[] = {{39,6,'}'},STOP}; +struct key K_ENTER[] = { + {43,6,0x2591},{44,6,0x2591},{45,6,0x2591},{46,6,0x2591}, + {43,7,0x2591},{44,7,0x2591},{45,7,0x21B5},{46,7,0x2591}, + {41,8,0x2591},{42,8,0x2591},{43,8,0x2591},{44,8,0x2591}, + {45,8,0x2591},{46,8,0x2591},STOP +}; +struct key K_DEL[] = {{50,6,'D'},{51,6,'E'},{52,6,'L'},STOP}; +struct key K_END[] = {{54,6,'E'},{55,6,'N'},{56,6,'D'},STOP}; +struct key K_PGD[] = {{58,6,'P'},{59,6,'G'},{60,6,'D'},STOP}; +struct key K_K_7[] = {{65,6,'7'},STOP}; +struct key K_K_8[] = {{68,6,'8'},STOP}; +struct key K_K_9[] = {{71,6,'9'},STOP}; +struct key K_K_PLUS[] = {{74,6,' '},{74,7,'+'},{74,8,' '},STOP}; + +struct key K_CAPS[] = {{1,8,'C'},{2,8,'A'},{3,8,'P'},{4,8,'S'},STOP}; +struct key K_a[] = {{7,8,'a'},STOP}; +struct key K_A[] = {{7,8,'A'},STOP}; +struct key K_s[] = {{10,8,'s'},STOP}; +struct key K_S[] = {{10,8,'S'},STOP}; +struct key K_d[] = {{13,8,'d'},STOP}; +struct key K_D[] = {{13,8,'D'},STOP}; +struct key K_f[] = {{16,8,'f'},STOP}; +struct key K_F[] = {{16,8,'F'},STOP}; +struct key K_g[] = {{19,8,'g'},STOP}; +struct key K_G[] = {{19,8,'G'},STOP}; +struct key K_h[] = {{22,8,'h'},STOP}; +struct key K_H[] = {{22,8,'H'},STOP}; +struct key K_j[] = {{25,8,'j'},STOP}; +struct key K_J[] = {{25,8,'J'},STOP}; +struct key K_k[] = {{28,8,'k'},STOP}; +struct key K_K[] = {{28,8,'K'},STOP}; +struct key K_l[] = {{31,8,'l'},STOP}; +struct key K_L[] = {{31,8,'L'},STOP}; +struct key K_SEMICOLON[] = {{34,8,';'},STOP}; +struct key K_PARENTHESIS[] = {{34,8,':'},STOP}; +struct key K_QUOTE[] = {{37,8,'\''},STOP}; +struct key K_DOUBLEQUOTE[] = {{37,8,'"'},STOP}; +struct key K_K_4[] = {{65,8,'4'},STOP}; +struct key K_K_5[] = {{68,8,'5'},STOP}; +struct key K_K_6[] = {{71,8,'6'},STOP}; + +struct key K_LSHIFT[] = {{1,10,'S'},{2,10,'H'},{3,10,'I'},{4,10,'F'},{5,10,'T'},STOP}; +struct key K_z[] = {{9,10,'z'},STOP}; +struct key K_Z[] = {{9,10,'Z'},STOP}; +struct key K_x[] = {{12,10,'x'},STOP}; +struct key K_X[] = {{12,10,'X'},STOP}; +struct key K_c[] = {{15,10,'c'},STOP}; +struct key K_C[] = {{15,10,'C'},STOP}; +struct key K_v[] = {{18,10,'v'},STOP}; +struct key K_V[] = {{18,10,'V'},STOP}; +struct key K_b[] = {{21,10,'b'},STOP}; +struct key K_B[] = {{21,10,'B'},STOP}; +struct key K_n[] = {{24,10,'n'},STOP}; +struct key K_N[] = {{24,10,'N'},STOP}; +struct key K_m[] = {{27,10,'m'},STOP}; +struct key K_M[] = {{27,10,'M'},STOP}; +struct key K_COMMA[] = {{30,10,','},STOP}; +struct key K_LANB[] = {{30,10,'<'},STOP}; +struct key K_PERIOD[] = {{33,10,'.'},STOP}; +struct key K_RANB[] = {{33,10,'>'},STOP}; +struct key K_SLASH[] = {{36,10,'/'},STOP}; +struct key K_QUESTION[] = {{36,10,'?'},STOP}; +struct key K_RSHIFT[] = {{42,10,'S'},{43,10,'H'},{44,10,'I'},{45,10,'F'},{46,10,'T'},STOP}; +struct key K_ARROW_UP[] = {{54,10,'('},{55,10,0x2191},{56,10,')'},STOP}; +struct key K_K_1[] = {{65,10,'1'},STOP}; +struct key K_K_2[] = {{68,10,'2'},STOP}; +struct key K_K_3[] = {{71,10,'3'},STOP}; +struct key K_K_ENTER[] = {{74,10,0x2591},{74,11,0x2591},{74,12,0x2591},STOP}; + +struct key K_LCTRL[] = {{1,12,'C'},{2,12,'T'},{3,12,'R'},{4,12,'L'},STOP}; +struct key K_LWIN[] = {{6,12,'W'},{7,12,'I'},{8,12,'N'},STOP}; +struct key K_LALT[] = {{10,12,'A'},{11,12,'L'},{12,12,'T'},STOP}; +struct key K_SPACE[] = { + {14,12,' '},{15,12,' '},{16,12,' '},{17,12,' '},{18,12,' '}, + {19,12,'S'},{20,12,'P'},{21,12,'A'},{22,12,'C'},{23,12,'E'}, + {24,12,' '},{25,12,' '},{26,12,' '},{27,12,' '},{28,12,' '}, + STOP +}; +struct key K_RALT[] = {{30,12,'A'},{31,12,'L'},{32,12,'T'},STOP}; +struct key K_RWIN[] = {{34,12,'W'},{35,12,'I'},{36,12,'N'},STOP}; +struct key K_RPROP[] = {{38,12,'P'},{39,12,'R'},{40,12,'O'},{41,12,'P'},STOP}; +struct key K_RCTRL[] = {{43,12,'C'},{44,12,'T'},{45,12,'R'},{46,12,'L'},STOP}; +struct key K_ARROW_LEFT[] = {{50,12,'('},{51,12,0x2190},{52,12,')'},STOP}; +struct key K_ARROW_DOWN[] = {{54,12,'('},{55,12,0x2193},{56,12,')'},STOP}; +struct key K_ARROW_RIGHT[] = {{58,12,'('},{59,12,0x2192},{60,12,')'},STOP}; +struct key K_K_0[] = {{65,12,' '},{66,12,'0'},{67,12,' '},{68,12,' '},STOP}; +struct key K_K_PERIOD[] = {{71,12,'.'},STOP}; + +struct combo { + struct key *keys[6]; +}; + +struct combo combos[] = { + {{K_TILDE, K_2, K_LCTRL, K_RCTRL, 0}}, + {{K_A, K_LCTRL, K_RCTRL, 0}}, + {{K_B, K_LCTRL, K_RCTRL, 0}}, + {{K_C, K_LCTRL, K_RCTRL, 0}}, + {{K_D, K_LCTRL, K_RCTRL, 0}}, + {{K_E, K_LCTRL, K_RCTRL, 0}}, + {{K_F, K_LCTRL, K_RCTRL, 0}}, + {{K_G, K_LCTRL, K_RCTRL, 0}}, + {{K_H, K_BACKSPACE, K_LCTRL, K_RCTRL, 0}}, + {{K_I, K_TAB, K_LCTRL, K_RCTRL, 0}}, + {{K_J, K_LCTRL, K_RCTRL, 0}}, + {{K_K, K_LCTRL, K_RCTRL, 0}}, + {{K_L, K_LCTRL, K_RCTRL, 0}}, + {{K_M, K_ENTER, K_K_ENTER, K_LCTRL, K_RCTRL, 0}}, + {{K_N, K_LCTRL, K_RCTRL, 0}}, + {{K_O, K_LCTRL, K_RCTRL, 0}}, + {{K_P, K_LCTRL, K_RCTRL, 0}}, + {{K_Q, K_LCTRL, K_RCTRL, 0}}, + {{K_R, K_LCTRL, K_RCTRL, 0}}, + {{K_S, K_LCTRL, K_RCTRL, 0}}, + {{K_T, K_LCTRL, K_RCTRL, 0}}, + {{K_U, K_LCTRL, K_RCTRL, 0}}, + {{K_V, K_LCTRL, K_RCTRL, 0}}, + {{K_W, K_LCTRL, K_RCTRL, 0}}, + {{K_X, K_LCTRL, K_RCTRL, 0}}, + {{K_Y, K_LCTRL, K_RCTRL, 0}}, + {{K_Z, K_LCTRL, K_RCTRL, 0}}, + {{K_LSQB, K_ESC, K_3, K_LCTRL, K_RCTRL, 0}}, + {{K_4, K_BACKSLASH, K_LCTRL, K_RCTRL, 0}}, + {{K_RSQB, K_5, K_LCTRL, K_RCTRL, 0}}, + {{K_6, K_LCTRL, K_RCTRL, 0}}, + {{K_7, K_SLASH, K_MINUS_SHIFT, K_LCTRL, K_RCTRL, 0}}, + {{K_SPACE,0}}, + {{K_1_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_DOUBLEQUOTE,K_LSHIFT,K_RSHIFT,0}}, + {{K_3_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_4_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_5_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_7_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_QUOTE,0}}, + {{K_9_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_0_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_8_SHIFT,K_K_STAR,K_LSHIFT,K_RSHIFT,0}}, + {{K_EQUALS_SHIFT,K_K_PLUS,K_LSHIFT,K_RSHIFT,0}}, + {{K_COMMA,0}}, + {{K_MINUS,K_K_MINUS,0}}, + {{K_PERIOD,K_K_PERIOD,0}}, + {{K_SLASH,K_K_SLASH,0}}, + {{K_0,K_K_0,0}}, + {{K_1,K_K_1,0}}, + {{K_2,K_K_2,0}}, + {{K_3,K_K_3,0}}, + {{K_4,K_K_4,0}}, + {{K_5,K_K_5,0}}, + {{K_6,K_K_6,0}}, + {{K_7,K_K_7,0}}, + {{K_8,K_K_8,0}}, + {{K_9,K_K_9,0}}, + {{K_PARENTHESIS,K_LSHIFT,K_RSHIFT,0}}, + {{K_SEMICOLON,0}}, + {{K_LANB,K_LSHIFT,K_RSHIFT,0}}, + {{K_EQUALS,0}}, + {{K_RANB,K_LSHIFT,K_RSHIFT,0}}, + {{K_QUESTION,K_LSHIFT,K_RSHIFT,0}}, + {{K_2_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_A,K_LSHIFT,K_RSHIFT,0}}, + {{K_B,K_LSHIFT,K_RSHIFT,0}}, + {{K_C,K_LSHIFT,K_RSHIFT,0}}, + {{K_D,K_LSHIFT,K_RSHIFT,0}}, + {{K_E,K_LSHIFT,K_RSHIFT,0}}, + {{K_F,K_LSHIFT,K_RSHIFT,0}}, + {{K_G,K_LSHIFT,K_RSHIFT,0}}, + {{K_H,K_LSHIFT,K_RSHIFT,0}}, + {{K_I,K_LSHIFT,K_RSHIFT,0}}, + {{K_J,K_LSHIFT,K_RSHIFT,0}}, + {{K_K,K_LSHIFT,K_RSHIFT,0}}, + {{K_L,K_LSHIFT,K_RSHIFT,0}}, + {{K_M,K_LSHIFT,K_RSHIFT,0}}, + {{K_N,K_LSHIFT,K_RSHIFT,0}}, + {{K_O,K_LSHIFT,K_RSHIFT,0}}, + {{K_P,K_LSHIFT,K_RSHIFT,0}}, + {{K_Q,K_LSHIFT,K_RSHIFT,0}}, + {{K_R,K_LSHIFT,K_RSHIFT,0}}, + {{K_S,K_LSHIFT,K_RSHIFT,0}}, + {{K_T,K_LSHIFT,K_RSHIFT,0}}, + {{K_U,K_LSHIFT,K_RSHIFT,0}}, + {{K_V,K_LSHIFT,K_RSHIFT,0}}, + {{K_W,K_LSHIFT,K_RSHIFT,0}}, + {{K_X,K_LSHIFT,K_RSHIFT,0}}, + {{K_Y,K_LSHIFT,K_RSHIFT,0}}, + {{K_Z,K_LSHIFT,K_RSHIFT,0}}, + {{K_LSQB,0}}, + {{K_BACKSLASH,0}}, + {{K_RSQB,0}}, + {{K_6_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_MINUS_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_TILDE,0}}, + {{K_a,0}}, + {{K_b,0}}, + {{K_c,0}}, + {{K_d,0}}, + {{K_e,0}}, + {{K_f,0}}, + {{K_g,0}}, + {{K_h,0}}, + {{K_i,0}}, + {{K_j,0}}, + {{K_k,0}}, + {{K_l,0}}, + {{K_m,0}}, + {{K_n,0}}, + {{K_o,0}}, + {{K_p,0}}, + {{K_q,0}}, + {{K_r,0}}, + {{K_s,0}}, + {{K_t,0}}, + {{K_u,0}}, + {{K_v,0}}, + {{K_w,0}}, + {{K_x,0}}, + {{K_y,0}}, + {{K_z,0}}, + {{K_LCUB,K_LSHIFT,K_RSHIFT,0}}, + {{K_BACKSLASH_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_RCUB,K_LSHIFT,K_RSHIFT,0}}, + {{K_TILDE_SHIFT,K_LSHIFT,K_RSHIFT,0}}, + {{K_8, K_BACKSPACE, K_LCTRL, K_RCTRL, 0}} +}; + +struct combo func_combos[] = { + {{K_F1,0}}, + {{K_F2,0}}, + {{K_F3,0}}, + {{K_F4,0}}, + {{K_F5,0}}, + {{K_F6,0}}, + {{K_F7,0}}, + {{K_F8,0}}, + {{K_F9,0}}, + {{K_F10,0}}, + {{K_F11,0}}, + {{K_F12,0}}, + {{K_INS,0}}, + {{K_DEL,0}}, + {{K_HOM,0}}, + {{K_END,0}}, + {{K_PGU,0}}, + {{K_PGD,0}}, + {{K_ARROW_UP,0}}, + {{K_ARROW_DOWN,0}}, + {{K_ARROW_LEFT,0}}, + {{K_ARROW_RIGHT,0}} +}; + +void print_tb(const char *str, int x, int y, uint16_t fg, uint16_t bg) +{ + while (*str) { + uint32_t uni; + str += tb_utf8_char_to_unicode(&uni, str); + tb_set_cell(x, y, uni, fg, bg); + x++; + } +} + +void printf_tb(int x, int y, uint16_t fg, uint16_t bg, const char *fmt, ...) +{ + char buf[4096]; + va_list vl; + va_start(vl, fmt); + vsnprintf(buf, sizeof(buf), fmt, vl); + va_end(vl); + print_tb(buf, x, y, fg, bg); +} + +void draw_key(struct key *k, uint16_t fg, uint16_t bg) +{ + while (k->x) { + tb_set_cell(k->x+2, k->y+4, k->ch, fg, bg); + k++; + } +} + +void draw_keyboard() +{ + int i; + tb_set_cell(0, 0, 0x250C, TB_WHITE, TB_DEFAULT); + tb_set_cell(79, 0, 0x2510, TB_WHITE, TB_DEFAULT); + tb_set_cell(0, 23, 0x2514, TB_WHITE, TB_DEFAULT); + tb_set_cell(79, 23, 0x2518, TB_WHITE, TB_DEFAULT); + + for (i = 1; i < 79; ++i) { + tb_set_cell(i, 0, 0x2500, TB_WHITE, TB_DEFAULT); + tb_set_cell(i, 23, 0x2500, TB_WHITE, TB_DEFAULT); + tb_set_cell(i, 17, 0x2500, TB_WHITE, TB_DEFAULT); + tb_set_cell(i, 4, 0x2500, TB_WHITE, TB_DEFAULT); + } + for (i = 1; i < 23; ++i) { + tb_set_cell(0, i, 0x2502, TB_WHITE, TB_DEFAULT); + tb_set_cell(79, i, 0x2502, TB_WHITE, TB_DEFAULT); + } + tb_set_cell(0, 17, 0x251C, TB_WHITE, TB_DEFAULT); + tb_set_cell(79, 17, 0x2524, TB_WHITE, TB_DEFAULT); + tb_set_cell(0, 4, 0x251C, TB_WHITE, TB_DEFAULT); + tb_set_cell(79, 4, 0x2524, TB_WHITE, TB_DEFAULT); + for (i = 5; i < 17; ++i) { + tb_set_cell(1, i, 0x2588, TB_YELLOW, TB_YELLOW); + tb_set_cell(78, i, 0x2588, TB_YELLOW, TB_YELLOW); + } + + draw_key(K_ESC, TB_WHITE, TB_BLUE); + draw_key(K_F1, TB_WHITE, TB_BLUE); + draw_key(K_F2, TB_WHITE, TB_BLUE); + draw_key(K_F3, TB_WHITE, TB_BLUE); + draw_key(K_F4, TB_WHITE, TB_BLUE); + draw_key(K_F5, TB_WHITE, TB_BLUE); + draw_key(K_F6, TB_WHITE, TB_BLUE); + draw_key(K_F7, TB_WHITE, TB_BLUE); + draw_key(K_F8, TB_WHITE, TB_BLUE); + draw_key(K_F9, TB_WHITE, TB_BLUE); + draw_key(K_F10, TB_WHITE, TB_BLUE); + draw_key(K_F11, TB_WHITE, TB_BLUE); + draw_key(K_F12, TB_WHITE, TB_BLUE); + draw_key(K_PRN, TB_WHITE, TB_BLUE); + draw_key(K_SCR, TB_WHITE, TB_BLUE); + draw_key(K_BRK, TB_WHITE, TB_BLUE); + draw_key(K_LED1, TB_WHITE, TB_BLUE); + draw_key(K_LED2, TB_WHITE, TB_BLUE); + draw_key(K_LED3, TB_WHITE, TB_BLUE); + + draw_key(K_TILDE, TB_WHITE, TB_BLUE); + draw_key(K_1, TB_WHITE, TB_BLUE); + draw_key(K_2, TB_WHITE, TB_BLUE); + draw_key(K_3, TB_WHITE, TB_BLUE); + draw_key(K_4, TB_WHITE, TB_BLUE); + draw_key(K_5, TB_WHITE, TB_BLUE); + draw_key(K_6, TB_WHITE, TB_BLUE); + draw_key(K_7, TB_WHITE, TB_BLUE); + draw_key(K_8, TB_WHITE, TB_BLUE); + draw_key(K_9, TB_WHITE, TB_BLUE); + draw_key(K_0, TB_WHITE, TB_BLUE); + draw_key(K_MINUS, TB_WHITE, TB_BLUE); + draw_key(K_EQUALS, TB_WHITE, TB_BLUE); + draw_key(K_BACKSLASH, TB_WHITE, TB_BLUE); + draw_key(K_BACKSPACE, TB_WHITE, TB_BLUE); + draw_key(K_INS, TB_WHITE, TB_BLUE); + draw_key(K_HOM, TB_WHITE, TB_BLUE); + draw_key(K_PGU, TB_WHITE, TB_BLUE); + draw_key(K_K_NUMLOCK, TB_WHITE, TB_BLUE); + draw_key(K_K_SLASH, TB_WHITE, TB_BLUE); + draw_key(K_K_STAR, TB_WHITE, TB_BLUE); + draw_key(K_K_MINUS, TB_WHITE, TB_BLUE); + + draw_key(K_TAB, TB_WHITE, TB_BLUE); + draw_key(K_q, TB_WHITE, TB_BLUE); + draw_key(K_w, TB_WHITE, TB_BLUE); + draw_key(K_e, TB_WHITE, TB_BLUE); + draw_key(K_r, TB_WHITE, TB_BLUE); + draw_key(K_t, TB_WHITE, TB_BLUE); + draw_key(K_y, TB_WHITE, TB_BLUE); + draw_key(K_u, TB_WHITE, TB_BLUE); + draw_key(K_i, TB_WHITE, TB_BLUE); + draw_key(K_o, TB_WHITE, TB_BLUE); + draw_key(K_p, TB_WHITE, TB_BLUE); + draw_key(K_LSQB, TB_WHITE, TB_BLUE); + draw_key(K_RSQB, TB_WHITE, TB_BLUE); + draw_key(K_ENTER, TB_WHITE, TB_BLUE); + draw_key(K_DEL, TB_WHITE, TB_BLUE); + draw_key(K_END, TB_WHITE, TB_BLUE); + draw_key(K_PGD, TB_WHITE, TB_BLUE); + draw_key(K_K_7, TB_WHITE, TB_BLUE); + draw_key(K_K_8, TB_WHITE, TB_BLUE); + draw_key(K_K_9, TB_WHITE, TB_BLUE); + draw_key(K_K_PLUS, TB_WHITE, TB_BLUE); + + draw_key(K_CAPS, TB_WHITE, TB_BLUE); + draw_key(K_a, TB_WHITE, TB_BLUE); + draw_key(K_s, TB_WHITE, TB_BLUE); + draw_key(K_d, TB_WHITE, TB_BLUE); + draw_key(K_f, TB_WHITE, TB_BLUE); + draw_key(K_g, TB_WHITE, TB_BLUE); + draw_key(K_h, TB_WHITE, TB_BLUE); + draw_key(K_j, TB_WHITE, TB_BLUE); + draw_key(K_k, TB_WHITE, TB_BLUE); + draw_key(K_l, TB_WHITE, TB_BLUE); + draw_key(K_SEMICOLON, TB_WHITE, TB_BLUE); + draw_key(K_QUOTE, TB_WHITE, TB_BLUE); + draw_key(K_K_4, TB_WHITE, TB_BLUE); + draw_key(K_K_5, TB_WHITE, TB_BLUE); + draw_key(K_K_6, TB_WHITE, TB_BLUE); + + draw_key(K_LSHIFT, TB_WHITE, TB_BLUE); + draw_key(K_z, TB_WHITE, TB_BLUE); + draw_key(K_x, TB_WHITE, TB_BLUE); + draw_key(K_c, TB_WHITE, TB_BLUE); + draw_key(K_v, TB_WHITE, TB_BLUE); + draw_key(K_b, TB_WHITE, TB_BLUE); + draw_key(K_n, TB_WHITE, TB_BLUE); + draw_key(K_m, TB_WHITE, TB_BLUE); + draw_key(K_COMMA, TB_WHITE, TB_BLUE); + draw_key(K_PERIOD, TB_WHITE, TB_BLUE); + draw_key(K_SLASH, TB_WHITE, TB_BLUE); + draw_key(K_RSHIFT, TB_WHITE, TB_BLUE); + draw_key(K_ARROW_UP, TB_WHITE, TB_BLUE); + draw_key(K_K_1, TB_WHITE, TB_BLUE); + draw_key(K_K_2, TB_WHITE, TB_BLUE); + draw_key(K_K_3, TB_WHITE, TB_BLUE); + draw_key(K_K_ENTER, TB_WHITE, TB_BLUE); + + draw_key(K_LCTRL, TB_WHITE, TB_BLUE); + draw_key(K_LWIN, TB_WHITE, TB_BLUE); + draw_key(K_LALT, TB_WHITE, TB_BLUE); + draw_key(K_SPACE, TB_WHITE, TB_BLUE); + draw_key(K_RCTRL, TB_WHITE, TB_BLUE); + draw_key(K_RPROP, TB_WHITE, TB_BLUE); + draw_key(K_RWIN, TB_WHITE, TB_BLUE); + draw_key(K_RALT, TB_WHITE, TB_BLUE); + draw_key(K_ARROW_LEFT, TB_WHITE, TB_BLUE); + draw_key(K_ARROW_DOWN, TB_WHITE, TB_BLUE); + draw_key(K_ARROW_RIGHT, TB_WHITE, TB_BLUE); + draw_key(K_K_0, TB_WHITE, TB_BLUE); + draw_key(K_K_PERIOD, TB_WHITE, TB_BLUE); + + printf_tb(33, 1, TB_MAGENTA | TB_BOLD, TB_DEFAULT, "Keyboard demo!"); + printf_tb(21, 2, TB_MAGENTA, TB_DEFAULT, "(press CTRL+X and then CTRL+Q to exit)"); + printf_tb(15, 3, TB_MAGENTA, TB_DEFAULT, "(press CTRL+X and then CTRL+C to change input mode)"); + + int inputmode = tb_set_input_mode(0); + char inputmode_str[64]; + + if (inputmode & TB_INPUT_ESC) + sprintf(inputmode_str, "TB_INPUT_ESC"); + if (inputmode & TB_INPUT_ALT) + sprintf(inputmode_str, "TB_INPUT_ALT"); + if (inputmode & TB_INPUT_MOUSE) + sprintf(inputmode_str, "%s | TB_INPUT_MOUSE", inputmode_str); + + printf_tb(3, 18, TB_WHITE, TB_DEFAULT, "Input mode: %s", inputmode_str); +} + +const char *funckeymap(int k) +{ + static const char *fcmap[] = { + "CTRL+2, CTRL+~", + "CTRL+A", + "CTRL+B", + "CTRL+C", + "CTRL+D", + "CTRL+E", + "CTRL+F", + "CTRL+G", + "CTRL+H, BACKSPACE", + "CTRL+I, TAB", + "CTRL+J", + "CTRL+K", + "CTRL+L", + "CTRL+M, ENTER", + "CTRL+N", + "CTRL+O", + "CTRL+P", + "CTRL+Q", + "CTRL+R", + "CTRL+S", + "CTRL+T", + "CTRL+U", + "CTRL+V", + "CTRL+W", + "CTRL+X", + "CTRL+Y", + "CTRL+Z", + "CTRL+3, ESC, CTRL+[", + "CTRL+4, CTRL+\\", + "CTRL+5, CTRL+]", + "CTRL+6", + "CTRL+7, CTRL+/, CTRL+_", + "SPACE" + }; + static const char *fkmap[] = { + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "INSERT", + "DELETE", + "HOME", + "END", + "PGUP", + "PGDN", + "ARROW UP", + "ARROW DOWN", + "ARROW LEFT", + "ARROW RIGHT" + }; + + if (k == TB_KEY_CTRL_8) + return "CTRL+8, BACKSPACE 2"; /* 0x7F */ + else if (k >= TB_KEY_ARROW_RIGHT && k <= 0xFFFF) + return fkmap[0xFFFF-k]; + else if (k <= TB_KEY_SPACE) + return fcmap[k]; + return "UNKNOWN"; +} + +void pretty_print_press(struct tb_event *ev) +{ + char buf[7]; + buf[tb_utf8_unicode_to_char(buf, ev->ch)] = '\0'; + printf_tb(3, 19, TB_WHITE , TB_DEFAULT, "Key: "); + printf_tb(8, 19, TB_YELLOW, TB_DEFAULT, "decimal: %d", ev->key); + printf_tb(8, 20, TB_GREEN , TB_DEFAULT, "hex: 0x%X", ev->key); + printf_tb(8, 21, TB_CYAN , TB_DEFAULT, "octal: 0%o", ev->key); + printf_tb(8, 22, TB_RED , TB_DEFAULT, "string: %s", funckeymap(ev->key)); + + printf_tb(54, 19, TB_WHITE , TB_DEFAULT, "Char: "); + printf_tb(60, 19, TB_YELLOW, TB_DEFAULT, "decimal: %d", ev->ch); + printf_tb(60, 20, TB_GREEN , TB_DEFAULT, "hex: 0x%X", ev->ch); + printf_tb(60, 21, TB_CYAN , TB_DEFAULT, "octal: 0%o", ev->ch); + printf_tb(60, 22, TB_RED , TB_DEFAULT, "string: %s", buf); + + printf_tb(54, 18, TB_WHITE, TB_DEFAULT, "Modifier: %c%c%c%c", + (ev->mod & TB_MOD_CTRL) ? 'C' : ' ', + (ev->mod & TB_MOD_ALT) ? 'A' : ' ', + (ev->mod & TB_MOD_SHIFT) ? 'S' : ' ', + (ev->mod & TB_MOD_MOTION) ? 'M' : ' '); + +} + +void pretty_print_resize(struct tb_event *ev) +{ + printf_tb(3, 19, TB_WHITE, TB_DEFAULT, "Resize event: %d x %d", ev->w, ev->h); +} + +int counter = 0; + +void pretty_print_mouse(struct tb_event *ev) { + printf_tb(3, 19, TB_WHITE, TB_DEFAULT, "Mouse event: %d x %d %c", ev->x, ev->y, (ev->mod & TB_MOD_MOTION) ? '*' : ' '); + char *btn = ""; + switch (ev->key) { + case TB_KEY_MOUSE_LEFT: + btn = "MouseLeft: %d"; + break; + case TB_KEY_MOUSE_MIDDLE: + btn = "MouseMiddle: %d"; + break; + case TB_KEY_MOUSE_RIGHT: + btn = "MouseRight: %d"; + break; + case TB_KEY_MOUSE_WHEEL_UP: + btn = "MouseWheelUp: %d"; + break; + case TB_KEY_MOUSE_WHEEL_DOWN: + btn = "MouseWheelDown: %d"; + break; + case TB_KEY_MOUSE_RELEASE: + btn = "MouseRelease: %d"; + } + counter++; + printf_tb(43, 19, TB_WHITE, TB_DEFAULT, "Key: "); + printf_tb(48, 19, TB_YELLOW, TB_DEFAULT, btn, counter); +} + +void dispatch_press(struct tb_event *ev) +{ + if (ev->mod & TB_MOD_ALT) { + draw_key(K_LALT, TB_WHITE, TB_RED); + draw_key(K_RALT, TB_WHITE, TB_RED); + } + if (ev->mod & TB_MOD_CTRL) { + draw_key(K_LCTRL, TB_WHITE, TB_RED); + draw_key(K_RCTRL, TB_WHITE, TB_RED); + } + if (ev->mod & TB_MOD_SHIFT) { + draw_key(K_LSHIFT, TB_WHITE, TB_RED); + draw_key(K_RSHIFT, TB_WHITE, TB_RED); + } + + struct combo *k = 0; + if (ev->key >= TB_KEY_ARROW_RIGHT) + k = &func_combos[0xFFFF-ev->key]; + else if (ev->ch < 128) { + if (ev->ch == 0 && ev->key < 128) + k = &combos[ev->key]; + else + k = &combos[ev->ch]; + } + if (!k) + return; + + struct key **keys = k->keys; + while (*keys) { + draw_key(*keys, TB_WHITE, TB_RED); + keys++; + } +} + +int main(int argc, char **argv) +{ + (void) argc; (void) argv; + int ret; + + ret = tb_init(); + if (ret) { + fprintf(stderr, "tb_init() failed with error code %d\n", ret); + return 1; + } + + tb_set_input_mode(TB_INPUT_ESC | TB_INPUT_MOUSE); + struct tb_event ev; + + tb_clear(); + draw_keyboard(); + tb_present(); + int inputmode = 0; + int ctrlxpressed = 0; + + while (tb_poll_event(&ev) == TB_OK) { + switch (ev.type) { + case TB_EVENT_KEY: + if (ev.key == TB_KEY_CTRL_Q && ctrlxpressed) { + tb_shutdown(); + return 0; + } + if (ev.key == TB_KEY_CTRL_C && ctrlxpressed) { + static int chmap[] = { + TB_INPUT_ESC | TB_INPUT_MOUSE, /* 101 */ + TB_INPUT_ALT | TB_INPUT_MOUSE, /* 110 */ + TB_INPUT_ESC, /* 001 */ + TB_INPUT_ALT, /* 010 */ + }; + inputmode++; + if (inputmode >= 4) { + inputmode = 0; + } + tb_set_input_mode(chmap[inputmode]); + } + if (ev.key == TB_KEY_CTRL_X) + ctrlxpressed = 1; + else + ctrlxpressed = 0; + + tb_clear(); + draw_keyboard(); + dispatch_press(&ev); + pretty_print_press(&ev); + tb_present(); + break; + case TB_EVENT_RESIZE: + tb_clear(); + draw_keyboard(); + pretty_print_resize(&ev); + tb_present(); + break; + case TB_EVENT_MOUSE: + tb_clear(); + draw_keyboard(); + pretty_print_mouse(&ev); + tb_present(); + break; + default: + break; + } + } + tb_shutdown(); + return 0; +} diff --git a/termbox.h b/termbox.h new file mode 100644 index 0000000..e711a01 --- /dev/null +++ b/termbox.h @@ -0,0 +1,2500 @@ +#ifndef __TERMBOX_H +#define __TERMBOX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { // __ffi_strip +#endif + +/* ASCII key constants (tb_event.key) */ +#define TB_KEY_CTRL_TILDE 0x00 +#define TB_KEY_CTRL_2 0x00 /* clash with 'CTRL_TILDE' */ +#define TB_KEY_CTRL_A 0x01 +#define TB_KEY_CTRL_B 0x02 +#define TB_KEY_CTRL_C 0x03 +#define TB_KEY_CTRL_D 0x04 +#define TB_KEY_CTRL_E 0x05 +#define TB_KEY_CTRL_F 0x06 +#define TB_KEY_CTRL_G 0x07 +#define TB_KEY_BACKSPACE 0x08 +#define TB_KEY_CTRL_H 0x08 /* clash with 'CTRL_BACKSPACE' */ +#define TB_KEY_TAB 0x09 +#define TB_KEY_CTRL_I 0x09 /* clash with 'TAB' */ +#define TB_KEY_CTRL_J 0x0a +#define TB_KEY_CTRL_K 0x0b +#define TB_KEY_CTRL_L 0x0c +#define TB_KEY_ENTER 0x0d +#define TB_KEY_CTRL_M 0x0d /* clash with 'ENTER' */ +#define TB_KEY_CTRL_N 0x0e +#define TB_KEY_CTRL_O 0x0f +#define TB_KEY_CTRL_P 0x10 +#define TB_KEY_CTRL_Q 0x11 +#define TB_KEY_CTRL_R 0x12 +#define TB_KEY_CTRL_S 0x13 +#define TB_KEY_CTRL_T 0x14 +#define TB_KEY_CTRL_U 0x15 +#define TB_KEY_CTRL_V 0x16 +#define TB_KEY_CTRL_W 0x17 +#define TB_KEY_CTRL_X 0x18 +#define TB_KEY_CTRL_Y 0x19 +#define TB_KEY_CTRL_Z 0x1a +#define TB_KEY_ESC 0x1b +#define TB_KEY_CTRL_LSQ_BRACKET 0x1b /* clash with 'ESC' */ +#define TB_KEY_CTRL_3 0x1b /* clash with 'ESC' */ +#define TB_KEY_CTRL_4 0x1c +#define TB_KEY_CTRL_BACKSLASH 0x1c /* clash with 'CTRL_4' */ +#define TB_KEY_CTRL_5 0x1d +#define TB_KEY_CTRL_RSQ_BRACKET 0x1d /* clash with 'CTRL_5' */ +#define TB_KEY_CTRL_6 0x1e +#define TB_KEY_CTRL_7 0x1f +#define TB_KEY_CTRL_SLASH 0x1f /* clash with 'CTRL_7' */ +#define TB_KEY_CTRL_UNDERSCORE 0x1f /* clash with 'CTRL_7' */ +#define TB_KEY_SPACE 0x20 +#define TB_KEY_BACKSPACE2 0x7f +#define TB_KEY_CTRL_8 0x7f /* clash with 'BACKSPACE2' */ + +#define tb_key_i(i) 0xffff - (i) +/* Terminal-dependent key constants (tb_event.key) and terminfo capabilities */ +/* BEGIN codegen h */ +/* Produced by ./codegen.sh on Sun, 29 Aug 2021 05:00:08 +0000 */ +#define TB_KEY_F1 (0xffff - 0) +#define TB_KEY_F2 (0xffff - 1) +#define TB_KEY_F3 (0xffff - 2) +#define TB_KEY_F4 (0xffff - 3) +#define TB_KEY_F5 (0xffff - 4) +#define TB_KEY_F6 (0xffff - 5) +#define TB_KEY_F7 (0xffff - 6) +#define TB_KEY_F8 (0xffff - 7) +#define TB_KEY_F9 (0xffff - 8) +#define TB_KEY_F10 (0xffff - 9) +#define TB_KEY_F11 (0xffff - 10) +#define TB_KEY_F12 (0xffff - 11) +#define TB_KEY_INSERT (0xffff - 12) +#define TB_KEY_DELETE (0xffff - 13) +#define TB_KEY_HOME (0xffff - 14) +#define TB_KEY_END (0xffff - 15) +#define TB_KEY_PGUP (0xffff - 16) +#define TB_KEY_PGDN (0xffff - 17) +#define TB_KEY_ARROW_UP (0xffff - 18) +#define TB_KEY_ARROW_DOWN (0xffff - 19) +#define TB_KEY_ARROW_LEFT (0xffff - 20) +#define TB_KEY_ARROW_RIGHT (0xffff - 21) +#define TB_KEY_MOUSE_LEFT (0xffff - 22) +#define TB_KEY_MOUSE_RIGHT (0xffff - 23) +#define TB_KEY_MOUSE_MIDDLE (0xffff - 24) +#define TB_KEY_MOUSE_RELEASE (0xffff - 25) +#define TB_KEY_MOUSE_WHEEL_UP (0xffff - 26) +#define TB_KEY_MOUSE_WHEEL_DOWN (0xffff - 27) + +#define TB_CAP_F1 0 +#define TB_CAP_F2 1 +#define TB_CAP_F3 2 +#define TB_CAP_F4 3 +#define TB_CAP_F5 4 +#define TB_CAP_F6 5 +#define TB_CAP_F7 6 +#define TB_CAP_F8 7 +#define TB_CAP_F9 8 +#define TB_CAP_F10 9 +#define TB_CAP_F11 10 +#define TB_CAP_F12 11 +#define TB_CAP_INSERT 12 +#define TB_CAP_DELETE 13 +#define TB_CAP_HOME 14 +#define TB_CAP_END 15 +#define TB_CAP_PGUP 16 +#define TB_CAP_PGDN 17 +#define TB_CAP_ARROW_UP 18 +#define TB_CAP_ARROW_DOWN 19 +#define TB_CAP_ARROW_LEFT 20 +#define TB_CAP_ARROW_RIGHT 21 +#define TB_CAP__COUNT_KEYS 22 +#define TB_CAP_ENTER_CA 22 +#define TB_CAP_EXIT_CA 23 +#define TB_CAP_SHOW_CURSOR 24 +#define TB_CAP_HIDE_CURSOR 25 +#define TB_CAP_CLEAR_SCREEN 26 +#define TB_CAP_SGR0 27 +#define TB_CAP_UNDERLINE 28 +#define TB_CAP_BOLD 29 +#define TB_CAP_BLINK 30 +#define TB_CAP_ITALIC 31 +#define TB_CAP_REVERSE 32 +#define TB_CAP_ENTER_KEYPAD 33 +#define TB_CAP_EXIT_KEYPAD 34 +#define TB_CAP__COUNT 35 +/* END codegen h */ + +/* Some hard-coded caps */ +#define TB_HARDCAP_ENTER_MOUSE "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" +#define TB_HARDCAP_EXIT_MOUSE "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" + +/* Colors (numeric) and attributes (bitwise) (tb_cell.fg, tb_cell.bg) */ +#define TB_DEFAULT 0x0000 +#define TB_BLACK 0x0001 +#define TB_RED 0x0002 +#define TB_GREEN 0x0003 +#define TB_YELLOW 0x0004 +#define TB_BLUE 0x0005 +#define TB_MAGENTA 0x0006 +#define TB_CYAN 0x0007 +#define TB_WHITE 0x0008 +#define TB_BOLD 0x0100 +#define TB_UNDERLINE 0x0200 +#define TB_REVERSE 0x0400 +#define TB_ITALIC 0x0800 + +/* Event types (tb_event.type) */ +#define TB_EVENT_KEY 1 +#define TB_EVENT_RESIZE 2 +#define TB_EVENT_MOUSE 3 + +/* Key modifiers (bitwise) (tb_event.mod) */ +#define TB_MOD_ALT 1 +#define TB_MOD_CTRL 2 +#define TB_MOD_SHIFT 4 +#define TB_MOD_MOTION 8 + +/* Input modes (bitwise) (tb_set_input_mode) */ +#define TB_INPUT_CURRENT 0 +#define TB_INPUT_ESC 1 +#define TB_INPUT_ALT 2 +#define TB_INPUT_MOUSE 4 + +/* Output modes (tb_set_output_mode) */ +#define TB_OUTPUT_CURRENT 0 +#define TB_OUTPUT_NORMAL 1 +#define TB_OUTPUT_256 2 +#define TB_OUTPUT_216 3 +#define TB_OUTPUT_GRAYSCALE 4 +#define TB_OUTPUT_TRUECOLOR 5 + +/* Common function return values unless otherwise noted */ +#define TB_OK 0 +#define TB_ERR -1 +#define TB_ERR_NEED_MORE -2 +#define TB_ERR_INIT_ALREADY -3 +#define TB_ERR_INIT_OPEN -4 +#define TB_ERR_MEM -5 +#define TB_ERR_NO_EVENT -6 +#define TB_ERR_NO_TERM -7 +#define TB_ERR_NOT_INIT -8 +#define TB_ERR_OUT_OF_BOUNDS -9 +#define TB_ERR_READ -10 +#define TB_ERR_RESIZE_IOCTL -11 +#define TB_ERR_RESIZE_PIPE -12 +#define TB_ERR_RESIZE_SIGACTION -13 +#define TB_ERR_SELECT -14 +#define TB_ERR_TCGETATTR -15 +#define TB_ERR_TCSETATTR -16 +#define TB_ERR_UNSUPPORTED_TERM -17 +#define TB_ERR_RESIZE_WRITE -18 +#define TB_ERR_RESIZE_SELECT -19 +#define TB_ERR_RESIZE_READ -20 +#define TB_ERR_RESIZE_SSCANF -21 + +/* Function types to be used with tb_set_func() */ +#define TB_FUNC_EXTRACT_PRE 0 +#define TB_FUNC_EXTRACT_POST 1 + +/* Define this to set the size of the buffer used in tb_printf() + * and tb_sendf() + */ +#ifndef TB_OPT_PRINTF_BUF +#define TB_OPT_PRINTF_BUF 4096 +#endif + +/* Define this for limited back compat with termbox v1 */ +#ifdef TB_OPT_V1_COMPAT +#define tb_change_cell tb_set_cell +#define tb_put_cell(x, y, c) tb_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) +#define tb_set_clear_attributes tb_set_clear_attrs +#define tb_select_input_mode tb_set_input_mode +#define tb_select_output_mode tb_set_output_mode +#endif + +/* Define these to swap in a different allocator */ +#ifndef tb_malloc +#define tb_malloc malloc +#define tb_realloc realloc +#define tb_free free +#endif + +#ifdef TB_OPT_TRUECOLOR +typedef uint32_t uintattr_t; +#else +typedef uint16_t uintattr_t; // __ffi_strip +#endif + +/* The terminal screen is represented as 2d array of cells. The structure is + * optimized for dealing with single-width (wcwidth()==1) Unicode code points, + * however some support for grapheme clusters (e.g., combining diacritical + * marks) and wide code points (e.g., Hiragana) is provided through ech, nech, + * cech via tb_set_cell_ex(). ech is only valid when nech>0, otherwise ch is + * used. + * + * For non-single-width code points, given N=wcwidth(ch)/wcswidth(ech): + * + * when N==0: termbox forces a single-width cell. Callers should avoid this + * if aiming to render text accurately. + * + * when N>1: termbox zeroes out the following N-1 cells and skips sending + * them to the tty. So, e.g., if the caller sets x=0,y=0 to an N==2 + * code point, the caller's next set should be at x=2,y=0. Anything + * set at x=1,y=0 will be ignored. If there are not enough columns + * remaining on the line to render N width, spaces are sent + * instead. + * + * See tb_present() for implementation. + */ +struct tb_cell { + uint32_t ch; /* a Unicode character */ + uintattr_t fg; /* bit-wise foreground attributes */ + uintattr_t bg; /* bit-wise background attributes */ + #ifdef TB_OPT_EGC + uint32_t *ech; /* a grapheme cluster of Unicode code points */ + size_t nech; /* length in bytes of ech, 0 means use ch instead of ech */ + size_t cech; /* capacity in bytes of ech */ + #endif +}; + +/* An incoming event from the tty. + * + * Given the event type, the following fields are relevant: + * + * when TB_EVENT_KEY: (key XOR ch, one will be zero), mod. Note there is + * overlap between TB_MOD_CTRL and TB_KEY_CTRL_*. + * TB_MOD_CTRL and TB_MOD_SHIFT are only set as + * modifiers to TB_KEY_ARROW_*. + * + * when TB_EVENT_RESIZE: w, h + * + * when TB_EVENT_MOUSE: key (TB_KEY_MOUSE_*), x, y + */ +struct tb_event { + uint8_t type; /* one of TB_EVENT_* constants */ + uint8_t mod; /* bit-wise TB_MOD_* constants */ + uint16_t key; /* one of TB_KEY_* constants */ + uint32_t ch; /* a Unicode code point */ + int32_t w; /* resize width */ + int32_t h; /* resize height */ + int32_t x; /* mouse x */ + int32_t y; /* mouse y */ +}; + +/* Initializes the termbox library. This function should be called before any + * other functions. Function tb_init() is same as tb_init_file("/dev/tty"). + * After successful initialization, the library must be finalized using the + * tb_shutdown() function. + */ +int tb_init(); +int tb_init_file(const char *path); +int tb_init_fd(int ttyfd); +int tb_shutdown(); + +/* Returns the size of the internal back buffer (which is the same as + * terminal's window size in characters). The internal buffer can be resized + * after tb_clear() or tb_present() function calls. Both dimensions have an + * unspecified negative value when called before tb_init() or after + * tb_shutdown(). + */ +int tb_width(); +int tb_height(); + +/* Clears the internal back buffer using TB_DEFAULT color or the + * color/attributes set by tb_set_clear_attrs() function. + */ +int tb_clear(); +int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); + +/* Synchronizes the internal back buffer with the terminal. */ +int tb_present(); + +/* Sets the position of the cursor. Upper-left character is (0, 0). */ +int tb_set_cursor(int cx, int cy); +int tb_hide_cursor(); + +/* Set cell contents in the internal back buffer at the specified position. Use + * tb_set_cell_ex() for rendering grapheme clusters (e.g., combining diacritical + * marks). Function tb_set_cell(x, y, ch, fg, bg) is equivalent to + * tb_set_cell_ex(x, y, &ch, 1, fg, bg). tb_extend_cell() is a shortcut for + * appending 1 code point to cell->ech. + */ +int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); +int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); +int tb_extend_cell(int x, int y, uint32_t ch); + +/* Sets the termbox input mode. Termbox has two input modes: + * 1. Esc input mode. + * When ESC sequence is in the buffer and it doesn't match any known + * ESC sequence => ESC means TB_KEY_ESC. + * 2. Alt input mode. + * When ESC sequence is in the buffer and it doesn't match any known + * sequence => ESC enables TB_MOD_ALT modifier for the next keyboard event. + * + * You can also apply TB_INPUT_MOUSE via bitwise OR operation to either of the + * modes (e.g. TB_INPUT_ESC | TB_INPUT_MOUSE). If none of the main two modes + * were set, but the mouse mode was, TB_INPUT_ESC mode is used. If for some + * reason you've decided to use (TB_INPUT_ESC | TB_INPUT_ALT) combination, it + * will behave as if only TB_INPUT_ESC was selected. + * + * If 'mode' is TB_INPUT_CURRENT, it returns the current input mode. + * + * Default termbox input mode is TB_INPUT_ESC. + */ +int tb_set_input_mode(int mode); + +/* Sets the termbox output mode. Termbox has three output options: + * 1. TB_OUTPUT_NORMAL => [1..8] + * This mode provides 8 different colors: + * black, red, green, yellow, blue, magenta, cyan, white + * Shortcut: TB_BLACK, TB_RED, ... + * Attributes: TB_BOLD, TB_UNDERLINE, TB_REVERSE + * + * Example usage: + * tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED); + * + * 2. TB_OUTPUT_256 => [0..256] + * In this mode you can leverage the 256 terminal mode: + * 0x00 - 0x07: the 8 colors as in TB_OUTPUT_NORMAL + * 0x08 - 0x0f: TB_* | TB_BOLD + * 0x10 - 0xe7: 216 different colors + * 0xe8 - 0xff: 24 different shades of grey + * + * Example usage: + * tb_set_cell(x, y, '@', 184, 240); + * tb_set_cell(x, y, '@', 0xb8, 0xf0); + * + * 3. TB_OUTPUT_216 => [0..216] + * This mode supports the 3rd range of the 256 mode only. + * But you don't need to provide an offset. + * + * 4. TB_OUTPUT_GRAYSCALE => [0..23] + * This mode supports the 4th range of the 256 mode only. + * But you dont need to provide an offset. + * + * Execute demo/output to see its impact on your terminal. + * + * If 'mode' is TB_OUTPUT_CURRENT, it returns the current output mode. + * + * Default termbox output mode is TB_OUTPUT_NORMAL. + */ +int tb_set_output_mode(int mode); + +/* Wait for an event up to 'timeout' milliseconds and fill the 'event' + * structure with it, when the event is available. Returns the type of the + * event (one of TB_EVENT_* constants) or -1 if there was an error or 0 in case + * there were no event during 'timeout' period. + */ +int tb_peek_event(struct tb_event *event, int timeout); + +/* Wait for an event forever and fill the 'event' structure with it, when the + * event is available. Returns the type of the event (one of TB_EVENT_* + * constants) or -1 if there was an error. + */ +int tb_poll_event(struct tb_event *event); + +/* Print and printf functions. Return number of cells printed to. */ +int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); +int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); + +/* Send raw bytes to terminal. */ +int tb_send(const char *buf, size_t nbuf); +int tb_sendf(const char *fmt, ...); + +/* Set custom functions. fn_type is one of TB_FUNC_* constants, fn is a + * compatible function pointer, or NULL to clear. + */ +int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)); + +/* Utility functions. */ +int tb_utf8_char_length(char c); +int tb_utf8_char_to_unicode(uint32_t *out, const char *c); +int tb_utf8_unicode_to_char(char *out, uint32_t c); +int tb_last_errno(); +struct tb_cell * tb_cell_buffer(); + +#ifdef __cplusplus +} // __ffi_strip +#endif + +#endif /* __TERMBOX_H */ + +#ifdef TB_IMPL + +#define if_err_return(rv, expr) if (((rv) = (expr)) != TB_OK) return (rv) +#define if_err_break(rv, expr) if (((rv) = (expr)) != TB_OK) break +#define if_ok_return(rv, expr) if (((rv) = (expr)) == TB_OK) return (rv) +#define if_ok_or_need_more_return(rv, expr) \ + if (((rv) = (expr)) == TB_OK || (rv) == TB_ERR_NEED_MORE) return (rv) + +#define send_literal(rv, a) \ + if_err_return((rv), bytebuf_nputs(&global.out, (a), sizeof(a) - 1)) + +#define send_num(rv, nbuf, n) \ + if_err_return((rv), bytebuf_nputs(&global.out, (nbuf), convert_num((n), (nbuf)))) + +#define snprintf_or_return(rv, str, sz, fmt, ...) do { \ + (rv) = snprintf((str), (sz), (fmt), __VA_ARGS__); \ + if ((rv) < 0 || (rv) >= (int)(sz)) return TB_ERR; \ +} while (0) + +#define if_not_init_return() \ + if (!global.initialized) return TB_ERR_NOT_INIT + +struct bytebuf_t { + char *buf; + size_t len; + size_t cap; +}; + +struct cellbuf_t { + int width; + int height; + struct tb_cell *cells; +}; + +struct tb_global_t { + int ttyfd; + int rfd; + int wfd; + int ttyfd_open; + int resize_pipefd[2]; + int width; + int height; + int cursor_x; + int cursor_y; + int last_x; + int last_y; + uintattr_t fg; + uintattr_t bg; + uintattr_t last_fg; + uintattr_t last_bg; + int input_mode; + int output_mode; + char *terminfo; + size_t nterminfo; + const char *caps[TB_CAP__COUNT]; + struct bytebuf_t in; + struct bytebuf_t out; + struct cellbuf_t back; + struct cellbuf_t front; + struct termios orig_tios; + int has_orig_tios; + int last_errno; + int initialized; + int need_resize; + int (*fn_extract_esc_pre)(struct tb_event *, size_t *); + int (*fn_extract_esc_post)(struct tb_event *, size_t *); +}; + +struct tb_global_t global = {0}; + +/* BEGIN codegen c */ +/* Produced by ./codegen.sh on Sun, 29 Aug 2021 05:00:08 +0000 */ + +static const int16_t terminfo_cap_indexes[] = { + 66, // kf1 (TB_CAP_F1) + 68, // kf2 (TB_CAP_F2) + 69, // kf3 (TB_CAP_F3) + 70, // kf4 (TB_CAP_F4) + 71, // kf5 (TB_CAP_F5) + 72, // kf6 (TB_CAP_F6) + 73, // kf7 (TB_CAP_F7) + 74, // kf8 (TB_CAP_F8) + 75, // kf9 (TB_CAP_F9) + 67, // kf10 (TB_CAP_F10) + 216, // kf11 (TB_CAP_F11) + 217, // kf12 (TB_CAP_F12) + 77, // kich1 (TB_CAP_INSERT) + 59, // kdch1 (TB_CAP_DELETE) + 76, // khome (TB_CAP_HOME) + 164, // kend (TB_CAP_END) + 82, // kpp (TB_CAP_PGUP) + 81, // knp (TB_CAP_PGDN) + 87, // kcuu1 (TB_CAP_ARROW_UP) + 61, // kcud1 (TB_CAP_ARROW_DOWN) + 79, // kcub1 (TB_CAP_ARROW_LEFT) + 83, // kcuf1 (TB_CAP_ARROW_RIGHT) + 28, // smcup (TB_CAP_ENTER_CA) + 40, // rmcup (TB_CAP_EXIT_CA) + 16, // cnorm (TB_CAP_SHOW_CURSOR) + 13, // civis (TB_CAP_HIDE_CURSOR) + 5, // clear (TB_CAP_CLEAR_SCREEN) + 39, // sgr0 (TB_CAP_SGR0) + 36, // smul (TB_CAP_UNDERLINE) + 27, // bold (TB_CAP_BOLD) + 26, // blink (TB_CAP_BLINK) + 311, // sitm (TB_CAP_ITALIC) + 34, // rev (TB_CAP_REVERSE) + 89, // smkx (TB_CAP_ENTER_KEYPAD) + 88, // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// xterm +static const char *xterm_caps[] = { + "\033OP", // kf1 (TB_CAP_F1) + "\033OQ", // kf2 (TB_CAP_F2) + "\033OR", // kf3 (TB_CAP_F3) + "\033OS", // kf4 (TB_CAP_F4) + "\033[15~", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033OH", // khome (TB_CAP_HOME) + "\033OF", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033OA", // kcuu1 (TB_CAP_ARROW_UP) + "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) + "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) + "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) + "\033[?1049h\033[22;0;0t", // smcup (TB_CAP_ENTER_CA) + "\033[?1049l\033[23;0;0t", // rmcup (TB_CAP_EXIT_CA) + "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) + "\033(B\033[m", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "\033[3m", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) + "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// linux +static const char *linux_caps[] = { + "\033[[A", // kf1 (TB_CAP_F1) + "\033[[B", // kf2 (TB_CAP_F2) + "\033[[C", // kf3 (TB_CAP_F3) + "\033[[D", // kf4 (TB_CAP_F4) + "\033[[E", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033[1~", // khome (TB_CAP_HOME) + "\033[4~", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033[A", // kcuu1 (TB_CAP_ARROW_UP) + "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) + "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) + "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) + "", // smcup (TB_CAP_ENTER_CA) + "", // rmcup (TB_CAP_EXIT_CA) + "\033[?25h\033[?0c", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l\033[?1c", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) + "\033[m\017", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "", // smkx (TB_CAP_ENTER_KEYPAD) + "", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// screen +static const char *screen_caps[] = { + "\033OP", // kf1 (TB_CAP_F1) + "\033OQ", // kf2 (TB_CAP_F2) + "\033OR", // kf3 (TB_CAP_F3) + "\033OS", // kf4 (TB_CAP_F4) + "\033[15~", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033[1~", // khome (TB_CAP_HOME) + "\033[4~", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033OA", // kcuu1 (TB_CAP_ARROW_UP) + "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) + "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) + "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) + "\033[?1049h", // smcup (TB_CAP_ENTER_CA) + "\033[?1049l", // rmcup (TB_CAP_EXIT_CA) + "\033[34h\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) + "\033[m\017", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) + "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// rxvt-256color +static const char *rxvt_256color_caps[] = { + "\033[11~", // kf1 (TB_CAP_F1) + "\033[12~", // kf2 (TB_CAP_F2) + "\033[13~", // kf3 (TB_CAP_F3) + "\033[14~", // kf4 (TB_CAP_F4) + "\033[15~", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033[7~", // khome (TB_CAP_HOME) + "\033[8~", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033[A", // kcuu1 (TB_CAP_ARROW_UP) + "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) + "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) + "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) + "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) + "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) + "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) + "\033[m\017", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "\033=", // smkx (TB_CAP_ENTER_KEYPAD) + "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// rxvt-unicode +static const char *rxvt_unicode_caps[] = { + "\033[11~", // kf1 (TB_CAP_F1) + "\033[12~", // kf2 (TB_CAP_F2) + "\033[13~", // kf3 (TB_CAP_F3) + "\033[14~", // kf4 (TB_CAP_F4) + "\033[15~", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033[7~", // khome (TB_CAP_HOME) + "\033[8~", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033[A", // kcuu1 (TB_CAP_ARROW_UP) + "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) + "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) + "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) + "\033[?1049h", // smcup (TB_CAP_ENTER_CA) + "\033[r\033[?1049l", // rmcup (TB_CAP_EXIT_CA) + "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) + "\033[m\033(B", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "\033[3m", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "\033=", // smkx (TB_CAP_ENTER_KEYPAD) + "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +// Eterm +static const char *eterm_caps[] = { + "\033[11~", // kf1 (TB_CAP_F1) + "\033[12~", // kf2 (TB_CAP_F2) + "\033[13~", // kf3 (TB_CAP_F3) + "\033[14~", // kf4 (TB_CAP_F4) + "\033[15~", // kf5 (TB_CAP_F5) + "\033[17~", // kf6 (TB_CAP_F6) + "\033[18~", // kf7 (TB_CAP_F7) + "\033[19~", // kf8 (TB_CAP_F8) + "\033[20~", // kf9 (TB_CAP_F9) + "\033[21~", // kf10 (TB_CAP_F10) + "\033[23~", // kf11 (TB_CAP_F11) + "\033[24~", // kf12 (TB_CAP_F12) + "\033[2~", // kich1 (TB_CAP_INSERT) + "\033[3~", // kdch1 (TB_CAP_DELETE) + "\033[7~", // khome (TB_CAP_HOME) + "\033[8~", // kend (TB_CAP_END) + "\033[5~", // kpp (TB_CAP_PGUP) + "\033[6~", // knp (TB_CAP_PGDN) + "\033[A", // kcuu1 (TB_CAP_ARROW_UP) + "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) + "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) + "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) + "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) + "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) + "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) + "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) + "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) + "\033[m\017", // sgr0 (TB_CAP_SGR0) + "\033[4m", // smul (TB_CAP_UNDERLINE) + "\033[1m", // bold (TB_CAP_BOLD) + "\033[5m", // blink (TB_CAP_BLINK) + "", // sitm (TB_CAP_ITALIC) + "\033[7m", // rev (TB_CAP_REVERSE) + "", // smkx (TB_CAP_ENTER_KEYPAD) + "", // rmkx (TB_CAP_EXIT_KEYPAD) +}; + +static struct { + const char *name; + const char **caps; + const char *alias; +} builtin_terms[] = { + { "xterm", xterm_caps, "" }, + { "linux", linux_caps, "" }, + { "screen", screen_caps, "tmux" }, + { "rxvt-256color", rxvt_256color_caps, "" }, + { "rxvt-unicode", rxvt_unicode_caps, "rxvt" }, + { "Eterm", eterm_caps, "" }, + { NULL, NULL, NULL }, +}; + +/* END codegen c */ + +static const unsigned char utf8_length[256] = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1 +}; + +static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; + +static int tb_reset(); +static int tb_init_rwfd(int rfd, int wfd); +static int init_term_attrs(); +static int init_term_caps(); +static int init_resize_handler(); +static int send_init_escape_codes(); +static int send_clear(); +static int update_term_size(); +static int update_term_size_via_esc(); +static int init_cellbuf(); +static int tb_deinit(); +static int load_terminfo(); +static int load_terminfo_from_path(const char *path, const char *term); +static int read_terminfo_path(const char *path); +static int parse_terminfo_caps(); +static int load_builtin_caps(); +static const char * get_terminfo_string(int16_t str_offsets_pos, int16_t str_table_pos, int16_t str_table_len, int16_t str_index); +static int wait_event(struct tb_event *event, struct timeval *timeout); +static int extract_event(struct tb_event *event); +static int extract_esc(struct tb_event *event); +static int extract_esc_user(struct tb_event *event, int is_post); +static int extract_esc_mouse(struct tb_event *event); +static int extract_esc_mod_arrow(struct tb_event *event); +static int extract_esc_cap(struct tb_event *event); +static int resize_if_needed(); +static void handle_resize(int sig); +static int send_attr(uintattr_t fg, uintattr_t bg); +static int send_sgr(uintattr_t fg, uintattr_t bg); +static int send_cursor_if(int x, int y); +static int send_char(int x, int y, uint32_t ch); +static int send_cluster(int x, int y, uint32_t *ch, size_t nch); +static int convert_num(uint32_t num, char *buf); +static int cell_cmp(struct tb_cell *a, struct tb_cell *b); +static int cell_copy(struct tb_cell *dst, struct tb_cell *src); +static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); +static int cell_reserve_ech(struct tb_cell *cell, size_t n); +static int cell_free(struct tb_cell *cell); +static int cellbuf_init(struct cellbuf_t *c, int w, int h); +static int cellbuf_free(struct cellbuf_t *c); +static int cellbuf_clear(struct cellbuf_t *c); +static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out); +static int cellbuf_resize(struct cellbuf_t *c, int w, int h); +static int bytebuf_puts(struct bytebuf_t *b, const char *str); +static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr); +static int bytebuf_shift(struct bytebuf_t *b, size_t n); +static int bytebuf_flush(struct bytebuf_t *b, int fd); +static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); +static int bytebuf_free(struct bytebuf_t *b); + +int tb_init() { + return tb_init_file("/dev/tty"); +} + +int tb_init_file(const char *path) { + if (global.initialized) { + return TB_ERR_INIT_ALREADY; + } + int ttyfd = open(path, O_RDWR); + if (ttyfd < 0) { + global.last_errno = errno; + return TB_ERR_INIT_OPEN; + } + global.ttyfd_open = 1; + return tb_init_fd(ttyfd); +} + +int tb_init_fd(int ttyfd) { + return tb_init_rwfd(ttyfd, ttyfd); +} + +int tb_shutdown() { + if_not_init_return(); + tb_deinit(); + return TB_OK; +} + +int tb_width() { + if_not_init_return(); + return global.width; +} + +int tb_height() { + if_not_init_return(); + return global.height; +} + +int tb_clear() { + int rv; + if_not_init_return(); + if_err_return(rv, resize_if_needed()); + return cellbuf_clear(&global.back); +} + +int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg) { + if_not_init_return(); + global.fg = fg; + global.bg = bg; + return TB_OK; +} + +int tb_present() { + if_not_init_return(); + + int rv; + if_err_return(rv, resize_if_needed()); + + // TODO Assert global.back.(width,height) == global.front.(width,height) + + global.last_x = -1; + global.last_y = -1; + + int x, y, i; + for (y = 0; y < global.front.height; y++) { + for (x = 0; x < global.front.width; ) { + struct tb_cell *back, *front; + if_err_return(rv, cellbuf_get(&global.back, x, y, &back)); + if_err_return(rv, cellbuf_get(&global.front, x, y, &front)); + + int w; + { + #ifdef TB_OPT_EGC + if (back->nech > 0) w = wcswidth((wchar_t*)back->ech, back->nech); + else + #endif + w = wcwidth(back->ch); + } + if (w < 1) { + w = 1; + } + + if (cell_cmp(back, front) != 0) { + cell_copy(front, back); + + send_attr(back->fg, back->bg); + if (w > 1 && x >= global.front.width - (w - 1)) { + for (i = x; i < global.front.width; i++) { + send_char(i, y, ' '); + } + } else { + { + #ifdef TB_OPT_EGC + if (back->nech > 0) send_cluster(x, y, back->ech, back->nech); + else + #endif + send_char(x, y, back->ch); + } + for (i = 1; i < w; i++) { + struct tb_cell *front_wide; + if_err_return(rv, cellbuf_get(&global.front, x + i, y, &front_wide)); + cell_set(front_wide, 0, 1, back->fg, back->bg); + } + } + } + x += w; + } + } + + if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); + if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); + + return TB_OK; +} + +int tb_set_cursor(int cx, int cy) { + if_not_init_return(); + int rv; + if (cx < 0) cx = 0; + if (cy < 0) cy = 0; + if (global.cursor_x == -1) { + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR])); + } + if_err_return(rv, send_cursor_if(cx, cy)); + global.cursor_x = cx; + global.cursor_y = cy; + return TB_OK; +} + +int tb_hide_cursor() { + if_not_init_return(); + int rv; + if (global.cursor_x >= 0) { + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); + } + global.cursor_x = -1; + global.cursor_y = -1; + return TB_OK; +} + +int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg) { + if_not_init_return(); + return tb_set_cell_ex(x, y, &ch, 1, fg, bg); +} + +int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg) { + if_not_init_return(); + int rv; + struct tb_cell *cell; + if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); + if_err_return(rv, cell_set(cell, ch, nch, fg, bg)); + return TB_OK; +} + +int tb_extend_cell(int x, int y, uint32_t ch) { + if_not_init_return(); + #ifdef TB_OPT_EGC + int rv; + struct tb_cell *cell; + size_t nech; + if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); + if (cell->nech > 0) { // append to ech + nech = cell->nech + 1; + if_err_return(rv, cell_reserve_ech(cell, nech)); + cell->ech[nech - 1] = ch; + } else { // make new ech + nech = 2; + if_err_return(rv, cell_reserve_ech(cell, nech)); + cell->ech[0] = cell->ch; + cell->ech[1] = ch; + } + cell->ech[nech] = '\0'; + cell->nech = nech; + return TB_OK; + #else + (void)x; + (void)y; + (void)ch; + return TB_ERR; + #endif +} + +int tb_set_input_mode(int mode) { + if_not_init_return(); + if (mode == TB_INPUT_CURRENT) { + return global.input_mode; + } + + if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == 0) { + mode |= TB_INPUT_ESC; + } + + if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == (TB_INPUT_ESC | TB_INPUT_ALT)) { + mode &= ~TB_INPUT_ALT; + } + + if (mode & TB_INPUT_MOUSE) { + bytebuf_puts(&global.out, TB_HARDCAP_ENTER_MOUSE); + bytebuf_flush(&global.out, global.wfd); + } else { + bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); + bytebuf_flush(&global.out, global.wfd); + } + + global.input_mode = mode; + return TB_OK; +} + +int tb_set_output_mode(int mode) { + if_not_init_return(); + if (mode == TB_OUTPUT_CURRENT) { + return global.output_mode; + } + global.output_mode = mode; + return TB_OK; +} + +int tb_peek_event(struct tb_event *event, int timeout) { + if_not_init_return(); + struct timeval tv; + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; + return wait_event(event, &tv); +} + +int tb_poll_event(struct tb_event *event) { + if_not_init_return(); + return wait_event(event, NULL); +} + +int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { + int rv; + uint32_t uni; + int w, ix = x; + while (*str) { + str += tb_utf8_char_to_unicode(&uni, str); + w = wcwidth(uni); + if (w < 0) { + w = 1; + } + if (w == 0 && x > ix) { + if_err_return(rv, tb_extend_cell(x - 1, y, uni)); + } else { + if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); + } + x += w; + } + return TB_OK; +} + +int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...) { + int rv; + char buf[TB_OPT_PRINTF_BUF]; + va_list vl; + va_start(vl, fmt); + rv = vsnprintf(buf, sizeof(buf), fmt, vl); + va_end(vl); + if (rv < 0 || rv >= (int)sizeof(buf)) { + return TB_ERR; + } + return tb_print(x, y, fg, bg, buf); +} + +int tb_send(const char *buf, size_t nbuf) { + return bytebuf_nputs(&global.out, buf, nbuf); +} + +int tb_sendf(const char *fmt, ...) { + int rv; + char buf[TB_OPT_PRINTF_BUF]; + va_list vl; + va_start(vl, fmt); + rv = vsnprintf(buf, sizeof(buf), fmt, vl); + va_end(vl); + if (rv < 0 || rv >= (int)sizeof(buf)) { + return TB_ERR; + } + return tb_send(buf, (size_t)rv); +} + +int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)) { + switch (fn_type) { + case TB_FUNC_EXTRACT_PRE: + global.fn_extract_esc_pre = fn; + return TB_OK; + case TB_FUNC_EXTRACT_POST: + global.fn_extract_esc_post = fn; + return TB_OK; + } + return TB_ERR; +} + +struct tb_cell * tb_cell_buffer() { + if (!global.initialized) return NULL; + return global.back.cells; +} + +int tb_utf8_char_length(char c) { + return utf8_length[(unsigned char)c]; +} + +int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { + if (*c == 0) { + return TB_ERR; + } + + int i; + unsigned char len = tb_utf8_char_length(*c); + unsigned char mask = utf8_mask[len - 1]; + uint32_t result = c[0] & mask; + for (i = 1; i < len; ++i) { + result <<= 6; + result |= c[i] & 0x3f; + } + + *out = result; + return (int)len; +} + +int tb_utf8_unicode_to_char(char *out, uint32_t c) { + int len = 0; + int first; + int i; + + if (c < 0x80) { + first = 0; + len = 1; + } else if (c < 0x800) { + first = 0xc0; + len = 2; + } else if (c < 0x10000) { + first = 0xe0; + len = 3; + } else if (c < 0x200000) { + first = 0xf0; + len = 4; + } else if (c < 0x4000000) { + first = 0xf8; + len = 5; + } else { + first = 0xfc; + len = 6; + } + + for (i = len - 1; i > 0; --i) { + out[i] = (c & 0x3f) | 0x80; + c >>= 6; + } + out[0] = c | first; + + return len; +} + +int tb_last_errno() { + return global.last_errno; +} + +static int tb_reset() { + int ttyfd_open = global.ttyfd_open; + memset(&global, 0, sizeof(global)); + global.ttyfd = -1; + global.rfd = -1; + global.wfd = -1; + global.ttyfd_open = ttyfd_open; + global.resize_pipefd[0] = -1; + global.resize_pipefd[1] = -1; + global.width = -1; + global.height = -1; + global.cursor_x = -1; + global.cursor_y = -1; + global.last_x = -1; + global.last_y = -1; + global.fg = TB_DEFAULT; + global.bg = TB_DEFAULT; + global.last_fg = ~global.fg; + global.last_bg = ~global.bg; + global.input_mode = TB_INPUT_ESC; + global.output_mode = TB_OUTPUT_NORMAL; + return TB_OK; +} + +static int tb_init_rwfd(int rfd, int wfd) { + int rv; + + tb_reset(); + global.ttyfd = rfd == wfd && isatty(rfd) ? rfd : -1; + global.rfd = rfd; + global.wfd = wfd; + + do { + if_err_break(rv, init_term_attrs()); + if_err_break(rv, init_term_caps()); + if_err_break(rv, init_resize_handler()); + if_err_break(rv, send_init_escape_codes()); + if_err_break(rv, send_clear()); + if_err_break(rv, update_term_size()); + if_err_break(rv, init_cellbuf()); + global.initialized = 1; + } while(0); + + if (rv != TB_OK) { + tb_deinit(); + } + + return rv; +} + +static int init_term_attrs() { + if (global.ttyfd < 0) { + return TB_OK; + } + + if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { + global.last_errno = errno; + return TB_ERR_TCGETATTR; + } + + struct termios tios; + memcpy(&tios, &global.orig_tios, sizeof(tios)); + global.has_orig_tios = 1; + + cfmakeraw(&tios); + tios.c_cc[VMIN] = 1; + tios.c_cc[VTIME] = 0; + + if (tcsetattr(global.ttyfd, TCSAFLUSH, &tios) != 0) { + global.last_errno = errno; + return TB_ERR_TCSETATTR; + } + + return TB_OK; +} + +static int init_term_caps() { + if (load_terminfo() == TB_OK) { + return parse_terminfo_caps(); + } + return load_builtin_caps(); +} + +static int init_resize_handler() { + if (pipe(global.resize_pipefd) != 0) { + global.last_errno = errno; + return TB_ERR_RESIZE_PIPE; + } + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_resize; + if (sigaction(SIGWINCH, &sa, NULL) != 0) { + global.last_errno = errno; + return TB_ERR_RESIZE_SIGACTION; + } + + return TB_OK; +} + +static int send_init_escape_codes() { + int rv; + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_CA])); + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_KEYPAD])); + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); + return TB_OK; +} + +static int send_clear() { + int rv; + + if_err_return(rv, send_attr(global.fg, global.bg)); + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN])); + + if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); + if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); + + global.last_x = -1; + global.last_y = -1; + + return TB_OK; +} + +static int update_term_size() { + int rv; + + if (global.ttyfd < 0) { + return TB_OK; + } + + struct winsize sz; + memset(&sz, 0, sizeof(sz)); + + if (ioctl(global.ttyfd, TIOCGWINSZ, &sz) == 0) { + global.width = sz.ws_col; + global.height = sz.ws_row; + return TB_OK; + } + + // If TB_RESIZE_FALLBACK deinfed, try >cursor(9999,9999), >u7, = 0) { + if (global.has_orig_tios) { + tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios); + } + if (global.ttyfd_open) { + close(global.ttyfd); + global.ttyfd_open = 0; + } + } + if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); + if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); + + cellbuf_free(&global.back); + cellbuf_free(&global.front); + bytebuf_free(&global.in); + bytebuf_free(&global.out); + + if (global.terminfo) tb_free(global.terminfo); + + tb_reset(); + return TB_OK; +} + +static int load_terminfo() { + int rv; + char tmp[PATH_MAX]; + + // See terminfo(5) "Fetching Compiled Descriptions" for a description of + // this behavior. Some of these paths are compile-time ncurses options, so + // best guesses are used here. + const char *term = getenv("TERM"); + if (!term) { + return TB_ERR; + } + + // If TERMINFO is set, try that directory and stop + const char *terminfo = getenv("TERMINFO"); + if (terminfo) { + return load_terminfo_from_path(terminfo, term); + } + + // Next try ~/.terminfo + const char *home = getenv("HOME"); + if (home) { + snprintf_or_return(rv, tmp, sizeof(tmp), "%s/.terminfo", home); + if_ok_return(rv, load_terminfo_from_path(tmp, term)); + } + + // Next try TERMINFO_DIRS + // + // Note, empty entries are supposed to be interpretted as the "compiled-in + // default", which is of course system-dependent. Previously /etc/terminfo + // was used here. Let's skip empty entries altogether rather than give + // precedence to a guess, and check common paths after this loop. + const char *dirs = getenv("TERMINFO_DIRS"); + if (dirs) { + snprintf_or_return(rv, tmp, sizeof(tmp), "%s", dirs); + char *dir = strtok(tmp, ":"); + while (dir) { + const char *cdir = dir; + if (*cdir != '\0') { + if_ok_return(rv, load_terminfo_from_path(cdir, term)); + } + dir = strtok(NULL, ":"); + } + } + + #ifdef TB_TERMINFO_DIR + if_ok_return(rv, load_terminfo_from_path(TB_TERMINFO_DIR, term)); + #endif + if_ok_return(rv, load_terminfo_from_path("/usr/local/etc/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/usr/local/share/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/usr/local/lib/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/etc/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/usr/share/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/usr/lib/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/usr/share/lib/terminfo", term)); + if_ok_return(rv, load_terminfo_from_path("/lib/terminfo", term)); + + return TB_ERR; +} + +static int load_terminfo_from_path(const char *path, const char *term) { + int rv; + char tmp[PATH_MAX]; + + // Look for term at this terminfo location, e.g., /x/xterm + snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term); + if_ok_return(rv, read_terminfo_path(tmp)); + + #ifdef __APPLE__ + // Try the Darwin equivalent path, e.g., /78/xterm + snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term); + return read_terminfo_path(tmp); + #endif + + return TB_ERR; +} + +static int read_terminfo_path(const char *path) { + FILE *fp = fopen(path, "rb"); + if (!fp) { + return TB_ERR; + } + + struct stat st; + if (fstat(fileno(fp), &st) != 0) { + fclose(fp); + return TB_ERR; + } + + size_t fsize = st.st_size; + char *data = tb_malloc(fsize); + if (!data) { + fclose(fp); + return TB_ERR; + } + + if (fread(data, 1, fsize, fp) != fsize) { + fclose(fp); + tb_free(data); + return TB_ERR; + } + + global.terminfo = data; + global.nterminfo = fsize; + + fclose(fp); + return TB_OK; +} + +static int parse_terminfo_caps() { + // See term(5) "LEGACY STORAGE FORMAT" and "EXTENDED STORAGE FORMAT" for a + // description of this behavior. + + // Ensure there's at least a header's worth of data + if (global.nterminfo < 6) { + return TB_ERR; + } + + int16_t *header = (int16_t *)global.terminfo; + // header[0] the magic number (octal 0432 or 01036) + // header[1] the size, in bytes, of the names section + // header[2] the number of bytes in the boolean section + // header[3] the number of short integers in the numbers section + // header[4] the number of offsets (short integers) in the strings section + // header[5] the size, in bytes, of the string table + + // Legacy ints are 16-bit, extended ints are 32-bit + const int bytes_per_int = + header[0] == 01036 + ? 4 // 32-bit + : 2; // 16-bit + + // > Between the boolean section and the number section, a null byte will be + // > inserted, if necessary, to ensure that the number section begins on an + // > even byte + const int align_offset = + (header[1] + header[2]) % 2 != 0 + ? 1 + : 0; + + const int pos_str_offsets = + (6 * sizeof(int16_t)) // header (12 bytes) + + header[1] // length of names section + + header[2] // length of boolean section + + align_offset + + (header[3] * bytes_per_int); // length of numbers section + + const int pos_str_table = + pos_str_offsets + + (header[4] * sizeof(int16_t)); // length of string offsets table + + // Load caps + int i; + for (i = 0; i < TB_CAP__COUNT; i++) { + const char *cap = get_terminfo_string( + pos_str_offsets, + pos_str_table, + header[5], + terminfo_cap_indexes[i] + ); + if (!cap) { + // Something is not right + return TB_ERR; + } + global.caps[i] = cap; + } + + return TB_OK; +} + +static int load_builtin_caps() { + int i, j; + const char *term = getenv("TERM"); + + if (!term) { + return TB_ERR_NO_TERM; + } + + // Check for exact TERM match + for (i = 0; builtin_terms[i].name != NULL; i++) { + if (strcmp(term, builtin_terms[i].name) == 0) { + for (j = 0; j < TB_CAP__COUNT; j++) { + global.caps[j] = builtin_terms[i].caps[j]; + } + return TB_OK; + } + } + + // Check for partial TERM or alias match + for (i = 0; builtin_terms[i].name != NULL; i++) { + if (strstr(term, builtin_terms[i].name) != NULL + || (*(builtin_terms[i].alias) != '\0' + && strstr(term, builtin_terms[i].alias) != NULL) + ) { + for (j = 0; j < TB_CAP__COUNT; j++) { + global.caps[j] = builtin_terms[i].caps[j]; + } + return TB_OK; + } + } + + return TB_ERR_UNSUPPORTED_TERM; +} + +static const char * get_terminfo_string(int16_t str_offsets_pos, int16_t str_table_pos, int16_t str_table_len, int16_t str_index) { + const int16_t *str_offset = (int16_t *)(global.terminfo + + (int)str_offsets_pos + + ((int)str_index * (int)sizeof(int16_t))); + if (*str_offset >= str_table_len) { + // Invalid string offset + return NULL; + } + if (((size_t)((int)str_table_pos + (int)*str_offset)) >= global.nterminfo) { + // Truncated/corrupt terminfo? + return NULL; + } + return (const char *)(global.terminfo + + (int)str_table_pos + + (int)*str_offset); +} + +static int wait_event(struct tb_event *event, struct timeval *timeout) { + int rv; + fd_set rfds; + char buf[64]; + + memset(event, 0, sizeof(*event)); + if_ok_return(rv, extract_event(event)); + + do { + FD_ZERO(&rfds); + FD_SET(global.rfd, &rfds); + FD_SET(global.resize_pipefd[0], &rfds); + + int maxfd = global.resize_pipefd[0] > global.rfd + ? global.resize_pipefd[0] + : global.rfd; + + int select_rv = select(maxfd + 1, &rfds, NULL, NULL, timeout); + + if (select_rv < 0) { + // Let EINTR/EAGAIN bubble up + global.last_errno = errno; + return TB_ERR_SELECT; + } else if (select_rv == 0) { + return TB_ERR_NO_EVENT; + } + + if (FD_ISSET(global.rfd, &rfds)) { + ssize_t read_rv = read(global.rfd, buf, sizeof(buf)); + if (read_rv < 0) { + global.last_errno = errno; + return TB_ERR_READ; + } else if (read_rv > 0) { + bytebuf_nputs(&global.in, buf, read_rv); + } + } + + if (FD_ISSET(global.resize_pipefd[0], &rfds)) { + int ignore = 0; + read(global.resize_pipefd[0], &ignore, sizeof(ignore)); + if_err_return(rv, update_term_size()); + event->type = TB_EVENT_RESIZE; + event->w = global.width; + event->h = global.height; + global.need_resize = 1; + return TB_OK; + } + + memset(event, 0, sizeof(*event)); + if_ok_return(rv, extract_event(event)); + } while (!timeout || timeout->tv_sec > 0 || timeout->tv_usec > 0); + + return TB_ERR_NO_EVENT; +} + +static int extract_event(struct tb_event *event) { + int rv; + struct bytebuf_t *in = &global.in; + + if (in->len == 0) { + return TB_ERR; + } + + if (in->buf[0] == '\x1b') { + // Escape sequence? + // In TB_INPUT_ESC, skip if the buffer is a single escape char + if (!((global.input_mode & TB_INPUT_ESC) && in->len == 1)) { + if_ok_or_need_more_return(rv, extract_esc(event)); + } + + // Escape key? + if (global.input_mode & TB_INPUT_ESC) { + event->type = TB_EVENT_KEY; + event->ch = 0; + event->key = TB_KEY_ESC; + event->mod = 0; + bytebuf_shift(in, 1); + return TB_OK; + } + + // Recurse for alt key + event->mod |= TB_MOD_ALT; + bytebuf_shift(in, 1); + return extract_event(event); + } + + // ASCII control key? + if ((uint16_t)in->buf[0] <= TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2) { + event->type = TB_EVENT_KEY; + event->ch = 0; + event->key = (uint16_t)in->buf[0]; + bytebuf_shift(in, 1); + return TB_OK; + } + + // UTF-8? + if (in->len >= (size_t)tb_utf8_char_length(in->buf[0])) { + event->type = TB_EVENT_KEY; + tb_utf8_char_to_unicode(&event->ch, in->buf); + event->key = 0; + bytebuf_shift(in, tb_utf8_char_length(in->buf[0])); + return TB_OK; + } + + // Need more input + return TB_ERR; +} + +static int extract_esc(struct tb_event *event) { + int rv; + if_ok_or_need_more_return(rv, extract_esc_user(event, 0)); + if_ok_or_need_more_return(rv, extract_esc_cap(event)); + if_ok_or_need_more_return(rv, extract_esc_mod_arrow(event)); + if_ok_or_need_more_return(rv, extract_esc_mouse(event)); + if_ok_or_need_more_return(rv, extract_esc_user(event, 1)); + return TB_ERR; +} + +static int extract_esc_user(struct tb_event *event, int is_post) { + int rv; + size_t consumed = 0; + struct bytebuf_t *in = &global.in; + int (*fn)(struct tb_event *, size_t *); + + fn = is_post + ? global.fn_extract_esc_post + : global.fn_extract_esc_pre; + + if (!fn) { + return TB_ERR; + } + + rv = fn(event, &consumed); + if (rv == TB_OK) { + bytebuf_shift(in, consumed); + } + + if_ok_or_need_more_return(rv, rv); + return TB_ERR; +} + +static int extract_esc_mouse(struct tb_event *event) { + (void)event; + // TODO mouse escape sequence parsing + return TB_ERR; + + /* + struct bytebuf_t *in = &global.in; + char *c = in->buf; + + int is_vt200 = (in->len >= 3 && strncmp("\x1b[M", in->buf, 3) == 0) ? 1 : 0; + int is_1006 = (in->len >= 3 && strncmp("\x1b[<", in->buf, 3) == 0) ? 1 : 0; + int is_1015 = (in->len >= 2 && in->buf[1] == '[') ? 1 : 0; + + + if (is_vt200 && in->len >= 6) { + // VT200 mode + uint8_t b = c[3], x = c[4], y = c[5]; + b -= 0x20; + event->type = TB_EVENT_MOUSE; + event->key = TB_KEY_MOUSE_LEFT; + event->x = x - 0x21; + event->y = y - 0x21; + if (b & 0x20) { + event->mod |= TB_MOD_MOTION; + } + bytebuf_shift(in, 6); + return TB_OK; + } + + + + if (is_vt200) { + return TB_ERR_NEED_MORE; + } + + return TB_ERR; + + if (len >= 6 && starts_with(buf, len, "\x1b[M")) { + // X10 mouse encoding, the simplest one + // \x1b [ M Cb Cx Cy + int b = buf[3] - 32; + switch (b & 3) { + case 0: + if ((b & 64) != 0) + event->key = TB_KEY_MOUSE_WHEEL_UP; + else + event->key = TB_KEY_MOUSE_LEFT; + break; + case 1: + if ((b & 64) != 0) + event->key = TB_KEY_MOUSE_WHEEL_DOWN; + else + event->key = TB_KEY_MOUSE_MIDDLE; + break; + case 2: + event->key = TB_KEY_MOUSE_RIGHT; + break; + case 3: + event->key = TB_KEY_MOUSE_RELEASE; + break; + default: + return -6; + } + event->type = TB_EVENT_MOUSE; // TB_EVENT_KEY by default + if ((b & 32) != 0) + event->mod |= TB_MOD_MOTION; + + // the coord is 1,1 for upper left + event->x = (uint8_t)buf[4] - 1 - 32; + event->y = (uint8_t)buf[5] - 1 - 32; + + return 6; + } else if (starts_with(buf, len, "\x1b[<") || + starts_with(buf, len, "\x1b[")) { + // xterm 1006 extended mode or urxvt 1015 extended mode + // xterm: \x1b [ < Cb ; Cx ; Cy (M or m) + // urxvt: \x1b [ Cb ; Cx ; Cy M + int i, mi = -1, starti = -1; + int isM, isU, s1 = -1, s2 = -1; + int n1 = 0, n2 = 0, n3 = 0; + + for (i = 0; i < len; i++) { + // We search the first (s1) and the last (s2) ';' + if (buf[i] == ';') { + if (s1 == -1) + s1 = i; + s2 = i; + } + + // We search for the first 'm' or 'M' + if ((buf[i] == 'm' || buf[i] == 'M') && mi == -1) { + mi = i; + break; + } + } + if (mi == -1) + return 0; + + // whether it's a capital M or not + isM = (buf[mi] == 'M'); + + if (buf[2] == '<') { + isU = 0; + starti = 3; + } else { + isU = 1; + starti = 2; + } + + if (s1 == -1 || s2 == -1 || s1 == s2) + return 0; + + n1 = strtoul(&buf[starti], NULL, 10); + n2 = strtoul(&buf[s1 + 1], NULL, 10); + n3 = strtoul(&buf[s2 + 1], NULL, 10); + + if (isU) + n1 -= 32; + + switch (n1 & 3) { + case 0: + if ((n1 & 64) != 0) { + event->key = TB_KEY_MOUSE_WHEEL_UP; + } else { + event->key = TB_KEY_MOUSE_LEFT; + } + break; + case 1: + if ((n1 & 64) != 0) { + event->key = TB_KEY_MOUSE_WHEEL_DOWN; + } else { + event->key = TB_KEY_MOUSE_MIDDLE; + } + break; + case 2: + event->key = TB_KEY_MOUSE_RIGHT; + break; + case 3: + event->key = TB_KEY_MOUSE_RELEASE; + break; + default: + return mi + 1; + } + + if (!isM) { + // on xterm mouse release is signaled by lowercase m + event->key = TB_KEY_MOUSE_RELEASE; + } + + event->type = TB_EVENT_MOUSE; // TB_EVENT_KEY by default + if ((n1 & 32) != 0) + event->mod |= TB_MOD_MOTION; + + event->x = (uint8_t)n2 - 1; + event->y = (uint8_t)n3 - 1; + + return mi + 1; + } + + return 0; + */ +} + +static int extract_esc_mod_arrow(struct tb_event *event) { + struct bytebuf_t *in = &global.in; + char *c = in->buf; + int dir_i; + + if (in->len >= 6 + && strncmp("\x1b[1;", in->buf, 4) == 0 + && c[4] >= '2' && c[4] <= '8' + && c[5] >= 'A' && c[5] <= 'D' + ) { + // xterm style + dir_i = 5; + switch (c[4]) { + case '2': event->mod |= TB_MOD_SHIFT; break; + case '3': event->mod |= TB_MOD_ALT; break; + case '4': event->mod |= TB_MOD_ALT | TB_MOD_SHIFT; break; + case '5': event->mod |= TB_MOD_CTRL; break; + case '6': event->mod |= TB_MOD_CTRL | TB_MOD_SHIFT; break; + case '7': event->mod |= TB_MOD_CTRL | TB_MOD_ALT; break; + case '8': event->mod |= TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT; break; + } + } else if (in->len >= 4 + && c[0] == '\x1b' + && c[1] == '\x1b' + && strchr("[O", c[2]) != NULL + && strchr("ABCDabcd", c[3]) != NULL + ) { + dir_i = 3; + if (c[2] == '[') { + if (c[3] >= 'A' && c[3] <= 'D') { + // rxvt/putty alt + event->mod |= TB_MOD_ALT; + } else { // (c[3] >= 'a' && c[3] <= 'd') + // rxvt alt-shift + event->mod |= TB_MOD_ALT | TB_MOD_SHIFT; + } + } else { // c[2] == 'O' + // rxvt/putty ctrl-alt + event->mod |= TB_MOD_CTRL | TB_MOD_ALT; + } + } else if (in->len >= 3 + && c[0] == '\x1b' + && strchr("[O", c[1]) != NULL + && strchr("ABCDabcd", c[2]) != NULL + ) { + dir_i = 2; + if (c[1] == '[') { + // rxvt/linux/putty shift + event->mod |= TB_MOD_SHIFT; + } else { // c[1] == 'O' + // rxvt/putty ctrl + event->mod |= TB_MOD_CTRL; + } + } else { + if ((in->len == 4 || in->len == 5) + && strncmp("\x1b[1;", in->buf, 4) == 0 + ) { + return TB_ERR_NEED_MORE; // maybe xterm + } else if (in->len == 3 && ( + strncmp("\x1b[1", in->buf, in->len) == 0 + || strncmp("\x1b\x1bO", in->buf, in->len) == 0 + || strncmp("\x1b\x1b[", in->buf, in->len) == 0 + )) { + return TB_ERR_NEED_MORE; // maybe rxvt/linux/putty + } else if (in->len == 2 && ( + strncmp("\x1b\x1b", in->buf, in->len) == 0 + || strncmp("\x1bO", in->buf, in->len) == 0 + || strncmp("\x1b[", in->buf, in->len) == 0 + )) { + return TB_ERR_NEED_MORE; // maybe rxvt/linux/putty + } else if (in->len == 1 && c[0] == '\x1b') { + return TB_ERR_NEED_MORE; // maybe anything + } + return TB_ERR; // Not a mod-arrow escape sequence + } + + event->type = TB_EVENT_KEY; + switch (c[dir_i]) { + case 'A': + case 'a': event->key = TB_KEY_ARROW_UP; break; + case 'B': + case 'b': event->key = TB_KEY_ARROW_DOWN; break; + case 'C': + case 'c': event->key = TB_KEY_ARROW_RIGHT; break; + case 'D': + case 'd': event->key = TB_KEY_ARROW_LEFT; break; + } + bytebuf_shift(in, dir_i + 1); + + return TB_OK; +} + +static int extract_esc_cap(struct tb_event *event) { + struct bytebuf_t *in = &global.in; + + // TODO A trie would be more efficient + // TODO TB_ERR_NEED_MORE + int i; + for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { + int cap_len = strlen(global.caps[i]); + if (strncmp(in->buf, global.caps[i], cap_len) == 0) { + event->type = TB_EVENT_KEY; + event->ch = 0; + event->key = tb_key_i(i); + bytebuf_shift(in, cap_len); + return TB_OK; + } + } + return TB_ERR; +} + +static int resize_if_needed() { + int rv; + if (!global.need_resize) { + return TB_OK; + } + if_err_return(rv, update_term_size()); // TODO is this needed? + if_err_return(rv, cellbuf_resize(&global.back, global.width, global.height)); + if_err_return(rv, cellbuf_resize(&global.front, global.width, global.height)); + if_err_return(rv, cellbuf_clear(&global.front)); + if_err_return(rv, send_clear()); + global.need_resize = 0; + return TB_OK; +} + +static void handle_resize(int sig) { + write(global.resize_pipefd[1], &sig, sizeof(sig)); +} + +static int send_attr(uintattr_t fg, uintattr_t bg) { + int rv; + + if (fg == global.last_fg && bg == global.last_bg) { + return TB_OK; + } + + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0])); + + uintattr_t cfg, cbg; + switch (global.output_mode) { + default: + case TB_OUTPUT_NORMAL: + cfg = fg & 0x0f; + cbg = bg & 0x0f; + break; + + case TB_OUTPUT_256: + cfg = fg & 0xff; + cbg = bg & 0xff; + break; + + case TB_OUTPUT_216: + cfg = fg & 0xff; if (cfg > 215) cfg = 7; + cbg = bg & 0xff; if (cbg > 215) cbg = 0; + cfg += 0x10; + cbg += 0x10; + break; + + case TB_OUTPUT_GRAYSCALE: + cfg = fg & 0xff; if (cfg > 23) cfg = 23; + cbg = bg & 0xff; if (cbg > 23) cbg = 0; + cfg += 0xe8; + cbg += 0xe8; + break; + + case TB_OUTPUT_TRUECOLOR: + cfg = fg; + cbg = bg; + break; + } + + if (fg & TB_BOLD) + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BOLD])); + + if (bg & TB_BOLD) + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BLINK])); + + if (fg & TB_UNDERLINE) + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_UNDERLINE])); + + if (fg & TB_ITALIC) + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ITALIC])); + + if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) + if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_REVERSE])); + + if_err_return(rv, send_sgr(cfg, cbg)); + + global.last_fg = fg; + global.last_bg = bg; + + return TB_OK; +} + +static int send_sgr(uintattr_t fg, uintattr_t bg) { + int rv; + char nbuf[32]; + + if (global.output_mode != TB_OUTPUT_TRUECOLOR && fg == TB_DEFAULT && bg == TB_DEFAULT) { + return TB_OK; + } + + switch (global.output_mode) { + default: + case TB_OUTPUT_NORMAL: + send_literal(rv, "\x1b["); + if (fg != TB_DEFAULT) { + send_literal(rv, "3"); + send_num(rv, nbuf, fg - 1); + if (bg != TB_DEFAULT) { + send_literal(rv, ";"); + } + } + if (bg != TB_DEFAULT) { + send_literal(rv, "4"); + send_num(rv, nbuf, bg - 1); + } + send_literal(rv, "m"); + break; + + case TB_OUTPUT_256: + case TB_OUTPUT_216: + case TB_OUTPUT_GRAYSCALE: + send_literal(rv, "\x1b["); + if (fg != TB_DEFAULT) { + send_literal(rv, "38;5;"); + send_num(rv, nbuf, fg); + if (bg != TB_DEFAULT) { + send_literal(rv, ";"); + } + } + if (bg != TB_DEFAULT) { + send_literal(rv, "48;5;"); + send_num(rv, nbuf, bg); + } + send_literal(rv, "m"); + break; + + case TB_OUTPUT_TRUECOLOR: + send_literal(rv, "\x1b[38;2;"); + send_num(rv, nbuf, fg >> 16 & 0xff); + send_literal(rv, ";"); + send_num(rv, nbuf, fg >> 8 & 0xff); + send_literal(rv, ";"); + send_num(rv, nbuf, fg & 0xff); + send_literal(rv, ";48;2;"); + send_num(rv, nbuf, bg >> 16 & 0xff); + send_literal(rv, ";"); + send_num(rv, nbuf, bg >> 8 & 0xff); + send_literal(rv, ";"); + send_num(rv, nbuf, bg & 0xff); + send_literal(rv, "m"); + break; + } + return TB_OK; +} + +static int send_cursor_if(int x, int y) { + int rv; + char nbuf[32]; + if (x < 0 || y < 0) { + return TB_OK; + } + send_literal(rv, "\x1b["); + send_num(rv, nbuf, y + 1); + send_literal(rv, ";"); + send_num(rv, nbuf, x + 1); + send_literal(rv, "H"); + return TB_OK; +} + +static int send_char(int x, int y, uint32_t ch) { + return send_cluster(x, y, &ch, 1); +} + +static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { + int rv; + char abuf[8]; + + if (global.last_x != x - 1 || global.last_y != y) { + if_err_return(rv, send_cursor_if(x, y)); + } + global.last_x = x; + global.last_y = y; + + int i; + for (i = 0; i < (int)nch; i++) { + uint32_t ach = *(ch + i); + int aw = tb_utf8_unicode_to_char(abuf, ach); + if (!ach) { + abuf[0] = ' '; + } + if_err_return(rv, bytebuf_nputs(&global.out, abuf, (size_t)aw)); + } + + return TB_OK; +} + +static int convert_num(uint32_t num, char *buf) { + int i, l = 0; + int ch; + do { + buf[l++] = '0' + (num % 10); + num /= 10; + } while (num); + for (i = 0; i < l / 2; i++) { + ch = buf[i]; + buf[i] = buf[l - 1 - i]; + buf[l - 1 - i] = ch; + } + return l; +} + +static int cell_cmp(struct tb_cell *a, struct tb_cell *b) { + if (a->ch != b->ch || a->fg != b->fg || a->bg != b->bg) { + return 1; + } + #ifdef TB_OPT_EGC + if (a->nech != b->nech) { + return 1; + } else if (a->nech > 0) { // a->nech == b->nech + return memcmp(a->ech, b->ech, a->nech); + } + #endif + return 0; +} + +static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { + #ifdef TB_OPT_EGC + if (src->nech > 0) { + return cell_set(dst, src->ech, src->nech, src->fg, src->bg); + } + #endif + return cell_set(dst, &src->ch, 1, src->fg, src->bg); +} + +static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg) { + cell->ch = ch ? *ch : 0; + cell->fg = fg; + cell->bg = bg; + #ifdef TB_OPT_EGC + if (nch <= 1) { + cell->nech = 0; + } else { + cell_reserve_ech(cell, nch + 1); + memcpy(cell->ech, ch, nch); + cell->ech[nch] = '\0'; + cell->nech = nch; + } + #else + (void)nch; + (void)cell_reserve_ech; + #endif + return TB_OK; +} + +static int cell_reserve_ech(struct tb_cell *cell, size_t n) { + #ifdef TB_OPT_EGC + if (cell->cech >= n) { + return TB_OK; + } + if (!(cell->ech = tb_realloc(cell->ech, n * sizeof(cell->ch)))) { + return TB_ERR_MEM; + } + cell->cech = n; + return TB_OK; + #else + (void)cell; + (void)n; + return TB_ERR; + #endif +} + +static int cell_free(struct tb_cell *cell) { + #ifdef TB_OPT_EGC + if (cell->ech) { + tb_free(cell->ech); + } + #endif + memset(cell, 0, sizeof(*cell)); + return TB_OK; +} + +static int cellbuf_init(struct cellbuf_t *c, int w, int h) { + c->cells = tb_malloc(sizeof(struct tb_cell) * w * h); + if (!c->cells) { + return TB_ERR_MEM; + } + memset(c->cells, 0, sizeof(struct tb_cell) * w * h); + c->width = w; + c->height = h; + return TB_OK; +} + +static int cellbuf_free(struct cellbuf_t *c) { + if (c->cells) { + int i; + for (i = 0; i < c->width * c->height; i++) { + cell_free(&c->cells[i]); + } + tb_free(c->cells); + } + memset(c, 0, sizeof(*c)); + return TB_OK; +} + +static int cellbuf_clear(struct cellbuf_t *c) { + int i; + uint32_t space = (uint32_t)' '; + for (i = 0; i < c->width * c->height; i++) { + cell_set(&c->cells[i], &space, 1, global.fg, global.bg); + } + return TB_OK; +} + +static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out) { + if (x < 0 || x >= c->width || y < 0 || y >= c->height) { + *out = NULL; + return TB_ERR_OUT_OF_BOUNDS; + } + *out = &c->cells[(y * c->width) + x]; + return TB_OK; +} + +static int cellbuf_resize(struct cellbuf_t *c, int w, int h) { + int rv; + + int ow = c->width; + int oh = c->height; + + if (ow == w && oh == h) { + return TB_OK; + } + + w = w < 1 ? 1 : w; + h = h < 1 ? 1 : h; + + int minw = (w < ow) ? w : ow; + int minh = (h < oh) ? h : oh; + + struct tb_cell *prev = c->cells; + + if_err_return(rv, cellbuf_init(c, w, h)); + if_err_return(rv, cellbuf_clear(c)); + + int x, y; + for (x = 0; x < minw; x++) { + for (y = 0; y < minh; y++) { + struct tb_cell *src, *dst; + src = &prev[(y * ow) + x]; + if_err_return(rv, cellbuf_get(c, x, y, &dst)); + if_err_return(rv, cell_copy(dst, src)); + } + } + + tb_free(prev); + + return TB_OK; +} + +static int bytebuf_puts(struct bytebuf_t *b, const char *str) { + return bytebuf_nputs(b, str, (size_t)strlen(str)); +} + +static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr) { + int rv; + if_err_return(rv, bytebuf_reserve(b, b->len + nstr + 1)); + memcpy(b->buf + b->len, str, nstr); + b->len += nstr; + b->buf[b->len] = '\0'; + return TB_OK; +} + +static int bytebuf_shift(struct bytebuf_t *b, size_t n) { + if (n > b->len) { + n = b->len; + } + size_t nmove = b->len - n; + memmove(b->buf, b->buf + n, nmove); + b->len -= n; + return TB_OK; +} + +static int bytebuf_flush(struct bytebuf_t *b, int fd) { + if (b->len <= 0) { + return TB_OK; + } + ssize_t write_rv = write(fd, b->buf, b->len); + if (write_rv < 0 || (size_t)write_rv != b->len) { + // Note, errno will be 0 on partial write + global.last_errno = errno; + return TB_ERR; + } + b->len = 0; + return TB_OK; +} + +static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { + if (b->cap >= sz) { + return TB_OK; + } + size_t newcap = b->cap > 0 ? b->cap : 1; + while (newcap < sz) { + newcap *= 2; + } + char *newbuf; + if (b->buf) { + newbuf = tb_realloc(b->buf, newcap); + } else { + newbuf = tb_malloc(newcap); + } + if (!newbuf) { + return TB_ERR_MEM; + } + b->buf = newbuf; + b->cap = newcap; + return TB_OK; +} + +static int bytebuf_free(struct bytebuf_t *b) { + if (b->buf) { + tb_free(b->buf); + } + memset(b, 0, sizeof(*b)); + return TB_OK; +} + +#endif /* TB_IMPL */ diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..54a58c4 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,14 @@ +FROM debian:10-slim +RUN apt update \ + && apt install -y lsb-release apt-transport-https ca-certificates wget \ + && wget -O /etc/apt/trusted.gpg.d/php.gpg 'https://packages.sury.org/php/apt.gpg' \ + && echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | \ + tee /etc/apt/sources.list.d/php.list \ + && apt update \ + && apt install -y make gcc php7.4 php7.4-mbstring xvfb xterm xvkbd locales locales-all +ENV LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US.UTF-8 +COPY . /termbox +WORKDIR /termbox +RUN ./tests/run.sh diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..f674d29 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -uo pipefail + +main() { + local timeout_s=5 + local x_display=':1000' + local xvfb_pipe='/tmp/.X11-unix/X1000' + local this_dir="$(cd $(dirname "${BASH_SOURCE[0]}") &>/dev/null && pwd)/" + local xterm_geom='80x24+0+0' + local xterm_bg='grey3' + local xterm_fg='grey93' + local main_ec=0 + + # loop through each 'test_*' dir + for test_php in $(find . -type f -wholename '*/test_*/test.php'); do + local test_dir=$(dirname $test_php) + local test_name=$(basename $test_dir) + echo -e "\x1b[1m$test_name\x1b[0m: BEGIN" + + # make log file + local test_log_cmd=$(mktemp '/tmp/tb_test.XXXXXXXXXX') + local test_log_xterm="${test_log_cmd}.xterm" + local test_log_php="${test_log_cmd}.php" + + # run Xvfb (headless X server) + echo -n ' waiting for Xvfb slot...' + while test -e $xvfb_pipe; do echo -n .; sleep 0.1; done + echo + Xvfb -screen 0 800x600x24 $x_display &>/dev/null & + local xvfb_pid=$! + + # wait for Xvfb to come up + echo -n ' starting Xvfb...' + local max_wait=30 + while ! test -e $xvfb_pipe; do + echo -n . + max_wait=$((max_wait-1)) + [ "$max_wait" -le 0 ] && break + sleep 1 + done + echo + + # run test_bin in xterm in Xvfb + echo ' running test in xterm' + xterm -display $x_display \ + -u8 -geometry $xterm_geom -bg $xterm_bg -fg $xterm_fg \ + -xrm 'xterm*metaSendsEscape:true' \ + -xrm 'xterm*translations:#override\nShift Home:print-immediate()' \ + -xrm 'xterm*printOptsImmediate:1' \ + -xrm 'xterm*printModeImmediate:2' \ + -xrm "xterm*printFileImmediate:$test_log_xterm" \ + -e "php -d auto_prepend_file=$this_dir/test_ffi.php $test_php $test_log_cmd &>$test_log_php" \ + &>/dev/null & + local xterm_pid=$! + + # tail test_log_cmd until we see 'screencap' + local test_log_cursor=0 + local test_log_size=0 + local test_end_ts=$(($(date +%s) + $timeout_s)) + while true; do + test_log_size=$(stat --format=%s $test_log_cmd 2>/dev/null) + [ -z "$test_log_size" ] && break # stat failed or deleted + [ "$test_log_size" -lt "$test_log_cursor" ] && break # truncated + local test_log_content=$(tail -c "+$test_log_cursor" $test_log_cmd | \ + head -c "$((test_log_size-test_log_cursor))") + test_log_cursor=$test_log_size + echo -n "$test_log_content" + grep -q 'screencap' <<<"$test_log_content" && break + sleep 0.1 + if [ "$(date +%s)" -ge "$test_end_ts" ]; then + echo -e '\n timeout' + break + fi + done + echo + + # take screencap + # xwd -root -display $x_display -out $test_dir/observed.xwd # graphical + rm -f "$test_dir/observed.*" + DISPLAY=$x_display xvkbd -window xterm -text '\S\[Home]' &>/dev/null # ansi + local test_log_xterm_count=$(ls -1 ${test_log_xterm}* 2>/dev/null | wc -l) + [ "$test_log_xterm_count" -eq 1 ] && cp ${test_log_xterm}* $test_dir/observed.ansi + + # diff screencap + # convert $test_dir/expected.xwd $test_dir/observed.gif + diff $test_dir/expected.ansi $test_dir/observed.ansi &>/dev/null + diff_ec=$? + + # print result + if [ "$diff_ec" -eq 0 ]; then + echo -e "\x1b[1m$test_name\x1b[0m: \x1b[32mOK\x1b[0m" + else + echo + echo -e ' \x1b[31mdiff!\x1b[0m' + # compare $test_dir/expected.gif $test_dir/observed.gif $test_dir/diff.gif + # xz -c $test_dir/diff.gif | base64 + + echo -e '\n [xterm expected]' + cat $test_dir/expected.ansi 2>/dev/null | sed 's/^/ /'; echo -e '\x1b[0m' + + echo -e '\n [xterm observed]' + cat $test_dir/observed.ansi 2>/dev/null | sed 's/^/ /'; echo -e '\x1b[0m' + + echo -e '\n [xterm observed.ansi.b64]' + base64 $test_dir/observed.ansi | sed 's/^/ /' + + if [ -s "$test_log_php" ]; then + echo -e '\n [php log]' + cat $test_log_php | sed 's/^/ /' + fi + + echo -e "\x1b[1m$test_name\x1b[0m: \x1b[31mERR\x1b[0m" + main_ec=1 + fi + echo + + # clean up + kill $xterm_pid &>/dev/null + kill $xvfb_pid &>/dev/null + rm -f $test_log_cmd $test_log_xterm $test_log_php + done + + return $main_ec +} + +main diff --git a/tests/test_basic/expected.ansi b/tests/test_basic/expected.ansi new file mode 100644 index 0000000..9d3592a --- /dev/null +++ b/tests/test_basic/expected.ansi @@ -0,0 +1,24 @@ +#5width=80 +#5height=24 +#5event rv=0 type=1 mod=0 key=1 ch=0 w=0 h=0 x=0 y=0 + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_basic/test.php b/tests/test_basic/test.php new file mode 100755 index 0000000..580b9c5 --- /dev/null +++ b/tests/test_basic/test.php @@ -0,0 +1,36 @@ +ffi->tb_init(); + +$w = $test->ffi->tb_width(); +$h = $test->ffi->tb_height(); + +$bg = $test->defines['TB_BLACK']; +$red = $test->defines['TB_RED']; +$green = $test->defines['TB_GREEN']; +$blue = $test->defines['TB_BLUE']; + +$test->ffi->tb_printf(0, 0, $red, $bg, "width=%d", $w); +$test->ffi->tb_printf(0, 1, $green, $bg, "height=%d", $h); + +$test->xvkbd('\Ca'); // Ctrl-A + +$event = $test->ffi->new('struct tb_event'); +$rv = $test->ffi->tb_peek_event(FFI::addr($event), 1000); + +$test->ffi->tb_printf(0, 2, $blue, $bg, "event rv=%d type=%d mod=%d key=%d ch=%d w=%d h=%d x=%d y=%d", + $rv, + $event->type, + $event->mod, + $event->key, + $event->ch, + $event->w, + $event->h, + $event->x, + $event->y +); + +$test->ffi->tb_present(); + +$test->screencap(); diff --git a/tests/test_ffi.php b/tests/test_ffi.php new file mode 100644 index 0000000..2194c1c --- /dev/null +++ b/tests/test_ffi.php @@ -0,0 +1,97 @@ +ffi = $this->makeFfi(); + $this->test_log = $GLOBALS['argv'][1] ?? ''; + } + + private function makeFfi(): object { + $repo_dir = dirname(__DIR__); + $termbox_h = "$repo_dir/termbox.h"; + $libtermbox_so = "$repo_dir/libtermbox.so"; + $header_data_and_impl = file_get_contents($termbox_h); + + $matches = []; + preg_match( + '@#define __TERMBOX_H\n(.*?)#endif /\* __TERMBOX_H \*/@sm', + $header_data_and_impl, + $matches + ); + $header_data = $matches[1] ?? ''; + + // Extract #define values + $matches = []; + preg_match_all('/^#define\s+(TB_\S+)\s+(.+)$/m', $header_data, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $define_name = $match[1]; + $define_value = $match[2]; + + // Remove comments + $define_value = trim(preg_replace('|/\*.*$|', '', $define_value)); + + // Special case for evaluating `(0xFFFF - ...)` values + $match2 = []; + if (preg_match('/^\(0xFFFF - (\d+)\)$/', $define_value, $match2)) { + $define_value = 0xFFFF - (int)$match[2]; + } else if (substr($define_value, 0, 2) === '0x') { + $define_value = hexdec(substr($define_value, 2)); + } + $this->defines[$define_name] = (int)$define_value; + } + + // Make FFI + $header_data = preg_replace('/^.*__ffi_strip.*$/m', '', $header_data); + $ffi = FFI::cdef($header_data, $libtermbox_so); + + // Return wrapper that logs FFI calls + return new class($ffi, $this) { + private FFI $ffi; + private object $test; + public function __construct($ffi, $test) { + $this->ffi = $ffi; + $this->test = $test; + } + public function __call(string $name, array $args) { + if ($name !== 'tb_set_cell') { + $this->test->log("ffi $name " . json_encode($args)); + } + return $this->ffi->$name(...$args); + } + }; + } + + public function xvkbd(string $xvkbd_cmd): int { + $this->log("xvkbd $xvkbd_cmd"); + $cmd = sprintf( + "DISPLAY=:1000 xvkbd -remote-display :1000 -window xterm -text %s", + escapeshellarg($xvkbd_cmd) + ); + $sh_cmd = sprintf( + 'sh -c %s >/dev/null 2>&1', + escapeshellarg($cmd) + ); + $output = []; + $exit_code = 1; + exec($sh_cmd, $output, $exit_code); + return $exit_code; + } + + public function log(string $str): void { + $lines = explode("\n", $str); + foreach ($lines as $line) { + file_put_contents($this->test_log, " $line\n", FILE_APPEND); + } + } + + public function screencap(): void { + $this->log('screencap'); + sleep(PHP_INT_MAX); + } +}; diff --git a/tests/test_non_spacing_mark/expected.ansi b/tests/test_non_spacing_mark/expected.ansi new file mode 100644 index 0000000..5b69b33 --- /dev/null +++ b/tests/test_non_spacing_mark/expected.ansi @@ -0,0 +1,24 @@ +#5STARGΛ̊TE SG-1 +#5a = v̇ = r̈, a⃑ ⊥ b⃑ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_non_spacing_mark/test.php b/tests/test_non_spacing_mark/test.php new file mode 100755 index 0000000..3309a60 --- /dev/null +++ b/tests/test_non_spacing_mark/test.php @@ -0,0 +1,11 @@ +ffi->tb_init(); + +$test->ffi->tb_print(0, 0, 0, 0, "STARG\xce\x9b\xcc\x8aTE SG-1"); +$test->ffi->tb_print(0, 1, 0, 0, "a = v\xcc\x87 = r\xcc\x88, a\xe2\x83\x91 \xe2\x8a\xa5 b\xe2\x83\x91"); + + +$test->ffi->tb_present(); +$test->screencap(); -- 2.39.5