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