fzy

terminal fuzzy finder picker

git clone https://9o.is/git/fzy.git

tty_interface.c

(12917B)


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