💾 Archived View for gemini.rmf-dev.com › repo › Vaati › mz › files › 4d4cfd44d9c0cf3228b1230bba4a2f6… captured on 2023-12-28 at 15:51:32. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
0 /*
1 * Copyright (c) 2023 RMF <rawmonk@firemail.cc>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15 #ifdef __linux__
16 #define _GNU_SOURCE
17 #else
18 #define _BSD_SOURCE
19 #endif
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <stdint.h>
23 #include <unistd.h>
24 #include <string.h>
25 #include <errno.h>
26 #include "termbox.h"
27 #include "view.h"
28 #include "client.h"
29 #include "file.h"
30 #include "strlcpy.h"
31 #include "wcwidth.h"
32 #include "utf8.h"
33 #include "trash.h"
34 #include "util.h"
35
36 #define TAB_WIDTH_LIMIT 20
37
38 struct client client;
39
40 static void display_errno() {
41 STRCPY(client.info, strerror(errno));
42 client.error = 1;
43 }
44
45 static int display_tab(struct view *view, int x) {
46 char *ptr = strrchr(view->path, '/');
47 size_t length;
48 char buf[1024];
49 if (!ptr) ptr = view->path;
50 else ptr++;
51 length = AZ(utf8_width(ptr, sizeof(view->path) - (ptr - view->path)));
52 if (length > TAB_WIDTH_LIMIT) {
53 length = TAB_WIDTH_LIMIT;
54 strlcpy(buf, ptr, length + 1); /* sizeof buf > length */
55 ptr = buf;
56 }
57 tb_printf(x, 0, (client.view == view ? TB_DEFAULT : TB_BLACK),
58 (client.view == view ? TB_DEFAULT : TB_WHITE), " %s ", ptr);
59 return length + 2;
60 }
61
62 static int name_length(struct view *view) {
63 char *ptr = strrchr(view->path, '/');
64 if (!ptr) return -1;
65 ptr++;
66 return MAX(AZ(
67 utf8_width(ptr, sizeof(view->path) - (ptr - view->path))),
68 TAB_WIDTH_LIMIT);
69 }
70
71 int client_init() {
72
73 PZERO(&client);
74
75 client.view = view_init(getenv("PWD"));
76 if (!client.view || file_ls(client.view)) return -1;
77
78 client.trash = trash_init();
79 if (client.trash < 0) return -1;
80
81 if (tb_init()) return -1;
82 client.width = tb_width();
83 client.height = tb_height();
84
85 setenv("EDITOR", "vi", 0);
86
87 return 0;
88 }
89
90 int client_clean() {
91 free(client.copy);
92 free(client.view);
93 return tb_shutdown();
94 }
95
96 static void client_tabbar(struct view *view) {
97 struct view *ptr, *start;
98 int x, width, prev_sum, next_sum;
99
100 width = client.width - name_length(view) - 2;
101 if (width <= 0) {
102 start = view;
103 goto draw;
104 }
105
106 ptr = view;
107 if (!ptr->prev) {
108 start = ptr;
109 goto draw;
110 }
111 prev_sum = 0;
112 while (ptr->prev) {
113 ptr = ptr->prev;
114 prev_sum += name_length(ptr) + 2;
115 }
116
117 if (prev_sum < width/2) {
118 start = ptr;
119 goto draw;
120 }
121
122 ptr = view;
123 next_sum = 0;
124 while (ptr->next) {
125 ptr = ptr->next;
126 next_sum += name_length(ptr) + 2;
127 }
128
129 if (next_sum < width/2) {
130 x = client.width;
131 while (ptr) {
132 int len = name_length(ptr) + 2;
133 if (x - len < 0) {
134 ptr = ptr->next;
135 break;
136 }
137 x -= len;
138 if (!ptr->prev) break;
139 ptr = ptr->prev;
140 }
141 start = ptr;
142 goto draw;
143 }
144
145 ptr = view;
146 x = width/2;
147 while (ptr) {
148 int len = name_length(ptr) + 2;
149 if (x - len < 0) {
150 ptr = ptr->next;
151 break;
152 }
153 x -= len;
154 if (!ptr->prev) break;
155 ptr = ptr->prev;
156 }
157 start = ptr;
158 draw:
159 ptr = start;
160 x = 0;
161 while (ptr && x < (signed)client.width) {
162 x += display_tab(ptr, x);
163 ptr = ptr->next;
164 }
165
166 while (x < (signed)client.width) {
167 tb_set_cell(x, 0, ' ', TB_DEFAULT, TB_WHITE);
168 x++;
169 }
170 return;
171 }
172
173 int client_update() {
174
175 char counter[32];
176 struct view *view = client.view;
177 size_t i;
178
179 tb_clear();
180
181 /* display list view */
182 view_draw(view);
183
184 /* display input field, error and counter */
185 tb_print(0, client.height - 1, TB_DEFAULT,
186 client.error ? TB_RED : TB_DEFAULT,
187 client.error ? client.info : client.field);
188
189 snprintf(V(counter), "%d", client.counter);
190 if (client.counter)
191 tb_print(client.width - 8, client.height - 1,
192 TB_DEFAULT, TB_DEFAULT, counter);
193
194 /* display white status bar */
195 i = 0;
196 while (i < client.width) {
197 tb_set_cell(i, client.height - 2, ' ', TB_BLACK, TB_WHITE);
198 i++;
199 }
200 tb_print(0, client.height - 2, TB_BLACK, TB_WHITE, view->path);
201
202
203 /* display tabs bar if there's more than one tab */
204 if (TABS)
205 client_tabbar(view);
206
207 tb_present();
208
209 if (client_input()) return 1;
210 return 0;
211 }
212
213 static void addtab(struct view *new) {
214 if (client.view->next) {
215 new->next = client.view->next;
216 client.view->next->prev = new;
217 }
218 new->prev = client.view;
219 client.view->next = new;
220 if (new->fd != TRASH_FD)
221 new->selected = client.view->selected;
222 client.view = client.view->next;
223 }
224
225 static int newtab() {
226 struct view *new;
227 char *path;
228
229 path = client.view->path;
230 if (client.view->fd == TRASH_FD)
231 path = NULL;
232
233 if (path)
234 chdir(path);
235 new = view_init(path);
236
237 if (!new || file_ls(new)) {
238 display_errno();
239 return -1;
240 }
241 addtab(new);
242
243 return 0;
244 }
245
246 static int closetab() {
247
248 struct view *view = client.view;
249
250 if (!view) return -1;
251 client.view = NULL;
252 if (view->prev) {
253 view->prev->next = view->next;
254 client.view = view->prev;
255 }
256 if (view->next) {
257 view->next->prev = view->prev;
258 client.view = view->next;
259 }
260 if (client.view && client.view->fd != TRASH_FD &&
261 file_ls(client.view)) display_errno();
262
263 if (view->fd > 0)
264 close(view->fd);
265 file_free(view);
266 free(view);
267 return client.view == NULL;
268 }
269
270 int parse_command() {
271
272 /* trim */
273 char *cmd = client.field + strnlen(V(client.field)) - 1;
274 for (; cmd >= client.field && (*cmd == ' ' || *cmd == '\t'); cmd--)
275 *cmd = '\0';
276
277 if (!STRCMP(client.field, ":q"))
278 return closetab();
279 if (!STRCMP(client.field, ":qa")) {
280 while (!closetab()) ;
281 return 1;
282 }
283 if (!STRCMP(client.field, ":nt") || !STRCMP(client.field, ":tabnew"))
284 return newtab();
285 if (!strncmp(client.field, V(":!") - 1)) { /* start with ":!" */
286 fchdir(client.view->fd);
287 tb_shutdown();
288 if (system(&client.field[2])) sleep(1);
289 tb_init();
290 file_ls(client.view);
291 return 0;
292 }
293 if (!STRCMP(client.field, ":sh")) {
294 fchdir(client.view->fd);
295 tb_shutdown();
296 system("$SHELL");
297 tb_init();
298 file_ls(client.view);
299 return 0;
300 }
301 if (!STRCMP(client.field, ":trash")) {
302 struct view *v = malloc(sizeof(struct view));
303 if (!v || trash_view(v)) display_errno();
304 else addtab(v);
305 return 0;
306 }
307 if (!STRCMP(client.field, ":trash clear")) {
308 if (trash_clear()) display_errno();
309 return 0;
310 }
311
312 snprintf(V(client.info), "Not a command: %s", &client.field[1]);
313 client.error = 1;
314 return 0;
315 }
316
317 static void client_select(int next) {
318
319 struct view *view = client.view;
320 size_t i;
321 int reset;
322
323 if (view->length < 1 || !*client.search)
324 return;
325
326 reset = 0;
327 i = view->selected + next;
328 while (i != view->selected || !next) {
329 if (!next) next = 1;
330 if (i == view->length) {
331 i = 0;
332 if (reset) break;
333 reset = 1;
334 }
335 if (strcasestr(view->entries[i].name, client.search)) {
336 view->selected = i;
337 break;
338 }
339 if (next < 0 && i == 0) i = view->length;
340 i += next;
341 }
342 }
343
344 int client_command(struct tb_event ev) {
345
346 int pos;
347
348 switch (ev.key) {
349 case TB_KEY_ESC:
350 client.mode = MODE_NORMAL;
351 client.field[0] = '\0';
352 return 0;
353 case TB_KEY_BACKSPACE2:
354 case TB_KEY_BACKSPACE:
355 pos = utf8_len(V(client.field));
356 if (pos > 1)
357 client.field[pos - utf8_last_len(V(client.field))] = 0;
358 else {
359 client.mode = MODE_NORMAL;
360 client.field[0] = '\0';
361 }
362 return 0;
363 case TB_KEY_ENTER:
364 pos = 0;
365 if (client.mode == MODE_COMMAND)
366 pos = parse_command();
367 client.mode = MODE_NORMAL;
368 client.field[0] = '\0';
369 return pos;
370 }
371
372 if (!ev.ch) return 0;
373
374 pos = utf8_len(client.field, sizeof(client.field) - 2);
375 pos += tb_utf8_unicode_to_char(&client.field[pos], ev.ch);
376 client.field[pos] = '\0';
377 if (client.mode == MODE_SEARCH) {
378 strlcpy(client.search, &client.field[1],
379 sizeof(client.search) - 2);
380 client_select(0);
381 }
382
383 return 0;
384 }
385
386 void client_reset() {
387 client.counter = client.g = client.y = 0;
388 }
389
390 int client_input() {
391
392 struct tb_event ev;
393 struct view *view = client.view;
394 size_t i = 0;
395 int ret;
396
397 ret = tb_poll_event(&ev);
398 if (ret != TB_OK && ret != TB_ERR_POLL) {
399 return -1;
400 }
401
402 switch (ev.type) {
403 case TB_EVENT_RESIZE:
404 client.width = ev.w;
405 client.height = ev.h;
406 return 0;
407 case TB_EVENT_KEY:
408 break;
409 default:
410 return 0;
411 }
412
413 if (client.mode == MODE_COMMAND || client.mode == MODE_SEARCH) {
414 return client_command(ev);
415 }
416
417 switch (ev.key) {
418 case TB_KEY_ARROW_DOWN:
419 ev.ch = 'j';
420 break;
421 case TB_KEY_ARROW_UP:
422 ev.ch = 'k';
423 break;
424 case TB_KEY_ARROW_LEFT:
425 ev.ch = 'h';
426 break;
427 case TB_KEY_ARROW_RIGHT:
428 ev.ch = 'l';
429 break;
430 case TB_KEY_PGDN:
431 client.counter = AZ(client.counter) * client.height;
432 ev.ch = 'j';
433 break;
434 case TB_KEY_PGUP:
435 client.counter = AZ(client.counter) * client.height;
436 ev.ch = 'k';
437 break;
438 case TB_KEY_ESC:
439 client_reset();
440 return 0;
441 }
442
443 if (ev.ch >= (client.counter ? '0' : '1') && ev.ch <= '9') {
444 int i = ev.ch - '0';
445 client.g = 0;
446 if (client.counter >= 100000000) return 0;
447 if (client.counter == 0 && i == 0) return 0;
448 client.counter = client.counter * 10 + i;
449 return 0;
450 }
451
452 if (ev.key == TB_KEY_ENTER) goto open;
453
454 switch (ev.ch) {
455 case 'j':
456 ADDMAX(view->selected, AZ(client.counter), view->length - 1);
457 client.counter = 0;
458 break;
459 case 'k':
460 SUBMIN(client.view->selected, AZ(client.counter), 0);
461 client.counter = 0;
462 break;
463 case 'l':
464 open:
465 view_open(view);
466 break;
467 case 'h':
468 {
469 char name[1024];
470 char *ptr = strrchr(view->path, '/');
471 if (!ptr)
472 break;
473 STRCPY(name, ptr + 1);
474 if (file_up(view))
475 break;
476 file_ls(view);
477 view_select(view, name);
478 }
479 break;
480 case 'T':
481 case 't':
482 if (!client.g) break;
483 if (ev.ch == 'T') { /* backward */
484 if (view->prev) client.view = view->prev;
485 else {
486 while (view->next) view = view->next;
487 client.view = view;
488 }
489 } else { /* forward */
490 if (view->next) client.view = view->next;
491 else {
492 while (view->prev) view = view->prev;
493 client.view = view;
494 }
495 }
496 file_ls(client.view);
497 client_reset();
498 break;
499 case '.':
500 TOGGLE(view->showhidden);
501 file_ls(view);
502 break;
503 case '/': /* search */
504 case ':': /* command */
505 client.mode = ev.ch == '/' ? MODE_SEARCH: MODE_COMMAND;
506 client.error = 0;
507 client_reset();
508 client.field[0] = ev.ch;
509 client.field[1] = '\0';
510 break;
511 case 'n': /* next occurence */
512 client_select(1);
513 break;
514 case 'N': /* previous occurence */
515 client_select(-1);
516 break;
517 case 'e':
518 if (EMPTY(view) || SELECTED(view).type == DT_DIR)
519 break;
520 {
521 char buf[PATH_MAX + 256];
522 if (view->fd == TRASH_FD) {
523 char path[PATH_MAX];
524 if (trash_rawpath(view, V(path))) break;
525 snprintf(V(buf), "$EDITOR \"%s\"", path);
526 } else {
527 snprintf(V(buf), "$EDITOR \"%s/%s\"",
528 view->path, SELECTED(view).name);
529 chdir(view->path);
530 }
531 tb_shutdown();
532 system(buf);
533 tb_init();
534 }
535 break;
536 case 'G':
537 view->selected = view->length - 1;
538 break;
539 case 'g':
540 if (client.g) {
541 view->selected = 0;
542 client.g = 0;
543 break;
544 }
545 client.g = 1;
546 break;
547 case 'r': /* restore */
548 if (view->fd != TRASH_FD) break;
549 if (trash_restore(view)) display_errno();
550 if (trash_refresh(view)) display_errno();
551 break;
552 case 'd': /* delete (move to trash) */
553 {
554 size_t i = 0;
555 while (i < view->length) {
556 size_t j = i++;
557 if (!view->entries[j].selected) continue;
558 if (trash_send(view->fd, view->path,
559 view->entries[j].name)) {
560 display_errno();
561 view_unselect(view);
562 break;
563 }
564 view->entries[j].selected = 0;
565 }
566 }
567 file_ls(view);
568 if (view->selected >= view->length)
569 view->selected = view->length - 1;
570 break;
571 case 'p': /* paste */
572 if (!client.copy_length) break;
573 i = 0;
574 while (i < client.copy_length) {
575 if (client.cut ?
576 file_move_entry(view, &client.copy[i]) :
577 file_copy_entry(view, &client.copy[i]))
578 display_errno();
579 i++;
580 }
581 free(client.copy);
582 client.copy = NULL;
583 client.copy_length = 0;
584 file_ls(view);
585 file_reload(view);
586 break;
587 case 'x': /* cut */
588 case 'c': /* copy */
589 {
590 size_t i = 0, j = 0, length = 0;
591 while (i < view->length) {
592 if (view->entries[i].selected) length++;
593 i++;
594 }
595 if (!length) break;
596 free(client.copy);
597 client.copy = malloc(sizeof(struct entry) * length);
598 STRCPY(client.copy_path, view->path);
599 client.copy_length = length;
600 i = 0;
601 while (i < view->length) {
602 if (view->entries[i].selected) {
603 client.copy[j] = view->entries[i];
604 view->entries[i].selected = 0;
605 j++;
606 }
607 i++;
608 }
609 client.cut = ev.ch == 'x';
610 }
611 break;
612 case 'y': /* copy selection path to clipboard */
613 if (EMPTY(view))
614 break;
615 if (!client.y) {
616 client.y = 1;
617 break;
618 }
619 {
620 char buf[2048];
621 client.y = 0;
622 if (!system("xclip >/dev/null 2>&1")) {
623 snprintf(V(buf),
624 "printf \"%s/%s\" | xclip -sel clip >/dev/null 2>&1",
625 view->path, SELECTED(view).name);
626 if (!system(buf)) break;
627 }
628 if (getenv("TMUX")) { /* try tmux if no xclip */
629 snprintf(V(buf), "printf \"%s/%s\" | tmux load-buffer -",
630 view->path, SELECTED(view).name);
631 system(buf);
632 }
633 }
634 break;
635 case ' ': /* select */
636 if (!EMPTY(view))
637 TOGGLE(SELECTED(view).selected);
638 break;
639 }
640
641 return 0;
642 }
643