Thumbnail

rani/cscroll.git

Clone URL: https://git.buni.party/rani/cscroll.git

Viewing file on branch master

1#include <ncurses.h>
2#include <stdbool.h>
3#include <strings.h>
4#include <stddef.h>
5#include <string.h>
6#include <locale.h>
7#include <stdlib.h>
8#include <ctype.h>
9#include <stdio.h>
10#include <time.h>
11
12#include "ui.h"
13#include "dir.h"
14#include "main.h"
15#include "config.h"
16#include "cvector.h"
17
18#define ARRLEN(x) ((sizeof(x)) / (sizeof*(x)))
19
20#define MAX(x, y) (((x) > (y)) ? (x) : (y))
21#define MIN(x, y) (((x) > (y)) ? (y) : (x))
22
23#define TITLEWINSTARTY 0
24#define TITLEWINLINES 1
25#define TITLEWINCOLS COLS
26
27#define STATUSWINSTARTY 1
28#define STATUSWINLINES 1
29#define STATUSWINCOLS COLS
30
31#define FILEWINSTARTY 2
32#define FILEWINLINES (LINES - 3)
33#define FILEWINCOLS COLS
34
35#define INPUTWINSTARTY (LINES - 1)
36#define INPUTWINLINES 1
37#define INPUTWINCOLS COLS
38
39#define PROMPTWINSTARTY (LINES / 2)
40#define PROMPTWINSTARTX (COLS / 2)
41#define PROMPTWINMAXLINES (LINES * 3 / 4)
42#define PROMPTWINMAXCOLS (COLS * 3 / 4)
43
44#define INPUTBUFSZ 2048
45
46static WINDOW * titlewin = NULL;
47static WINDOW * statuswin = NULL;
48static WINDOW * filewin = NULL;
49static WINDOW * inputwin = NULL;
50
51static void win_set(WINDOW * win, const char * str, int attrs);
52static int ui_dirent_color(const dirent_t * de);
53static int ui_link_color(const dirent_t * de);
54static void ui_print_dirent(const dirent_t * de, size_t pos, bool selected, const dir_t * dir);
55static void ui_wlpadstr(WINDOW * win, const char * s, size_t len);
56static void ui_wrpadstr(WINDOW * win, const char * s, size_t len);
57static void ui_get_first_last(size_t n_dirents, size_t cursor, size_t * first, size_t * last);
58
59void ui_init(void) {
60 setlocale(LC_CTYPE, "");
61
62 initscr();
63 raw();
64 keypad(stdscr, TRUE);
65 curs_set(0);
66 noecho();
67
68 titlewin = newwin(TITLEWINLINES, TITLEWINCOLS, TITLEWINSTARTY, 0);
69 statuswin = newwin(STATUSWINLINES, STATUSWINCOLS, STATUSWINSTARTY, 0);
70 filewin = newwin(FILEWINLINES, FILEWINCOLS, FILEWINSTARTY, 0);
71 inputwin = newwin(INPUTWINLINES, INPUTWINCOLS, INPUTWINSTARTY, 0);
72
73 clear();
74 refresh();
75
76 use_default_colors();
77 start_color();
78
79 init_pair(COLOR_FILE, -1, -1);
80 init_pair(COLOR_DIR, COLOR_BLUE, -1);
81 init_pair(COLOR_FIFO, COLOR_YELLOW, -1);
82 init_pair(COLOR_LINK, COLOR_CYAN, -1);
83 init_pair(COLOR_BLOCK, COLOR_YELLOW, -1);
84 init_pair(COLOR_CHAR, COLOR_YELLOW, -1);
85 init_pair(COLOR_SOCKET, COLOR_MAGENTA, -1);
86 init_pair(COLOR_UNKNOWN, COLOR_RED, -1);
87 init_pair(COLOR_EXEC, COLOR_GREEN, -1);
88
89 init_pair(RED, COLOR_RED, -1);
90 init_pair(YELLOW, COLOR_YELLOW, -1);
91 init_pair(MAGENTA, COLOR_MAGENTA, -1);
92 init_pair(WHITE, -1, -1);
93}
94
95void ui_deinit(void) {
96 endwin();
97}
98
99void ui_reinit(void) {
100 refresh();
101 ui_refresh();
102}
103
104void win_set(WINDOW * win, const char * str, int attrs) {
105 werase(win);
106 wattron(win, attrs);
107 waddstr(win, str);
108 wattroff(win, attrs);
109}
110
111void ui_set_title(const char * title, const char * home) {
112 size_t homelen = strlen(home);
113 if (strncmp(title, home, homelen) == 0) {
114 title += homelen;
115 win_set(titlewin, "~", 0);
116 waddstr(titlewin, title);
117 } else {
118 win_set(titlewin, title, 0);
119 }
120}
121
122void ui_status_info(const char * status) {
123 win_set(statuswin, status, 0);
124}
125
126void ui_status_error(const char * status) {
127 win_set(statuswin, status, COLOR_PAIR(RED));
128}
129
130void ui_erase(void) {
131 werase(filewin);
132 werase(inputwin);
133}
134
135void ui_refresh(void) {
136 wnoutrefresh(titlewin);
137 wnoutrefresh(statuswin);
138 wnoutrefresh(filewin);
139 wnoutrefresh(inputwin);
140 doupdate();
141}
142
143static void ui_wlpadstr(WINDOW * win, const char * s, size_t len) {
144 size_t slen = strlen(s);
145
146 for (size_t i = slen; i < len; i++) {
147 waddch(win, ' ');
148 }
149
150 waddstr(win, s);
151}
152
153static void ui_wrpadstr(WINDOW * win, const char * s, size_t len) {
154 size_t slen = strlen(s);
155
156 waddstr(win, s);
157
158 for (size_t i = slen; i < len; i++) {
159 waddch(win, ' ');
160 }
161}
162
163void ui_print_dirent(const dirent_t * de, size_t pos, bool selected, const dir_t * dir) {
164 static const enum ui_color mode_colors[] = {
165 ['r'] = RED, ['w'] = MAGENTA, ['x'] = COLOR_EXEC,
166 ['s'] = YELLOW, ['S'] = YELLOW, ['t'] = RED,
167 ['T'] = RED, ['-'] = WHITE, ['?'] = RED,
168
169 ['.'] = WHITE,
170 ['d'] = COLOR_DIR, ['b'] = COLOR_BLOCK, ['c'] = COLOR_CHAR,
171 ['l'] = COLOR_LINK, ['|'] = COLOR_FIFO, ['='] = COLOR_SOCKET,
172 };
173
174 static char stime[128];
175 static char isbuf[40]; // should be big enough to hold any integer
176
177 size_t lines, cols;
178 getmaxyx(filewin, lines, cols);
179
180 size_t dirlen = dir_len(dir);
181
182 if (config.longmode && (config.longinline || (!config.longinline && selected))) {
183 if (config.longinline) wmove(filewin, pos, 0);
184 else {
185 // place the longmode info right on top of the "cursor" info
186 if (dirlen < lines - 3) wmove(filewin, dirlen + 1, 0);
187 else wmove(filewin, lines - 2, 0);
188 }
189
190 const char * pmode = dirent_prettymode(de);
191 for (const char * p = pmode; *p; p++) {
192 int color = COLOR_PAIR(mode_colors[(unsigned)*p]);
193 if (*p == '-' || *p == '.') color |= A_DIM;
194 wattron(filewin, color);
195 waddch(filewin, *p);
196 wattroff(filewin, color);
197 }
198
199 if (de->uname) ui_wlpadstr(filewin, de->uname, dir->longest_uname + 1);
200 else waddstr(filewin, " ?");
201
202 if (de->gname) ui_wlpadstr(filewin, de->gname, dir->longest_gname + 1);
203 else waddstr(filewin, " ?");
204
205 if (config.humansize) {
206 const char * unit = dirent_size_unit(de);
207 snprintf(isbuf, sizeof(isbuf), "%u", de->size_small);
208 ui_wlpadstr(filewin, isbuf, dir->longest_size_small + 1);
209 ui_wrpadstr(filewin, unit, dir->longest_size_unit);
210 } else {
211 snprintf(isbuf, sizeof(isbuf), "%zu", de->size);
212 ui_wlpadstr(filewin, isbuf, dir->longest_size + 1);
213 }
214
215 strftime(stime, sizeof(stime), "%b %d %H:%M %Y", localtime(&de->mtime));
216 wprintw(filewin, " %s ", stime);
217 }
218
219 if (!config.longmode || (config.longmode && !config.longinline))
220 wmove(filewin, pos, 0);
221
222 int color = COLOR_PAIR(ui_dirent_color(de));
223 if (selected) color |= A_REVERSE;
224 if (de->marked) waddch(filewin, '+');
225 else waddch(filewin, ' ');
226 wattron(filewin, color);
227 waddstr(filewin, de->name);
228 wattroff(filewin, color);
229 char c = dirent_crepr(de);
230 if (c) waddch(filewin, c);
231
232 if (de->type == DE_LINK) {
233 waddstr(filewin, " -> ");
234 color = COLOR_PAIR(ui_link_color(de));
235 if (selected) color |= A_REVERSE;
236 wattron(filewin, color);
237 waddstr(filewin, de->linkname);
238 wattroff(filewin, color);
239 char c = dirent_creprl(de);
240 if (c) waddch(filewin, c);
241 }
242}
243
244static int ui_local_bsearch_str_compar(const void * a, const void * b) {
245 return strcasecmp(*(const char**)a, *(const char**)b);
246}
247
248static int ui_extension_color(const char * name) {
249 static const char * const mediaexts[] = {
250 "3g2", "3gp", "ai", "amv", "asf", "avi",
251 "bmp", "drc", "eps", "f4p", "f4v", "flv",
252 "gif", "gifv", "h264", "heif", "ico", "jpeg",
253 "jpg", "m2ts", "m2v", "m4v", "mkv", "mng",
254 "mov", "mp4", "mpeg", "mpg", "mts", "mxf",
255 "nsv", "ogv", "pbm", "pgm", "png", "pnm",
256 "ppm", "ps", "psd", "qt", "rm", "rmvb",
257 "roq", "svg", "svi", "tif", "tiff", "ts",
258 "viv", "vob", "webm", "webp", "wmv", "yuv",
259 };
260 static const char * const archiveexts[] = {
261 "1", "7z", "7zip", "a", "alz", "bz2",
262 "cbr", "cpgz", "cso", "dar", "dbz", "deb",
263 "dz", "ear", "gip", "gz", "htmlz", "igz",
264 "ipa", "lz", "lz4", "maff", "mpq", "npk",
265 "nxz", "pkg", "pup", "pz", "pzip", "rar",
266 "rpa", "rpm", "sar", "shar", "sis", "sisx",
267 "tar", "tgz", "txz", "uha", "vsix", "xap",
268 "xz", "xzm", "xzn", "z", "zab", "zi",
269 "zip", "zlib", "zst", "zstd", "zxp", "zz",
270 };
271 // uncomment this check when changing the extensions arrays
272// static bool check_sorted = false;
273// // the file extensions must be sorted
274// if (!check_sorted) {
275// for (unsigned i = 1; i < ARRLEN(mediaexts); i++) {
276// if (strcmp(mediaexts[i - 1], mediaexts[i]) > 0) {
277// ui_deinit();
278// puts("Error: ui_dirent_extension_color: Media extensions not sorted");
279// exit(1);
280// }
281// }
282// for (unsigned i = 1; i < ARRLEN(archiveexts); i++) {
283// if (strcmp(archiveexts[i - 1], archiveexts[i]) > 0) {
284// ui_deinit();
285// puts("Error: ui_dirent_extension_color: Archive extensions not sorted");
286// exit(1);
287// }
288// }
289// check_sorted = true;
290// }
291
292 const char * ext = strrchr(name, '.');
293 if (!ext) return COLOR_FILE;
294 if (!*(++ext)) return COLOR_FILE;
295
296 const char * match = bsearch(
297 &ext,
298 mediaexts,
299 ARRLEN(mediaexts),
300 sizeof(mediaexts[0]),
301 ui_local_bsearch_str_compar
302 );
303 if (match) return MAGENTA;
304
305 match = bsearch(
306 &ext,
307 archiveexts,
308 ARRLEN(archiveexts),
309 sizeof(archiveexts[0]),
310 ui_local_bsearch_str_compar
311 );
312 if (match) return RED;
313
314 return COLOR_FILE;
315}
316
317static int ui_dirent_color(const dirent_t * de) {
318 int color;
319 switch (de->type) {
320 case DE_FILE: color = COLOR_FILE; break;
321 case DE_DIR: color = COLOR_DIR; break;
322 case DE_FIFO: color = COLOR_FIFO; break;
323 case DE_LINK: color = COLOR_LINK; break;
324 case DE_BLOCK: color = COLOR_BLOCK; break;
325 case DE_CHAR: color = COLOR_CHAR; break;
326 case DE_SOCKET: color = COLOR_SOCKET; break;
327 case DE_UNKNOWN: color = COLOR_UNKNOWN; break;
328 default: color = WHITE; break;
329 }
330
331 if (de->type == DE_FILE && dirent_isexec(de)) {
332 color = COLOR_EXEC;
333 }
334
335 if (color == COLOR_FILE) color = ui_extension_color(de->name);
336
337 return color;
338}
339
340static int ui_link_color(const dirent_t * de) {
341 switch (de->linktype) {
342 case DE_FILE: return ui_extension_color(de->linkname);
343 case DE_DIR: return COLOR_DIR;
344 case DE_FIFO: return COLOR_FIFO;
345 case DE_LINK: return COLOR_LINK;
346 case DE_BLOCK: return COLOR_BLOCK;
347 case DE_CHAR: return COLOR_CHAR;
348 case DE_SOCKET: return COLOR_SOCKET;
349 case DE_UNKNOWN: return COLOR_UNKNOWN;
350 default: return WHITE;
351 }
352}
353
354void ui_print_dir(const dir_t * dir, size_t cursor) {
355 size_t first;
356 size_t last;
357 size_t len = dir_len(dir);
358 ui_get_first_last(len, cursor, &first, &last);
359
360 for (size_t i = first; i < last; i++) {
361 size_t pos = i - first;
362 dirent_t * de = dir_get_entry(dir, i);
363 ui_print_dirent(de, pos, cursor == i, dir);
364 }
365}
366
367void ui_print_cursor(size_t cursor, size_t total, enum prog_mode mode, size_t total_marked) {
368 size_t lines, cols;
369 getmaxyx(filewin, lines, cols);
370 if (config.longmode && !config.longinline) {
371 if (total < lines - 3) wmove(filewin, total + 2, 0);
372 else wmove(filewin, lines - 1, 0);
373 } else {
374 if (total < lines - 2) wmove(filewin, total + 1, 0);
375 else wmove(filewin, lines - 1, 0);
376 }
377
378 if (total > 0) {
379 wprintw(filewin, "%zu/%zu", cursor + 1, total);
380 } else {
381 waddstr(filewin, "0/0");
382 }
383
384 if (total_marked > 0) switch (mode) {
385 case MODE_NORMAL:
386 wprintw(filewin, " | %zu Marked", total_marked);
387 break;
388 case MODE_CUT:
389 wprintw(filewin, " | Cutting %zu", total_marked);
390 break;
391 default: break;
392 }
393}
394
395static void ui_get_first_last(size_t dir_len, size_t cursor, size_t * first, size_t * last) {
396 size_t lines, cols;
397 getmaxyx(filewin, lines, cols);
398 lines -= 2;
399 if (config.longmode && !config.longinline) lines--;
400 // try to keep cursor in the middle of the window
401 if (dir_len < lines) {
402 *first = 0;
403 *last = dir_len;
404 } else {
405 size_t off_lo = lines / 2;
406 size_t off_hi = lines - off_lo;
407 if (cursor < off_lo) {
408 *first = 0;
409 if (dir_len > lines) *last = lines;
410 else *last = dir_len;
411 } else {
412 *first = cursor - off_lo;
413 if (dir_len > cursor + off_hi) *last = cursor + off_hi;
414 else *last = dir_len;
415 }
416 }
417}
418
419void ui_resize(void) {
420 // unless a WINCH handler is specified, ncurses resizes the terminal
421 // automatically, so this function just resizes the windows
422 wresize(titlewin, TITLEWINLINES, TITLEWINCOLS);
423 wresize(statuswin, STATUSWINLINES, STATUSWINCOLS);
424 wresize(filewin, FILEWINLINES, FILEWINCOLS);
425 wresize(inputwin, INPUTWINLINES, INPUTWINCOLS);
426
427 mvwin(titlewin, TITLEWINSTARTY, 0);
428 mvwin(statuswin, STATUSWINSTARTY, 0);
429 mvwin(filewin, FILEWINSTARTY, 0);
430 mvwin(inputwin, INPUTWINSTARTY, 0);
431
432 ui_deinit();
433 ui_reinit();
434}
435
436const char * ui_readline(const char * prompt) {
437 static char inputbuf[INPUTBUFSZ];
438
439 char * p = inputbuf;
440 *p = 0;
441
442 werase(inputwin);
443 waddstr(inputwin, prompt);
444 wattron(inputwin, A_REVERSE);
445 waddch(inputwin, ' ');
446 wattroff(inputwin, A_REVERSE);
447 wrefresh(inputwin);
448
449 int c;
450 while ((c = wgetch(inputwin)) != '\n') {
451 if (c == KEY_ESC) {
452 curs_set(0);
453 return NULL;
454 }
455
456 if (c == KEY_DEL || c == KEY_BACKSPACE) {
457 if (p != inputbuf) *--p = 0;
458 else return NULL;
459 } else if (isprint(c)) {
460 if (p - inputbuf < INPUTBUFSZ - 1) {
461 *p++ = (char)c;
462 *p = 0;
463 }
464 }
465
466 werase(inputwin);
467 waddstr(inputwin, prompt);
468 waddstr(inputwin, inputbuf);
469 wattron(inputwin, A_REVERSE);
470 waddch(inputwin, ' ');
471 wattroff(inputwin, A_REVERSE);
472 wrefresh(inputwin);
473 }
474
475 if (p == inputbuf) return NULL;
476 return inputbuf;
477}
478
479const char * ui_prompt(const char * prompt, prompt_opts_t opts) {
480 size_t n_opts = 0;
481 for (const char * const * p = opts; *p; p++) {
482 n_opts++;
483 }
484
485 size_t prompt_len = strlen(prompt);
486 // find width of options, assuming they all fit on one line
487 // there are n_opts - 1 spaces in total between the options
488 size_t opts_len = n_opts - 1;
489 for (size_t i = 0; i < n_opts; i++) {
490 opts_len += strlen(opts[i]);
491 }
492
493 /* top mid btm prompt opts */
494 size_t oglines = 1 + 1 + 1 + 1 + 1;
495 /* border left right */
496 size_t ogcols = 2 + 1 + 1 + MAX(prompt_len, opts_len);
497 size_t lines, cols;
498
499 lines = MIN(oglines, (unsigned)PROMPTWINMAXLINES);
500 cols = MIN(ogcols, (unsigned)PROMPTWINMAXCOLS);
501
502 WINDOW * promptwin = newwin(
503 lines,
504 cols,
505 PROMPTWINSTARTY - lines / 2,
506 PROMPTWINSTARTX - cols / 2
507 );
508
509 keypad(promptwin, TRUE);
510 wrefresh(promptwin);
511
512 size_t cursor = 0;
513
514 bool done = false;
515 const char * ret = NULL;
516 while (!done) {
517 werase(promptwin);
518 box(promptwin, 0, 0);
519
520 wmove(promptwin, 1, cols / 2 - prompt_len / 2);
521 waddstr(promptwin, prompt);
522
523 wmove(promptwin, 3, cols / 2 - opts_len / 2);
524 for (size_t i = 0; i < n_opts; i++) {
525 int attr = 0;
526 if (cursor == i) attr = A_REVERSE;
527 wattron(promptwin, attr);
528 waddstr(promptwin, opts[i]);
529 wattroff(promptwin, attr);
530 if (i < n_opts - 1) waddch(promptwin, ' ');
531 }
532 wrefresh(promptwin);
533
534 int c = wgetch(promptwin);
535 if (n_opts == 0) {
536 ret = NULL;
537 done = true;
538 } else switch (c) {
539 LEFT_KEYS:
540 if (cursor > 0) cursor--;
541 break;
542 RIGHT_KEYS:
543 if (cursor < n_opts - 1) cursor++;
544 break;
545 case '\n':
546 case KEY_ENTER:
547 ret = opts[cursor];
548 done = true;
549 break;
550 case KEY_RESIZE:
551 ui_resize();
552
553 lines = MIN(oglines, (unsigned)PROMPTWINMAXLINES);
554 cols = MIN(ogcols, (unsigned)PROMPTWINMAXCOLS);
555
556 wresize(promptwin, lines, cols);
557 mvwin(promptwin,
558 PROMPTWINSTARTY - lines / 2,
559 PROMPTWINSTARTX - cols / 2
560 );
561 wrefresh(promptwin);
562 break;
563 case 'q':
564 case KEY_ESC:
565 ret = NULL;
566 done = true;
567 break;
568 }
569 }
570
571 delwin(promptwin);
572 return ret;
573}
574
575static bool ui_internal_prompt_dirent_deletion(const dirent_t * de) {
576 static const char * no = "No";
577 static const char * yes = "Yes";
578
579 size_t nfiles = 0;
580 if (de->type == DE_DIR) {
581 ui_status_info("listing...");
582 ui_refresh();
583 nfiles = dirent_subfiles(de);
584 ui_status_info("");
585 ui_refresh();
586 }
587
588 char * prompt;
589 if (nfiles != 0) {
590 char * fstr = "Delete '%s' and all %zu files inside it?";
591 size_t len = snprintf(NULL, 0, fstr, de->name, nfiles);
592 prompt = malloc(len + 1);
593 sprintf(prompt, fstr, de->name, nfiles);
594 } else {
595 char * fstr = "Delete '%s'?";
596 size_t len = snprintf(NULL, 0, fstr, de->name);
597 prompt = malloc(len + 1);
598 sprintf(prompt, fstr, de->name);
599 }
600
601 const char * ret = ui_prompt(prompt, (prompt_opts_t){no, yes, NULL});
602 free(prompt);
603 if (ret != yes) return false;
604
605 if (nfiles != 0) {
606 ret = ui_prompt("This action cannot be undone. Proceed?", (prompt_opts_t){no, yes, NULL});
607 if (ret != yes) return false;
608 }
609
610 return true;
611}
612
613static bool ui_internal_prompt_marked_deletion(size_t total_marked) {
614 static const char * no = "No";
615 static const char * yes = "Yes";
616
617 ui_status_info("listing...");
618 ui_refresh();
619 size_t subfiles = dir_marked_subfiles();
620 ui_status_info("");
621 ui_refresh();
622
623 char * prompt;
624 if (subfiles == 0) {
625 const char * fstr = "Delete %zu marked files?";
626 size_t len = snprintf(NULL, 0, fstr, total_marked);
627 prompt = malloc(len + 1);
628 sprintf(prompt, fstr, total_marked);
629 } else {
630 const char * fstr = "Delete %zu marked files and %zu subfiles?";
631 size_t len = snprintf(NULL, 0, fstr, total_marked, subfiles);
632 prompt = malloc(len + 1);
633 sprintf(prompt, fstr, total_marked, subfiles);
634 }
635
636 const char * ret = ui_prompt(prompt, (prompt_opts_t){no, yes, NULL});
637 free(prompt);
638 if (ret != yes) return false;
639
640 ret = ui_prompt("This action cannot be undone. Proceed?", (prompt_opts_t){no, yes, NULL});
641 if (ret != yes) return false;
642 return true;
643}
644
645bool ui_prompt_deletion(const dirent_t * de, size_t total_marked) {
646 if (total_marked > 0)
647 return ui_internal_prompt_marked_deletion(total_marked);
648 else
649 return ui_internal_prompt_dirent_deletion(de);
650}
651
652bool ui_prompt_paste(size_t total_marked) {
653 static const char * no = "No";
654 static const char * yes = "Yes";
655 static const char * fstr = "Paste %zu files into this directory?";
656
657 size_t len = snprintf(NULL, 0, fstr, total_marked);
658 char * prompt = malloc(len + 1);
659 sprintf(prompt, fstr, total_marked);
660
661 const char * ret = ui_prompt(prompt, (prompt_opts_t){no, yes, NULL});
662 free(prompt);
663 if (ret != yes) return false;
664 return true;
665}
666