systemSetup

system setup, configuration and dotfiles
git clone https://noulin.net/git/systemSetup.git
Log | Files | Refs | README | LICENSE

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 }