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 }