fe

terminal file explorer and picker

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

tty_interface.c

(15458B)


      1 #include <sys/types.h>
      2 #include <sys/wait.h>
      3 #include <stdlib.h>
      4 #include <errno.h>
      5 #include <stdarg.h>
      6 #include <unistd.h>
      7 #include <stdio.h>
      8 #include <string.h>
      9 #include "tty_interface.h"
     10 
     11 static void clear(tty_interface_t *state) {
     12     tty_t *tty = state->tty;
     13 
     14     tty_setcol(tty, 0);
     15     size_t line = 0;
     16     while (line++ <= state->options->num_files) {
     17         tty_newline(tty);
     18     }
     19     tty_clearline(tty);
     20     if (state->options->num_files > 0) {
     21         tty_moveup(tty, line - 1);
     22     }
     23     tty_unhide_cursor(tty);
     24     tty_title(tty, "");
     25     tty_flush(tty);
     26 }
     27 
     28 static int confirm_remove(tty_interface_t *state, const char *name) {
     29     tty_t *tty = state->tty;
     30     char fullpath[PATH_MAX];
     31 
     32     if (strcmp(state->entries->path, "/") == 0) {
     33         strlcpy(fullpath, "/", sizeof(fullpath));
     34         strlcat(fullpath, name, sizeof(fullpath));
     35     } else {
     36         strlcpy(fullpath, state->entries->path, sizeof(fullpath));
     37         strlcat(fullpath, "/", sizeof(fullpath));
     38         strlcat(fullpath, name, sizeof(fullpath));
     39     }
     40 
     41     tty_unhide_cursor(tty);
     42     tty_setcol(tty, 0);
     43     tty_printf(tty, "Remove '%s'? [y/N] ", fullpath);
     44     tty_flush(tty);
     45 
     46     char response = tty_getchar(tty);
     47 
     48     return (response == 'y' || response == 'Y');
     49 }
     50 
     51 static void draw(tty_interface_t *state) {
     52     tty_t *tty = state->tty;
     53     entries_t *entries = state->entries;
     54     const options_t *options = state->options;
     55 
     56     unsigned int num_header = 1;
     57     unsigned int num_footer = 1;
     58     unsigned int num_files = options->num_files;
     59 
     60     size_t start = 0;
     61     size_t scroll_threshold = (num_files / 2) + 1;
     62 
     63     if (entries->selection >= scroll_threshold && entries->size > num_files) {
     64         start = entries->selection - scroll_threshold + num_header;
     65 
     66         if (start + num_files >= entries->size && entries->size > 0) {
     67            start = entries->size - num_files;
     68         }
     69     }
     70 
     71     tty_title(tty, "");
     72     tty_clearline(tty);
     73     tty_setcol(tty, 0);
     74     tty_printf(tty, "%s", entries->path);
     75     tty_clearline(tty);
     76     tty_setcol(tty, 0);
     77     tty_printf(tty, "\n");
     78 
     79     for (size_t i = start; i < start + num_files; i++) {
     80         tty_clearline(tty);
     81         tty_setcol(tty, 0);
     82         tty_printf(tty, "\n");
     83 
     84         struct entry *entry = entries_item(entries, i);
     85         if (entry) {
     86             tty_clearline(tty);
     87             tty_setcol(tty, 2);
     88             tty_setfg(tty, entry->color);
     89 
     90             if (i == entries->selection) tty_setinvert(tty);
     91             tty_printf(tty, "%s%c", entry->name, entry->cm);
     92             tty_setnormal(tty);
     93         }
     94     }
     95 
     96     tty_clearline(tty);
     97     tty_setcol(tty, 0);
     98     tty_printf(tty, "\n");
     99     tty_moveup(tty, num_files + num_header + num_footer);
    100     tty_flush(tty);
    101 }
    102 
    103 #define EXEC_REPLACE 1
    104 #define EXEC_SHELL   2
    105 #define EXEC_CAPTURE 4
    106 
    107 static int xspawn(const char *cmd, char *const argv[], int flags, char *out_buf, size_t out_sz) {
    108     int pipefd[2];
    109 
    110     if (flags & EXEC_CAPTURE) {
    111         if (pipe(pipefd) == -1) return -1;
    112     }
    113 
    114     pid_t pid = (flags & EXEC_REPLACE) ? 0 : fork();
    115 
    116     if (pid == -1) return -1;
    117 
    118     if (pid == 0) {
    119         if (flags & EXEC_CAPTURE) {
    120             dup2(pipefd[1], STDOUT_FILENO);
    121             close(pipefd[0]);
    122             close(pipefd[1]);
    123         }
    124 
    125         if (flags & EXEC_SHELL) {
    126             execlp("sh", "sh", "-c", cmd, (char *)NULL);
    127         } else {
    128             execvp(cmd, argv);
    129         }
    130 
    131         perror("exec failed");
    132         if (flags & EXEC_REPLACE) return -1;
    133         _exit(EXIT_FAILURE);
    134     }
    135 
    136     if (flags & EXEC_CAPTURE) {
    137         close(pipefd[1]);
    138         read(pipefd[0], out_buf, out_sz - 1);
    139         close(pipefd[0]);
    140     }
    141 
    142     int status;
    143     waitpid(pid, &status, 0);
    144     return WEXITSTATUS(status);
    145 }
    146 
    147 void action_ignore(tty_interface_t *state, const char *argv) {
    148     (void)state;
    149     (void)argv;
    150 }
    151 
    152 void action_select(tty_interface_t *state, const char *argv) {
    153     (void)argv;
    154     int done = entries_select(state->entries);
    155 
    156     if (!done) {
    157         draw(state);
    158     } else {
    159         clear(state);
    160         const struct entry *selection = entries_selected(state->entries);
    161 
    162         if (state->options->run == NULL) {
    163             printf("%s/%s\n", state->entries->path, selection->name);
    164             state->exit = EXIT_SUCCESS;
    165         } else {
    166             char fullpath[2*PATH_MAX];
    167             snprintf(fullpath, sizeof(fullpath), "%s/%s", state->entries->path, selection->name);
    168 
    169             char *const sargv[] = {(char *)state->options->run, fullpath, NULL};
    170             xspawn(state->options->run, sargv, EXEC_REPLACE, NULL, 0);
    171             state->exit = EXIT_FAILURE;
    172         }
    173         tty_close(state->tty);
    174     }
    175 }
    176 
    177 void action_reload(tty_interface_t *state, const char *argv) {
    178     (void)argv;
    179     entries_reload(state->entries);
    180     draw(state);
    181 }
    182 
    183 void action_parent(tty_interface_t *state, const char *argv) {
    184     (void)argv;
    185     entries_parent(state->entries);
    186     draw(state);
    187 }
    188 
    189 void action_prev(tty_interface_t *state, const char *argv) {
    190     (void)argv;
    191     entries_prev(state->entries);
    192     draw(state);
    193 }
    194 
    195 void action_next(tty_interface_t *state, const char *argv) {
    196     (void)argv;
    197     entries_next(state->entries);
    198     draw(state);
    199 }
    200 
    201 void action_halfpageup(tty_interface_t *state, const char *argv) {
    202     (void)argv;
    203     for (size_t i = 0; i < (state->options->num_files / 2) && state->entries->selection > 0; i++)
    204         entries_prev(state->entries);
    205 
    206     draw(state);
    207 }
    208 
    209 void action_halfpagedown(tty_interface_t *state, const char *argv) {
    210     (void)argv;
    211     for (size_t i = 0; i < (state->options->num_files / 2) && state->entries->selection < state->entries->size - 1; i++)
    212         entries_next(state->entries);
    213 
    214     draw(state);
    215 }
    216 
    217 void action_pageup(tty_interface_t *state, const char *argv) {
    218     (void)argv;
    219     for (size_t i = 0; i < state->options->num_files && state->entries->selection > 0; i++)
    220         entries_prev(state->entries);
    221 
    222     draw(state);
    223 }
    224 
    225 void action_pagedown(tty_interface_t *state, const char *argv) {
    226     (void)argv;
    227     for (size_t i = 0; i < state->options->num_files && state->entries->selection < state->entries->size - 1; i++)
    228         entries_next(state->entries);
    229 
    230     draw(state);
    231 }
    232 
    233 void action_first(tty_interface_t *state, const char *argv) {
    234     (void)argv;
    235     entries_position(state->entries, 0);
    236     draw(state);
    237 }
    238 
    239 void action_last(tty_interface_t *state, const char *argv) {
    240     (void)argv;
    241     entries_position(state->entries, state->entries->size - 1);
    242     draw(state);
    243 }
    244 
    245 void action_home(tty_interface_t *state, const char *argv) {
    246     (void)argv;
    247     entries_setpath(state->entries, getenv("HOME"));
    248     draw(state);
    249 }
    250 
    251 void action_togglehidden(tty_interface_t *state, const char *argv) {
    252     (void)argv;
    253     entries_togglehidden();
    254     draw(state);
    255 }
    256 
    257 void action_run(tty_interface_t *state, const char *argv) {
    258     (void)argv;
    259 
    260     const struct entry *entry = entries_item(state->entries, state->entries->selection);
    261     if (!entry) return;
    262 
    263     size_t input_len = strlen(argv) + PATH_MAX;
    264     char *input = malloc(input_len);
    265 
    266     if (input == NULL) {
    267         perror("Failed to allocate memory");
    268         return;
    269     }
    270 
    271     snprintf(input, input_len, argv, state->entries->path, entry->name);
    272 
    273     clear(state);
    274     tty_unhide_cursor(state->tty);
    275     xspawn(input, NULL, EXEC_SHELL, NULL, 0);
    276 
    277     free(input);
    278     tty_hide_cursor(state->tty);
    279     draw(state);
    280 }
    281 
    282 void action_setpath(tty_interface_t *state, const char *argv) {
    283     size_t input_len = strlen(argv) + PATH_MAX;
    284     char *input = malloc(input_len);
    285 
    286     if (input == NULL) {
    287         perror("Failed to allocate memory");
    288         return;
    289     }
    290 
    291     snprintf(input, input_len, argv, state->entries->path);
    292 
    293     char output[PATH_MAX];
    294     clear(state);
    295 
    296     if (0 == xspawn(input, NULL, EXEC_SHELL | EXEC_CAPTURE, output, sizeof(output)))
    297         entries_setpath(state->entries, output);
    298 
    299     free(input);
    300     tty_hide_cursor(state->tty);
    301     draw(state);
    302 }
    303 
    304 void action_remove(tty_interface_t *state, const char *argv) {
    305 	(void)argv;
    306 
    307 	const struct entry *entry = entries_selected(state->entries);
    308 	if (!entry) return;
    309 
    310 	if (confirm_remove(state, entry->name)) {
    311 		if (entries_remove(state->entries) == 0) {
    312 			size_t selection = state->entries->selection;
    313 			entries_setpath(state->entries, state->entries->path);
    314 
    315 			size_t pos = selection < state->entries->size
    316 				? selection : state->entries->size - 1;
    317 			entries_position(state->entries, pos);
    318 		}
    319 	}
    320 
    321 	tty_hide_cursor(state->tty);
    322 	draw(state);
    323 }
    324 
    325 static int prompt_input(tty_interface_t *state, const char *prompt, char *buffer, size_t size) {
    326 	tty_t *tty = state->tty;
    327 	int len = 0;
    328 
    329 	tty_unhide_cursor(tty);
    330 	tty_setcol(tty, 0);
    331 	tty_clearline(tty);
    332 	tty_printf(tty, "%s", prompt);
    333 	tty_flush(tty);
    334 
    335 	for (;;) {
    336 		char c = tty_getchar(tty);
    337 		if (c == '\r' || c == '\n') {
    338 			break;
    339 		} else if (c == '\x1b') {
    340 			buffer[0] = '\0';
    341 			return -1;
    342 		} else if (c == '\x7f' || c == '\b') {
    343 			if (len > 0) {
    344 				len--;
    345 				buffer[len] = '\0';
    346 				tty_printf(tty, "\b \b");
    347 				tty_flush(tty);
    348 			}
    349 		} else if (len < (int)size - 1 && c >= ' ' && c <= '~') {
    350 			buffer[len++] = c;
    351 			buffer[len] = '\0';
    352 			tty_printf(tty, "%c", c);
    353 			tty_flush(tty);
    354 		}
    355 	}
    356 	return len;
    357 }
    358 
    359 void action_create(tty_interface_t *state, const char *argv) {
    360 	(void)argv;
    361 	char filename[PATH_MAX] = "";
    362 
    363 	if (prompt_input(state, "Create file: ", filename, sizeof(filename)) > 0) {
    364 		if (entries_create_file(state->entries, filename) == 0) {
    365 			entries_setpath(state->entries, state->entries->path);
    366 			int index = entries_find_file(state->entries, filename);
    367 			if (index != -1) {
    368 				entries_position(state->entries, (size_t)index);
    369 			}
    370 		}
    371 	}
    372 
    373 	tty_hide_cursor(state->tty);
    374 	draw(state);
    375 }
    376 
    377 void action_mkdir(tty_interface_t *state, const char *argv) {
    378 	(void)argv;
    379 	char dirname[PATH_MAX] = "";
    380 
    381 	if (prompt_input(state, "Create directory: ", dirname, sizeof(dirname)) > 0) {
    382 		if (entries_create_dir(state->entries, dirname) == 0) {
    383 			entries_setpath(state->entries, state->entries->path);
    384 			int index = entries_find_file(state->entries, dirname);
    385 			if (index != -1) {
    386 				entries_position(state->entries, (size_t)index);
    387 			}
    388 		}
    389 	}
    390 
    391 	tty_hide_cursor(state->tty);
    392 	draw(state);
    393 }
    394 
    395 void action_copy(tty_interface_t *state, const char *argv) {
    396 	(void)argv;
    397 	const struct entry *entry = entries_selected(state->entries);
    398 	if (!entry) return;
    399 
    400 	if (strcmp(state->entries->path, "/") == 0) {
    401 		strlcpy(state->copy_buffer, "/", sizeof(state->copy_buffer));
    402 		strlcat(state->copy_buffer, entry->name, sizeof(state->copy_buffer));
    403 	} else {
    404 		strlcpy(state->copy_buffer, state->entries->path, sizeof(state->copy_buffer));
    405 		strlcat(state->copy_buffer, "/", sizeof(state->copy_buffer));
    406 		strlcat(state->copy_buffer, entry->name, sizeof(state->copy_buffer));
    407 	}
    408 	state->has_copied = 1;
    409 	state->is_cut = 0;
    410 
    411 	tty_setcol(state->tty, 0);
    412 	tty_clearline(state->tty);
    413 	tty_printf(state->tty, "Copied %s", state->copy_buffer);
    414 	tty_flush(state->tty);
    415 }
    416 
    417 void action_cut(tty_interface_t *state, const char *argv) {
    418 	(void)argv;
    419 	const struct entry *entry = entries_selected(state->entries);
    420 	if (!entry) return;
    421 
    422 	if (strcmp(state->entries->path, "/") == 0) {
    423 		strlcpy(state->copy_buffer, "/", sizeof(state->copy_buffer));
    424 		strlcat(state->copy_buffer, entry->name, sizeof(state->copy_buffer));
    425 	} else {
    426 		strlcpy(state->copy_buffer, state->entries->path, sizeof(state->copy_buffer));
    427 		strlcat(state->copy_buffer, "/", sizeof(state->copy_buffer));
    428 		strlcat(state->copy_buffer, entry->name, sizeof(state->copy_buffer));
    429 	}
    430 	state->has_copied = 1;
    431 	state->is_cut = 1;
    432 
    433 	tty_setcol(state->tty, 0);
    434 	tty_clearline(state->tty);
    435 	tty_printf(state->tty, "Cut %s", state->copy_buffer);
    436 	tty_flush(state->tty);
    437 }
    438 
    439 void action_paste(tty_interface_t *state, const char *argv) {
    440 	(void)argv;
    441 	if (!state->has_copied) return;
    442 
    443 	char *filename = strrchr(state->copy_buffer, '/');
    444 	if (!filename) filename = state->copy_buffer;
    445 	else filename++;
    446 
    447 	char dst_path[PATH_MAX];
    448 	if (strcmp(state->entries->path, "/") == 0) {
    449 		strlcpy(dst_path, "/", sizeof(dst_path));
    450 		strlcat(dst_path, filename, sizeof(dst_path));
    451 	} else {
    452 		strlcpy(dst_path, state->entries->path, sizeof(dst_path));
    453 		strlcat(dst_path, "/", sizeof(dst_path));
    454 		strlcat(dst_path, filename, sizeof(dst_path));
    455 	}
    456 
    457 	int result = 0;
    458 	if (state->is_cut) {
    459 		result = entries_move_file(state->copy_buffer, dst_path);
    460 		if (result == 0) {
    461 			state->has_copied = 0;
    462 			state->is_cut = 0;
    463 		}
    464 	} else {
    465 		result = entries_copy_file(state->copy_buffer, dst_path);
    466 	}
    467 
    468 	if (result == 0) {
    469 		entries_setpath(state->entries, state->entries->path);
    470 		int index = entries_find_file(state->entries, filename);
    471 		if (index != -1) {
    472 			entries_position(state->entries, (size_t)index);
    473 		}
    474 	}
    475 
    476 draw(state);
    477 
    478 	if (result == 0) {
    479 		tty_setcol(state->tty, 0);
    480 		tty_clearline(state->tty);
    481 		if (state->is_cut) {
    482 			tty_printf(state->tty, "Moved %s", state->copy_buffer);
    483 		} else {
    484 			tty_printf(state->tty, "Pasted %s", state->copy_buffer);
    485 		}
    486 		tty_flush(state->tty);
    487 	}
    488 }
    489 
    490 void action_exit(tty_interface_t *state, const char *argv) {
    491     (void)argv;
    492     clear(state);
    493     tty_close(state->tty);
    494     state->exit = EXIT_FAILURE;
    495 }
    496 
    497 
    498 void tty_interface_init(tty_interface_t *state, tty_t *tty, entries_t *entries, options_t *options) {
    499     if (options->num_files + 1 > tty_getheight(tty)) {
    500         options->num_files = tty_getheight(tty) - 3;
    501     }
    502 
    503     state->tty = tty;
    504     state->entries = entries;
    505     state->options = options;
    506     state->ambiguous_key_pending = 0;
    507     state->exit = -1;
    508     state->has_copied = 0;
    509     state->is_cut = 0;
    510     strcpy(state->input, "");
    511 
    512     tty_hide_cursor(tty);
    513     draw(state);
    514 }
    515 
    516 static void handle_input(tty_interface_t *state, const char *s, int handle_ambiguous_key) {
    517     state->ambiguous_key_pending = 0;
    518 
    519     char *input = state->input;
    520     strcat(state->input, s);
    521 
    522     /* Figure out if we have completed a keybinding and whether we're in the
    523      * middle of one (both can happen, because of Esc). */
    524     int found_keybinding = -1;
    525     int in_middle = 0;
    526     const keybinding_t *keybindings = state->options->keybindings;
    527     for (int i = 0; keybindings[i].key; i++) {
    528         if (!strcmp(input, keybindings[i].key))
    529             found_keybinding = i;
    530         else if (!strncmp(input, keybindings[i].key, strlen(state->input)))
    531             in_middle = 1;
    532     }
    533 
    534     if (found_keybinding != -1 && (!in_middle || handle_ambiguous_key)) {
    535         keybindings[found_keybinding].action(state, keybindings[found_keybinding].argv);
    536         strcpy(input, "");
    537         return;
    538     }
    539 
    540     if (in_middle) {
    541         state->ambiguous_key_pending = 1;
    542         return;
    543     }
    544 
    545     strcpy(input, "");
    546 }
    547 
    548 int tty_interface_run(tty_interface_t *state) {
    549     for (;;) {
    550         do {
    551             while(!tty_input_ready(state->tty, -1, 1)) {
    552                 draw(state);
    553             }
    554 
    555             const char s[2] = {tty_getchar(state->tty), '\0'};
    556             handle_input(state, s, 0);
    557 
    558             if (state->ambiguous_key_pending == 1)
    559                 continue;
    560 
    561             if (state->exit >= 0)
    562                 return state->exit;
    563 
    564         } while (tty_input_ready(state->tty, state->ambiguous_key_pending ? state->options->keytimeout : 0, 0));
    565 
    566         if (state->ambiguous_key_pending) {
    567             const char s[1] = "";
    568             handle_input(state, s, 1);
    569 
    570             if (state->exit >= 0)
    571                 return state->exit;
    572         }
    573     }
    574 
    575     return state->exit;
    576 }