3 * Copyright 2020 Bruce Hill
4 * Released under the MIT License
6 * This file defines some Text User Interface (TUI) functionality.
17 #include <sys/ioctl.h>
22 #define BTUI_VERSION 5
24 // Terminal escape sequences:
26 #define T_SHOW_CURSOR "25"
27 #define T_MOUSE_XY "1000"
28 #define T_MOUSE_CELL "1002"
29 #define T_MOUSE_SGR "1006"
30 #define T_ALT_SCREEN "1049"
31 #define T_ON(opt) "\033[?" opt "h"
32 #define T_OFF(opt) "\033[?" opt "l"
34 // Maximum time in milliseconds between double clicks
35 #ifndef BTUI_DOUBLECLICK_THRESHOLD
36 #define BTUI_DOUBLECLICK_THRESHOLD 200
39 // Keyboard modifiers:
40 #define MOD_BITSHIFT 9
41 #define MOD_META (1 << (MOD_BITSHIFT + 0))
42 #define MOD_CTRL (1 << (MOD_BITSHIFT + 1))
43 #define MOD_ALT (1 << (MOD_BITSHIFT + 2))
44 #define MOD_SHIFT (1 << (MOD_BITSHIFT + 3))
47 BTUI_MODE_UNINITIALIZED = 0,
54 KEY_CTRL_AT = 0x00, KEY_CTRL_A, KEY_CTRL_B, KEY_CTRL_C, KEY_CTRL_D,
55 KEY_CTRL_E, KEY_CTRL_F, KEY_CTRL_G, KEY_CTRL_H, KEY_CTRL_I, KEY_CTRL_J,
56 KEY_CTRL_K, KEY_CTRL_L, KEY_CTRL_M, KEY_CTRL_N, KEY_CTRL_O, KEY_CTRL_P,
57 KEY_CTRL_Q, KEY_CTRL_R, KEY_CTRL_S, KEY_CTRL_T, KEY_CTRL_U, KEY_CTRL_V,
58 KEY_CTRL_W, KEY_CTRL_X, KEY_CTRL_Y, KEY_CTRL_Z,
59 KEY_CTRL_LSQ_BRACKET, KEY_CTRL_BACKSLASH, KEY_CTRL_RSQ_BRACKET,
60 KEY_CTRL_CARET, KEY_CTRL_UNDERSCORE, KEY_SPACE,
61 // Printable chars would be here
62 KEY_BACKSPACE2 = 0x7F,
64 // Non-ascii multi-byte keys:
65 KEY_F0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8,
66 KEY_F9, KEY_F10, KEY_F11, KEY_F12,
67 KEY_INSERT, KEY_DELETE, KEY_HOME, KEY_END, KEY_PGUP, KEY_PGDN,
68 KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT,
69 MOUSE_LEFT_PRESS, MOUSE_RIGHT_PRESS, MOUSE_MIDDLE_PRESS,
70 MOUSE_LEFT_DRAG, MOUSE_RIGHT_DRAG, MOUSE_MIDDLE_DRAG,
71 MOUSE_LEFT_RELEASE, MOUSE_RIGHT_RELEASE, MOUSE_MIDDLE_RELEASE,
72 MOUSE_LEFT_DOUBLE, MOUSE_RIGHT_DOUBLE, MOUSE_MIDDLE_DOUBLE,
73 MOUSE_WHEEL_RELEASE, MOUSE_WHEEL_PRESS,
80 CURSOR_BLINKING_BLOCK = 1,
81 CURSOR_STEADY_BLOCK = 2,
82 CURSOR_BLINKING_UNDERLINE = 3,
83 CURSOR_STEADY_UNDERLINE = 4,
84 CURSOR_BLINKING_BAR = 5,
85 CURSOR_STEADY_BAR = 6,
88 // Overlapping key codes:
89 const btui_key_t KEY_CTRL_BACKTICK = KEY_CTRL_AT;
90 const btui_key_t KEY_CTRL_2 = KEY_CTRL_AT;
91 const btui_key_t KEY_CTRL_SPACE = KEY_CTRL_AT;
92 const btui_key_t KEY_BACKSPACE = KEY_CTRL_H;
93 const btui_key_t KEY_TAB = KEY_CTRL_I;
94 const btui_key_t KEY_ENTER = KEY_CTRL_M;
95 const btui_key_t KEY_ESC = KEY_CTRL_LSQ_BRACKET;
96 const btui_key_t KEY_CTRL_3 = KEY_CTRL_LSQ_BRACKET;
97 const btui_key_t KEY_CTRL_4 = KEY_CTRL_BACKSLASH;
98 const btui_key_t KEY_CTRL_5 = KEY_CTRL_RSQ_BRACKET;
99 const btui_key_t KEY_CTRL_TILDE = KEY_CTRL_CARET;
100 const btui_key_t KEY_CTRL_6 = KEY_CTRL_CARET;
101 const btui_key_t KEY_CTRL_7 = KEY_CTRL_UNDERSCORE;
102 const btui_key_t KEY_CTRL_SLASH = KEY_CTRL_UNDERSCORE;
103 const btui_key_t KEY_CTRL_8 = KEY_BACKSPACE2;
106 BTUI_CLEAR_SCREEN = 0,
107 BTUI_CLEAR_ABOVE = 1,
108 BTUI_CLEAR_BELOW = 2,
111 BTUI_CLEAR_RIGHT = 5,
115 typedef uint64_t attr_t;
116 const attr_t BTUI_NORMAL = 1ul << 0;
117 const attr_t BTUI_BOLD = 1ul << 1;
118 const attr_t BTUI_FAINT = 1ul << 2;
119 const attr_t BTUI_ITALIC = 1ul << 3;
120 const attr_t BTUI_UNDERLINE = 1ul << 4;
121 const attr_t BTUI_BLINK_SLOW = 1ul << 5;
122 const attr_t BTUI_BLINK_FAST = 1ul << 6;
123 const attr_t BTUI_REVERSE = 1ul << 7;
124 const attr_t BTUI_CONCEAL = 1ul << 8;
125 const attr_t BTUI_STRIKETHROUGH = 1ul << 9;
126 // 10-19: alternative fonts
127 const attr_t BTUI_FRAKTUR = 1ul << 20;
128 const attr_t BTUI_DOUBLE_UNDERLINE = 1ul << 21;
129 const attr_t BTUI_NO_BOLD_OR_FAINT = 1ul << 22;
130 const attr_t BTUI_NO_ITALIC_OR_FRAKTUR = 1ul << 23;
131 const attr_t BTUI_NO_UNDERLINE = 1ul << 24;
132 const attr_t BTUI_NO_BLINK = 1ul << 25;
134 const attr_t BTUI_NO_REVERSE = 1ul << 27;
135 const attr_t BTUI_NO_CONCEAL = 1ul << 28;
136 const attr_t BTUI_NO_STRIKETHROUGH = 1ul << 29;
137 const attr_t BTUI_FG_BLACK = 1ul << 30;
138 const attr_t BTUI_FG_RED = 1ul << 31;
139 const attr_t BTUI_FG_GREEN = 1ul << 32;
140 const attr_t BTUI_FG_YELLOW = 1ul << 33;
141 const attr_t BTUI_FG_BLUE = 1ul << 34;
142 const attr_t BTUI_FG_MAGENTA = 1ul << 35;
143 const attr_t BTUI_FG_CYAN = 1ul << 36;
144 const attr_t BTUI_FG_WHITE = 1ul << 37;
145 // 38: 256/24bit color
146 const attr_t BTUI_FG_NORMAL = 1ul << 39;
147 const attr_t BTUI_BG_BLACK = 1ul << 40;
148 const attr_t BTUI_BG_RED = 1ul << 41;
149 const attr_t BTUI_BG_GREEN = 1ul << 42;
150 const attr_t BTUI_BG_YELLOW = 1ul << 43;
151 const attr_t BTUI_BG_BLUE = 1ul << 44;
152 const attr_t BTUI_BG_MAGENTA = 1ul << 45;
153 const attr_t BTUI_BG_CYAN = 1ul << 46;
154 const attr_t BTUI_BG_WHITE = 1ul << 47;
155 // 48: 256/24bit color
156 const attr_t BTUI_BG_NORMAL = 1ul << 49;
157 // 50: Inverse of 26 (reserved)
158 const attr_t BTUI_FRAMED = 1ul << 51;
159 const attr_t BTUI_ENCIRCLED = 1ul << 52;
160 const attr_t BTUI_OVERLINED = 1ul << 53;
161 const attr_t BTUI_NO_FRAMED_OR_ENCIRCLED = 1ul << 54;
162 const attr_t BTUI_NO_OVERLINED = 1ul << 55;
164 // 60-65: Ideogram stuff
183 int btui_clear(btui_t *bt, int mode);
184 void btui_disable(btui_t *bt);
185 void btui_draw_linebox(btui_t *bt, int x, int y, int w, int h);
186 void btui_draw_shadow(btui_t *bt, int x, int y, int w, int h);
187 btui_t* btui_create(btui_mode_t mode);
188 #define btui_enable() btui_create(BTUI_MODE_TUI)
189 void btui_fill_box(btui_t *bt, int x, int y, int w, int h);
190 int btui_flush(btui_t *bt);
191 void btui_force_close(btui_t *bt);
192 int btui_getkey(btui_t *bt, int timeout, int *mouse_x, int *mouse_y);
193 int btui_hide_cursor(btui_t *bt);
194 char *btui_keyname(int key, char *buf);
195 int btui_keynamed(const char *name);
196 int btui_move_cursor(btui_t *bt, int x, int y);
197 #define btui_printf(bt, ...) fprintf((bt)->out, __VA_ARGS__)
198 int btui_puts(btui_t *bt, const char *s);
199 int btui_scroll(btui_t *bt, int firstline, int lastline, int scroll_amount);
200 int btui_set_attributes(btui_t *bt, attr_t attrs);
201 int btui_set_bg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
202 int btui_set_bg_hex(btui_t *bt, int hex);
203 int btui_set_cursor(btui_t *bt, cursor_t cur);
204 int btui_set_fg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
205 int btui_set_fg_hex(btui_t *bt, int hex);
206 void btui_set_mode(btui_t *bt, btui_mode_t mode);
207 int btui_show_cursor(btui_t *bt);
208 int btui_suspend(btui_t *bt);
211 // File-local variables:
212 static btui_t current_bt = {.in = NULL, .out = NULL, .mode = BTUI_MODE_UNINITIALIZED};
214 // The names of keys that don't render well:
215 static keyname_t key_names[] = {
216 {KEY_SPACE, "Space"}, {KEY_BACKSPACE2, "Backspace"},
217 {KEY_INSERT, "Insert"}, {KEY_DELETE, "Delete"},
218 {KEY_TAB, "Tab"}, {KEY_ENTER, "Enter"}, {KEY_ENTER, "Return"},
219 {KEY_HOME, "Home"}, {KEY_END, "End"},
220 {KEY_PGUP, "PgUp"}, {KEY_PGUP, "Page Up"},
221 {KEY_PGDN, "PgDn"}, {KEY_PGDN, "Page Down"},
222 {KEY_ARROW_UP, "Up"}, {KEY_ARROW_DOWN, "Down"}, {KEY_ARROW_LEFT, "Left"}, {KEY_ARROW_RIGHT, "Right"},
223 {MOUSE_LEFT_PRESS, "Left press"}, {MOUSE_RIGHT_PRESS, "Right press"}, {MOUSE_MIDDLE_PRESS, "Middle press"},
224 {MOUSE_LEFT_DRAG, "Left drag"}, {MOUSE_RIGHT_DRAG, "Right drag"}, {MOUSE_MIDDLE_DRAG, "Middle drag"},
225 {MOUSE_LEFT_RELEASE, "Left release"}, {MOUSE_RIGHT_RELEASE, "Right release"}, {MOUSE_MIDDLE_RELEASE, "Middle release"},
226 {MOUSE_LEFT_RELEASE, "Left up"}, {MOUSE_RIGHT_RELEASE, "Right up"}, {MOUSE_MIDDLE_RELEASE, "Middle up"},
227 {MOUSE_LEFT_RELEASE, "Left click"}, {MOUSE_RIGHT_RELEASE, "Right click"}, {MOUSE_MIDDLE_RELEASE, "Middle click"},
228 {MOUSE_LEFT_DOUBLE, "Double left click"}, {MOUSE_RIGHT_DOUBLE, "Double right click"}, {MOUSE_MIDDLE_DOUBLE, "Double middle click"},
229 {MOUSE_WHEEL_RELEASE, "Mouse wheel up"}, {MOUSE_WHEEL_PRESS, "Mouse wheel down"},
230 {KEY_ESC, "Esc"}, {KEY_ESC, "Escape"},
231 {KEY_CTRL_A, "Ctrl-a"}, {KEY_CTRL_B, "Ctrl-b"}, {KEY_CTRL_C, "Ctrl-c"},
232 {KEY_CTRL_D, "Ctrl-d"}, {KEY_CTRL_E, "Ctrl-e"}, {KEY_CTRL_F, "Ctrl-f"},
233 {KEY_CTRL_G, "Ctrl-g"}, {KEY_CTRL_H, "Ctrl-h"}, {KEY_CTRL_I, "Ctrl-i"},
234 {KEY_CTRL_J, "Ctrl-j"}, {KEY_CTRL_K, "Ctrl-k"}, {KEY_CTRL_L, "Ctrl-l"},
235 {KEY_CTRL_M, "Ctrl-m"}, {KEY_CTRL_N, "Ctrl-n"}, {KEY_CTRL_O, "Ctrl-o"},
236 {KEY_CTRL_P, "Ctrl-p"}, {KEY_CTRL_Q, "Ctrl-q"}, {KEY_CTRL_R, "Ctrl-r"},
237 {KEY_CTRL_S, "Ctrl-s"}, {KEY_CTRL_T, "Ctrl-t"}, {KEY_CTRL_U, "Ctrl-u"},
238 {KEY_CTRL_V, "Ctrl-v"}, {KEY_CTRL_W, "Ctrl-w"}, {KEY_CTRL_X, "Ctrl-x"},
239 {KEY_CTRL_Y, "Ctrl-y"}, {KEY_CTRL_Z, "Ctrl-z"},
240 {KEY_CTRL_TILDE, "Ctrl-~"}, {KEY_CTRL_BACKSLASH, "Ctrl-\\"},
241 {KEY_CTRL_LSQ_BRACKET, "Ctrl-]"}, {KEY_CTRL_RSQ_BRACKET, "Ctrl-]"},
242 {KEY_CTRL_UNDERSCORE, "Ctrl-_"}, {KEY_CTRL_SLASH, "Ctrl-/"},
243 {KEY_CTRL_AT, "Ctrl-@"}, {KEY_CTRL_CARET, "Ctrl-^"},
244 {KEY_CTRL_BACKTICK, "Ctrl-`"},
245 {KEY_CTRL_2, "Ctrl-2"}, {KEY_CTRL_3, "Ctrl-3"}, {KEY_CTRL_4, "Ctrl-4"},
246 {KEY_CTRL_5, "Ctrl-5"}, {KEY_CTRL_6, "Ctrl-6"}, {KEY_CTRL_7, "Ctrl-7"},
247 {KEY_CTRL_5, "Ctrl-8"}, {KEY_CTRL_6, "Ctrl-9"},
248 {KEY_F1, "F1"}, {KEY_F2, "F2"}, {KEY_F3, "F3"}, {KEY_F4, "F4"}, {KEY_F5, "F5"},
249 {KEY_F6, "F6"}, {KEY_F7, "F7"}, {KEY_F8, "F8"}, {KEY_F9, "F9"}, {KEY_F10, "F10"},
250 {KEY_F11, "F11"}, {KEY_F12, "F12"},
251 {RESIZE_EVENT, "Resize"},
254 // This is the default termios for normal terminal behavior and the text-user-interface one:
255 static struct termios normal_termios, tui_termios;
257 // File-local functions:
260 * Read and return the next character from the file descriptor, or -1 if no
261 * character is available. (Helper method for nextnum() and btui_getkey())
263 static inline int nextchar(int fd)
266 return read(fd, &c, 1) == 1 ? c : -1;
270 * Given a file descriptor, parse an integer value, updating *c to hold the
271 * next character after the integer value. Return the parsed integer value.
272 * (Helper method for btui_getkey())
274 static inline int nextnum(int fd, int *c)
278 for (n = 0; '0' <= *c && *c <= '9'; *c = nextchar(fd))
279 n = 10*n + (*c - '0');
284 * Reset the terminal back to its normal state.
286 static void btui_cleanup(void)
288 if (!current_bt.out) return;
289 tcsetattr(fileno(current_bt.out), TCSANOW, &normal_termios);
290 btui_set_cursor(¤t_bt, CURSOR_DEFAULT);
291 btui_set_mode(¤t_bt, BTUI_MODE_UNINITIALIZED);
292 fflush(current_bt.out);
293 fclose(current_bt.in);
294 fclose(current_bt.out);
295 memset(¤t_bt, 0, sizeof(btui_t));
299 * Reset the terminal back to its normal state and raise the given signal.
300 * (This is used as a signal handler that gracefully exits without gunking up
303 static void btui_cleanup_and_raise(int sig)
305 btui_mode_t mode = current_bt.mode;
308 // This code will only ever be run if sig is SIGTSTP/SIGSTOP, otherwise, raise() won't return:
310 struct sigaction sa = {.sa_handler = &btui_cleanup_and_raise, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
311 sigaction(sig, &sa, NULL);
315 * A signal handler used to update BTUI's internal window size values when a
316 * SIGWINCH event occurs.
318 static void update_term_size(int sig)
321 struct winsize winsize;
322 ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize);
323 if (winsize.ws_col != current_bt.width || winsize.ws_row != current_bt.height) {
324 current_bt.width = winsize.ws_col;
325 current_bt.height = winsize.ws_row;
326 current_bt.size_changed = 1;
330 // Public API functions:
333 * Clear all or part of the screen. `mode` should be one of:
334 * BTUI_CLEAR_(BELOW|ABOVE|SCREEN|RIGHT|LEFT|LINE)
336 int btui_clear(btui_t *bt, int mode)
339 case BTUI_CLEAR_BELOW: return fputs("\033[J", bt->out);
340 case BTUI_CLEAR_ABOVE: return fputs("\033[1J", bt->out);
341 case BTUI_CLEAR_SCREEN: return fputs("\033[2J", bt->out);
342 case BTUI_CLEAR_RIGHT: return fputs("\033[K", bt->out);
343 case BTUI_CLEAR_LEFT: return fputs("\033[1K", bt->out);
344 case BTUI_CLEAR_LINE: return fputs("\033[2K", bt->out);
350 * Disable TUI mode (return to the normal terminal with the normal terminal
353 void btui_disable(btui_t *bt)
360 * Draw a box using the special box-drawing characters at the given x,y
361 * position with the given width,height.
363 void btui_draw_linebox(btui_t *bt, int x, int y, int w, int h)
365 btui_move_cursor(bt, x-1, y-1);
367 fputs("\033(0l", bt->out);
368 for (int i = 0; i < w; i++)
372 for (int i = 0; i < h; i++) {
373 btui_move_cursor(bt, x-1, y + i);
375 btui_move_cursor(bt, x + w, y + i);
379 btui_move_cursor(bt, x-1, y + h);
381 for (int i = 0; i < w; i++)
383 fputs("j\033(B", bt->out);
387 * Draw a shadow to the bottom right of the given box coordinates.
389 void btui_draw_shadow(btui_t *bt, int x, int y, int w, int h)
391 fputs("\033(0", bt->out);
392 for (int i = 0; i < h-1; i++) {
393 btui_move_cursor(bt, x + w, y + 1 + i);
396 btui_move_cursor(bt, x + 1, y + h);
397 for (int i = 0; i < w; i++) {
400 fputs("\033(B", bt->out);
404 * Enable TUI mode for this terminal and return a pointer to the BTUI struct
405 * that should be passed to future API calls.
407 btui_t *btui_create(btui_mode_t mode)
409 FILE *in = fopen("/dev/tty", "r");
410 if (!in) return NULL;
411 FILE *out = fopen("/dev/tty", "w");
417 if (tcgetattr(fileno(in), &normal_termios)
418 || tcgetattr(fileno(in), &tui_termios)
419 || (cfmakeraw(&tui_termios),
420 tcsetattr(fileno(out), TCSANOW, &tui_termios))) {
427 current_bt.out = out;
428 current_bt.mode = BTUI_MODE_NORMAL;
429 atexit(btui_cleanup);
431 struct sigaction sa_winch = {.sa_handler = &update_term_size};
432 sigaction(SIGWINCH, &sa_winch, NULL);
433 int signals[] = {SIGTERM, SIGINT, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGSEGV, SIGTSTP, SIGPIPE};
434 struct sigaction sa = {.sa_handler = &btui_cleanup_and_raise, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
435 for (size_t i = 0; i < sizeof(signals)/sizeof(signals[0]); i++)
436 sigaction(signals[i], &sa, NULL);
438 update_term_size(SIGWINCH);
439 current_bt.size_changed = 0;
440 btui_set_mode(¤t_bt, mode);
445 * Set the display mode of BTUI
447 void btui_set_mode(btui_t *bt, btui_mode_t mode)
449 if (mode == bt->mode) return;
451 case BTUI_MODE_NORMAL: case BTUI_MODE_UNINITIALIZED:
452 if (bt->mode == BTUI_MODE_TUI)
453 fputs(T_OFF(T_ALT_SCREEN), bt->out);
454 fputs(T_ON(T_SHOW_CURSOR ";" T_WRAP) T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR) "\033[0m", bt->out);
457 fputs(T_OFF(T_SHOW_CURSOR ";" T_WRAP) T_ON(T_ALT_SCREEN ";" T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR), bt->out);
466 * Fill the given rectangular area (x,y coordinates and width,height) with
469 void btui_fill_box(btui_t *bt, int x, int y, int w, int h)
471 int left = x, bottom = y + h;
472 for ( ; y < bottom; y++) {
474 btui_move_cursor(bt, x, y);
475 for ( ; x < left + w; x++) {
482 * Flush BTUI's output.
484 int btui_flush(btui_t *bt)
486 return fflush(bt->out);
490 * Close BTUI files and prevent cleaning up (useful for fork/exec)
492 void btui_force_close(btui_t *bt)
494 if (!bt->out) return;
497 memset(bt, 0, sizeof(btui_t));
501 * Get one key of input from the given file. Returns -1 on failure.
502 * If mouse_x or mouse_y are non-null and a mouse event occurs, they will be
503 * set to the position of the mouse (0-indexed).
505 int btui_getkey(btui_t *bt, int timeout, int *mouse_x, int *mouse_y)
507 int new_vmin = timeout < 0 ? 1 : 0, new_vtime = timeout < 0 ? 0 : timeout;
508 if (new_vmin != tui_termios.c_cc[VMIN] || new_vtime != tui_termios.c_cc[VTIME]) {
509 tui_termios.c_cc[VMIN] = new_vmin;
510 tui_termios.c_cc[VTIME] = new_vtime;
511 if (tcsetattr(fileno(bt->out), TCSANOW, &tui_termios) == -1)
515 if (mouse_x) *mouse_x = -1;
516 if (mouse_y) *mouse_y = -1;
517 int fd = fileno(bt->in);
518 int numcode = 0, modifiers = 0;
519 int c = nextchar(fd);
522 } else if (c == -1 && bt->size_changed) {
523 bt->size_changed = 0;
531 // Actual escape key:
536 case '\x1b': return KEY_ESC;
537 case '[': c = nextchar(fd); goto CSI_start;
540 default: return MOD_ALT | c;
545 return MOD_ALT | '[';
548 case 'A': return modifiers | KEY_ARROW_UP;
549 case 'B': return modifiers | KEY_ARROW_DOWN;
550 case 'C': return modifiers | KEY_ARROW_RIGHT;
551 case 'D': return modifiers | KEY_ARROW_LEFT;
552 case 'F': return modifiers | KEY_END;
553 case 'H': return modifiers | KEY_HOME;
554 case 'J': return numcode == 2 ? (MOD_SHIFT | KEY_HOME) : -1;
555 case 'K': return MOD_SHIFT | KEY_END;
556 case 'M': return MOD_CTRL | KEY_DELETE;
557 case 'P': return modifiers | (numcode == 1 ? KEY_F1 : KEY_DELETE);
558 case 'Q': return numcode == 1 ? (modifiers | KEY_F2) : -1;
559 case 'R': return numcode == 1 ? (modifiers | KEY_F3) : -1;
560 case 'S': return numcode == 1 ? (modifiers | KEY_F4) : -1;
561 case 'Z': return MOD_SHIFT | modifiers | (int)KEY_TAB;
564 case 1: return modifiers | KEY_HOME;
565 case 2: return modifiers | KEY_INSERT;
566 case 3: return modifiers | KEY_DELETE;
567 case 4: return modifiers | KEY_END;
568 case 5: return modifiers | KEY_PGUP;
569 case 6: return modifiers | KEY_PGDN;
570 case 7: return modifiers | KEY_HOME;
571 case 8: return modifiers | KEY_END;
572 case 10: return modifiers | KEY_F0;
573 case 11: return modifiers | KEY_F1;
574 case 12: return modifiers | KEY_F2;
575 case 13: return modifiers | KEY_F3;
576 case 14: return modifiers | KEY_F4;
577 case 15: return modifiers | KEY_F5;
578 case 17: return modifiers | KEY_F6;
579 case 18: return modifiers | KEY_F7;
580 case 19: return modifiers | KEY_F8;
581 case 20: return modifiers | KEY_F9;
582 case 21: return modifiers | KEY_F10;
583 case 23: return modifiers | KEY_F11;
584 case 24: return modifiers | KEY_F12;
588 case '<': { // Mouse clicks
589 int buttons = nextnum(fd, &c);
590 if (c != ';') return -1;
591 int x = nextnum(fd, &c);
592 if (c != ';') return -1;
593 int y = nextnum(fd, &c);
594 if (c != 'm' && c != 'M') return -1;
596 if (mouse_x) *mouse_x = x - 1;
597 if (mouse_y) *mouse_y = y - 1;
599 if (buttons & 4) modifiers |= MOD_SHIFT;
600 if (buttons & 8) modifiers |= MOD_META;
601 if (buttons & 16) modifiers |= MOD_CTRL;
603 switch (buttons & ~(4|8|16)) {
604 case 0: key = c == 'm' ? MOUSE_LEFT_RELEASE : MOUSE_LEFT_PRESS; break;
605 case 1: key = c == 'm' ? MOUSE_MIDDLE_RELEASE : MOUSE_MIDDLE_PRESS; break;
606 case 2: key = c == 'm' ? MOUSE_RIGHT_RELEASE : MOUSE_RIGHT_PRESS; break;
607 case 32: key = MOUSE_LEFT_DRAG; break;
608 case 33: key = MOUSE_MIDDLE_DRAG; break;
609 case 34: key = MOUSE_RIGHT_DRAG; break;
610 case 64: key = MOUSE_WHEEL_RELEASE; break;
611 case 65: key = MOUSE_WHEEL_PRESS; break;
614 if (key == MOUSE_LEFT_RELEASE || key == MOUSE_RIGHT_RELEASE || key == MOUSE_MIDDLE_RELEASE) {
615 static int lastclick = -1;
616 static struct timespec lastclicktime = {0, 0};
617 struct timespec clicktime;
618 clock_gettime(CLOCK_MONOTONIC, &clicktime);
619 if (key == lastclick) {
620 double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclicktime.tv_sec)
621 + 1e-6*(double)(clicktime.tv_nsec - lastclicktime.tv_nsec);
622 if (dt_ms < BTUI_DOUBLECLICK_THRESHOLD) {
624 case MOUSE_LEFT_RELEASE: key = MOUSE_LEFT_DOUBLE; break;
625 case MOUSE_RIGHT_RELEASE: key = MOUSE_RIGHT_DOUBLE; break;
626 case MOUSE_MIDDLE_RELEASE: key = MOUSE_MIDDLE_DOUBLE; break;
631 lastclicktime = clicktime;
634 return modifiers | key;
637 if ('0' <= c && c <= '9') {
639 for (numcode = 0; '0' <= c && c <= '9'; c = nextchar(fd))
640 numcode = 10*numcode + (c - '0');
642 modifiers = nextnum(fd, &c);
643 modifiers = (modifiers >> 1) << MOD_BITSHIFT;
654 switch (nextchar(fd)) {
655 case 'P': return KEY_F1;
656 case 'Q': return KEY_F2;
657 case 'R': return KEY_F3;
658 case 'S': return KEY_F4;
665 * Populate `buf` with the name of a key.
667 char *btui_keyname(int key, char *buf)
669 if (key == -1) return buf + sprintf(buf, "<none>");
670 if (key & MOD_META) buf = stpcpy(buf, "Super-");
671 if (key & MOD_CTRL) buf = stpcpy(buf, "Ctrl-");
672 if (key & MOD_ALT) buf = stpcpy(buf, "Alt-");
673 if (key & MOD_SHIFT) buf = stpcpy(buf, "Shift-");
674 key &= ~(MOD_META | MOD_CTRL | MOD_ALT | MOD_SHIFT);
675 for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) {
676 if (key_names[i].key == key) {
677 return stpcpy(buf, key_names[i].name);
680 if (' ' < key && key <= '~')
681 return buf + sprintf(buf, "%c", key);
683 return buf + sprintf(buf, "\\x%02X", (unsigned int)key);
687 * Return the key with the given name, if one exists, otherwise -1.
688 * (i.e. btui_keynamed("Space") == ' ', btui_keynamed("x") == 'x', btui_keynamed("F1") == KEY_F1, btui_keynamed("???") == -1)
690 int btui_keynamed(const char *name)
693 static const struct { const char *prefix; int modifier; } modnames[] = {
694 {"Super-", MOD_META}, {"Ctrl-", MOD_CTRL}, {"Alt-", MOD_ALT}, {"Shift-", MOD_SHIFT}
697 for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) {
698 if (strcmp(key_names[i].name, name) == 0)
699 return modifiers | key_names[i].key;
701 for (size_t i = 0; i < sizeof(modnames)/sizeof(modnames[0]); i++) {
702 if (strncmp(name, modnames[i].prefix, strlen(modnames[i].prefix)) == 0) {
703 modifiers |= modnames[i].modifier;
704 name += strlen(modnames[i].prefix);
708 return strlen(name) == 1 ? name[0] : -1;
712 * Move the terminal's cursor to the given x,y coordinates.
714 int btui_move_cursor(btui_t *bt, int x, int y)
716 return fprintf(bt->out, "\033[%d;%dH", y+1, x+1);
720 * Hide the terminal cursor.
722 int btui_hide_cursor(btui_t *bt)
724 return fputs(T_OFF(T_SHOW_CURSOR), bt->out);
728 * Output a string to the terminal.
730 int btui_puts(btui_t *bt, const char *s)
732 int ret = fputs(s, bt->out);
737 * Scroll the given screen region by the given amount. This is much faster than
738 * redrawing many lines.
740 int btui_scroll(btui_t *bt, int firstline, int lastline, int scroll_amount)
742 if (scroll_amount > 0) {
743 return fprintf(bt->out, "\033[%d;%dr\033[%dS\033[r",
744 firstline+1, lastline+1, scroll_amount);
745 } else if (scroll_amount < 0) {
746 return fprintf(bt->out, "\033[%d;%dr\033[%dT\033[r",
747 firstline+1, lastline+1, -scroll_amount);
753 * Set the given text attributes on the terminal output.
755 int btui_set_attributes(btui_t *bt, attr_t attrs)
757 int printed = fputs("\033[", bt->out);
758 for (int i = 0; i < 64; i++) {
759 if (attrs & (1ul << i)) {
762 printed += fprintf(bt->out, "%d;", i);
764 printed += fprintf(bt->out, "%d", i);
767 printed += fputs("m", bt->out);
772 * Set the terminal text background color to the given RGB value.
774 int btui_set_bg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b)
776 return fprintf(bt->out, "\033[48;2;%d;%d;%dm", r, g, b);
780 * Set the terminal text background color to the given hexidecimal value.
782 int btui_set_bg_hex(btui_t *bt, int hex)
784 return fprintf(bt->out, "\033[48;2;%d;%d;%dm",
785 (hex >> 16) & 0xFF, (hex >> 8) & 0xFF, hex & 0xFF);
789 * Set the cursor shape.
791 int btui_set_cursor(btui_t *bt, cursor_t cur)
793 return fprintf(bt->out, "\033[%u q", cur);
797 * Set the terminal text foreground color to the given RGB value.
799 int btui_set_fg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b)
801 return fprintf(bt->out, "\033[38;2;%d;%d;%dm", r, g, b);
805 * Set the terminal text foreground color to the given hexidecimal value.
807 int btui_set_fg_hex(btui_t *bt, int hex)
809 return fprintf(bt->out, "\033[38;2;%d;%d;%dm",
810 (hex >> 16) & 0xFF, (hex >> 8) & 0xFF, hex & 0xFF);
814 * Show the terminal cursor.
816 int btui_show_cursor(btui_t *bt)
818 return fputs(T_ON(T_SHOW_CURSOR), bt->out);
822 * Suspend the current application. This will leave TUI mode and typically drop
823 * to the console. Normally, this would be caused by Ctrl-z, but BTUI
824 * intercepts Ctrl-z and requires you to handle it manually.
826 int btui_suspend(btui_t *bt)
829 return kill(getpid(), SIGTSTP);
833 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1