tty_interface.c (10812B)
1 #include <ctype.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 6 #include "match.h" 7 #include "tty_interface.h" 8 #include "config.h" 9 10 static int isprint_unicode(char c) { 11 return isprint(c) || c & (1 << 7); 12 } 13 14 static int is_boundary(char c) { 15 return ~c & (1 << 7) || c & (1 << 6); 16 } 17 18 static void clear(tty_interface_t *state) { 19 tty_t *tty = state->tty; 20 21 tty_setcol(tty, 0); 22 size_t line = 0; 23 while (line++ < state->options->num_lines + (state->options->show_info ? 1 : 0)) { 24 tty_newline(tty); 25 } 26 tty_clearline(tty); 27 if (state->options->num_lines > 0) { 28 tty_moveup(tty, line - 1); 29 } 30 tty_flush(tty); 31 } 32 33 static void draw_match(tty_interface_t *state, const char *choice, int selected) { 34 tty_t *tty = state->tty; 35 options_t *options = state->options; 36 char *search = state->last_search; 37 38 int n = strlen(search); 39 size_t positions[MATCH_MAX_LEN]; 40 for (int i = 0; i < n + 1 && i < MATCH_MAX_LEN; i++) 41 positions[i] = -1; 42 43 score_t score = match_positions(search, choice, &positions[0]); 44 45 if (options->show_scores) { 46 if (score == SCORE_MIN) { 47 tty_printf(tty, "( ) "); 48 } else { 49 tty_printf(tty, "(%5.2f) ", score); 50 } 51 } 52 53 if (selected) 54 #ifdef TTY_SELECTION_UNDERLINE 55 tty_setunderline(tty); 56 #else 57 tty_setinvert(tty); 58 #endif 59 60 tty_setnowrap(tty); 61 for (size_t i = 0, p = 0; choice[i] != '\0'; i++) { 62 if (positions[p] == i) { 63 tty_setfg(tty, TTY_COLOR_HIGHLIGHT); 64 p++; 65 } else { 66 tty_setfg(tty, TTY_COLOR_NORMAL); 67 } 68 if (choice[i] == '\n') { 69 tty_putc(tty, ' '); 70 } else { 71 tty_printf(tty, "%c", choice[i]); 72 } 73 } 74 tty_setwrap(tty); 75 tty_setnormal(tty); 76 } 77 78 static void draw(tty_interface_t *state) { 79 tty_t *tty = state->tty; 80 choices_t *choices = state->choices; 81 options_t *options = state->options; 82 83 unsigned int num_lines = options->num_lines; 84 size_t start = 0; 85 size_t current_selection = choices->selection; 86 if (current_selection + options->scrolloff >= num_lines) { 87 start = current_selection + options->scrolloff - num_lines + 1; 88 size_t available = choices_available(choices); 89 if (start + num_lines >= available && available > 0) { 90 start = available - num_lines; 91 } 92 } 93 94 tty_setcol(tty, 0); 95 tty_printf(tty, "%s%s", options->prompt, state->search); 96 tty_clearline(tty); 97 98 if (options->show_info) { 99 tty_printf(tty, "\n[%lu/%lu]", choices->available, choices->size); 100 tty_clearline(tty); 101 } 102 103 for (size_t i = start; i < start + num_lines; i++) { 104 tty_printf(tty, "\n"); 105 tty_clearline(tty); 106 const char *choice = choices_get(choices, i); 107 if (choice) { 108 draw_match(state, choice, i == choices->selection); 109 } 110 } 111 112 if (num_lines + options->show_info) 113 tty_moveup(tty, num_lines + options->show_info); 114 115 tty_setcol(tty, 0); 116 fputs(options->prompt, tty->fout); 117 for (size_t i = 0; i < state->cursor; i++) 118 fputc(state->search[i], tty->fout); 119 tty_flush(tty); 120 } 121 122 static void update_search(tty_interface_t *state) { 123 choices_search(state->choices, state->search); 124 strcpy(state->last_search, state->search); 125 } 126 127 static void update_state(tty_interface_t *state) { 128 if (strcmp(state->last_search, state->search)) { 129 update_search(state); 130 draw(state); 131 } 132 } 133 134 static void action_emit(tty_interface_t *state) { 135 update_state(state); 136 137 /* Reset the tty as close as possible to the previous state */ 138 clear(state); 139 140 /* ttyout should be flushed before outputting on stdout */ 141 tty_close(state->tty); 142 143 const char *selection = choices_get(state->choices, state->choices->selection); 144 if (selection) { 145 /* output the selected result */ 146 printf("%s\n", selection); 147 } else { 148 /* No match, output the query instead */ 149 printf("%s\n", state->search); 150 } 151 152 state->exit = EXIT_SUCCESS; 153 } 154 155 static void action_del_char(tty_interface_t *state) { 156 size_t length = strlen(state->search); 157 if (state->cursor == 0) { 158 return; 159 } 160 size_t original_cursor = state->cursor; 161 162 do { 163 state->cursor--; 164 } while (!is_boundary(state->search[state->cursor]) && state->cursor); 165 166 memmove(&state->search[state->cursor], &state->search[original_cursor], length - original_cursor + 1); 167 } 168 169 static void action_del_word(tty_interface_t *state) { 170 size_t original_cursor = state->cursor; 171 size_t cursor = state->cursor; 172 173 while (cursor && isspace(state->search[cursor - 1])) 174 cursor--; 175 176 while (cursor && !isspace(state->search[cursor - 1])) 177 cursor--; 178 179 memmove(&state->search[cursor], &state->search[original_cursor], strlen(state->search) - original_cursor + 1); 180 state->cursor = cursor; 181 } 182 183 static void action_del_all(tty_interface_t *state) { 184 memmove(state->search, &state->search[state->cursor], strlen(state->search) - state->cursor + 1); 185 state->cursor = 0; 186 } 187 188 static void action_prev(tty_interface_t *state) { 189 update_state(state); 190 choices_prev(state->choices); 191 } 192 193 static void action_ignore(tty_interface_t *state) { 194 (void)state; 195 } 196 197 static void action_next(tty_interface_t *state) { 198 update_state(state); 199 choices_next(state->choices); 200 } 201 202 static void action_left(tty_interface_t *state) { 203 if (state->cursor > 0) { 204 state->cursor--; 205 while (!is_boundary(state->search[state->cursor]) && state->cursor) 206 state->cursor--; 207 } 208 } 209 210 static void action_right(tty_interface_t *state) { 211 if (state->cursor < strlen(state->search)) { 212 state->cursor++; 213 while (!is_boundary(state->search[state->cursor])) 214 state->cursor++; 215 } 216 } 217 218 static void action_beginning(tty_interface_t *state) { 219 state->cursor = 0; 220 } 221 222 static void action_end(tty_interface_t *state) { 223 state->cursor = strlen(state->search); 224 } 225 226 static void action_pageup(tty_interface_t *state) { 227 update_state(state); 228 for (size_t i = 0; i < state->options->num_lines && state->choices->selection > 0; i++) 229 choices_prev(state->choices); 230 } 231 232 static void action_pagedown(tty_interface_t *state) { 233 update_state(state); 234 for (size_t i = 0; i < state->options->num_lines && state->choices->selection < state->choices->available - 1; i++) 235 choices_next(state->choices); 236 } 237 238 static void action_autocomplete(tty_interface_t *state) { 239 update_state(state); 240 const char *current_selection = choices_get(state->choices, state->choices->selection); 241 if (current_selection) { 242 strncpy(state->search, choices_get(state->choices, state->choices->selection), SEARCH_SIZE_MAX); 243 state->cursor = strlen(state->search); 244 } 245 } 246 247 static void action_exit(tty_interface_t *state) { 248 clear(state); 249 tty_close(state->tty); 250 251 state->exit = EXIT_FAILURE; 252 } 253 254 static void append_search(tty_interface_t *state, char ch) { 255 char *search = state->search; 256 size_t search_size = strlen(search); 257 if (search_size < SEARCH_SIZE_MAX) { 258 memmove(&search[state->cursor+1], &search[state->cursor], search_size - state->cursor + 1); 259 search[state->cursor] = ch; 260 261 state->cursor++; 262 } 263 } 264 265 void tty_interface_init(tty_interface_t *state, tty_t *tty, choices_t *choices, options_t *options) { 266 state->tty = tty; 267 state->choices = choices; 268 state->options = options; 269 state->ambiguous_key_pending = 0; 270 271 strcpy(state->input, ""); 272 strcpy(state->search, ""); 273 strcpy(state->last_search, ""); 274 275 state->exit = -1; 276 277 if (options->init_search) 278 strncpy(state->search, options->init_search, SEARCH_SIZE_MAX); 279 280 state->cursor = strlen(state->search); 281 282 update_search(state); 283 } 284 285 typedef struct { 286 const char *key; 287 void (*action)(tty_interface_t *); 288 } keybinding_t; 289 290 #define KEY_CTRL(key) ((const char[]){((key) - ('@')), '\0'}) 291 292 static const keybinding_t keybindings[] = {{"\x1b", action_exit}, /* ESC */ 293 {"\x7f", action_del_char}, /* DEL */ 294 295 {KEY_CTRL('H'), action_del_char}, /* Backspace (C-H) */ 296 {KEY_CTRL('W'), action_del_word}, /* C-W */ 297 {KEY_CTRL('U'), action_del_all}, /* C-U */ 298 {KEY_CTRL('I'), action_autocomplete}, /* TAB (C-I ) */ 299 {KEY_CTRL('C'), action_exit}, /* C-C */ 300 {KEY_CTRL('D'), action_exit}, /* C-D */ 301 {KEY_CTRL('G'), action_exit}, /* C-G */ 302 {KEY_CTRL('M'), action_emit}, /* CR */ 303 {KEY_CTRL('P'), action_prev}, /* C-P */ 304 {KEY_CTRL('N'), action_next}, /* C-N */ 305 {KEY_CTRL('K'), action_prev}, /* C-K */ 306 {KEY_CTRL('J'), action_next}, /* C-J */ 307 {KEY_CTRL('A'), action_beginning}, /* C-A */ 308 {KEY_CTRL('E'), action_end}, /* C-E */ 309 310 {"\x1bOD", action_left}, /* LEFT */ 311 {"\x1b[D", action_left}, /* LEFT */ 312 {"\x1bOC", action_right}, /* RIGHT */ 313 {"\x1b[C", action_right}, /* RIGHT */ 314 {"\x1b[1~", action_beginning}, /* HOME */ 315 {"\x1b[H", action_beginning}, /* HOME */ 316 {"\x1b[4~", action_end}, /* END */ 317 {"\x1b[F", action_end}, /* END */ 318 {"\x1b[A", action_prev}, /* UP */ 319 {"\x1bOA", action_prev}, /* UP */ 320 {"\x1b[B", action_next}, /* DOWN */ 321 {"\x1bOB", action_next}, /* DOWN */ 322 {"\x1b[5~", action_pageup}, 323 {"\x1b[6~", action_pagedown}, 324 {"\x1b[200~", action_ignore}, 325 {"\x1b[201~", action_ignore}, 326 {NULL, NULL}}; 327 328 #undef KEY_CTRL 329 330 static void handle_input(tty_interface_t *state, const char *s, int handle_ambiguous_key) { 331 state->ambiguous_key_pending = 0; 332 333 char *input = state->input; 334 strcat(state->input, s); 335 336 /* Figure out if we have completed a keybinding and whether we're in the 337 * middle of one (both can happen, because of Esc). */ 338 int found_keybinding = -1; 339 int in_middle = 0; 340 for (int i = 0; keybindings[i].key; i++) { 341 if (!strcmp(input, keybindings[i].key)) 342 found_keybinding = i; 343 else if (!strncmp(input, keybindings[i].key, strlen(state->input))) 344 in_middle = 1; 345 } 346 347 /* If we have an unambiguous keybinding, run it. */ 348 if (found_keybinding != -1 && (!in_middle || handle_ambiguous_key)) { 349 keybindings[found_keybinding].action(state); 350 strcpy(input, ""); 351 return; 352 } 353 354 /* We could have a complete keybinding, or could be in the middle of one. 355 * We'll need to wait a few milliseconds to find out. */ 356 if (found_keybinding != -1 && in_middle) { 357 state->ambiguous_key_pending = 1; 358 return; 359 } 360 361 /* Wait for more if we are in the middle of a keybinding */ 362 if (in_middle) 363 return; 364 365 /* No matching keybinding, add to search */ 366 for (int i = 0; input[i]; i++) 367 if (isprint_unicode(input[i])) 368 append_search(state, input[i]); 369 370 /* We have processed the input, so clear it */ 371 strcpy(input, ""); 372 } 373 374 int tty_interface_run(tty_interface_t *state) { 375 draw(state); 376 377 for (;;) { 378 do { 379 while(!tty_input_ready(state->tty, -1, 1)) { 380 /* We received a signal (probably WINCH) */ 381 draw(state); 382 } 383 384 char s[2] = {tty_getchar(state->tty), '\0'}; 385 handle_input(state, s, 0); 386 387 if (state->exit >= 0) 388 return state->exit; 389 390 draw(state); 391 } while (tty_input_ready(state->tty, state->ambiguous_key_pending ? KEYTIMEOUT : 0, 0)); 392 393 if (state->ambiguous_key_pending) { 394 char s[1] = ""; 395 handle_input(state, s, 1); 396 397 if (state->exit >= 0) 398 return state->exit; 399 } 400 401 update_state(state); 402 } 403 404 return state->exit; 405 }