💾 Archived View for gemini.rmf-dev.com › repo › Vaati › mz › files › 15bac1fa0b990c719a4361881fa7709… captured on 2023-11-14 at 08:29:23. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-11-04)
-=-=-=-=-=-=-
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_ESC:
431 client_reset();
432 return 0;
433 }
434
435 if (ev.ch >= (client.counter ? '0' : '1') && ev.ch <= '9') {
436 int i = ev.ch - '0';
437 client.g = 0;
438 if (client.counter >= 100000000) return 0;
439 if (client.counter == 0 && i == 0) return 0;
440 client.counter = client.counter * 10 + i;
441 return 0;
442 }
443
444 if (ev.key == TB_KEY_ENTER) goto open;
445
446 switch (ev.ch) {
447 case 'j':
448 ADDMAX(view->selected, AZ(client.counter), view->length - 1);
449 client.counter = 0;
450 break;
451 case 'k':
452 SUBMIN(client.view->selected, AZ(client.counter), 0);
453 client.counter = 0;
454 break;
455 case 'l':
456 open:
457 view_open(view);
458 break;
459 case 'h':
460 {
461 char name[1024];
462 char *ptr = strrchr(view->path, '/');
463 if (!ptr)
464 break;
465 STRCPY(name, ptr + 1);
466 if (file_up(view))
467 break;
468 file_ls(view);
469 view_select(view, name);
470 }
471 break;
472 case 'T':
473 case 't':
474 if (!client.g) break;
475 if (ev.ch == 'T') { /* backward */
476 if (view->prev) client.view = view->prev;
477 else {
478 while (view->next) view = view->next;
479 client.view = view;
480 }
481 } else { /* forward */
482 if (view->next) client.view = view->next;
483 else {
484 while (view->prev) view = view->prev;
485 client.view = view;
486 }
487 }
488 file_ls(client.view);
489 client_reset();
490 break;
491 case '.':
492 TOGGLE(view->showhidden);
493 file_ls(view);
494 break;
495 case '/': /* search */
496 case ':': /* command */
497 client.mode = ev.ch == '/' ? MODE_SEARCH: MODE_COMMAND;
498 client.error = 0;
499 client_reset();
500 client.field[0] = ev.ch;
501 client.field[1] = '\0';
502 break;
503 case 'n': /* next occurence */
504 client_select(1);
505 break;
506 case 'N': /* previous occurence */
507 client_select(-1);
508 break;
509 case 'e':
510 if (EMPTY(view) || SELECTED(view).type == DT_DIR)
511 break;
512 {
513 char buf[PATH_MAX + 256];
514 if (view->fd == TRASH_FD) {
515 char path[PATH_MAX];
516 if (trash_rawpath(view, V(path))) break;
517 snprintf(V(buf), "$EDITOR \"%s\"", path);
518 } else {
519 snprintf(V(buf), "$EDITOR \"%s/%s\"",
520 view->path, SELECTED(view).name);
521 chdir(view->path);
522 }
523 tb_shutdown();
524 system(buf);
525 tb_init();
526 }
527 break;
528 case 'G':
529 view->selected = view->length - 1;
530 break;
531 case 'g':
532 if (client.g) {
533 view->selected = 0;
534 client.g = 0;
535 break;
536 }
537 client.g = 1;
538 break;
539 case 'r': /* restore */
540 if (view->fd != TRASH_FD) break;
541 if (trash_restore(view)) display_errno();
542 if (trash_refresh(view)) display_errno();
543 break;
544 case 'd': /* delete (move to trash) */
545 {
546 size_t i = 0;
547 while (i < view->length) {
548 size_t j = i++;
549 if (!view->entries[j].selected) continue;
550 if (trash_send(view->fd, view->path,
551 view->entries[j].name)) {
552 display_errno();
553 view_unselect(view);
554 break;
555 }
556 view->entries[j].selected = 0;
557 }
558 }
559 file_ls(view);
560 if (view->selected >= view->length)
561 view->selected = view->length - 1;
562 break;
563 case 'p': /* paste */
564 if (!client.copy_length) break;
565 i = 0;
566 while (i < client.copy_length) {
567 if (client.cut ?
568 file_move_entry(view, &client.copy[i]) :
569 file_copy_entry(view, &client.copy[i]))
570 display_errno();
571 i++;
572 }
573 free(client.copy);
574 client.copy = NULL;
575 client.copy_length = 0;
576 file_ls(view);
577 file_reload(view);
578 break;
579 case 'x': /* cut */
580 case 'c': /* copy */
581 {
582 size_t i = 0, j = 0, length = 0;
583 while (i < view->length) {
584 if (view->entries[i].selected) length++;
585 i++;
586 }
587 if (!length) break;
588 free(client.copy);
589 client.copy = malloc(sizeof(struct entry) * length);
590 STRCPY(client.copy_path, view->path);
591 client.copy_length = length;
592 i = 0;
593 while (i < view->length) {
594 if (view->entries[i].selected) {
595 client.copy[j] = view->entries[i];
596 view->entries[i].selected = 0;
597 j++;
598 }
599 i++;
600 }
601 client.cut = ev.ch == 'x';
602 }
603 break;
604 case 'y': /* copy selection path to clipboard */
605 if (EMPTY(view))
606 break;
607 if (!client.y) {
608 client.y = 1;
609 break;
610 }
611 {
612 char buf[2048];
613 client.y = 0;
614 if (!system("xclip >/dev/null 2>&1")) {
615 snprintf(V(buf),
616 "printf \"%s/%s\" | xclip -sel clip >/dev/null 2>&1",
617 view->path, SELECTED(view).name);
618 if (!system(buf)) break;
619 }
620 if (getenv("TMUX")) { /* try tmux if no xclip */
621 snprintf(V(buf), "printf \"%s/%s\" | tmux load-buffer -",
622 view->path, SELECTED(view).name);
623 system(buf);
624 }
625 }
626 break;
627 case ' ': /* select */
628 if (!EMPTY(view))
629 TOGGLE(SELECTED(view).selected);
630 break;
631 }
632
633 return 0;
634 }
635