💾 Archived View for gemini.rmf-dev.com › repo › Vaati › mz › files › 9f90752434064fbcdcf741f555480c1… captured on 2023-07-22 at 16:57:52. 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 sstrcpy(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->other);
267 free(view);
268 return client.view == NULL;
269 }
270
271 int parse_command() {
272
273 char *cmd = &client.field[1];
274
275 if (cmd[0] == 'q' && cmd[1] == '\0')
276 return closetab();
277 if (cmd[0] == 'q' && cmd[1] == 'a' && cmd[2] == '\0') {
278 while (!closetab()) ;
279 return 1;
280 }
281 if ((cmd[0] == 'n' && cmd[1] == 't' && cmd[2] == '\0') ||
282 !strncmp(cmd, "tabnew", sizeof(client.field) - 1))
283 return newtab();
284 if (cmd[0] == '!') {
285 fchdir(client.view->fd);
286 tb_shutdown();
287 if (system(&cmd[1])) sleep(1);
288 tb_init();
289 file_ls(client.view);
290 return 0;
291 }
292 if (!strncmp(cmd, "sh", sizeof(client.field) - 1)) {
293 fchdir(client.view->fd);
294 tb_shutdown();
295 system("$SHELL");
296 tb_init();
297 file_ls(client.view);
298 return 0;
299 }
300 if (!strncmp(cmd, "trash", sizeof(client.field) - 1)) {
301 struct view *v = malloc(sizeof(struct view));
302 if (!v || trash_view(v)) display_errno();
303 else addtab(v);
304 return 0;
305 }
306 if (!strncmp(cmd, "trash clear", sizeof(client.field) - 1)) {
307 if (trash_clear()) display_errno();
308 return 0;
309 }
310
311 snprintf(V(client.info), "Not a command: %s", cmd);
312 client.error = 1;
313 return 0;
314 }
315
316 static void client_select(int next) {
317
318 struct view *view = client.view;
319 size_t i;
320 int reset;
321
322 if (view->length < 1 || !*client.search)
323 return;
324
325 reset = 0;
326 i = view->selected + next;
327 while (i != view->selected || !next) {
328 if (!next) next = 1;
329 if (i == view->length) {
330 i = 0;
331 if (reset) break;
332 reset = 1;
333 }
334 if (strcasestr(view->entries[i].name, client.search)) {
335 view->selected = i;
336 break;
337 }
338 if (next < 0 && i == 0) i = view->length;
339 i += next;
340 }
341 }
342
343 int client_command(struct tb_event ev) {
344
345 int pos;
346
347 switch (ev.key) {
348 case TB_KEY_ESC:
349 client.mode = MODE_NORMAL;
350 client.field[0] = '\0';
351 return 0;
352 case TB_KEY_BACKSPACE2:
353 case TB_KEY_BACKSPACE:
354 pos = utf8_len(V(client.field));
355 if (pos > 1)
356 client.field[pos - utf8_last_len(V(client.field))] = 0;
357 else {
358 client.mode = MODE_NORMAL;
359 client.field[0] = '\0';
360 }
361 return 0;
362 case TB_KEY_ENTER:
363 pos = 0;
364 if (client.mode == MODE_COMMAND)
365 pos = parse_command();
366 client.mode = MODE_NORMAL;
367 client.field[0] = '\0';
368 return pos;
369 }
370
371 if (!ev.ch) return 0;
372
373 pos = utf8_len(client.field, sizeof(client.field) - 2);
374 pos += tb_utf8_unicode_to_char(&client.field[pos], ev.ch);
375 client.field[pos] = '\0';
376 if (client.mode == MODE_SEARCH) {
377 strlcpy(client.search, &client.field[1],
378 sizeof(client.search) - 2);
379 client_select(0);
380 }
381
382 return 0;
383 }
384
385 void client_reset() {
386 client.counter = client.g = client.y = 0;
387 }
388
389 int client_input() {
390 struct tb_event ev;
391 struct view *view = client.view;
392 size_t i = 0;
393 int ret;
394
395 ret = tb_poll_event(&ev);
396 if (ret != TB_OK && ret != TB_ERR_POLL) {
397 return -1;
398 }
399
400 if (ev.type == TB_EVENT_RESIZE) {
401 client.width = ev.w;
402 client.height = ev.h;
403 return 0;
404 }
405
406 if (ev.type != TB_EVENT_KEY) {
407 return 0;
408 }
409
410 if (client.mode == MODE_COMMAND || client.mode == MODE_SEARCH) {
411 return client_command(ev);
412 }
413
414 if (ev.key == TB_KEY_ESC) {
415 client_reset();
416 return 0;
417 }
418
419 if (ev.ch >= (client.counter ? '0' : '1') && ev.ch <= '9') {
420 int i = ev.ch - '0';
421 client.g = 0;
422 if (client.counter >= 100000000) return 0;
423 if (client.counter == 0 && i == 0) return 0;
424 client.counter = client.counter * 10 + i;
425 return 0;
426 }
427
428 if (ev.key == TB_KEY_ENTER) goto open;
429
430 switch (ev.ch) {
431 case 'j':
432 ADDMAX(view->selected, AZ(client.counter), view->length - 1);
433 client.counter = 0;
434 break;
435 case 'k':
436 SUBMIN(client.view->selected, AZ(client.counter), 0);
437 client.counter = 0;
438 break;
439 case 'l':
440 open:
441 view_open(view);
442 break;
443 case 'h':
444 {
445 char name[1024];
446 char *ptr = strrchr(view->path, '/');
447 if (!ptr)
448 break;
449 sstrcpy(name, ptr + 1);
450 if (file_up(view))
451 break;
452 file_ls(view);
453 view_select(view, name);
454 }
455 break;
456 case 'T':
457 case 't':
458 if (!client.g) break;
459 if (ev.ch == 'T') { /* backward */
460 if (view->prev) client.view = view->prev;
461 else {
462 while (view->next) view = view->next;
463 client.view = view;
464 }
465 } else { /* forward */
466 if (view->next) client.view = view->next;
467 else {
468 while (view->prev) view = view->prev;
469 client.view = view;
470 }
471 }
472 file_ls(client.view);
473 client_reset();
474 break;
475 case '.':
476 TOGGLE(view->showhidden);
477 file_ls(view);
478 break;
479 case '/': /* search */
480 case ':': /* command */
481 client.mode = ev.ch == '/' ? MODE_SEARCH: MODE_COMMAND;
482 client.error = 0;
483 client_reset();
484 client.field[0] = ev.ch;
485 client.field[1] = '\0';
486 break;
487 case 'n': /* next occurence */
488 client_select(1);
489 break;
490 case 'N': /* previous occurence */
491 client_select(-1);
492 break;
493 case 'e':
494 if (EMPTY(view) || SELECTED(view).type == DT_DIR)
495 break;
496 {
497 char buf[2048];
498 tb_shutdown();
499 chdir(view->path);
500 snprintf(V(buf), "$EDITOR \"%s/%s\"",
501 view->path, SELECTED(view).name);
502 system(buf);
503 tb_init();
504 }
505 break;
506 case 'G':
507 view->selected = view->length - 1;
508 break;
509 case 'g':
510 if (client.g) {
511 view->selected = 0;
512 client.g = 0;
513 break;
514 }
515 client.g = 1;
516 break;
517 case 'r': /* restore */
518 if (view->fd != TRASH_FD) break;
519 if (trash_restore(view)) display_errno();
520 if (trash_refresh(view)) display_errno();
521 break;
522 case 'd': /* delete (move to trash) */
523 {
524 size_t i = 0;
525 while (i < view->length) {
526 size_t j = i++;
527 if (!view->entries[j].selected) continue;
528 if (trash_send(view->fd, view->path,
529 view->entries[j].name)) {
530 display_errno();
531 view_unselect(view);
532 break;
533 }
534 view->entries[j].selected = 0;
535 }
536 }
537 file_ls(view);
538 if (view->selected >= view->length)
539 view->selected = view->length - 1;
540 break;
541 case 'p': /* paste */
542 if (!client.copy_length) break;
543 i = 0;
544 while (i < client.copy_length) {
545 if (client.cut ?
546 file_move_entry(view, &client.copy[i]) :
547 file_copy_entry(view, &client.copy[i]))
548 display_errno();
549 i++;
550 }
551 free(client.copy);
552 client.copy = NULL;
553 client.copy_length = 0;
554 file_ls(view);
555 file_reload(view);
556 break;
557 case 'x': /* cut */
558 case 'c': /* copy */
559 {
560 size_t i = 0, j = 0, length = 0;
561 while (i < view->length) {
562 if (view->entries[i].selected) length++;
563 i++;
564 }
565 if (!length) break;
566 free(client.copy);
567 client.copy = malloc(sizeof(struct entry) * length);
568 sstrcpy(client.copy_path, view->path);
569 client.copy_length = length;
570 i = 0;
571 while (i < view->length) {
572 if (view->entries[i].selected) {
573 client.copy[j] = view->entries[i];
574 view->entries[i].selected = 0;
575 j++;
576 }
577 i++;
578 }
579 client.cut = ev.ch == 'x';
580 }
581 break;
582 case 'y': /* copy selection path to clipboard */
583 if (EMPTY(view))
584 break;
585 if (!client.y) {
586 client.y = 1;
587 break;
588 }
589 {
590 char buf[2048];
591 client.y = 0;
592 if (!system("xclip >/dev/null 2>&1")) {
593 snprintf(V(buf),
594 "printf \"%s/%s\" | xclip -sel clip >/dev/null 2>&1",
595 view->path, SELECTED(view).name);
596 if (!system(buf)) break;
597 }
598 if (getenv("TMUX")) { /* try tmux if no xclip */
599 snprintf(V(buf), "printf \"%s/%s\" | tmux load-buffer -",
600 view->path, SELECTED(view).name);
601 system(buf);
602 }
603 }
604 break;
605 case ' ': /* select */
606 if (!EMPTY(view))
607 TOGGLE(SELECTED(view).selected);
608 break;
609 }
610
611 return 0;
612 }
613