Skip to content

Commit 0b40a99

Browse files
committed
add experimental windows support via windows.sh
windows support has been requested several times throughout the years, however i've always been reluctant to pollute the library with ifdefs and/or abstraction layers for this sake. we avoid this with a shell script (`windows.sh`) that does surgery on `termbox2.h` to generate a windows-compatible version. some headers are swapped, some functions are altered or replaced, but most of the library stays the same. a couple marker comments (`__posix_(start|end)`) are added to `termbox2.h` which `windows.sh` uses to anchor various sed/awk commands. theoretically the test suite could run on windows. it would take some research to figure out the windows equivalents of things in `tests/run.sh` like `xvkbd` etc. github docs state that `windows-latest` has php pre-installed, and php does have windows ffi support. the memfd-based tests would have to be skipped (or proper rwfd support would need to be added to the windows lib). instead of running the test suite, we ensure we can at least compile the keyboard demo on windows. in my opinion this suffices for experimental support.
1 parent ed68bcc commit 0b40a99

4 files changed

Lines changed: 237 additions & 2 deletions

File tree

.github/workflows/termbox_test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
name: termbox_test
22
on: [push, pull_request]
33
jobs:
4-
termbox_test_job:
4+
posix:
55
runs-on: ubuntu-latest
66
steps:
77
- uses: actions/checkout@v2
88
- run: make clean test
99
- run: CFLAGS='-UTB_LIB_OPTS' make clean test # non-egc, non-truecolor
10+
win32:
11+
runs-on: windows-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- uses: ilammy/msvc-dev-cmd@v1
15+
- shell: bash
16+
run: ./windows.sh && cp -vf termbox2_win.h termbox2.h
17+
- shell: cmd
18+
run: |
19+
cl /c /I. /DTB_IMPL demo/keyboard.c
20+
cl /c /I. /DTB_IMPL /DTB_LIB_OPTS demo/keyboard.c

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ termbox2 also does not contain a layout engine for the same reason. However,
130130
there is at least one layout engine with termbox2 support:
131131
[Clay](https://github.com/nicbarker/clay).
132132

133+
### Windows support
134+
135+
termbox2 has experimental support for Windows. Run `./windows.sh` to generate
136+
`termbox2_win.h` which is a drop-in replacement for `termbox2.h`.
137+
133138
### Examples
134139

135140
* [mle](https://github.com/adsr/mle) - flexible terminal-based text editor

termbox2.h

100644100755
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,17 @@ SOFTWARE.
4343
#include <stdio.h>
4444
#include <stdlib.h>
4545
#include <string.h>
46+
#include <wchar.h>
47+
// __posix_header_start
4648
#include <sys/ioctl.h>
4749
#include <sys/select.h>
4850
#include <sys/stat.h>
4951
#include <sys/time.h>
5052
#include <sys/types.h>
5153
#include <termios.h>
5254
#include <unistd.h>
53-
#include <wchar.h>
5455
#include <wctype.h>
56+
// __posix_header_end
5557

5658
#ifdef PATH_MAX
5759
#define TB_PATH_MAX PATH_MAX
@@ -2354,9 +2356,12 @@ int tb_init_rwfd(int rfd, int wfd) {
23542356
int rv;
23552357

23562358
tb_reset();
2359+
2360+
// __posix_start
23572361
global.ttyfd = isatty(rfd) ? rfd : (isatty(wfd) ? wfd : -1);
23582362
global.rfd = rfd;
23592363
global.wfd = wfd;
2364+
// __posix_end
23602365

23612366
do {
23622367
if_err_break(rv, init_term_attrs());
@@ -2836,7 +2841,9 @@ const char *tb_strerror(int err) {
28362841
case TB_ERR_RESIZE_POLL:
28372842
case TB_ERR_RESIZE_READ:
28382843
default:
2844+
// __posix_start
28392845
strerror_r(global.last_errno, global.errbuf, sizeof(global.errbuf));
2846+
// __posix_end
28402847
return (const char *)global.errbuf;
28412848
}
28422849
}
@@ -3176,6 +3183,8 @@ static int tb_deinit(void) {
31763183
bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE);
31773184
bytebuf_flush(&global.out, global.wfd);
31783185
}
3186+
3187+
// __posix_start
31793188
if (global.ttyfd >= 0) {
31803189
if (global.has_orig_tios) {
31813190
tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios);
@@ -3192,6 +3201,7 @@ static int tb_deinit(void) {
31923201
sigaction(SIGWINCH, &sa, NULL);
31933202
if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]);
31943203
if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]);
3204+
// __posix_end
31953205

31963206
cellbuf_free(&global.back);
31973207
cellbuf_free(&global.front);
@@ -4191,12 +4201,16 @@ static int bytebuf_shift(struct bytebuf *b, size_t n) {
41914201

41924202
static int bytebuf_flush(struct bytebuf *b, int fd) {
41934203
if (b->len <= 0) return TB_OK;
4204+
4205+
// __posix_start
41944206
ssize_t write_rv = write(fd, b->buf, b->len);
41954207
if (write_rv < 0 || (size_t)write_rv != b->len) {
41964208
// Note, errno will be 0 on partial write
41974209
global.last_errno = errno;
41984210
return TB_ERR;
41994211
}
4212+
// __posix_end
4213+
42004214
b->len = 0;
42014215
return TB_OK;
42024216
}

windows.sh

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
main() {
5+
init_files "$@"
6+
alter_headers
7+
alter_structs
8+
alter_errors
9+
alter_functions
10+
}
11+
12+
init_files() {
13+
in=${1:-termbox2.h}
14+
out=${1:-termbox2_win.h}
15+
cp -vf "$in" "$out"
16+
tmp=$(mktemp)
17+
}
18+
19+
alter_headers() {
20+
my_sed '/__posix_header_start/,/__posix_header_end/c\#include <windows.h>\n#include <synchapi.h>\n'
21+
}
22+
23+
alter_structs() {
24+
my_sed 's/^(\s+)int (rfd|wfd|ttyfd);/\1HANDLE \2;/g'
25+
my_sed 's/^(\s+)struct termios orig_tios;/\1DWORD orig_tios[2];/g'
26+
}
27+
28+
alter_errors() {
29+
local err_line
30+
local err_line_num
31+
local err_val
32+
err_line=$(grep -E -n '^#define TB_ERR_.+[0-9]+$' "$out" | tail -n1)
33+
err_line_num=$(cut -d: -f1 <<<"$err_line")
34+
err_val=$(awk '{print $NF}' <<<"$err_line")
35+
my_sed "${err_line_num}a #define TB_ERR_WIN_UNSUPPORTED $((err_val - 5))"
36+
my_sed "${err_line_num}a #define TB_ERR_WIN_NO_STDIO $((err_val - 4))"
37+
my_sed "${err_line_num}a #define TB_ERR_WIN_SET_CONMODE $((err_val - 3))"
38+
my_sed "${err_line_num}a #define TB_ERR_WIN_GET_CONMODE $((err_val - 2))"
39+
my_sed "${err_line_num}a #define TB_ERR_WIN_RESIZE $((err_val - 1))"
40+
my_sed '/case TB_ERR:/i \ case TB_ERR_WIN_UNSUPPORTED: return "Unsupporrted (Windows)";'
41+
my_sed '/case TB_ERR:/i \ case TB_ERR_WIN_NO_STDIO: return "Stdio not available (Windows)";'
42+
my_sed '/case TB_ERR:/i \ case TB_ERR_WIN_SET_CONMODE: return "Failed to set console mode (Windows)";'
43+
my_sed '/case TB_ERR:/i \ case TB_ERR_WIN_GET_CONMODE: return "Failed to get console mode (Windows)";'
44+
my_sed '/case TB_ERR:/i \ case TB_ERR_WIN_RESIZE: return "Failed to resize console (Windows)";'
45+
}
46+
47+
alter_functions() {
48+
replace_function 'int tb_get_fds' <<<' return TB_ERR_WIN_UNSUPPORTED;'
49+
replace_function 'static int update_term_size_via_esc' <<<' return TB_ERR;'
50+
replace_function 'static int load_terminfo' <<<' return TB_ERR;'
51+
replace_function 'static void handle_resize' <<<' (void)sig;';
52+
replace_function 'static int init_resize_handler' <<<' return TB_OK;'
53+
54+
my_sed 's/getenv\("TERM"\)/"xterm-256color"/'
55+
56+
alter_function 'const char \*tb_strerror' <<'EOD'
57+
snprintf(global.errbuf, sizeof(global.errbuf), "Unknown error (Windows)");
58+
EOD
59+
60+
replace_function 'int tb_init_file' <<'EOD'
61+
if (global.initialized) return TB_ERR_INIT_ALREADY;
62+
if (strcmp(path, "/dev/tty")) return TB_ERR_WIN_UNSUPPORTED;
63+
return tb_init_fd(-1);
64+
EOD
65+
66+
my_sed 's/^(\s+)global\.(rfd|wfd|ttyfd) = -1;$/\1global.\2 = NULL;/'
67+
alter_function 'int tb_init_rwfd' <<'EOD'
68+
if (rfd != -1 || wfd != -1) return TB_ERR_WIN_UNSUPPORTED;
69+
global.rfd = GetStdHandle(STD_INPUT_HANDLE);
70+
global.wfd = GetStdHandle(STD_OUTPUT_HANDLE);
71+
if (!global.rfd || global.rfd == INVALID_HANDLE_VALUE) return TB_ERR_WIN_NO_STDIO;
72+
if (!global.wfd || global.wfd == INVALID_HANDLE_VALUE) return TB_ERR_WIN_NO_STDIO;
73+
global.ttyfd = global.wfd;
74+
global.ttyfd_open = 1;
75+
EOD
76+
77+
alter_function 'static int tb_deinit' <<'EOD'
78+
if (global.has_orig_tios) {
79+
SetConsoleMode(global.rfd, global.orig_tios[0]);
80+
SetConsoleMode(global.wfd, global.orig_tios[1]);
81+
}
82+
EOD
83+
84+
my_sed 's/int bytebuf_flush\(struct bytebuf \*b, int fd\)/int bytebuf_flush(struct bytebuf *b, HANDLE fd)/'
85+
alter_function 'static int bytebuf_flush' <<'EOD'
86+
DWORD nw;
87+
if (!WriteConsoleA(fd, b->buf, (DWORD)b->len, &nw, NULL) || nw != b->len) {
88+
return TB_ERR; // TODO: GetLastError
89+
}
90+
EOD
91+
92+
replace_function 'static int init_term_attrs' <<'EOD'
93+
if ( !SetConsoleCP(CP_UTF8)
94+
|| !SetConsoleOutputCP(CP_UTF8)) {
95+
return TB_ERR_WIN_SET_CONMODE;
96+
} else if (!GetConsoleMode(global.rfd, &global.orig_tios[0])
97+
|| !GetConsoleMode(global.wfd, &global.orig_tios[1])
98+
|| !(global.has_orig_tios = 1)) {
99+
return TB_ERR_WIN_GET_CONMODE;
100+
} else if (!SetConsoleMode(global.rfd, ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT)
101+
|| !SetConsoleMode(global.wfd, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) {
102+
return TB_ERR_WIN_SET_CONMODE;
103+
}
104+
return TB_OK;
105+
EOD
106+
107+
108+
replace_function 'static int update_term_size' <<'EOD'
109+
CONSOLE_SCREEN_BUFFER_INFO info;
110+
if (!GetConsoleScreenBufferInfo(global.wfd, &info)) {
111+
return TB_ERR_WIN_RESIZE;
112+
}
113+
global.width = info.dwSize.X;
114+
global.height = info.dwSize.Y;
115+
return TB_OK;
116+
EOD
117+
118+
replace_function 'static int init_term_caps' <<'EOD'
119+
(void)load_terminfo;
120+
(void)parse_terminfo_caps;
121+
return load_builtin_caps();
122+
EOD
123+
124+
replace_function 'static int wait_event' <<'EOD'
125+
int rv;
126+
INPUT_RECORD buf[TB_OPT_READ_BUF];
127+
128+
memset(event, 0, sizeof(*event));
129+
if_ok_return(rv, extract_event(event));
130+
131+
do {
132+
DWORD wait_rv = WaitForSingleObject(global.rfd, (DWORD)timeout);
133+
if (wait_rv == WAIT_TIMEOUT) {
134+
return TB_ERR_NO_EVENT;
135+
} else if (wait_rv != WAIT_OBJECT_0) {
136+
return TB_ERR_POLL;
137+
}
138+
139+
DWORD nevent = 0;
140+
if (!ReadConsoleInputA(global.rfd, buf, TB_OPT_READ_BUF, &nevent)) {
141+
return TB_ERR_READ;
142+
}
143+
144+
DWORD i;
145+
for (i = 0; i < nevent; i++) {
146+
if (buf[i].EventType == KEY_EVENT) {
147+
if (buf[i].Event.KeyEvent.bKeyDown &&
148+
buf[i].Event.KeyEvent.uChar.AsciiChar != 0)
149+
{
150+
bytebuf_nputs(&global.in,
151+
&(buf[i].Event.KeyEvent.uChar.AsciiChar), 1);
152+
}
153+
}
154+
}
155+
156+
for (i = 0; i < nevent; i++) {
157+
if (buf[i].EventType == WINDOW_BUFFER_SIZE_EVENT) {
158+
if_err_return(rv, update_term_size());
159+
if_err_return(rv, resize_cellbufs());
160+
event->type = TB_EVENT_RESIZE;
161+
event->w = global.width;
162+
event->h = global.height;
163+
return TB_OK;
164+
}
165+
}
166+
167+
memset(event, 0, sizeof(*event));
168+
if_ok_return(rv, extract_event(event));
169+
} while (timeout == -1);
170+
171+
return rv;
172+
EOD
173+
}
174+
175+
alter_function() {
176+
local fsig=$1
177+
local fcode=$(cat)
178+
awk -va=0 -vb=0 -vfcode="$fcode" \
179+
"/^${fsig}\(.* {$/ { a=1; print; next }
180+
a==1 && /__posix_start/ { b=1 }
181+
b==0 { print }
182+
b==1 && /__posix_end/ { print fcode; b=0; a=0 }" \
183+
"$out" >"$tmp"
184+
mv -f "$tmp" "$out"
185+
echo "alter_function $fsig"
186+
}
187+
188+
replace_function() {
189+
local fsig=$1
190+
local fbody=$(cat)
191+
awk -va=0 -vfbody="$fbody" \
192+
"/^${fsig}\(.* {$/ { a=1; print; next }
193+
a==0 { print }
194+
a==1 && /^}$/ { print fbody; print; a=0 }" \
195+
"$out" >"$tmp"
196+
mv -f "$tmp" "$out"
197+
echo "replace_function $fsig"
198+
}
199+
200+
my_sed() {
201+
sed -i -E "$@" "$out"
202+
echo "sed ${1:-}"
203+
}
204+
205+
main "$@"

0 commit comments

Comments
 (0)