3 * Copyright 2025 Bruce Hill
4 * Released under the MIT License
6 * This file defines some Text User Interface (TUI) functionality.
18 #include <sys/ioctl.h>
23 #define BTUI_VERSION 5
25 // Terminal escape sequences:
27 #define T_SHOW_CURSOR "25"
28 #define T_MOUSE_XY "1000"
29 #define T_MOUSE_CELL "1002"
30 #define T_MOUSE_SGR "1006"
31 #define T_ALT_SCREEN "1049"
32 #define T_ON(opt) "\033[?" opt "h"
33 #define T_OFF(opt) "\033[?" opt "l"
35 // Maximum time in milliseconds between double clicks
36 #ifndef BTUI_DOUBLECLICK_THRESHOLD
37 #define BTUI_DOUBLECLICK_THRESHOLD 200
40 // Keyboard modifiers:
41 #define MOD_BITSHIFT 9
42 #define MOD_META (1 << (MOD_BITSHIFT + 0))
43 #define MOD_CTRL (1 << (MOD_BITSHIFT + 1))
44 #define MOD_ALT (1 << (MOD_BITSHIFT + 2))
45 #define MOD_SHIFT (1 << (MOD_BITSHIFT + 3))
48 BTUI_MODE_UNINITIALIZED = 0,
89 // Printable chars would be here
90 KEY_BACKSPACE2 = 0x7F,
92 // Non-ascii multi-byte keys:
124 MOUSE_MIDDLE_RELEASE,
136 CURSOR_BLINKING_BLOCK = 1,
137 CURSOR_STEADY_BLOCK = 2,
138 CURSOR_BLINKING_UNDERLINE = 3,
139 CURSOR_STEADY_UNDERLINE = 4,
140 CURSOR_BLINKING_BAR = 5,
141 CURSOR_STEADY_BAR = 6,
144 // Overlapping key codes:
145 const btui_key_t KEY_CTRL_BACKTICK = KEY_CTRL_AT;
146 const btui_key_t KEY_CTRL_2 = KEY_CTRL_AT;
147 const btui_key_t KEY_CTRL_SPACE = KEY_CTRL_AT;
148 const btui_key_t KEY_BACKSPACE = KEY_CTRL_H;
149 const btui_key_t KEY_TAB = KEY_CTRL_I;
150 const btui_key_t KEY_ENTER = KEY_CTRL_M;
151 const btui_key_t KEY_ESC = KEY_CTRL_LSQ_BRACKET;
152 const btui_key_t KEY_CTRL_3 = KEY_CTRL_LSQ_BRACKET;
153 const btui_key_t KEY_CTRL_4 = KEY_CTRL_BACKSLASH;
154 const btui_key_t KEY_CTRL_5 = KEY_CTRL_RSQ_BRACKET;
155 const btui_key_t KEY_CTRL_TILDE = KEY_CTRL_CARET;
156 const btui_key_t KEY_CTRL_6 = KEY_CTRL_CARET;
157 const btui_key_t KEY_CTRL_7 = KEY_CTRL_UNDERSCORE;
158 const btui_key_t KEY_CTRL_SLASH = KEY_CTRL_UNDERSCORE;
159 const btui_key_t KEY_CTRL_8 = KEY_BACKSPACE2;
162 BTUI_CLEAR_SCREEN = 0,
163 BTUI_CLEAR_ABOVE = 1,
164 BTUI_CLEAR_BELOW = 2,
167 BTUI_CLEAR_RIGHT = 5,
171 typedef uint64_t attr_t;
172 const attr_t BTUI_NORMAL = 1ul << 0;
173 const attr_t BTUI_BOLD = 1ul << 1;
174 const attr_t BTUI_FAINT = 1ul << 2;
175 const attr_t BTUI_ITALIC = 1ul << 3;
176 const attr_t BTUI_UNDERLINE = 1ul << 4;
177 const attr_t BTUI_BLINK_SLOW = 1ul << 5;
178 const attr_t BTUI_BLINK_FAST = 1ul << 6;
179 const attr_t BTUI_REVERSE = 1ul << 7;
180 const attr_t BTUI_CONCEAL = 1ul << 8;
181 const attr_t BTUI_STRIKETHROUGH = 1ul << 9;
182 // 10-19: alternative fonts
183 const attr_t BTUI_FRAKTUR = 1ul << 20;
184 const attr_t BTUI_DOUBLE_UNDERLINE = 1ul << 21;
185 const attr_t BTUI_NO_BOLD_OR_FAINT = 1ul << 22;
186 const attr_t BTUI_NO_ITALIC_OR_FRAKTUR = 1ul << 23;
187 const attr_t BTUI_NO_UNDERLINE = 1ul << 24;
188 const attr_t BTUI_NO_BLINK = 1ul << 25;
190 const attr_t BTUI_NO_REVERSE = 1ul << 27;
191 const attr_t BTUI_NO_CONCEAL = 1ul << 28;
192 const attr_t BTUI_NO_STRIKETHROUGH = 1ul << 29;
193 const attr_t BTUI_FG_BLACK = 1ul << 30;
194 const attr_t BTUI_FG_RED = 1ul << 31;
195 const attr_t BTUI_FG_GREEN = 1ul << 32;
196 const attr_t BTUI_FG_YELLOW = 1ul << 33;
197 const attr_t BTUI_FG_BLUE = 1ul << 34;
198 const attr_t BTUI_FG_MAGENTA = 1ul << 35;
199 const attr_t BTUI_FG_CYAN = 1ul << 36;
200 const attr_t BTUI_FG_WHITE = 1ul << 37;
201 // 38: 256/24bit color
202 const attr_t BTUI_FG_NORMAL = 1ul << 39;
203 const attr_t BTUI_BG_BLACK = 1ul << 40;
204 const attr_t BTUI_BG_RED = 1ul << 41;
205 const attr_t BTUI_BG_GREEN = 1ul << 42;
206 const attr_t BTUI_BG_YELLOW = 1ul << 43;
207 const attr_t BTUI_BG_BLUE = 1ul << 44;
208 const attr_t BTUI_BG_MAGENTA = 1ul << 45;
209 const attr_t BTUI_BG_CYAN = 1ul << 46;
210 const attr_t BTUI_BG_WHITE = 1ul << 47;
211 // 48: 256/24bit color
212 const attr_t BTUI_BG_NORMAL = 1ul << 49;
213 // 50: Inverse of 26 (reserved)
214 const attr_t BTUI_FRAMED = 1ul << 51;
215 const attr_t BTUI_ENCIRCLED = 1ul << 52;
216 const attr_t BTUI_OVERLINED = 1ul << 53;
217 const attr_t BTUI_NO_FRAMED_OR_ENCIRCLED = 1ul << 54;
218 const attr_t BTUI_NO_OVERLINED = 1ul << 55;
220 // 60-65: Ideogram stuff
237 int btui_clear(int mode);
238 void btui_disable(void);
239 void btui_draw_linebox(int x, int y, int w, int h);
240 void btui_draw_shadow(int x, int y, int w, int h);
241 void btui_fill_box(int x, int y, int w, int h);
242 int btui_flush(void);
243 void btui_force_close(void);
244 int btui_getkey(int timeout, int *mouse_x, int *mouse_y);
245 int btui_hide_cursor(void);
246 void btui_init(void);
247 char *btui_keyname(int key, char *buf);
248 int btui_keynamed(const char *name);
249 int btui_move_cursor(int x, int y);
250 int btui_move_cursor_relative(int x, int y);
251 #define btui_printf(bt, ...) fprintf((bt)->out, __VA_ARGS__)
252 int btui_puts(const char *s);
253 int btui_scroll(int firstline, int lastline, int scroll_amount);
254 int btui_set_attributes(attr_t attrs);
255 int btui_set_bg(unsigned char r, unsigned char g, unsigned char b);
256 int btui_set_bg_hex(uint32_t hex);
257 int btui_set_cursor(cursor_t cur);
258 int btui_set_fg(unsigned char r, unsigned char g, unsigned char b);
259 int btui_set_fg_hex(uint32_t hex);
260 void btui_set_mode(btui_mode_t mode);
261 int btui_show_cursor(void);
262 int btui_suspend(void);
264 // File-local variables:
265 static btui_t bt = {.in = NULL, .out = NULL, .mode = BTUI_MODE_UNINITIALIZED};
267 // The names of keys that don't render well:
268 static keyname_t key_names[] = {
269 {KEY_SPACE, "Space"},
270 {KEY_BACKSPACE2, "Backspace"},
271 {KEY_INSERT, "Insert"},
272 {KEY_DELETE, "Delete"},
274 {KEY_ENTER, "Enter"},
275 {KEY_ENTER, "Return"},
279 {KEY_PGUP, "Page Up"},
281 {KEY_PGDN, "Page Down"},
282 {KEY_ARROW_UP, "Up"},
283 {KEY_ARROW_DOWN, "Down"},
284 {KEY_ARROW_LEFT, "Left"},
285 {KEY_ARROW_RIGHT, "Right"},
286 {MOUSE_LEFT_PRESS, "Left press"},
287 {MOUSE_RIGHT_PRESS, "Right press"},
288 {MOUSE_MIDDLE_PRESS, "Middle press"},
289 {MOUSE_LEFT_DRAG, "Left drag"},
290 {MOUSE_RIGHT_DRAG, "Right drag"},
291 {MOUSE_MIDDLE_DRAG, "Middle drag"},
292 {MOUSE_LEFT_RELEASE, "Left release"},
293 {MOUSE_RIGHT_RELEASE, "Right release"},
294 {MOUSE_MIDDLE_RELEASE, "Middle release"},
295 {MOUSE_LEFT_RELEASE, "Left up"},
296 {MOUSE_RIGHT_RELEASE, "Right up"},
297 {MOUSE_MIDDLE_RELEASE, "Middle up"},
298 {MOUSE_LEFT_RELEASE, "Left click"},
299 {MOUSE_RIGHT_RELEASE, "Right click"},
300 {MOUSE_MIDDLE_RELEASE, "Middle click"},
301 {MOUSE_LEFT_DOUBLE, "Double left click"},
302 {MOUSE_RIGHT_DOUBLE, "Double right click"},
303 {MOUSE_MIDDLE_DOUBLE, "Double middle click"},
304 {MOUSE_WHEEL_RELEASE, "Mouse wheel up"},
305 {MOUSE_WHEEL_PRESS, "Mouse wheel down"},
308 {KEY_CTRL_A, "Ctrl-a"},
309 {KEY_CTRL_B, "Ctrl-b"},
310 {KEY_CTRL_C, "Ctrl-c"},
311 {KEY_CTRL_D, "Ctrl-d"},
312 {KEY_CTRL_E, "Ctrl-e"},
313 {KEY_CTRL_F, "Ctrl-f"},
314 {KEY_CTRL_G, "Ctrl-g"},
315 {KEY_CTRL_H, "Ctrl-h"},
316 {KEY_CTRL_I, "Ctrl-i"},
317 {KEY_CTRL_J, "Ctrl-j"},
318 {KEY_CTRL_K, "Ctrl-k"},
319 {KEY_CTRL_L, "Ctrl-l"},
320 {KEY_CTRL_M, "Ctrl-m"},
321 {KEY_CTRL_N, "Ctrl-n"},
322 {KEY_CTRL_O, "Ctrl-o"},
323 {KEY_CTRL_P, "Ctrl-p"},
324 {KEY_CTRL_Q, "Ctrl-q"},
325 {KEY_CTRL_R, "Ctrl-r"},
326 {KEY_CTRL_S, "Ctrl-s"},
327 {KEY_CTRL_T, "Ctrl-t"},
328 {KEY_CTRL_U, "Ctrl-u"},
329 {KEY_CTRL_V, "Ctrl-v"},
330 {KEY_CTRL_W, "Ctrl-w"},
331 {KEY_CTRL_X, "Ctrl-x"},
332 {KEY_CTRL_Y, "Ctrl-y"},
333 {KEY_CTRL_Z, "Ctrl-z"},
334 {KEY_CTRL_TILDE, "Ctrl-~"},
335 {KEY_CTRL_BACKSLASH, "Ctrl-\\"},
336 {KEY_CTRL_LSQ_BRACKET, "Ctrl-]"},
337 {KEY_CTRL_RSQ_BRACKET, "Ctrl-]"},
338 {KEY_CTRL_UNDERSCORE, "Ctrl-_"},
339 {KEY_CTRL_SLASH, "Ctrl-/"},
340 {KEY_CTRL_AT, "Ctrl-@"},
341 {KEY_CTRL_CARET, "Ctrl-^"},
342 {KEY_CTRL_BACKTICK, "Ctrl-`"},
343 {KEY_CTRL_2, "Ctrl-2"},
344 {KEY_CTRL_3, "Ctrl-3"},
345 {KEY_CTRL_4, "Ctrl-4"},
346 {KEY_CTRL_5, "Ctrl-5"},
347 {KEY_CTRL_6, "Ctrl-6"},
348 {KEY_CTRL_7, "Ctrl-7"},
349 {KEY_CTRL_5, "Ctrl-8"},
350 {KEY_CTRL_6, "Ctrl-9"},
363 {RESIZE_EVENT, "Resize"},
366 // This is the default termios for normal terminal behavior and the
367 // text-user-interface one:
368 static struct termios normal_termios, tui_termios;
370 // File-local functions:
373 * Read and return the next character from the file descriptor, or -1 if no
374 * character is available. (Helper method for nextnum() and btui_getkey())
376 static inline int nextchar(int fd) {
378 return read(fd, &c, 1) == 1 ? c : -1;
382 * Given a file descriptor, parse an integer value, updating *c to hold the
383 * next character after the integer value. Return the parsed integer value.
384 * (Helper method for btui_getkey())
386 static inline int nextnum(int fd, int *c) {
389 for (n = 0; '0' <= *c && *c <= '9'; *c = nextchar(fd))
390 n = 10 * n + (*c - '0');
395 * Reset the terminal back to its normal state and raise the given signal.
396 * (This is used as a signal handler that gracefully exits without gunking up
399 static void btui_disable_and_raise(int sig) {
400 btui_mode_t mode = bt.mode;
403 // This code will only ever be run if sig is SIGTSTP/SIGSTOP, otherwise,
404 // raise() won't return:
406 struct sigaction sa = {.sa_handler = &btui_disable_and_raise,
407 .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
408 sigaction(sig, &sa, NULL);
412 * A signal handler used to update BTUI's internal window size values when a
413 * SIGWINCH event occurs.
415 static void update_term_size(int sig) {
417 struct winsize winsize;
418 if (ioctl(fileno(bt.in), TIOCGWINSZ, &winsize) == -1) {
420 err(1, "Failed to get terminal size!");
422 if (winsize.ws_col != bt.width || winsize.ws_row != bt.height) {
423 bt.width = winsize.ws_col;
424 bt.height = winsize.ws_row;
429 // Public API functions:
432 * Clear all or part of the screen. `mode` should be one of:
433 * BTUI_CLEAR_(BELOW|ABOVE|SCREEN|RIGHT|LEFT|LINE)
435 int btui_clear(int mode) {
437 case BTUI_CLEAR_BELOW: return fputs("\033[J", bt.out);
438 case BTUI_CLEAR_ABOVE: return fputs("\033[1J", bt.out);
439 case BTUI_CLEAR_SCREEN: return fputs("\033[2J", bt.out);
440 case BTUI_CLEAR_RIGHT: return fputs("\033[K", bt.out);
441 case BTUI_CLEAR_LEFT: return fputs("\033[1K", bt.out);
442 case BTUI_CLEAR_LINE: return fputs("\033[2K", bt.out);
448 * Disable TUI mode (return to the normal terminal with the normal terminal
451 void btui_disable(void) {
453 tcsetattr(fileno(bt.out), TCSANOW, &normal_termios);
454 btui_set_cursor(CURSOR_DEFAULT);
455 btui_set_mode(BTUI_MODE_UNINITIALIZED);
459 memset(&bt, 0, sizeof(btui_t));
463 * Draw a box using the special box-drawing characters at the given x,y
464 * position with the given width,height.
466 void btui_draw_linebox(int x, int y, int w, int h) {
467 btui_move_cursor(x - 1, y - 1);
469 fputs("\033(0l", bt.out);
470 for (int i = 0; i < w; i++)
474 for (int i = 0; i < h; i++) {
475 btui_move_cursor(x - 1, y + i);
477 btui_move_cursor(x + w, y + i);
481 btui_move_cursor(x - 1, y + h);
483 for (int i = 0; i < w; i++)
485 fputs("j\033(B", bt.out);
489 * Draw a shadow to the bottom right of the given box coordinates.
491 void btui_draw_shadow(int x, int y, int w, int h) {
492 fputs("\033(0", bt.out);
493 for (int i = 0; i < h - 1; i++) {
494 btui_move_cursor(x + w, y + 1 + i);
497 btui_move_cursor(x + 1, y + h);
498 for (int i = 0; i < w; i++) {
501 fputs("\033(B", bt.out);
505 * Enable TUI mode for this terminal and return a pointer to the BTUI struct
506 * that should be passed to future API calls.
508 void btui_init(void) {
509 bt.in = fopen("/dev/tty", "r");
510 if (!bt.in) err(1, "Couldn't open /dev/tty for reading");
511 bt.out = fopen("/dev/tty", "w");
512 if (!bt.out) err(1, "Couldn't open /dev/tty for writing");
514 if (tcgetattr(fileno(bt.in), &normal_termios)) err(1, "Couldn't get termios attributes");
516 memcpy(&tui_termios, &normal_termios, sizeof(tui_termios));
517 cfmakeraw(&tui_termios);
519 bt.mode = BTUI_MODE_DISABLED;
520 atexit(btui_disable);
522 struct sigaction sa_winch = {.sa_handler = &update_term_size};
523 sigaction(SIGWINCH, &sa_winch, NULL);
524 int signals[] = {SIGTERM, SIGINT, SIGXCPU, SIGXFSZ, SIGVTALRM,
525 SIGPROF, SIGSEGV, SIGTSTP, SIGPIPE};
526 struct sigaction sa = {.sa_handler = &btui_disable_and_raise,
527 .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
528 for (size_t i = 0; i < sizeof(signals) / sizeof(signals[0]); i++)
529 sigaction(signals[i], &sa, NULL);
531 update_term_size(SIGWINCH);
536 * Set the display mode of BTUI
538 void btui_set_mode(btui_mode_t mode) {
539 if (mode == bt.mode) return;
541 if (bt.mode == BTUI_MODE_UNINITIALIZED) btui_init();
543 if (mode == BTUI_MODE_NORMAL || mode == BTUI_MODE_TUI) {
544 if (tcsetattr(fileno(bt.out), TCSANOW, &tui_termios)) errx(1, "Failed to set attr");
548 case BTUI_MODE_UNINITIALIZED:
549 case BTUI_MODE_NORMAL:
550 case BTUI_MODE_DISABLED:
551 if (bt.mode == BTUI_MODE_TUI) fputs(T_OFF(T_ALT_SCREEN), bt.out);
552 fputs(T_ON(T_SHOW_CURSOR ";" T_WRAP)
553 T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR) "\033[0m",
557 fputs(T_OFF(T_SHOW_CURSOR ";" T_WRAP)
558 T_ON(T_ALT_SCREEN ";" T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR),
568 * Fill the given rectangular area (x,y coordinates and width,height) with
571 void btui_fill_box(int x, int y, int w, int h) {
572 int left = x, bottom = y + h;
573 for (; y < bottom; y++) {
575 btui_move_cursor(x, y);
576 for (; x < left + w; x++) {
583 * Flush BTUI's output.
585 int btui_flush(void) { return fflush(bt.out); }
588 * Close BTUI files and prevent cleaning up (useful for fork/exec)
590 void btui_force_close(void) {
594 memset(&bt, 0, sizeof(btui_t));
598 * Get one key of input from the given file. Returns -1 on failure.
599 * If mouse_x or mouse_y are non-null and a mouse event occurs, they will be
600 * set to the position of the mouse (0-indexed).
602 int btui_getkey(int timeout, int *mouse_x, int *mouse_y) {
603 int new_vmin = timeout < 0 ? 1 : 0, new_vtime = timeout < 0 ? 0 : timeout;
604 if (new_vmin != tui_termios.c_cc[VMIN] || new_vtime != tui_termios.c_cc[VTIME]) {
605 tui_termios.c_cc[VMIN] = new_vmin;
606 tui_termios.c_cc[VTIME] = new_vtime;
607 if (tcsetattr(fileno(bt.out), TCSANOW, &tui_termios) == -1) return -1;
610 if (mouse_x) *mouse_x = -1;
611 if (mouse_y) *mouse_y = -1;
612 int fd = fileno(bt.in);
613 int numcode = 0, modifiers = 0;
614 int c = nextchar(fd);
617 } else if (c == -1 && bt.size_changed) {
626 // Actual escape key:
627 if (c < 0) return KEY_ESC;
630 case '\x1b': return KEY_ESC;
631 case '[': c = nextchar(fd); goto CSI_start;
634 default: return MOD_ALT | c;
638 if (c == -1) return MOD_ALT | '[';
641 case 'A': return modifiers | KEY_ARROW_UP;
642 case 'B': return modifiers | KEY_ARROW_DOWN;
643 case 'C': return modifiers | KEY_ARROW_RIGHT;
644 case 'D': return modifiers | KEY_ARROW_LEFT;
645 case 'F': return modifiers | KEY_END;
646 case 'H': return modifiers | KEY_HOME;
647 case 'J': return numcode == 2 ? (MOD_SHIFT | KEY_HOME) : -1;
648 case 'K': return MOD_SHIFT | KEY_END;
649 case 'M': return MOD_CTRL | KEY_DELETE;
650 case 'P': return modifiers | (numcode == 1 ? KEY_F1 : KEY_DELETE);
651 case 'Q': return numcode == 1 ? (modifiers | KEY_F2) : -1;
652 case 'R': return numcode == 1 ? (modifiers | KEY_F3) : -1;
653 case 'S': return numcode == 1 ? (modifiers | KEY_F4) : -1;
654 case 'Z': return MOD_SHIFT | modifiers | (int)KEY_TAB;
657 case 1: return modifiers | KEY_HOME;
658 case 2: return modifiers | KEY_INSERT;
659 case 3: return modifiers | KEY_DELETE;
660 case 4: return modifiers | KEY_END;
661 case 5: return modifiers | KEY_PGUP;
662 case 6: return modifiers | KEY_PGDN;
663 case 7: return modifiers | KEY_HOME;
664 case 8: return modifiers | KEY_END;
665 case 10: return modifiers | KEY_F0;
666 case 11: return modifiers | KEY_F1;
667 case 12: return modifiers | KEY_F2;
668 case 13: return modifiers | KEY_F3;
669 case 14: return modifiers | KEY_F4;
670 case 15: return modifiers | KEY_F5;
671 case 17: return modifiers | KEY_F6;
672 case 18: return modifiers | KEY_F7;
673 case 19: return modifiers | KEY_F8;
674 case 20: return modifiers | KEY_F9;
675 case 21: return modifiers | KEY_F10;
676 case 23: return modifiers | KEY_F11;
677 case 24: return modifiers | KEY_F12;
681 case '<': { // Mouse clicks
682 int buttons = nextnum(fd, &c);
683 if (c != ';') return -1;
684 int x = nextnum(fd, &c);
685 if (c != ';') return -1;
686 int y = nextnum(fd, &c);
687 if (c != 'm' && c != 'M') return -1;
689 if (mouse_x) *mouse_x = x - 1;
690 if (mouse_y) *mouse_y = y - 1;
692 if (buttons & 4) modifiers |= MOD_SHIFT;
693 if (buttons & 8) modifiers |= MOD_META;
694 if (buttons & 16) modifiers |= MOD_CTRL;
696 switch (buttons & ~(4 | 8 | 16)) {
697 case 0: key = c == 'm' ? MOUSE_LEFT_RELEASE : MOUSE_LEFT_PRESS; break;
698 case 1: key = c == 'm' ? MOUSE_MIDDLE_RELEASE : MOUSE_MIDDLE_PRESS; break;
699 case 2: key = c == 'm' ? MOUSE_RIGHT_RELEASE : MOUSE_RIGHT_PRESS; break;
700 case 32: key = MOUSE_LEFT_DRAG; break;
701 case 33: key = MOUSE_MIDDLE_DRAG; break;
702 case 34: key = MOUSE_RIGHT_DRAG; break;
703 case 64: key = MOUSE_WHEEL_RELEASE; break;
704 case 65: key = MOUSE_WHEEL_PRESS; break;
707 if (key == MOUSE_LEFT_RELEASE || key == MOUSE_RIGHT_RELEASE
708 || key == MOUSE_MIDDLE_RELEASE) {
709 static int lastclick = -1;
710 static struct timespec lastclicktime = {0, 0};
711 struct timespec clicktime;
712 clock_gettime(CLOCK_MONOTONIC, &clicktime);
713 if (key == lastclick) {
714 double dt_ms = 1e3 * (double)(clicktime.tv_sec - lastclicktime.tv_sec)
715 + 1e-6 * (double)(clicktime.tv_nsec - lastclicktime.tv_nsec);
716 if (dt_ms < BTUI_DOUBLECLICK_THRESHOLD) {
718 case MOUSE_LEFT_RELEASE: key = MOUSE_LEFT_DOUBLE; break;
719 case MOUSE_RIGHT_RELEASE: key = MOUSE_RIGHT_DOUBLE; break;
720 case MOUSE_MIDDLE_RELEASE: key = MOUSE_MIDDLE_DOUBLE; break;
725 lastclicktime = clicktime;
728 return modifiers | key;
731 if ('0' <= c && c <= '9') {
733 for (numcode = 0; '0' <= c && c <= '9'; c = nextchar(fd))
734 numcode = 10 * numcode + (c - '0');
736 modifiers = nextnum(fd, &c);
737 modifiers = (modifiers >> 1) << MOD_BITSHIFT;
748 switch (nextchar(fd)) {
749 case 'P': return KEY_F1;
750 case 'Q': return KEY_F2;
751 case 'R': return KEY_F3;
752 case 'S': return KEY_F4;
759 * Populate `buf` with the name of a key.
761 char *btui_keyname(int key, char *buf) {
762 if (key == -1) return buf + sprintf(buf, "<none>");
763 if (key & MOD_META) buf = stpcpy(buf, "Super-");
764 if (key & MOD_CTRL) buf = stpcpy(buf, "Ctrl-");
765 if (key & MOD_ALT) buf = stpcpy(buf, "Alt-");
766 if (key & MOD_SHIFT) buf = stpcpy(buf, "Shift-");
767 key &= ~(MOD_META | MOD_CTRL | MOD_ALT | MOD_SHIFT);
768 for (size_t i = 0; i < sizeof(key_names) / sizeof(key_names[0]); i++) {
769 if (key_names[i].key == key) {
770 return stpcpy(buf, key_names[i].name);
773 if (' ' < key && key <= '~') return buf + sprintf(buf, "%c", key);
774 else return buf + sprintf(buf, "\\x%02X", (unsigned int)key);
778 * Return the key with the given name, if one exists, otherwise -1.
779 * (i.e. btui_keynamed("Space") == ' ', btui_keynamed("x") == 'x',
780 * btui_keynamed("F1") == KEY_F1, btui_keynamed("???") == -1)
782 int btui_keynamed(const char *name) {
784 static const struct {
788 {"Super-", MOD_META}, {"Ctrl-", MOD_CTRL}, {"Alt-", MOD_ALT}, {"Shift-", MOD_SHIFT}};
790 for (size_t i = 0; i < sizeof(key_names) / sizeof(key_names[0]); i++) {
791 if (strcmp(key_names[i].name, name) == 0) return modifiers | key_names[i].key;
793 for (size_t i = 0; i < sizeof(modnames) / sizeof(modnames[0]); i++) {
794 if (strncmp(name, modnames[i].prefix, strlen(modnames[i].prefix)) == 0) {
795 modifiers |= modnames[i].modifier;
796 name += strlen(modnames[i].prefix);
800 return strlen(name) == 1 ? name[0] : -1;
804 * Move the terminal's cursor to the given x,y coordinates.
806 int btui_move_cursor(int x, int y) { return fprintf(bt.out, "\033[%d;%dH", y + 1, x + 1); }
809 * Move the terminal's cursor relative to its current position.
811 int btui_move_cursor_relative(int x, int y) {
813 if (x > 0) n += fprintf(bt.out, "\033[%d A", x);
814 else if (x < 0) n += fprintf(bt.out, "\033[%d @", -x);
815 if (y > 0) n += fprintf(bt.out, "\033[%dB", y);
816 else if (y < 0) n += fprintf(bt.out, "\033[%dA", -y);
821 * Hide the terminal cursor.
823 int btui_hide_cursor(void) { return fputs(T_OFF(T_SHOW_CURSOR), bt.out); }
826 * Output a string to the terminal.
828 int btui_puts(const char *s) {
829 int ret = fputs(s, bt.out);
834 * Scroll the given screen region by the given amount. This is much faster than
835 * redrawing many lines.
837 int btui_scroll(int firstline, int lastline, int scroll_amount) {
838 if (scroll_amount > 0) {
839 return fprintf(bt.out, "\033[%d;%dr\033[%dS\033[r", firstline + 1, lastline + 1,
841 } else if (scroll_amount < 0) {
842 return fprintf(bt.out, "\033[%d;%dr\033[%dT\033[r", firstline + 1, lastline + 1,
849 * Set the given text attributes on the terminal output.
851 int btui_set_attributes(attr_t attrs) {
852 int printed = fputs("\033[", bt.out);
853 for (int i = 63; i >= 0; i--) {
854 if (attrs & (1ul << i)) {
856 if (attrs) printed += fprintf(bt.out, "%d;", i);
857 else printed += fprintf(bt.out, "%d", i);
860 printed += fputs("m", bt.out);
865 * Set the terminal text background color to the given RGB value.
867 int btui_set_bg(unsigned char r, unsigned char g, unsigned char b) {
868 return fprintf(bt.out, "\033[48;2;%d;%d;%dm", r, g, b);
872 * Set the terminal text background color to the given RGB value.
874 int btui_set_bg256(unsigned char n) { return fprintf(bt.out, "\033[48;5;%dm", n); }
877 * Set the terminal text background color to the given hexidecimal value.
879 int btui_set_bg_hex(uint32_t hex) {
880 return fprintf(bt.out, "\033[48;2;%d;%d;%dm", (hex >> 16) & 0xFF, (hex >> 8) & 0xFF,
885 * Set the cursor shape.
887 int btui_set_cursor(cursor_t cur) { return fprintf(bt.out, "\033[%u q", cur); }
890 * Set the terminal text foreground color to the given RGB value.
892 int btui_set_fg(unsigned char r, unsigned char g, unsigned char b) {
893 return fprintf(bt.out, "\033[38;2;%d;%d;%dm", r, g, b);
897 * Set the terminal text background color to the given RGB value.
899 int btui_set_fg256(unsigned char n) { return fprintf(bt.out, "\033[38;5;%dm", n); }
902 * Set the terminal text foreground color to the given hexidecimal value.
904 int btui_set_fg_hex(uint32_t hex) {
905 return fprintf(bt.out, "\033[38;2;%d;%d;%dm", (hex >> 16) & 0xFF, (hex >> 8) & 0xFF,
910 * Show the terminal cursor.
912 int btui_show_cursor(void) { return fputs(T_ON(T_SHOW_CURSOR), bt.out); }
915 * Suspend the current application. This will leave TUI mode and typically drop
916 * to the console. Normally, this would be caused by Ctrl-z, but BTUI
917 * intercepts Ctrl-z and requires you to handle it manually.
919 int btui_suspend(void) { return kill(getpid(), SIGTSTP); }
922 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1