Blob


1 #define _XOPEN_SOURCE 700
2 #define _XOPEN_SOURCE_EXTENDED
3 #define _FILE_OFFSET_BITS 64
5 #include <stdlib.h>
6 #include <stdint.h>
7 #include <ctype.h>
8 #include <wchar.h>
9 #include <wctype.h>
10 #include <string.h>
11 #include <sys/types.h> /* pid_t, ... */
12 #include <stdio.h>
13 #include <limits.h> /* PATH_MAX */
14 #include <locale.h> /* setlocale(), LC_ALL */
15 #include <unistd.h> /* chdir(), getcwd(), read(), close(), ... */
16 #include <dirent.h> /* DIR, struct dirent, opendir(), ... */
17 #include <libgen.h>
18 #include <sys/stat.h>
19 #include <fcntl.h> /* open() */
20 #include <sys/wait.h> /* waitpid() */
21 #include <signal.h> /* struct sigaction, sigaction() */
22 #include <errno.h>
23 #include <stdarg.h>
24 #include <curses.h>
26 #include "config.h"
28 /* This signal is not defined by POSIX, but should be
29 present on all systems that have resizable terminals. */
30 #ifndef SIGWINCH
31 #define SIGWINCH 28
32 #endif
34 /* String buffers. */
35 #define BUFLEN PATH_MAX
36 static char BUF1[BUFLEN];
37 static char BUF2[BUFLEN];
38 static char INPUT[BUFLEN];
39 static wchar_t WBUF[BUFLEN];
41 /* Listing view parameters. */
42 #define HEIGHT (LINES-4)
43 #define STATUSPOS (COLS-16)
45 /* Listing view flags. */
46 #define SHOW_FILES 0x01u
47 #define SHOW_DIRS 0x02u
48 #define SHOW_HIDDEN 0x04u
50 /* Marks parameters. */
51 #define BULK_INIT 5
52 #define BULK_THRESH 256
54 /* Information associated to each entry in listing. */
55 typedef struct Row {
56 char *name;
57 off_t size;
58 mode_t mode;
59 int islink;
60 int marked;
61 } Row;
63 /* Dynamic array of marked entries. */
64 typedef struct Marks {
65 char dirpath[PATH_MAX];
66 int bulk;
67 int nentries;
68 char **entries;
69 } Marks;
71 /* Line editing state. */
72 typedef struct Edit {
73 wchar_t buffer[BUFLEN+1];
74 int left, right;
75 } Edit;
77 /* Each tab only stores the following information. */
78 typedef struct Tab {
79 int scroll;
80 int esel;
81 uint8_t flags;
82 char cwd[PATH_MAX];
83 } Tab;
85 typedef struct Prog {
86 off_t partial;
87 off_t total;
88 const char *msg;
89 } Prog;
91 /* Global state. */
92 static struct Rover {
93 int tab;
94 int nfiles;
95 Row *rows;
96 WINDOW *window;
97 Marks marks;
98 Edit edit;
99 int edit_scroll;
100 volatile sig_atomic_t pending_winch;
101 Prog prog;
102 Tab tabs[10];
103 } rover;
105 /* Macros for accessing global state. */
106 #define ENAME(I) rover.rows[I].name
107 #define ESIZE(I) rover.rows[I].size
108 #define EMODE(I) rover.rows[I].mode
109 #define ISLINK(I) rover.rows[I].islink
110 #define MARKED(I) rover.rows[I].marked
111 #define SCROLL rover.tabs[rover.tab].scroll
112 #define ESEL rover.tabs[rover.tab].esel
113 #define FLAGS rover.tabs[rover.tab].flags
114 #define CWD rover.tabs[rover.tab].cwd
116 /* Helpers. */
117 #define MIN(A, B) ((A) < (B) ? (A) : (B))
118 #define MAX(A, B) ((A) > (B) ? (A) : (B))
119 #define ISDIR(E) (strchr((E), '/') != NULL)
121 /* Line Editing Macros. */
122 #define EDIT_FULL(E) ((E).left == (E).right)
123 #define EDIT_CAN_LEFT(E) ((E).left)
124 #define EDIT_CAN_RIGHT(E) ((E).right < BUFLEN-1)
125 #define EDIT_LEFT(E) (E).buffer[(E).right--] = (E).buffer[--(E).left]
126 #define EDIT_RIGHT(E) (E).buffer[(E).left++] = (E).buffer[++(E).right]
127 #define EDIT_INSERT(E, C) (E).buffer[(E).left++] = (C)
128 #define EDIT_BACKSPACE(E) (E).left--
129 #define EDIT_DELETE(E) (E).right++
130 #define EDIT_CLEAR(E) do { (E).left = 0; (E).right = BUFLEN-1; } while(0)
132 typedef enum EditStat {CONTINUE, CONFIRM, CANCEL} EditStat;
133 typedef enum Color {DEFAULT, RED, GREEN, YELLOW, BLUE, CYAN, MAGENTA, WHITE, BLACK} Color;
134 typedef int (*PROCESS)(const char *path);
136 static void
137 init_marks(Marks *marks)
139 strcpy(marks->dirpath, "");
140 marks->bulk = BULK_INIT;
141 marks->nentries = 0;
142 marks->entries = calloc(marks->bulk, sizeof *marks->entries);
145 /* Unmark all entries. */
146 static void
147 mark_none(Marks *marks)
149 int i;
151 strcpy(marks->dirpath, "");
152 for (i = 0; i < marks->bulk && marks->nentries; i++)
153 if (marks->entries[i]) {
154 free(marks->entries[i]);
155 marks->entries[i] = NULL;
156 marks->nentries--;
158 if (marks->bulk > BULK_THRESH) {
159 /* Reset bulk to free some memory. */
160 free(marks->entries);
161 marks->bulk = BULK_INIT;
162 marks->entries = calloc(marks->bulk, sizeof *marks->entries);
166 static void
167 add_mark(Marks *marks, char *dirpath, char *entry)
169 int i;
171 if (!strcmp(marks->dirpath, dirpath)) {
172 /* Append mark to directory. */
173 if (marks->nentries == marks->bulk) {
174 /* Expand bulk to accomodate new entry. */
175 int extra = marks->bulk / 2;
176 marks->bulk += extra; /* bulk *= 1.5; */
177 marks->entries = realloc(marks->entries,
178 marks->bulk * sizeof *marks->entries);
179 memset(&marks->entries[marks->nentries], 0,
180 extra * sizeof *marks->entries);
181 i = marks->nentries;
182 } else {
183 /* Search for empty slot (there must be one). */
184 for (i = 0; i < marks->bulk; i++)
185 if (!marks->entries[i])
186 break;
188 } else {
189 /* Directory changed. Discard old marks. */
190 mark_none(marks);
191 strcpy(marks->dirpath, dirpath);
192 i = 0;
194 marks->entries[i] = malloc(strlen(entry) + 1);
195 strcpy(marks->entries[i], entry);
196 marks->nentries++;
199 static void
200 del_mark(Marks *marks, char *entry)
202 int i;
204 if (marks->nentries > 1) {
205 for (i = 0; i < marks->bulk; i++)
206 if (marks->entries[i] && !strcmp(marks->entries[i], entry))
207 break;
208 free(marks->entries[i]);
209 marks->entries[i] = NULL;
210 marks->nentries--;
211 } else
212 mark_none(marks);
215 static void
216 free_marks(Marks *marks)
218 int i;
220 for (i = 0; i < marks->bulk && marks->nentries; i++)
221 if (marks->entries[i]) {
222 free(marks->entries[i]);
223 marks->nentries--;
225 free(marks->entries);
228 static void
229 handle_winch(int sig)
231 rover.pending_winch = 1;
234 static void
235 enable_handlers()
237 struct sigaction sa;
239 memset(&sa, 0, sizeof (struct sigaction));
240 sa.sa_handler = handle_winch;
241 sigaction(SIGWINCH, &sa, NULL);
244 static void
245 disable_handlers()
247 struct sigaction sa;
249 memset(&sa, 0, sizeof (struct sigaction));
250 sa.sa_handler = SIG_DFL;
251 sigaction(SIGWINCH, &sa, NULL);
254 static void update_view();
256 /* Handle any signals received since last call. */
257 static void
258 sync_signals()
260 if (rover.pending_winch) {
261 /* SIGWINCH received: resize application accordingly. */
262 delwin(rover.window);
263 endwin();
264 refresh();
265 clear();
266 rover.window = subwin(stdscr, LINES - 2, COLS, 1, 0);
267 if (HEIGHT < rover.nfiles && SCROLL + HEIGHT > rover.nfiles)
268 SCROLL = ESEL - HEIGHT;
269 update_view();
270 rover.pending_winch = 0;
274 /* This function must be used in place of getch().
275 It handles signals while waiting for user input. */
276 static int
277 rover_getch()
279 int ch;
281 while ((ch = getch()) == ERR)
282 sync_signals();
283 return ch;
286 /* This function must be used in place of get_wch().
287 It handles signals while waiting for user input. */
288 static int
289 rover_get_wch(wint_t *wch)
291 wint_t ret;
293 while ((ret = get_wch(wch)) == (wint_t) ERR)
294 sync_signals();
295 return ret;
298 /* Do a fork-exec to external program (e.g. $EDITOR). */
299 static void
300 spawn(char **args)
302 pid_t pid;
303 int status;
305 setenv("RVSEL", rover.nfiles ? ENAME(ESEL) : "", 1);
306 pid = fork();
307 if (pid > 0) {
308 /* fork() succeeded. */
309 disable_handlers();
310 endwin();
311 waitpid(pid, &status, 0);
312 enable_handlers();
313 kill(getpid(), SIGWINCH);
314 } else if (pid == 0) {
315 /* Child process. */
316 execvp(args[0], args);
320 static void
321 shell_escaped_cat(char *buf, char *str, size_t n)
323 char *p = buf + strlen(buf);
324 *p++ = '\'';
325 for (n--; n; n--, str++) {
326 switch (*str) {
327 case '\'':
328 if (n < 4)
329 goto done;
330 strcpy(p, "'\\''");
331 n -= 4;
332 p += 4;
333 break;
334 case '\0':
335 goto done;
336 default:
337 *p = *str;
338 p++;
341 done:
342 strncat(p, "'", n);
345 static int
346 open_with_env(const char *env, char *path)
348 char *program = getenv(env);
349 if (program) {
350 #ifdef RV_SHELL
351 strncpy(BUF1, program, BUFLEN - 1);
352 strncat(BUF1, " ", BUFLEN - strlen(program) - 1);
353 shell_escaped_cat(BUF1, path, BUFLEN - strlen(program) - 2);
354 spawn((char *[]) {RV_SHELL, "-c", BUF1, NULL});
355 #else
356 spawn((char *[]) {program, path, NULL});
357 #endif
358 return 1;
360 return 0;
363 /* Curses setup. */
364 static void
365 init_term()
367 setlocale(LC_ALL, "");
368 initscr();
369 cbreak(); /* Get one character at a time. */
370 timeout(100); /* For getch(). */
371 noecho();
372 nonl(); /* No NL->CR/NL on output. */
373 intrflush(stdscr, FALSE);
374 keypad(stdscr, TRUE);
375 curs_set(FALSE); /* Hide blinking cursor. */
376 if (has_colors()) {
377 short bg;
378 start_color();
379 #ifdef NCURSES_EXT_FUNCS
380 use_default_colors();
381 bg = -1;
382 #else
383 bg = COLOR_BLACK;
384 #endif
385 init_pair(RED, COLOR_RED, bg);
386 init_pair(GREEN, COLOR_GREEN, bg);
387 init_pair(YELLOW, COLOR_YELLOW, bg);
388 init_pair(BLUE, COLOR_BLUE, bg);
389 init_pair(CYAN, COLOR_CYAN, bg);
390 init_pair(MAGENTA, COLOR_MAGENTA, bg);
391 init_pair(WHITE, COLOR_WHITE, bg);
392 init_pair(BLACK, COLOR_BLACK, bg);
394 atexit((void (*)(void)) endwin);
395 enable_handlers();
398 /* Update the listing view. */
399 static void
400 update_view()
402 int i, j;
403 int numsize;
404 int ishidden;
405 int marking;
407 mvhline(0, 0, ' ', COLS);
408 attr_on(A_BOLD, NULL);
409 color_set(RVC_TABNUM, NULL);
410 mvaddch(0, COLS - 2, rover.tab + '0');
411 attr_off(A_BOLD, NULL);
412 if (rover.marks.nentries) {
413 numsize = snprintf(BUF1, BUFLEN, "%d", rover.marks.nentries);
414 color_set(RVC_MARKS, NULL);
415 mvaddstr(0, COLS - 3 - numsize, BUF1);
416 } else
417 numsize = -1;
418 color_set(RVC_CWD, NULL);
419 mbstowcs(WBUF, CWD, PATH_MAX);
420 mvaddnwstr(0, 0, WBUF, COLS - 4 - numsize);
421 wcolor_set(rover.window, RVC_BORDER, NULL);
422 wborder(rover.window, 0, 0, 0, 0, 0, 0, 0, 0);
423 ESEL = MAX(MIN(ESEL, rover.nfiles - 1), 0);
424 /* Selection might not be visible, due to cursor wrapping or window
425 shrinking. In that case, the scroll must be moved to make it visible. */
426 if (rover.nfiles > HEIGHT) {
427 SCROLL = MAX(MIN(SCROLL, ESEL), ESEL - HEIGHT + 1);
428 SCROLL = MIN(MAX(SCROLL, 0), rover.nfiles - HEIGHT);
429 } else
430 SCROLL = 0;
431 marking = !strcmp(CWD, rover.marks.dirpath);
432 for (i = 0, j = SCROLL; i < HEIGHT && j < rover.nfiles; i++, j++) {
433 ishidden = ENAME(j)[0] == '.';
434 if (j == ESEL)
435 wattr_on(rover.window, A_REVERSE, NULL);
436 if (ISLINK(j))
437 wcolor_set(rover.window, RVC_LINK, NULL);
438 else if (ishidden)
439 wcolor_set(rover.window, RVC_HIDDEN, NULL);
440 else if (S_ISREG(EMODE(j))) {
441 if (EMODE(j) & (S_IXUSR | S_IXGRP | S_IXOTH))
442 wcolor_set(rover.window, RVC_EXEC, NULL);
443 else
444 wcolor_set(rover.window, RVC_REG, NULL);
445 } else if (S_ISDIR(EMODE(j)))
446 wcolor_set(rover.window, RVC_DIR, NULL);
447 else if (S_ISCHR(EMODE(j)))
448 wcolor_set(rover.window, RVC_CHR, NULL);
449 else if (S_ISBLK(EMODE(j)))
450 wcolor_set(rover.window, RVC_BLK, NULL);
451 else if (S_ISFIFO(EMODE(j)))
452 wcolor_set(rover.window, RVC_FIFO, NULL);
453 else if (S_ISSOCK(EMODE(j)))
454 wcolor_set(rover.window, RVC_SOCK, NULL);
455 if (S_ISDIR(EMODE(j))) {
456 mbstowcs(WBUF, ENAME(j), PATH_MAX);
457 if (ISLINK(j))
458 wcscat(WBUF, L"/");
459 } else {
460 char *suffix, *suffixes = "BKMGTPEZY";
461 off_t human_size = ESIZE(j) * 10;
462 int length = mbstowcs(NULL, ENAME(j), 0);
463 for (suffix = suffixes; human_size >= 10240; suffix++)
464 human_size = (human_size + 512) / 1024;
465 if (*suffix == 'B')
466 swprintf(WBUF, PATH_MAX, L"%s%*d %c", ENAME(j),
467 (int) (COLS - length - 6),
468 (int) human_size / 10, *suffix);
469 else
470 swprintf(WBUF, PATH_MAX, L"%s%*d.%d %c", ENAME(j),
471 (int) (COLS - length - 8),
472 (int) human_size / 10, (int) human_size % 10, *suffix);
474 mvwhline(rover.window, i + 1, 1, ' ', COLS - 2);
475 mvwaddnwstr(rover.window, i + 1, 2, WBUF, COLS - 4);
476 if (marking && MARKED(j)) {
477 wcolor_set(rover.window, RVC_MARKS, NULL);
478 mvwaddch(rover.window, i + 1, 1, RVS_MARK);
479 } else
480 mvwaddch(rover.window, i + 1, 1, ' ');
481 if (j == ESEL)
482 wattr_off(rover.window, A_REVERSE, NULL);
484 for (; i < HEIGHT; i++)
485 mvwhline(rover.window, i + 1, 1, ' ', COLS - 2);
486 if (rover.nfiles > HEIGHT) {
487 int center, height;
488 center = (SCROLL + HEIGHT / 2) * HEIGHT / rover.nfiles;
489 height = (HEIGHT-1) * HEIGHT / rover.nfiles;
490 if (!height) height = 1;
491 wcolor_set(rover.window, RVC_SCROLLBAR, NULL);
492 mvwvline(rover.window, center-height/2+1, COLS-1, RVS_SCROLLBAR, height);
494 BUF1[0] = FLAGS & SHOW_FILES ? 'F' : ' ';
495 BUF1[1] = FLAGS & SHOW_DIRS ? 'D' : ' ';
496 BUF1[2] = FLAGS & SHOW_HIDDEN ? 'H' : ' ';
497 if (!rover.nfiles)
498 strcpy(BUF2, "0/0");
499 else
500 snprintf(BUF2, BUFLEN, "%d/%d", ESEL + 1, rover.nfiles);
501 snprintf(BUF1+3, BUFLEN-3, "%12s", BUF2);
502 color_set(RVC_STATUS, NULL);
503 mvaddstr(LINES - 1, STATUSPOS, BUF1);
504 wrefresh(rover.window);
507 /* Show a message on the status bar. */
508 static void
509 message(Color color, char *fmt, ...)
511 int len, pos;
512 va_list args;
514 va_start(args, fmt);
515 vsnprintf(BUF1, MIN(BUFLEN, STATUSPOS), fmt, args);
516 va_end(args);
517 len = strlen(BUF1);
518 pos = (STATUSPOS - len) / 2;
519 attr_on(A_BOLD, NULL);
520 color_set(color, NULL);
521 mvaddstr(LINES - 1, pos, BUF1);
522 color_set(DEFAULT, NULL);
523 attr_off(A_BOLD, NULL);
526 /* Clear message area, leaving only status info. */
527 static void
528 clear_message()
530 mvhline(LINES - 1, 0, ' ', STATUSPOS);
533 /* Comparison used to sort listing entries. */
534 static int
535 rowcmp(const void *a, const void *b)
537 int isdir1, isdir2, cmpdir;
538 const Row *r1 = a;
539 const Row *r2 = b;
540 isdir1 = S_ISDIR(r1->mode);
541 isdir2 = S_ISDIR(r2->mode);
542 cmpdir = isdir2 - isdir1;
543 return cmpdir ? cmpdir : strcoll(r1->name, r2->name);
546 /* Get all entries in current working directory. */
547 static int
548 ls(Row **rowsp, uint8_t flags)
550 DIR *dp;
551 struct dirent *ep;
552 struct stat statbuf;
553 Row *rows;
554 int i, n;
556 if(!(dp = opendir("."))) return -1;
557 n = -2; /* We don't want the entries "." and "..". */
558 while (readdir(dp)) n++;
559 rewinddir(dp);
560 rows = malloc(n * sizeof *rows);
561 i = 0;
562 while ((ep = readdir(dp))) {
563 if (!strcmp(ep->d_name, ".") || !strcmp(ep->d_name, ".."))
564 continue;
565 if (!(flags & SHOW_HIDDEN) && ep->d_name[0] == '.')
566 continue;
567 lstat(ep->d_name, &statbuf);
568 rows[i].islink = S_ISLNK(statbuf.st_mode);
569 stat(ep->d_name, &statbuf);
570 if (S_ISDIR(statbuf.st_mode)) {
571 if (flags & SHOW_DIRS) {
572 rows[i].name = malloc(strlen(ep->d_name) + 2);
573 strcpy(rows[i].name, ep->d_name);
574 if (!rows[i].islink)
575 strcat(rows[i].name, "/");
576 rows[i].mode = statbuf.st_mode;
577 i++;
579 } else if (flags & SHOW_FILES) {
580 rows[i].name = malloc(strlen(ep->d_name) + 1);
581 strcpy(rows[i].name, ep->d_name);
582 rows[i].size = statbuf.st_size;
583 rows[i].mode = statbuf.st_mode;
584 i++;
587 n = i; /* Ignore unused space in array caused by filters. */
588 qsort(rows, n, sizeof (*rows), rowcmp);
589 closedir(dp);
590 *rowsp = rows;
591 return n;
594 static void
595 free_rows(Row **rowsp, int nfiles)
597 int i;
599 for (i = 0; i < nfiles; i++)
600 free((*rowsp)[i].name);
601 free(*rowsp);
602 *rowsp = NULL;
605 /* Change working directory to the path in CWD. */
606 static void
607 cd(int reset)
609 int i, j;
611 message(CYAN, "Loading...");
612 refresh();
613 if (reset) ESEL = SCROLL = 0;
614 chdir(CWD);
615 if (rover.nfiles)
616 free_rows(&rover.rows, rover.nfiles);
617 rover.nfiles = ls(&rover.rows, FLAGS);
618 if (!strcmp(CWD, rover.marks.dirpath)) {
619 for (i = 0; i < rover.nfiles; i++) {
620 for (j = 0; j < rover.marks.bulk; j++)
621 if (
622 rover.marks.entries[j] &&
623 !strcmp(rover.marks.entries[j], ENAME(i))
625 break;
626 MARKED(i) = j < rover.marks.bulk;
628 } else
629 for (i = 0; i < rover.nfiles; i++)
630 MARKED(i) = 0;
631 clear_message();
632 update_view();
635 /* Select a target entry, if it is present. */
636 static void
637 try_to_sel(const char *target)
639 ESEL = 0;
640 if (!ISDIR(target))
641 while ((ESEL+1) < rover.nfiles && S_ISDIR(EMODE(ESEL)))
642 ESEL++;
643 while ((ESEL+1) < rover.nfiles && strcoll(ENAME(ESEL), target) < 0)
644 ESEL++;
647 /* Reload CWD, but try to keep selection. */
648 static void
649 reload()
651 if (rover.nfiles) {
652 strcpy(INPUT, ENAME(ESEL));
653 cd(0);
654 try_to_sel(INPUT);
655 update_view();
656 } else
657 cd(1);
660 static off_t
661 count_dir(const char *path)
663 DIR *dp;
664 struct dirent *ep;
665 struct stat statbuf;
666 char subpath[PATH_MAX];
667 off_t total;
669 if(!(dp = opendir(path))) return 0;
670 total = 0;
671 while ((ep = readdir(dp))) {
672 if (!strcmp(ep->d_name, ".") || !strcmp(ep->d_name, ".."))
673 continue;
674 snprintf(subpath, PATH_MAX, "%s%s", path, ep->d_name);
675 lstat(subpath, &statbuf);
676 if (S_ISDIR(statbuf.st_mode)) {
677 strcat(subpath, "/");
678 total += count_dir(subpath);
679 } else
680 total += statbuf.st_size;
682 closedir(dp);
683 return total;
686 static off_t
687 count_marked()
689 int i;
690 char *entry;
691 off_t total;
692 struct stat statbuf;
694 total = 0;
695 chdir(rover.marks.dirpath);
696 for (i = 0; i < rover.marks.bulk; i++) {
697 entry = rover.marks.entries[i];
698 if (entry) {
699 if (ISDIR(entry)) {
700 total += count_dir(entry);
701 } else {
702 lstat(entry, &statbuf);
703 total += statbuf.st_size;
707 chdir(CWD);
708 return total;
711 /* Recursively process a source directory using CWD as destination root.
712 For each node (i.e. directory), do the following:
713 1. call pre(destination);
714 2. call proc() on every child leaf (i.e. files);
715 3. recurse into every child node;
716 4. call pos(source).
717 E.g. to move directory /src/ (and all its contents) inside /dst/:
718 strcpy(CWD, "/dst/");
719 process_dir(adddir, movfile, deldir, "/src/"); */
720 static int
721 process_dir(PROCESS pre, PROCESS proc, PROCESS pos, const char *path)
723 int ret;
724 DIR *dp;
725 struct dirent *ep;
726 struct stat statbuf;
727 char subpath[PATH_MAX];
729 ret = 0;
730 if (pre) {
731 char dstpath[PATH_MAX];
732 strcpy(dstpath, CWD);
733 strcat(dstpath, path + strlen(rover.marks.dirpath));
734 ret |= pre(dstpath);
736 if(!(dp = opendir(path))) return -1;
737 while ((ep = readdir(dp))) {
738 if (!strcmp(ep->d_name, ".") || !strcmp(ep->d_name, ".."))
739 continue;
740 snprintf(subpath, PATH_MAX, "%s%s", path, ep->d_name);
741 lstat(subpath, &statbuf);
742 if (S_ISDIR(statbuf.st_mode)) {
743 strcat(subpath, "/");
744 ret |= process_dir(pre, proc, pos, subpath);
745 } else
746 ret |= proc(subpath);
748 closedir(dp);
749 if (pos) ret |= pos(path);
750 return ret;
753 /* Process all marked entries using CWD as destination root.
754 All marked entries that are directories will be recursively processed.
755 See process_dir() for details on the parameters. */
756 static void
757 process_marked(PROCESS pre, PROCESS proc, PROCESS pos,
758 const char *msg_doing, const char *msg_done)
760 int i, ret;
761 char *entry;
762 char path[PATH_MAX];
764 clear_message();
765 message(CYAN, "%s...", msg_doing);
766 refresh();
767 rover.prog = (Prog) {0, count_marked(), msg_doing};
768 for (i = 0; i < rover.marks.bulk; i++) {
769 entry = rover.marks.entries[i];
770 if (entry) {
771 ret = 0;
772 snprintf(path, PATH_MAX, "%s%s", rover.marks.dirpath, entry);
773 if (ISDIR(entry)) {
774 if (!strncmp(path, CWD, strlen(path)))
775 ret = -1;
776 else
777 ret = process_dir(pre, proc, pos, path);
778 } else
779 ret = proc(path);
780 if (!ret) {
781 del_mark(&rover.marks, entry);
782 reload();
786 rover.prog.total = 0;
787 reload();
788 if (!rover.marks.nentries)
789 message(GREEN, "%s all marked entries.", msg_done);
790 else
791 message(RED, "Some errors occured while %s.", msg_doing);
792 RV_ALERT();
795 static void
796 update_progress(off_t delta)
798 int percent;
800 if (!rover.prog.total) return;
801 rover.prog.partial += delta;
802 percent = (int) (rover.prog.partial * 100 / rover.prog.total);
803 message(CYAN, "%s...%d%%", rover.prog.msg, percent);
804 refresh();
807 /* Wrappers for file operations. */
808 static int delfile(const char *path) {
809 int ret;
810 struct stat st;
812 ret = lstat(path, &st);
813 if (ret < 0) return ret;
814 update_progress(st.st_size);
815 return unlink(path);
817 static PROCESS deldir = rmdir;
818 static int addfile(const char *path) {
819 /* Using creat(2) because mknod(2) doesn't seem to be portable. */
820 int ret;
822 ret = creat(path, 0644);
823 if (ret < 0) return ret;
824 return close(ret);
826 static int cpyfile(const char *srcpath) {
827 int src, dst, ret;
828 size_t size;
829 struct stat st;
830 char buf[BUFSIZ];
831 char dstpath[PATH_MAX];
833 strcpy(dstpath, CWD);
834 strcat(dstpath, srcpath + strlen(rover.marks.dirpath));
835 ret = lstat(srcpath, &st);
836 if (ret < 0) return ret;
837 if (S_ISLNK(st.st_mode)) {
838 ret = readlink(srcpath, BUF1, BUFLEN-1);
839 if (ret < 0) return ret;
840 BUF1[ret] = '\0';
841 ret = symlink(BUF1, dstpath);
842 } else {
843 ret = src = open(srcpath, O_RDONLY);
844 if (ret < 0) return ret;
845 ret = dst = creat(dstpath, st.st_mode);
846 if (ret < 0) return ret;
847 while ((size = read(src, buf, BUFSIZ)) > 0) {
848 write(dst, buf, size);
849 update_progress(size);
850 sync_signals();
852 close(src);
853 close(dst);
854 ret = 0;
856 return ret;
858 static int adddir(const char *path) {
859 int ret;
860 struct stat st;
862 ret = stat(CWD, &st);
863 if (ret < 0) return ret;
864 return mkdir(path, st.st_mode);
866 static int movfile(const char *srcpath) {
867 int ret;
868 struct stat st;
869 char dstpath[PATH_MAX];
871 strcpy(dstpath, CWD);
872 strcat(dstpath, srcpath + strlen(rover.marks.dirpath));
873 ret = rename(srcpath, dstpath);
874 if (ret == 0) {
875 ret = lstat(dstpath, &st);
876 if (ret < 0) return ret;
877 update_progress(st.st_size);
878 } else if (errno == EXDEV) {
879 ret = cpyfile(srcpath);
880 if (ret < 0) return ret;
881 ret = unlink(srcpath);
883 return ret;
886 static void
887 start_line_edit(const char *init_input)
889 curs_set(TRUE);
890 strncpy(INPUT, init_input, BUFLEN);
891 rover.edit.left = mbstowcs(rover.edit.buffer, init_input, BUFLEN);
892 rover.edit.right = BUFLEN - 1;
893 rover.edit.buffer[BUFLEN] = L'\0';
894 rover.edit_scroll = 0;
897 /* Read input and change editing state accordingly. */
898 static EditStat
899 get_line_edit()
901 wchar_t eraser, killer, wch;
902 int ret, length;
904 ret = rover_get_wch((wint_t *) &wch);
905 erasewchar(&eraser);
906 killwchar(&killer);
907 if (ret == KEY_CODE_YES) {
908 if (wch == KEY_ENTER) {
909 curs_set(FALSE);
910 return CONFIRM;
911 } else if (wch == KEY_LEFT) {
912 if (EDIT_CAN_LEFT(rover.edit)) EDIT_LEFT(rover.edit);
913 } else if (wch == KEY_RIGHT) {
914 if (EDIT_CAN_RIGHT(rover.edit)) EDIT_RIGHT(rover.edit);
915 } else if (wch == KEY_UP) {
916 while (EDIT_CAN_LEFT(rover.edit)) EDIT_LEFT(rover.edit);
917 } else if (wch == KEY_DOWN) {
918 while (EDIT_CAN_RIGHT(rover.edit)) EDIT_RIGHT(rover.edit);
919 } else if (wch == KEY_BACKSPACE) {
920 if (EDIT_CAN_LEFT(rover.edit)) EDIT_BACKSPACE(rover.edit);
921 } else if (wch == KEY_DC) {
922 if (EDIT_CAN_RIGHT(rover.edit)) EDIT_DELETE(rover.edit);
924 } else {
925 if (wch == L'\r' || wch == L'\n') {
926 curs_set(FALSE);
927 return CONFIRM;
928 } else if (wch == L'\t') {
929 curs_set(FALSE);
930 return CANCEL;
931 } else if (wch == eraser) {
932 if (EDIT_CAN_LEFT(rover.edit)) EDIT_BACKSPACE(rover.edit);
933 } else if (wch == killer) {
934 EDIT_CLEAR(rover.edit);
935 clear_message();
936 } else if (iswprint(wch)) {
937 if (!EDIT_FULL(rover.edit)) EDIT_INSERT(rover.edit, wch);
940 /* Encode edit contents in INPUT. */
941 rover.edit.buffer[rover.edit.left] = L'\0';
942 length = wcstombs(INPUT, rover.edit.buffer, BUFLEN);
943 wcstombs(&INPUT[length], &rover.edit.buffer[rover.edit.right+1],
944 BUFLEN-length);
945 return CONTINUE;
948 /* Update line input on the screen. */
949 static void
950 update_input(const char *prompt, Color color)
952 int plen, ilen, maxlen;
954 plen = strlen(prompt);
955 ilen = mbstowcs(NULL, INPUT, 0);
956 maxlen = STATUSPOS - plen - 2;
957 if (ilen - rover.edit_scroll < maxlen)
958 rover.edit_scroll = MAX(ilen - maxlen, 0);
959 else if (rover.edit.left > rover.edit_scroll + maxlen - 1)
960 rover.edit_scroll = rover.edit.left - maxlen;
961 else if (rover.edit.left < rover.edit_scroll)
962 rover.edit_scroll = MAX(rover.edit.left - maxlen, 0);
963 color_set(RVC_PROMPT, NULL);
964 mvaddstr(LINES - 1, 0, prompt);
965 color_set(color, NULL);
966 mbstowcs(WBUF, INPUT, COLS);
967 mvaddnwstr(LINES - 1, plen, &WBUF[rover.edit_scroll], maxlen);
968 mvaddch(LINES - 1, plen + MIN(ilen - rover.edit_scroll, maxlen + 1), ' ');
969 color_set(DEFAULT, NULL);
970 if (rover.edit_scroll)
971 mvaddch(LINES - 1, plen - 1, '<');
972 if (ilen > rover.edit_scroll + maxlen)
973 mvaddch(LINES - 1, plen + maxlen, '>');
974 move(LINES - 1, plen + rover.edit.left - rover.edit_scroll);
977 int
978 main(int argc, char *argv[])
980 int i, ch;
981 char *program;
982 char *entry;
983 const char *key;
984 DIR *d;
985 EditStat edit_stat;
986 FILE *save_cwd_file = NULL;
987 FILE *save_marks_file = NULL;
989 if (argc >= 2) {
990 if (!strcmp(argv[1], "-v") || !strcmp(argv[1], "--version")) {
991 printf("rover %s\n", RV_VERSION);
992 return 0;
993 } else if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
994 printf(
995 "Usage: rover [OPTIONS] [DIR [DIR [...]]]\n"
996 " Browse current directory or the ones specified.\n\n"
997 " or: rover -h|--help\n"
998 " Print this help message and exit.\n\n"
999 " or: rover -v|--version\n"
1000 " Print program version and exit.\n\n"
1001 "See rover(1) for more information.\n"
1002 "Rover homepage: <https://github.com/lecram/rover>.\n"
1004 return 0;
1005 } else if (!strcmp(argv[1], "-d") || !strcmp(argv[1], "--save-cwd")) {
1006 if (argc > 2) {
1007 save_cwd_file = fopen(argv[2], "w");
1008 argc -= 2; argv += 2;
1009 } else {
1010 fprintf(stderr, "error: missing argument to %s\n", argv[1]);
1011 return 1;
1013 } else if (!strcmp(argv[1], "-m") || !strcmp(argv[1], "--save-marks")) {
1014 if (argc > 2) {
1015 save_marks_file = fopen(argv[2], "a");
1016 argc -= 2; argv += 2;
1017 } else {
1018 fprintf(stderr, "error: missing argument to %s\n", argv[1]);
1019 return 1;
1023 init_term();
1024 rover.nfiles = 0;
1025 for (i = 0; i < 10; i++) {
1026 rover.tabs[i].esel = rover.tabs[i].scroll = 0;
1027 rover.tabs[i].flags = SHOW_FILES | SHOW_DIRS;
1029 strcpy(rover.tabs[0].cwd, getenv("HOME"));
1030 for (i = 1; i < argc && i < 10; i++) {
1031 if ((d = opendir(argv[i]))) {
1032 realpath(argv[i], rover.tabs[i].cwd);
1033 closedir(d);
1034 } else
1035 strcpy(rover.tabs[i].cwd, rover.tabs[0].cwd);
1037 getcwd(rover.tabs[i].cwd, PATH_MAX);
1038 for (i++; i < 10; i++)
1039 strcpy(rover.tabs[i].cwd, rover.tabs[i-1].cwd);
1040 for (i = 0; i < 10; i++)
1041 if (rover.tabs[i].cwd[strlen(rover.tabs[i].cwd) - 1] != '/')
1042 strcat(rover.tabs[i].cwd, "/");
1043 rover.tab = 1;
1044 rover.window = subwin(stdscr, LINES - 2, COLS, 1, 0);
1045 init_marks(&rover.marks);
1046 cd(1);
1047 while (1) {
1048 ch = rover_getch();
1049 key = keyname(ch);
1050 clear_message();
1051 if (!strcmp(key, RVK_QUIT)) break;
1052 else if (ch >= '0' && ch <= '9') {
1053 rover.tab = ch - '0';
1054 cd(0);
1055 } else if (!strcmp(key, RVK_HELP)) {
1056 spawn((char *[]) {"man", "rover", NULL});
1057 } else if (!strcmp(key, RVK_DOWN)) {
1058 if (!rover.nfiles) continue;
1059 ESEL = MIN(ESEL + 1, rover.nfiles - 1);
1060 update_view();
1061 } else if (!strcmp(key, RVK_UP)) {
1062 if (!rover.nfiles) continue;
1063 ESEL = MAX(ESEL - 1, 0);
1064 update_view();
1065 } else if (!strcmp(key, RVK_JUMP_DOWN)) {
1066 if (!rover.nfiles) continue;
1067 ESEL = MIN(ESEL + RV_JUMP, rover.nfiles - 1);
1068 if (rover.nfiles > HEIGHT)
1069 SCROLL = MIN(SCROLL + RV_JUMP, rover.nfiles - HEIGHT);
1070 update_view();
1071 } else if (!strcmp(key, RVK_JUMP_UP)) {
1072 if (!rover.nfiles) continue;
1073 ESEL = MAX(ESEL - RV_JUMP, 0);
1074 SCROLL = MAX(SCROLL - RV_JUMP, 0);
1075 update_view();
1076 } else if (!strcmp(key, RVK_JUMP_TOP)) {
1077 if (!rover.nfiles) continue;
1078 ESEL = 0;
1079 update_view();
1080 } else if (!strcmp(key, RVK_JUMP_BOTTOM)) {
1081 if (!rover.nfiles) continue;
1082 ESEL = rover.nfiles - 1;
1083 update_view();
1084 } else if (!strcmp(key, RVK_CD_DOWN)) {
1085 if (!rover.nfiles || !S_ISDIR(EMODE(ESEL))) continue;
1086 if (chdir(ENAME(ESEL)) == -1) {
1087 message(RED, "Cannot access \"%s\".", ENAME(ESEL));
1088 continue;
1090 strcat(CWD, ENAME(ESEL));
1091 cd(1);
1092 } else if (!strcmp(key, RVK_CD_UP)) {
1093 char *dirname, first;
1094 if (!strcmp(CWD, "/")) continue;
1095 CWD[strlen(CWD) - 1] = '\0';
1096 dirname = strrchr(CWD, '/') + 1;
1097 first = dirname[0];
1098 dirname[0] = '\0';
1099 cd(1);
1100 dirname[0] = first;
1101 dirname[strlen(dirname)] = '/';
1102 try_to_sel(dirname);
1103 dirname[0] = '\0';
1104 if (rover.nfiles > HEIGHT)
1105 SCROLL = ESEL - HEIGHT / 2;
1106 update_view();
1107 } else if (!strcmp(key, RVK_HOME)) {
1108 strcpy(CWD, getenv("HOME"));
1109 if (CWD[strlen(CWD) - 1] != '/')
1110 strcat(CWD, "/");
1111 cd(1);
1112 } else if (!strcmp(key, RVK_TARGET)) {
1113 char *bname, first;
1114 int is_dir = S_ISDIR(EMODE(ESEL));
1115 ssize_t len = readlink(ENAME(ESEL), BUF1, BUFLEN-1);
1116 if (len == -1) continue;
1117 BUF1[len] = '\0';
1118 if (access(BUF1, F_OK) == -1) {
1119 char *msg;
1120 switch (errno) {
1121 case EACCES:
1122 msg = "Cannot access \"%s\".";
1123 break;
1124 case ENOENT:
1125 msg = "\"%s\" does not exist.";
1126 break;
1127 default:
1128 msg = "Cannot navigate to \"%s\".";
1130 strcpy(BUF2, BUF1); /* message() uses BUF1. */
1131 message(RED, msg, BUF2);
1132 continue;
1134 realpath(BUF1, CWD);
1135 len = strlen(CWD);
1136 if (CWD[len - 1] == '/')
1137 CWD[len - 1] = '\0';
1138 bname = strrchr(CWD, '/') + 1;
1139 first = *bname;
1140 *bname = '\0';
1141 cd(1);
1142 *bname = first;
1143 if (is_dir)
1144 strcat(CWD, "/");
1145 try_to_sel(bname);
1146 *bname = '\0';
1147 update_view();
1148 } else if (!strcmp(key, RVK_REFRESH)) {
1149 reload();
1150 } else if (!strcmp(key, RVK_SHELL)) {
1151 program = getenv("SHELL");
1152 if (program) {
1153 #ifdef RV_SHELL
1154 spawn((char *[]) {RV_SHELL, "-c", program, NULL});
1155 #else
1156 spawn((char *[]) {program, NULL});
1157 #endif
1158 reload();
1160 } else if (!strcmp(key, RVK_VIEW)) {
1161 if (!rover.nfiles || S_ISDIR(EMODE(ESEL))) continue;
1162 if (open_with_env("PAGER", ENAME(ESEL)))
1163 cd(0);
1164 } else if (!strcmp(key, RVK_EDIT)) {
1165 if (!rover.nfiles || S_ISDIR(EMODE(ESEL))) continue;
1166 if (open_with_env("EDITOR", ENAME(ESEL)))
1167 cd(0);
1168 } else if (!strcmp(key, RVK_OPEN)) {
1169 if (!rover.nfiles || S_ISDIR(EMODE(ESEL))) continue;
1170 if (open_with_env("ROVER_OPEN", ENAME(ESEL)))
1171 cd(0);
1172 } else if (!strcmp(key, RVK_SEARCH)) {
1173 int oldsel, oldscroll, length;
1174 if (!rover.nfiles) continue;
1175 oldsel = ESEL;
1176 oldscroll = SCROLL;
1177 start_line_edit("");
1178 update_input(RVP_SEARCH, RED);
1179 while ((edit_stat = get_line_edit()) == CONTINUE) {
1180 int sel;
1181 Color color = RED;
1182 length = strlen(INPUT);
1183 if (length) {
1184 for (sel = 0; sel < rover.nfiles; sel++)
1185 if (!strncmp(ENAME(sel), INPUT, length))
1186 break;
1187 if (sel < rover.nfiles) {
1188 color = GREEN;
1189 ESEL = sel;
1190 if (rover.nfiles > HEIGHT) {
1191 if (sel < 3)
1192 SCROLL = 0;
1193 else if (sel - 3 > rover.nfiles - HEIGHT)
1194 SCROLL = rover.nfiles - HEIGHT;
1195 else
1196 SCROLL = sel - 3;
1199 } else {
1200 ESEL = oldsel;
1201 SCROLL = oldscroll;
1203 update_view();
1204 update_input(RVP_SEARCH, color);
1206 if (edit_stat == CANCEL) {
1207 ESEL = oldsel;
1208 SCROLL = oldscroll;
1210 clear_message();
1211 update_view();
1212 } else if (!strcmp(key, RVK_TG_FILES)) {
1213 FLAGS ^= SHOW_FILES;
1214 reload();
1215 } else if (!strcmp(key, RVK_TG_DIRS)) {
1216 FLAGS ^= SHOW_DIRS;
1217 reload();
1218 } else if (!strcmp(key, RVK_TG_HIDDEN)) {
1219 FLAGS ^= SHOW_HIDDEN;
1220 reload();
1221 } else if (!strcmp(key, RVK_NEW_FILE)) {
1222 int ok = 0;
1223 start_line_edit("");
1224 update_input(RVP_NEW_FILE, RED);
1225 while ((edit_stat = get_line_edit()) == CONTINUE) {
1226 int length = strlen(INPUT);
1227 ok = length;
1228 for (i = 0; i < rover.nfiles; i++) {
1229 if (
1230 !strncmp(ENAME(i), INPUT, length) &&
1231 (!strcmp(ENAME(i) + length, "") ||
1232 !strcmp(ENAME(i) + length, "/"))
1233 ) {
1234 ok = 0;
1235 break;
1238 update_input(RVP_NEW_FILE, ok ? GREEN : RED);
1240 clear_message();
1241 if (edit_stat == CONFIRM) {
1242 if (ok) {
1243 if (addfile(INPUT) == 0) {
1244 cd(1);
1245 try_to_sel(INPUT);
1246 update_view();
1247 } else
1248 message(RED, "Could not create \"%s\".", INPUT);
1249 } else
1250 message(RED, "\"%s\" already exists.", INPUT);
1252 } else if (!strcmp(key, RVK_NEW_DIR)) {
1253 int ok = 0;
1254 start_line_edit("");
1255 update_input(RVP_NEW_DIR, RED);
1256 while ((edit_stat = get_line_edit()) == CONTINUE) {
1257 int length = strlen(INPUT);
1258 ok = length;
1259 for (i = 0; i < rover.nfiles; i++) {
1260 if (
1261 !strncmp(ENAME(i), INPUT, length) &&
1262 (!strcmp(ENAME(i) + length, "") ||
1263 !strcmp(ENAME(i) + length, "/"))
1264 ) {
1265 ok = 0;
1266 break;
1269 update_input(RVP_NEW_DIR, ok ? GREEN : RED);
1271 clear_message();
1272 if (edit_stat == CONFIRM) {
1273 if (ok) {
1274 if (adddir(INPUT) == 0) {
1275 cd(1);
1276 strcat(INPUT, "/");
1277 try_to_sel(INPUT);
1278 update_view();
1279 } else
1280 message(RED, "Could not create \"%s/\".", INPUT);
1281 } else
1282 message(RED, "\"%s\" already exists.", INPUT);
1284 } else if (!strcmp(key, RVK_RENAME)) {
1285 int ok = 0;
1286 char *last;
1287 int isdir;
1288 strcpy(INPUT, ENAME(ESEL));
1289 last = INPUT + strlen(INPUT) - 1;
1290 if ((isdir = *last == '/'))
1291 *last = '\0';
1292 start_line_edit(INPUT);
1293 update_input(RVP_RENAME, RED);
1294 while ((edit_stat = get_line_edit()) == CONTINUE) {
1295 int length = strlen(INPUT);
1296 ok = length;
1297 for (i = 0; i < rover.nfiles; i++)
1298 if (
1299 !strncmp(ENAME(i), INPUT, length) &&
1300 (!strcmp(ENAME(i) + length, "") ||
1301 !strcmp(ENAME(i) + length, "/"))
1302 ) {
1303 ok = 0;
1304 break;
1306 update_input(RVP_RENAME, ok ? GREEN : RED);
1308 clear_message();
1309 if (edit_stat == CONFIRM) {
1310 if (isdir)
1311 strcat(INPUT, "/");
1312 if (ok) {
1313 if (!rename(ENAME(ESEL), INPUT) && MARKED(ESEL)) {
1314 del_mark(&rover.marks, ENAME(ESEL));
1315 add_mark(&rover.marks, CWD, INPUT);
1317 cd(1);
1318 try_to_sel(INPUT);
1319 update_view();
1320 } else
1321 message(RED, "\"%s\" already exists.", INPUT);
1323 } else if (!strcmp(key, RVK_DELETE)) {
1324 if (rover.nfiles) {
1325 message(YELLOW, "Delete \"%s\"? (Y/n)", ENAME(ESEL));
1326 if (rover_getch() == 'Y') {
1327 const char *name = ENAME(ESEL);
1328 int ret = ISDIR(ENAME(ESEL)) ? deldir(name) : delfile(name);
1329 reload();
1330 if (ret)
1331 message(RED, "Could not delete \"%s\".", ENAME(ESEL));
1332 } else
1333 clear_message();
1334 } else
1335 message(RED, "No entry selected for deletion.");
1336 } else if (!strcmp(key, RVK_TG_MARK)) {
1337 if (MARKED(ESEL))
1338 del_mark(&rover.marks, ENAME(ESEL));
1339 else
1340 add_mark(&rover.marks, CWD, ENAME(ESEL));
1341 MARKED(ESEL) = !MARKED(ESEL);
1342 ESEL = (ESEL + 1) % rover.nfiles;
1343 update_view();
1344 } else if (!strcmp(key, RVK_INVMARK)) {
1345 for (i = 0; i < rover.nfiles; i++) {
1346 if (MARKED(i))
1347 del_mark(&rover.marks, ENAME(i));
1348 else
1349 add_mark(&rover.marks, CWD, ENAME(i));
1350 MARKED(i) = !MARKED(i);
1352 update_view();
1353 } else if (!strcmp(key, RVK_MARKALL)) {
1354 for (i = 0; i < rover.nfiles; i++)
1355 if (!MARKED(i)) {
1356 add_mark(&rover.marks, CWD, ENAME(i));
1357 MARKED(i) = 1;
1359 update_view();
1360 } else if (!strcmp(key, RVK_MARK_DELETE)) {
1361 if (rover.marks.nentries) {
1362 message(YELLOW, "Delete all marked entries? (Y/n)");
1363 if (rover_getch() == 'Y')
1364 process_marked(NULL, delfile, deldir, "Deleting", "Deleted");
1365 else
1366 clear_message();
1367 } else
1368 message(RED, "No entries marked for deletion.");
1369 } else if (!strcmp(key, RVK_MARK_COPY)) {
1370 if (rover.marks.nentries)
1371 process_marked(adddir, cpyfile, NULL, "Copying", "Copied");
1372 else
1373 message(RED, "No entries marked for copying.");
1374 } else if (!strcmp(key, RVK_MARK_MOVE)) {
1375 if (rover.marks.nentries)
1376 process_marked(adddir, movfile, deldir, "Moving", "Moved");
1377 else
1378 message(RED, "No entries marked for moving.");
1381 if (rover.nfiles)
1382 free_rows(&rover.rows, rover.nfiles);
1383 delwin(rover.window);
1384 if (save_cwd_file != NULL) {
1385 fputs(CWD, save_cwd_file);
1386 fclose(save_cwd_file);
1388 if (save_marks_file != NULL) {
1389 for (i = 0; i < rover.marks.bulk; i++) {
1390 entry = rover.marks.entries[i];
1391 if (entry)
1392 fprintf(save_marks_file, "%s%s\n", rover.marks.dirpath, entry);
1394 fclose(save_marks_file);
1396 free_marks(&rover.marks);
1397 return 0;