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 }