remove gcc for MacOS in conan.yml
[liba.git] / lua / isocline / src / tty.c
blobee2ed392a421fb0cbe8e50650a3257ce76a2bf5e
1 /* ----------------------------------------------------------------------------
2 Copyright (c) 2021, Daan Leijen
3 This is free software; you can redistribute it and/or modify it
4 under the terms of the MIT License. A copy of the license can be
5 found in the "LICENSE" file at the root of this distribution.
6 -----------------------------------------------------------------------------*/
7 #include <string.h>
8 #include <stdbool.h>
9 #include <stdio.h>
10 #include <stdarg.h>
11 #include <locale.h>
13 #include "tty.h"
15 #if defined(_WIN32)
16 #include <windows.h>
17 #include <io.h>
18 #undef isatty
19 #define isatty(fd) _isatty(fd)
20 #define read(fd,s,n) _read(fd,s,n)
21 #define STDIN_FILENO 0
22 #if (_WIN32_WINNT < 0x0600)
23 WINBASEAPI ULONGLONG WINAPI GetTickCount64(VOID);
24 #endif
25 #else
26 #include <signal.h>
27 #include <errno.h>
28 #include <unistd.h>
29 #include <termios.h>
30 #include <sys/ioctl.h>
31 #include <sys/select.h>
32 #if !defined(FIONREAD)
33 #include <fcntl.h>
34 #endif
35 #endif
37 #define TTY_PUSH_MAX (32)
39 struct tty_s {
40 int fd_in; // input handle
41 bool raw_enabled; // is raw mode enabled?
42 bool is_utf8; // is the input stream in utf-8 mode?
43 bool has_term_resize_event; // are resize events generated?
44 bool term_resize_event; // did a term resize happen?
45 alloc_t* mem; // memory allocator
46 code_t pushbuf[TTY_PUSH_MAX]; // push back buffer for full key codes
47 ssize_t push_count;
48 uint8_t cpushbuf[TTY_PUSH_MAX]; // low level push back buffer for bytes
49 ssize_t cpush_count;
50 long esc_initial_timeout; // initial ms wait to see if ESC starts an escape sequence
51 long esc_timeout; // follow up delay for characters in an escape sequence
52 #if defined(_WIN32)
53 HANDLE hcon; // console input handle
54 DWORD hcon_orig_mode; // original console mode
55 #else
56 struct termios orig_ios; // original terminal settings
57 struct termios raw_ios; // raw terminal settings
58 #endif
62 //-------------------------------------------------------------
63 // Forward declarations of platform dependent primitives below
64 //-------------------------------------------------------------
66 ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms); // does not modify `c` when no input (false is returned)
68 //-------------------------------------------------------------
69 // Key code helpers
70 //-------------------------------------------------------------
72 ic_private bool code_is_ascii_char(code_t c, char* chr ) {
73 if (c >= ' ' && c <= 0x7F) {
74 if (chr != NULL) { *chr = (char)c; }
75 return true;
77 else {
78 if (chr != NULL) { *chr = 0; }
79 return false;
83 ic_private bool code_is_unicode(code_t c, unicode_t* uchr) {
84 if (c <= KEY_UNICODE_MAX) {
85 if (uchr != NULL) { *uchr = c; }
86 return true;
88 else {
89 if (uchr != NULL) { *uchr = 0; }
90 return false;
94 ic_private bool code_is_virt_key(code_t c ) {
95 return (KEY_NO_MODS(c) <= 0x20 || KEY_NO_MODS(c) >= KEY_VIRT);
99 //-------------------------------------------------------------
100 // Read a key code
101 //-------------------------------------------------------------
102 static code_t modify_code( code_t code );
104 static code_t tty_read_utf8( tty_t* tty, uint8_t c0 ) {
105 uint8_t buf[5];
106 memset(buf, 0, 5);
108 // try to read as many bytes as potentially needed
109 buf[0] = c0;
110 ssize_t count = 1;
111 if (c0 > 0x7F) {
112 if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {
113 count++;
114 if (c0 > 0xDF) {
115 if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {
116 count++;
117 if (c0 > 0xEF) {
118 if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {
119 count++;
127 buf[count] = 0;
128 debug_msg("tty: read utf8: count: %" PRIz "d: %02x,%02x,%02x,%02x", count, buf[0], buf[1], buf[2], buf[3]);
130 // decode the utf8 to unicode
131 ssize_t read = 0;
132 code_t code = key_unicode(unicode_from_qutf8(buf, count, &read));
134 // push back unused bytes (in the case of invalid utf8)
135 while (count > read) {
136 count--;
137 if (count >= 0 && count <= 4) { // to help the static analyzer
138 tty_cpush_char(tty, buf[count]);
141 return code;
144 // pop a code from the pushback buffer.
145 static bool tty_code_pop(tty_t* tty, code_t* code);
148 // read a single char/key
149 ic_private bool tty_read_timeout(tty_t* tty, long timeout_ms, code_t* code)
151 // is there a push_count back code?
152 if (tty_code_pop(tty,code)) {
153 return (bool)code;
156 // read a single char/byte from a character stream
157 uint8_t c;
158 if (!tty_readc_noblock(tty, &c, timeout_ms)) { return false; }
160 if (c == KEY_ESC) {
161 // escape sequence?
162 *code = tty_read_esc(tty, tty->esc_initial_timeout, tty->esc_timeout);
164 else if (c <= 0x7F) {
165 // ascii
166 *code = key_unicode(c);
168 else if (tty->is_utf8) {
169 // utf8 sequence
170 *code = tty_read_utf8(tty,c);
172 else {
173 // c >= 0x80 but tty is not utf8; use raw plane so we can translate it back in the end
174 *code = key_unicode( unicode_from_raw(c) );
177 *code = modify_code(*code);
178 return true;
181 // Transform virtual keys to be more portable across platforms
182 static code_t modify_code( code_t code ) {
183 code_t key = KEY_NO_MODS(code);
184 code_t mods = KEY_MODS(code);
185 debug_msg( "tty: readc %s%s%s 0x%03x ('%c')\n",
186 mods&KEY_MOD_SHIFT ? "shift+" : "", mods&KEY_MOD_CTRL ? "ctrl+" : "", mods&KEY_MOD_ALT ? "alt+" : "",
187 key, (key >= ' ' && key <= '~' ? key : ' '));
189 // treat KEY_RUBOUT (0x7F) as KEY_BACKSP
190 if (key == KEY_RUBOUT) {
191 code = KEY_BACKSP | mods;
193 // ctrl+'_' is translated to '\x1F' on Linux, translate it back
194 else if (key == key_char('\x1F') && (mods & KEY_MOD_ALT) == 0) {
195 key = '_';
196 code = WITH_CTRL(key_char('_'));
198 // treat ctrl/shift + enter always as KEY_LINEFEED for portability
199 else if (key == KEY_ENTER && (mods == KEY_MOD_SHIFT || mods == KEY_MOD_ALT || mods == KEY_MOD_CTRL)) {
200 code = KEY_LINEFEED;
202 // treat ctrl+tab always as shift+tab for portability
203 else if (code == WITH_CTRL(KEY_TAB)) {
204 code = KEY_SHIFT_TAB;
206 // treat ctrl+end/alt+>/alt-down and ctrl+home/alt+</alt-up always as pagedown/pageup for portability
207 else if (code == WITH_ALT(KEY_DOWN) || code == WITH_ALT('>') || code == WITH_CTRL(KEY_END)) {
208 code = KEY_PAGEDOWN;
210 else if (code == WITH_ALT(KEY_UP) || code == WITH_ALT('<') || code == WITH_CTRL(KEY_HOME)) {
211 code = KEY_PAGEUP;
214 // treat C0 codes without KEY_MOD_CTRL
215 if (key < ' ' && (mods&KEY_MOD_CTRL) != 0) {
216 code &= ~KEY_MOD_CTRL;
219 return code;
223 // read a single char/key
224 ic_private code_t tty_read(tty_t* tty)
226 code_t code;
227 if (!tty_read_timeout(tty, -1, &code)) { return KEY_NONE; }
228 return code;
231 //-------------------------------------------------------------
232 // Read back an ANSI query response
233 //-------------------------------------------------------------
235 ic_private bool tty_read_esc_response(tty_t* tty, char esc_start, bool final_st, char* buf, ssize_t buflen )
237 buf[0] = 0;
238 ssize_t len = 0;
239 uint8_t c = 0;
240 if (!tty_readc_noblock(tty, &c, 2*tty->esc_initial_timeout) || c != '\x1B') {
241 debug_msg("initial esc response failed: 0x%02x\n", c);
242 return false;
244 if (!tty_readc_noblock(tty, &c, tty->esc_timeout) || (c != esc_start)) { return false; }
245 while( len < buflen ) {
246 if (!tty_readc_noblock(tty, &c, tty->esc_timeout)) { return false; }
247 if (final_st) {
248 // OSC is terminated by BELL, or ESC \ (ST) (and STX)
249 if (c == '\x07' || c == '\x02') {
250 break;
252 else if (c == '\x1B') {
253 uint8_t c1;
254 if (!tty_readc_noblock(tty, &c1, tty->esc_timeout)) { return false; }
255 if (c1 == '\\') { break; }
256 tty_cpush_char(tty,c1);
259 else {
260 if (c == '\x02') { // STX
261 break;
263 else if (!((c >= '0' && c <= '9') || strchr("<=>?;:",c) != NULL)) {
264 buf[len++] = (char)c; // for non-OSC save the terminating character
265 break;
268 buf[len++] = (char)c;
270 buf[len] = 0;
271 debug_msg("tty: escape query response: %s\n", buf);
272 return true;
275 //-------------------------------------------------------------
276 // High level code pushback
277 //-------------------------------------------------------------
279 static bool tty_code_pop( tty_t* tty, code_t* code ) {
280 if (tty->push_count <= 0) { return false; }
281 tty->push_count--;
282 *code = tty->pushbuf[tty->push_count];
283 return true;
286 ic_private void tty_code_pushback( tty_t* tty, code_t c ) {
287 // note: must be signal safe
288 if (tty->push_count >= TTY_PUSH_MAX) { return; }
289 tty->pushbuf[tty->push_count] = c;
290 tty->push_count++;
294 //-------------------------------------------------------------
295 // low-level character pushback (for escape sequences and windows)
296 //-------------------------------------------------------------
298 ic_private bool tty_cpop(tty_t* tty, uint8_t* c) {
299 if (tty->cpush_count <= 0) { // do not modify c on failure (see `tty_decode_unicode`)
300 return false;
302 else {
303 tty->cpush_count--;
304 *c = tty->cpushbuf[tty->cpush_count];
305 return true;
309 static void tty_cpush(tty_t* tty, const char* s) {
310 ssize_t len = ic_strlen(s);
311 if (tty->push_count + len > TTY_PUSH_MAX) {
312 debug_msg("tty: cpush buffer full! (pushing %s)\n", s);
313 assert(false);
314 return;
316 for (ssize_t i = 0; i < len; i++) {
317 tty->cpushbuf[tty->cpush_count + i] = (uint8_t)( s[len - i - 1] );
319 tty->cpush_count += len;
320 return;
323 // convenience function for small sequences
324 static void tty_cpushf(tty_t* tty, const char* fmt, ...) {
325 va_list args;
326 va_start(args,fmt);
327 char buf[TTY_PUSH_MAX+1];
328 vsnprintf(buf,TTY_PUSH_MAX,fmt,args);
329 buf[TTY_PUSH_MAX] = 0;
330 tty_cpush(tty,buf);
331 va_end(args);
332 return;
335 ic_private void tty_cpush_char(tty_t* tty, uint8_t c) {
336 uint8_t buf[2];
337 buf[0] = c;
338 buf[1] = 0;
339 tty_cpush(tty, (const char*)buf);
343 //-------------------------------------------------------------
344 // Push escape codes (used on Windows to insert keys)
345 //-------------------------------------------------------------
347 static unsigned csi_mods(code_t mods) {
348 unsigned m = 1;
349 if (mods&KEY_MOD_SHIFT) { m += 1; }
350 if (mods&KEY_MOD_ALT) { m += 2; }
351 if (mods&KEY_MOD_CTRL) { m += 4; }
352 return m;
355 // Push ESC [ <vtcode> ; <mods> ~
356 static void tty_cpush_csi_vt( tty_t* tty, code_t mods, uint32_t vtcode ) {
357 tty_cpushf(tty,"\x1B[%u;%u~", vtcode, csi_mods(mods) );
360 // push ESC [ 1 ; <mods> <xcmd>
361 static void tty_cpush_csi_xterm( tty_t* tty, code_t mods, char xcode ) {
362 tty_cpushf(tty,"\x1B[1;%u%c", csi_mods(mods), xcode );
365 // push ESC [ <unicode> ; <mods> u
366 static void tty_cpush_csi_unicode( tty_t* tty, code_t mods, uint32_t unicode ) {
367 if ((unicode < 0x80 && mods == 0) ||
368 (mods == KEY_MOD_CTRL && unicode < ' ' && unicode != KEY_TAB && unicode != KEY_ENTER
369 && unicode != KEY_LINEFEED && unicode != KEY_BACKSP) ||
370 (mods == KEY_MOD_SHIFT && unicode >= ' ' && unicode <= KEY_RUBOUT)) {
371 tty_cpush_char(tty,(uint8_t)unicode);
373 else {
374 tty_cpushf(tty,"\x1B[%u;%uu", unicode, csi_mods(mods) );
378 //-------------------------------------------------------------
379 // Init
380 //-------------------------------------------------------------
382 static bool tty_init_raw(tty_t* tty);
383 static void tty_done_raw(tty_t* tty);
385 static bool tty_init_utf8(tty_t* tty) {
386 #ifdef _WIN32
387 tty->is_utf8 = true;
388 #else
389 const char* loc = setlocale(LC_ALL,"");
390 tty->is_utf8 = (ic_icontains(loc,"UTF-8") || ic_icontains(loc,"utf8") || ic_stricmp(loc,"C") == 0);
391 debug_msg("tty: utf8: %s (loc=%s)\n", tty->is_utf8 ? "true" : "false", loc);
392 #endif
393 return true;
396 ic_private tty_t* tty_new(alloc_t* mem, int fd_in)
398 tty_t* tty = mem_zalloc_tp(mem, tty_t);
399 if (tty == NULL) { return NULL; }
400 tty->mem = mem;
401 tty->fd_in = (fd_in < 0 ? STDIN_FILENO : fd_in);
402 #if defined(__APPLE__)
403 tty->esc_initial_timeout = 200; // apple use ESC+<key> for alt-<key>
404 #else
405 tty->esc_initial_timeout = 100;
406 #endif
407 tty->esc_timeout = 10;
408 if (!(isatty(tty->fd_in) && tty_init_raw(tty) && tty_init_utf8(tty))) {
409 tty_free(tty);
410 return NULL;
412 return tty;
415 ic_private void tty_free(tty_t* tty) {
416 if (tty == NULL) { return; }
417 tty_end_raw(tty);
418 tty_done_raw(tty);
419 mem_free(tty->mem,tty);
422 ic_private bool tty_is_utf8(const tty_t* tty) {
423 if (tty == NULL) { return true; }
424 return (tty->is_utf8);
427 ic_private bool tty_term_resize_event(tty_t* tty) {
428 if (tty == NULL) { return true; }
429 if (tty->has_term_resize_event) {
430 if (!tty->term_resize_event) { return false; }
431 tty->term_resize_event = false; // reset.
433 return true; // always return true on systems without a resize event (more expensive but still ok)
436 ic_private void tty_set_esc_delay(tty_t* tty, long initial_delay_ms, long followup_delay_ms) {
437 tty->esc_initial_timeout = (initial_delay_ms < 0 ? 0 : (initial_delay_ms > 1000 ? 1000 : initial_delay_ms));
438 tty->esc_timeout = (followup_delay_ms < 0 ? 0 : (followup_delay_ms > 1000 ? 1000 : followup_delay_ms));
441 //-------------------------------------------------------------
442 // Unix
443 //-------------------------------------------------------------
444 #if !defined(_WIN32)
446 static bool tty_readc_blocking(tty_t* tty, uint8_t* c) {
447 if (tty_cpop(tty,c)) { return true; }
448 *c = 0;
449 ssize_t nread = read(tty->fd_in, (char*)c, 1);
450 if (nread < 0 && errno == EINTR) {
451 // can happen on SIGWINCH signal for terminal resize
453 return (nread == 1);
457 // non blocking read -- with a small timeout used for reading escape sequences.
458 ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms)
460 // in our pushback buffer?
461 if (tty_cpop(tty, c)) { return true; }
463 // blocking read?
464 if (timeout_ms < 0) {
465 return tty_readc_blocking(tty,c);
468 // if supported, peek first if any char is available.
469 #if defined(FIONREAD)
470 { int navail = 0;
471 if (ioctl(0, FIONREAD, &navail) == 0) {
472 if (navail >= 1) {
473 return tty_readc_blocking(tty, c);
475 else if (timeout_ms == 0) {
476 return false; // return early if there is no input available (with a zero timeout)
480 #endif
482 // otherwise block for at most timeout milliseconds
483 #if defined(FD_SET)
484 // we can use select to detect when input becomes available
485 fd_set readset;
486 struct timeval time;
487 FD_ZERO(&readset);
488 FD_SET(tty->fd_in, &readset);
489 time.tv_sec = (timeout_ms > 0 ? timeout_ms / 1000 : 0);
490 time.tv_usec = (timeout_ms > 0 ? 1000*(timeout_ms % 1000) : 0);
491 if (select(tty->fd_in + 1, &readset, NULL, NULL, &time) == 1) {
492 // input available
493 return tty_readc_blocking(tty, c);
495 #else
496 // no select, we cannot timeout; use usleeps :-(
497 // todo: this seems very rare nowadays; should be even support this?
498 do {
499 // peek ahead if possible
500 #if defined(FIONREAD)
501 int navail = 0;
502 if (ioctl(0, FIONREAD, &navail) == 0 && navail >= 1) {
503 return tty_readc_blocking(tty, c);
505 #elif defined(O_NONBLOCK)
506 // use a temporary non-blocking read mode
507 int fstatus = fcntl(tty->fd_in, F_GETFL, 0);
508 if (fstatus != -1) {
509 if (fcntl(tty->fd_in, F_SETFL, (fstatus | O_NONBLOCK)) != -1) {
510 char buf[2] = { 0, 0 };
511 ssize_t nread = read(tty->fd_in, buf, 1);
512 fcntl(tty->fd_in, F_SETFL, fstatus);
513 if (nread >= 1) {
514 *c = (uint8_t)buf[0];
515 return true;
519 #else
520 #error "define an nonblocking read for this platform"
521 #endif
522 // and sleep a bit
523 if (timeout_ms > 0) {
524 usleep(50*1000L); // sleep at most 0.05s at a time
525 timeout_ms -= 100;
526 if (timeout_ms < 0) { timeout_ms = 0; }
529 while (timeout_ms > 0);
530 #endif
531 return false;
534 #if defined(TIOCSTI)
535 ic_private bool tty_async_stop(const tty_t* tty) {
536 // insert ^C in the input stream
537 char c = KEY_CTRL_C;
538 return (ioctl(tty->fd_in, TIOCSTI, &c) >= 0);
540 #else
541 ic_private bool tty_async_stop(const tty_t* tty) {
542 return false;
544 #endif
546 // We install various signal handlers to restore the terminal settings
547 // in case of a terminating signal. This is also used to catch terminal window resizes.
548 // This is not strictly needed so this can be disabled on
549 // (older) platforms that do not support signal handling well.
550 #if defined(SIGWINCH) && defined(SA_RESTART) // ensure basic signal functionality is defined
552 // store the tty in a global so we access it on unexpected termination
553 static tty_t* sig_tty; // = NULL
555 // Catch all termination signals (and SIGWINCH)
556 typedef struct signal_handler_s {
557 int signum;
558 union {
559 int _avoid_warning;
560 struct sigaction previous;
561 } action;
562 } signal_handler_t;
564 static signal_handler_t sighandlers[] = {
565 { SIGWINCH, {0} },
566 { SIGTERM , {0} },
567 { SIGINT , {0} },
568 { SIGQUIT , {0} },
569 { SIGHUP , {0} },
570 { SIGSEGV , {0} },
571 { SIGTRAP , {0} },
572 { SIGBUS , {0} },
573 { SIGTSTP , {0} },
574 { SIGTTIN , {0} },
575 { SIGTTOU , {0} },
576 { 0 , {0} }
579 static bool sigaction_is_valid( struct sigaction* sa ) {
580 return (sa->sa_sigaction != NULL && sa->sa_handler != SIG_DFL && sa->sa_handler != SIG_IGN);
583 // Generic signal handler
584 static void sig_handler(int signum, siginfo_t* siginfo, void* uap ) {
585 if (signum == SIGWINCH) {
586 if (sig_tty != NULL) {
587 sig_tty->term_resize_event = true;
590 else {
591 // the rest are termination signals; restore the terminal mode. (`tcsetattr` is signal-safe)
592 if (sig_tty != NULL && sig_tty->raw_enabled) {
593 tcsetattr(sig_tty->fd_in, TCSAFLUSH, &sig_tty->orig_ios);
594 sig_tty->raw_enabled = false;
597 // call previous handler
598 signal_handler_t* sh = sighandlers;
599 while( sh->signum != 0 && sh->signum != signum) { sh++; }
600 if (sh->signum == signum) {
601 if (sigaction_is_valid(&sh->action.previous)) {
602 (sh->action.previous.sa_sigaction)(signum, siginfo, uap);
607 static void signals_install(tty_t* tty) {
608 sig_tty = tty;
609 // generic signal handler
610 struct sigaction handler;
611 memset(&handler,0,sizeof(handler));
612 sigemptyset(&handler.sa_mask);
613 handler.sa_sigaction = &sig_handler;
614 handler.sa_flags = SA_RESTART;
615 // install for all signals
616 for( signal_handler_t* sh = sighandlers; sh->signum != 0; sh++ ) {
617 if (sigaction( sh->signum, NULL, &sh->action.previous) == 0) { // get previous
618 if (sh->action.previous.sa_handler != SIG_IGN) { // if not to be ignored
619 if (sigaction( sh->signum, &handler, &sh->action.previous ) < 0) { // install our handler
620 sh->action.previous.sa_sigaction = NULL; // do not restore on error
622 else if (sh->signum == SIGWINCH) {
623 sig_tty->has_term_resize_event = true;
630 static void signals_restore(void) {
631 // restore all signal handlers
632 for( signal_handler_t* sh = sighandlers; sh->signum != 0; sh++ ) {
633 if (sigaction_is_valid(&sh->action.previous)) {
634 sigaction( sh->signum, &sh->action.previous, NULL );
637 sig_tty = NULL;
640 #else
641 static void signals_install(tty_t* tty) {
642 ic_unused(tty);
643 // nothing
645 static void signals_restore(void) {
646 // nothing
649 #endif
651 ic_private bool tty_start_raw(tty_t* tty) {
652 if (tty == NULL) { return false; }
653 if (tty->raw_enabled) { return true; }
654 if (tcsetattr(tty->fd_in,TCSAFLUSH,&tty->raw_ios) < 0) { return false; }
655 tty->raw_enabled = true;
656 return true;
659 ic_private void tty_end_raw(tty_t* tty) {
660 if (tty == NULL) { return; }
661 if (!tty->raw_enabled) { return; }
662 tty->cpush_count = 0;
663 if (tcsetattr(tty->fd_in,TCSAFLUSH,&tty->orig_ios) < 0) { return; }
664 tty->raw_enabled = false;
667 static bool tty_init_raw(tty_t* tty)
669 // Set input to raw mode. See <https://man7.org/linux/man-pages/man3/termios.3.html>.
670 if (tcgetattr(tty->fd_in,&tty->orig_ios) == -1) { return false; }
671 tty->raw_ios = tty->orig_ios;
672 // input: no break signal, no \r to \n, no parity check, no 8-bit to 7-bit, no flow control
673 tty->raw_ios.c_iflag &= ~(unsigned long)(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
674 // control: allow 8-bit
675 tty->raw_ios.c_cflag |= CS8;
676 // local: no echo, no line-by-line (canonical), no extended input processing, no signals for ^z,^c
677 tty->raw_ios.c_lflag &= ~(unsigned long)(ECHO | ICANON | IEXTEN | ISIG);
678 // 1 byte at a time, no delay
679 tty->raw_ios.c_cc[VTIME] = 0;
680 tty->raw_ios.c_cc[VMIN] = 1;
682 // store in global so our signal handlers can restore the terminal mode
683 signals_install(tty);
685 return true;
688 static void tty_done_raw(tty_t* tty) {
689 ic_unused(tty);
690 signals_restore();
694 #else
696 //-------------------------------------------------------------
697 // Windows
698 // For best portability we push CSI escape sequences directly
699 // to the character stream (instead of returning key codes).
700 //-------------------------------------------------------------
702 static void tty_waitc_console(tty_t* tty, long timeout_ms);
704 ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms) { // don't modify `c` if there is no input
705 // in our pushback buffer?
706 if (tty_cpop(tty, c)) { return true; }
707 // any events in the input queue?
708 tty_waitc_console(tty, timeout_ms);
709 return tty_cpop(tty, c);
712 // Read from the console input events and push escape codes into the tty cbuffer.
713 static void tty_waitc_console(tty_t* tty, long timeout_ms)
715 // wait for a key down event
716 INPUT_RECORD inp;
717 DWORD count;
718 uint32_t surrogate_hi = 0;
719 while (true) {
720 // check if there are events if in non-blocking timeout mode
721 if (timeout_ms >= 0) {
722 // first peek ahead
723 if (!GetNumberOfConsoleInputEvents(tty->hcon, &count)) { return; }
724 if (count == 0) {
725 if (timeout_ms == 0) {
726 // out of time
727 return;
729 else {
730 // wait for input events for at most timeout milli seconds
731 ULONGLONG start_ms = GetTickCount64();
732 DWORD res = WaitForSingleObject(tty->hcon, (DWORD)timeout_ms);
733 switch (res) {
734 case WAIT_OBJECT_0: {
735 // input is available, decrease our timeout
736 ULONGLONG waited_ms = (GetTickCount64() - start_ms);
737 timeout_ms -= (long)waited_ms;
738 if (timeout_ms < 0) {
739 timeout_ms = 0;
741 break;
743 case WAIT_TIMEOUT:
744 case WAIT_ABANDONED:
745 case WAIT_FAILED:
746 default:
747 return;
753 // (blocking) Read from the input
754 if (!ReadConsoleInputW(tty->hcon, &inp, 1, &count)) { return; }
755 if (count != 1) { return; }
757 // resize event?
758 if (inp.EventType == WINDOW_BUFFER_SIZE_EVENT) {
759 tty->term_resize_event = true;
760 continue;
763 // wait for key down events
764 if (inp.EventType != KEY_EVENT) { continue; }
766 // the modifier state
767 DWORD modstate = inp.Event.KeyEvent.dwControlKeyState;
769 // we need to handle shift up events separately
770 if (!inp.Event.KeyEvent.bKeyDown && inp.Event.KeyEvent.wVirtualKeyCode == VK_SHIFT) {
771 modstate &= (DWORD)~SHIFT_PRESSED;
774 // ignore AltGr
775 DWORD altgr = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED;
776 if ((modstate & altgr) == altgr) { modstate &= ~altgr; }
779 // get modifiers
780 code_t mods = 0;
781 if ((modstate & ( RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED )) != 0) { mods |= KEY_MOD_CTRL; }
782 if ((modstate & ( RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED )) != 0) { mods |= KEY_MOD_ALT; }
783 if ((modstate & SHIFT_PRESSED) != 0) { mods |= KEY_MOD_SHIFT; }
785 // virtual keys
786 uint32_t chr = (uint32_t)inp.Event.KeyEvent.uChar.UnicodeChar;
787 WORD virt = inp.Event.KeyEvent.wVirtualKeyCode;
788 debug_msg("tty: console %s: %s%s%s virt 0x%04x, chr 0x%04x ('%c')\n", inp.Event.KeyEvent.bKeyDown ? "down" : "up", mods&KEY_MOD_CTRL ? "ctrl-" : "", mods&KEY_MOD_ALT ? "alt-" : "", mods&KEY_MOD_SHIFT ? "shift-" : "", virt, chr, chr);
790 // only process keydown events (except for Alt-up which is used for unicode pasting...)
791 if (!inp.Event.KeyEvent.bKeyDown && virt != VK_MENU) {
792 continue;
795 if (chr == 0) {
796 switch (virt) {
797 case VK_UP: tty_cpush_csi_xterm(tty, mods, 'A'); return;
798 case VK_DOWN: tty_cpush_csi_xterm(tty, mods, 'B'); return;
799 case VK_RIGHT: tty_cpush_csi_xterm(tty, mods, 'C'); return;
800 case VK_LEFT: tty_cpush_csi_xterm(tty, mods, 'D'); return;
801 case VK_END: tty_cpush_csi_xterm(tty, mods, 'F'); return;
802 case VK_HOME: tty_cpush_csi_xterm(tty, mods, 'H'); return;
803 case VK_DELETE: tty_cpush_csi_vt(tty,mods,3); return;
804 case VK_PRIOR: tty_cpush_csi_vt(tty,mods,5); return; //page up
805 case VK_NEXT: tty_cpush_csi_vt(tty,mods,6); return; //page down
806 case VK_TAB: tty_cpush_csi_unicode(tty,mods,9); return;
807 case VK_RETURN: tty_cpush_csi_unicode(tty,mods,13); return;
808 default: {
809 uint32_t vtcode = 0;
810 if (virt >= VK_F1 && virt <= VK_F5) {
811 vtcode = 10 + (virt - VK_F1);
813 else if (virt >= VK_F6 && virt <= VK_F10) {
814 vtcode = 17 + (virt - VK_F6);
816 else if (virt >= VK_F11 && virt <= VK_F12) {
817 vtcode = 13 + (virt - VK_F11);
819 if (vtcode > 0) {
820 tty_cpush_csi_vt(tty,mods,vtcode);
821 return;
825 // ignore other control keys (shift etc).
827 // high surrogate pair
828 else if (chr >= 0xD800 && chr <= 0xDBFF) {
829 surrogate_hi = (chr - 0xD800);
831 // low surrogate pair
832 else if (chr >= 0xDC00 && chr <= 0xDFFF) {
833 chr = ((surrogate_hi << 10) + (chr - 0xDC00) + 0x10000);
834 tty_cpush_csi_unicode(tty,mods,chr);
835 surrogate_hi = 0;
836 return;
838 // regular character
839 else {
840 tty_cpush_csi_unicode(tty,mods,chr);
841 return;
846 ic_private bool tty_async_stop(const tty_t* tty) {
847 // send ^c
848 INPUT_RECORD events[2];
849 memset(events, 0, 2*sizeof(INPUT_RECORD));
850 events[0].EventType = KEY_EVENT;
851 events[0].Event.KeyEvent.bKeyDown = TRUE;
852 events[0].Event.KeyEvent.uChar.AsciiChar = KEY_CTRL_C;
853 events[1] = events[0];
854 events[1].Event.KeyEvent.bKeyDown = FALSE;
855 DWORD nwritten = 0;
856 WriteConsoleInput(tty->hcon, events, 2, &nwritten);
857 return (nwritten == 2);
860 ic_private bool tty_start_raw(tty_t* tty) {
861 if (tty->raw_enabled) { return true; }
862 GetConsoleMode(tty->hcon,&tty->hcon_orig_mode);
863 DWORD mode = ENABLE_QUICK_EDIT_MODE // cut&paste allowed
864 | ENABLE_WINDOW_INPUT // to catch resize events
865 // | ENABLE_VIRTUAL_TERMINAL_INPUT
866 // | ENABLE_PROCESSED_INPUT
868 SetConsoleMode(tty->hcon, mode );
869 tty->raw_enabled = true;
870 return true;
873 ic_private void tty_end_raw(tty_t* tty) {
874 if (!tty->raw_enabled) { return; }
875 SetConsoleMode(tty->hcon, tty->hcon_orig_mode );
876 tty->raw_enabled = false;
879 static bool tty_init_raw(tty_t* tty) {
880 tty->hcon = GetStdHandle( STD_INPUT_HANDLE );
881 tty->has_term_resize_event = true;
882 return true;
885 static void tty_done_raw(tty_t* tty) {
886 ic_unused(tty);
889 #endif