tui.c (8723B)
1 // Copyright 2023, Brian Swetland <swetland@frotz.net> 2 // Licensed under the Apache License, Version 2.0. 3 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <string.h> 7 #include <stdint.h> 8 #include <pthread.h> 9 10 #include <tui.h> 11 #include <termbox.h> 12 13 #define MAXWIDTH 128 14 #define MAXCMD (MAXWIDTH - 1) 15 16 typedef struct line LINE; 17 typedef struct ux UX; 18 19 struct line { 20 LINE* prev; 21 LINE* next; 22 uint16_t len; 23 uint8_t fg; 24 uint8_t bg; 25 uint8_t text[MAXWIDTH]; 26 }; 27 28 struct ux { 29 pthread_mutex_t lock; 30 31 int w; 32 int h; 33 int invalid; 34 int running; 35 36 char status_lhs[32]; 37 char status_rhs[32]; 38 39 LINE list; 40 41 // edit buffer and head of the circular history list 42 LINE history; 43 44 // points at active edit buffer 45 LINE *cmd; 46 47 // points at the line *before* the bottom-most list line 48 LINE *display; 49 }; 50 51 52 static void tui_add_cmd(UX* ux, uint8_t* text, unsigned len) { 53 LINE* line = malloc(sizeof(LINE)); 54 if (line == NULL) { 55 return; 56 } 57 line->len = len; 58 line->fg = line->bg = TB_DEFAULT; 59 memcpy(line->text, text, len); 60 61 line->prev = ux->history.prev; 62 line->next = &ux->history; 63 64 line->prev->next = line; 65 ux->history.prev = line; 66 } 67 68 static void paint(int x, int y, const char* str) { 69 while (*str) { 70 tb_change_cell(x++, y, *str++, TB_DEFAULT, TB_DEFAULT); 71 } 72 } 73 74 static void paint_titlebar(UX *ux) { 75 } 76 77 static void paint_infobar(UX *ux) { 78 int lhw = strlen(ux->status_lhs); 79 int rhw = strlen(ux->status_rhs); 80 int gap = ux->w - 2 - lhw - rhw; 81 if (gap < 0) gap = 0; 82 83 unsigned fg = TB_DEFAULT | TB_REVERSE; 84 unsigned bg = TB_DEFAULT; 85 int x = 0; 86 int y = ux->h - 2; 87 88 tb_change_cell(x++, y, '-', fg, bg); 89 char *s = ux->status_lhs; 90 while (x < ux->w && lhw-- > 0) { 91 tb_change_cell(x++, y, *s++, fg, bg); 92 } 93 while (x < ux->w && gap-- > 0) { 94 tb_change_cell(x++, y, '-', fg, bg); 95 } 96 s = ux->status_rhs; 97 while (x < ux->w && rhw-- > 0) { 98 tb_change_cell(x++, y, *s++, fg, bg); 99 } 100 tb_change_cell(x, y, '-', fg, bg); 101 } 102 103 static void paint_cmdline(UX *ux) { 104 uint8_t* ch = ux->cmd->text; 105 int len = ux->cmd->len; 106 int y = ux->h - 1; 107 int w = ux->w; 108 for (int x = 0; x < w; x++) { 109 if (x < len) { 110 tb_change_cell(x, y, *ch++, TB_DEFAULT, TB_DEFAULT); 111 } else { 112 tb_change_cell(x, y, ' ', TB_DEFAULT, TB_DEFAULT); 113 } 114 } 115 tb_set_cursor(len >= w ? w - 1 : len, y); 116 } 117 118 static void paint_log(UX *ux) { 119 LINE* list = &ux->list; 120 int y = ux->h - 3; 121 int w = ux->w; 122 uint8_t c; 123 124 for (LINE* line = ux->display->prev; (line != list) && (y >= 0); line = line->prev) { 125 for (int x = 0; x < w; x++) { 126 c = (x < line->len) ? line->text[x] : ' '; 127 tb_change_cell(x, y, c, line->fg, line->bg); 128 } 129 y--; 130 } 131 } 132 133 static int repaint(UX* ux) { 134 // clear entire display and adjust to any resize events 135 tb_clear(); 136 ux->w = tb_width(); 137 ux->h = tb_height(); 138 139 if ((ux->w < 40) || (ux->h < 8)) { 140 paint(0, 0, "WINDOW TOO SMALL"); 141 return 1; 142 } 143 144 paint_titlebar(ux); 145 paint_infobar(ux); 146 paint_cmdline(ux); 147 paint_log(ux); 148 149 if (ux->display != &ux->list) { 150 int x = ux->w - 8; 151 char *s = " SCROLL "; 152 while (*s != 0) { 153 tb_change_cell(x++, 0, *s++, TB_REVERSE | TB_DEFAULT, TB_DEFAULT); 154 } 155 } 156 return 0; 157 } 158 159 static void tui_scroll(UX* ux, int delta) { 160 LINE* list = &ux->list; 161 LINE* line = ux->display; 162 163 while (delta > 0) { 164 if (line->prev == list) goto done; 165 delta--; 166 line = line->prev; 167 } 168 while (delta < 0) { 169 if (line == list) goto done; 170 delta++; 171 line = line->next; 172 } 173 done: 174 ux->display = line; 175 repaint(ux); 176 tb_present(); 177 } 178 179 static int handle_event(UX* ux, struct tb_event* ev, char* line, unsigned* len) { 180 // always process full repaints due to resize or user request 181 if ((ev->type == TB_EVENT_RESIZE) || 182 (ev->key == TB_KEY_CTRL_L)) { 183 ux->invalid = repaint(ux); 184 return 0; 185 } 186 187 // ignore other input of the window is too small 188 if (ux->invalid) { 189 return 0; 190 } 191 192 switch (ev->key) { 193 case 0: // printable character 194 // ignore fancy unicode characters 195 if (ev->ch > 255) { 196 break; 197 } 198 if (ux->cmd->len >= MAXCMD) { 199 break; 200 } 201 ux->cmd->text[ux->cmd->len++] = ev->ch; 202 paint_cmdline(ux); 203 break; 204 case TB_KEY_BACKSPACE: 205 case TB_KEY_BACKSPACE2: 206 if (ux->cmd->len > 0 ) { 207 ux->cmd->len--; 208 paint_cmdline(ux); 209 } 210 break; 211 case TB_KEY_ENTER: { 212 // pass commandline out to caller 213 *len = ux->cmd->len; 214 memcpy(line, ux->cmd->text, ux->cmd->len); 215 line[ux->cmd->len] = 0; 216 217 // add new command to history 218 tui_add_cmd(ux, ux->cmd->text, ux->cmd->len); 219 220 // reset to an empty edit buffer 221 ux->cmd = &ux->history; 222 ux->cmd->len = 0; 223 224 // update display 225 paint_cmdline(ux); 226 tb_present(); 227 228 return 1; 229 } 230 case TB_KEY_ARROW_LEFT: 231 case TB_KEY_ARROW_RIGHT: 232 case TB_KEY_HOME: 233 case TB_KEY_END: 234 case TB_KEY_INSERT: 235 case TB_KEY_DELETE: 236 break; 237 case TB_KEY_ARROW_UP: 238 if (ux->cmd->prev != &ux->history) { 239 ux->cmd = ux->cmd->prev; 240 paint_cmdline(ux); 241 tb_present(); 242 } 243 break; 244 case TB_KEY_ARROW_DOWN: 245 if (ux->cmd != &ux->history) { 246 ux->cmd = ux->cmd->next; 247 paint_cmdline(ux); 248 tb_present(); 249 } 250 break; 251 case TB_KEY_PGUP: 252 tui_scroll(ux, ux->h - 3); 253 break; 254 case TB_KEY_PGDN: 255 tui_scroll(ux, -(ux->h - 3)); 256 break; 257 case TB_KEY_ESC: { 258 *len = 5; 259 memcpy(line, "@ESC@", 6); 260 return 1; 261 } 262 default: 263 #if 0 // debug unexpected keys 264 if (ux->len < (MAXWIDTH - 6)) { 265 char tmp[5]; 266 sprintf(tmp, "%04x", ev->key); 267 ux->cmd[ux->len++] = '<'; 268 ux->cmd[ux->len++] = tmp[0]; 269 ux->cmd[ux->len++] = tmp[1]; 270 ux->cmd[ux->len++] = tmp[2]; 271 ux->cmd[ux->len++] = tmp[3]; 272 ux->cmd[ux->len++] = '>'; 273 paint_cmdline(ux); 274 } 275 #endif 276 break; 277 } 278 return 0; 279 } 280 281 static UX ux = { 282 .lock = PTHREAD_MUTEX_INITIALIZER, 283 .list = { 284 .prev = &ux.list, 285 .next = &ux.list, 286 }, 287 .history = { 288 .prev = &ux.history, 289 .next = &ux.history, 290 }, 291 .cmd = &ux.history, 292 .display = &ux.list, 293 .running = 1, 294 }; 295 296 void tui_init(void) { 297 if (tb_init()) { 298 fprintf(stderr, "termbox init failed\n"); 299 return; 300 } 301 tb_select_input_mode(TB_INPUT_ESC | TB_INPUT_SPACE); 302 repaint(&ux); 303 } 304 305 void tui_exit(void) { 306 pthread_mutex_lock(&ux.lock); 307 tb_shutdown(); 308 ux.running = 0; 309 pthread_mutex_unlock(&ux.lock); 310 } 311 312 int tui_handle_event(void (*cb)(char*, unsigned)) { 313 struct tb_event ev; 314 char line[MAXWIDTH]; 315 unsigned len; 316 int r; 317 318 pthread_mutex_lock(&ux.lock); 319 if (ux.running) { 320 tb_present(); 321 } 322 pthread_mutex_unlock(&ux.lock); 323 324 if ((tb_poll_event(&ev) < 0) || 325 (ev.key == TB_KEY_CTRL_C)) { 326 return -1; 327 } 328 329 pthread_mutex_lock(&ux.lock); 330 if (ux.running) { 331 r = handle_event(&ux, &ev, line, &len); 332 } else { 333 r = -1; 334 } 335 pthread_mutex_unlock(&ux.lock); 336 337 if (r == 1) { 338 cb(line, len); 339 return 0; 340 } else { 341 return r; 342 } 343 } 344 345 void tui_status_rhs(const char* status) { 346 pthread_mutex_lock(&ux.lock); 347 if (ux.running) { 348 strncpy(ux.status_rhs, status, sizeof(ux.status_rhs) - 1); 349 paint_infobar(&ux); 350 tb_present(); 351 } 352 pthread_mutex_unlock(&ux.lock); 353 } 354 355 void tui_status_lhs(const char* status) { 356 pthread_mutex_lock(&ux.lock); 357 if (ux.running) { 358 strncpy(ux.status_lhs, status, sizeof(ux.status_lhs) - 1); 359 paint_infobar(&ux); 360 tb_present(); 361 } 362 pthread_mutex_unlock(&ux.lock); 363 } 364 365 static void tui_logline(uint8_t* text, unsigned len) { 366 LINE* line = malloc(sizeof(LINE)); 367 if (line == NULL) return; 368 memcpy(line->text, text, len); 369 line->len = len; 370 line->fg = TB_DEFAULT; 371 line->bg = TB_DEFAULT; 372 373 pthread_mutex_lock(&ux.lock); 374 if (ux.running) { 375 // add line to the log 376 line->prev = ux.list.prev; 377 line->next = &ux.list; 378 line->prev->next = line; 379 ux.list.prev = line; 380 381 // refresh the log 382 paint_log(&ux); 383 tb_present(); 384 } 385 pthread_mutex_unlock(&ux.lock); 386 } 387 388 struct tui_ch { 389 unsigned len; 390 uint8_t buffer[MAXWIDTH]; 391 }; 392 393 int tui_ch_create(tui_ch_t** out, unsigned flags) { 394 tui_ch_t* ch = calloc(1, sizeof(tui_ch_t)); 395 if (ch == NULL) { 396 return -1; 397 } else { 398 *out = ch; 399 return 0; 400 } 401 } 402 403 void tui_ch_destroy(tui_ch_t* ch) { 404 free(ch); 405 } 406 407 void tui_ch_vprintf(tui_ch_t* ch, const char* fmt, va_list ap) { 408 char tmp[1024]; 409 int n = vsnprintf(tmp, sizeof(tmp), fmt, ap); 410 char *x = tmp; 411 while (n > 0) { 412 uint8_t c = *x++; 413 n--; 414 if ((c == '\n') && (ch->len > 0)) { 415 tui_logline(ch->buffer, ch->len); 416 ch->len = 0; 417 continue; 418 } 419 if ((c < ' ') || (c > 0x7e)) { 420 continue; 421 } 422 if (ch->len < MAXWIDTH) { 423 ch->buffer[ch->len++] = c; 424 } 425 } 426 } 427 428 void tui_ch_printf(tui_ch_t* ch, const char* fmt, ...) { 429 va_list ap; 430 va_start(ap, fmt); 431 tui_ch_vprintf(ch, fmt, ap); 432 va_end(ap); 433 } 434 435 void tui_printf(const char* fmt, ...) { 436 va_list ap; 437 tui_ch_t ch = { 0 }; 438 va_start(ap, fmt); 439 tui_ch_vprintf(&ch, fmt, ap); 440 va_end(ap); 441 if (ch.len > 0) { 442 tui_logline(ch.buffer, ch.len); 443 } 444 } 445 446 void tui_vprintf(const char* fmt, va_list ap) { 447 tui_ch_t ch = { 0 }; 448 tui_ch_vprintf(&ch, fmt, ap); 449 if (ch.len > 0) { 450 tui_logline(ch.buffer, ch.len); 451 } 452 } 453