commit - /dev/null
commit + 71ee162773e8955eb4d4ece8959a8fb005bba95d
blob - /dev/null
blob + d85ed884e19d232bd9354cd1886d757c53f870f3 (mode 644)
--- /dev/null
+++ .gitignore
+rover
blob - /dev/null
blob + 48a221a49900cdc8a307e3e7027dd5a5e6905567 (mode 644)
--- /dev/null
+++ Makefile
+CLINK = -lcurses
+CFLAGS = -Wall -Wextra -Werror -std=c89
+DEBUG ?= 0
+ifeq ($(DEBUG), 1)
+ CFLAGS += -O0 -g
+else
+ CFLAGS += -O2
+endif
+
+all : rover
+
+rover : rover.c config.h
+ $(CC) $(CFLAGS) -o $@ $< $(CLINK)
blob - /dev/null
blob + 2b49ec11645715470257939247bc337bd3b44606 (mode 644)
--- /dev/null
+++ config.h
+#define RVK_QUIT 'q'
+#define RVK_DOWN 'j'
+#define RVK_UP 'k'
+#define RVK_JUMP_DOWN 'J'
+#define RVK_JUMP_UP 'K'
+#define RVK_CD_DOWN 'l'
+#define RVK_CD_UP 'h'
+#define RVK_HOME 'H'
+#define RVK_SHELL '\r'
+#define RVK_EDIT ' '
+
+#define RV_JUMP 10
blob - /dev/null
blob + 9fde40875ba652b6f1a539db580e56a856eb91e2 (mode 644)
--- /dev/null
+++ rover.c
+/* ToDo
+ * - incremental search inside directory;
+ * - filters (show/hide diretories/files, apply glob pattern)
+ * - tabs (only store paths?);
+ * - browsing history (use keys < & > to navigate);
+ */
+
+/* POSIX 2008: http://pubs.opengroup.org/onlinepubs/9699919799/toc.htm */
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h> /* ? */
+#include <stdio.h> /* FILENAME_MAX */
+#include <locale.h> /* setlocale(), LC_ALL */
+#include <unistd.h> /* chdir(), getcwd() */
+#include <dirent.h> /* DIR, struct dirent, opendir(), ... */
+#include <sys/stat.h>
+#include <sys/wait.h> /* waitpid() */
+#include <curses.h>
+
+#include "config.h"
+
+#define TXTBUFSZ 256
+char TXTBUF[TXTBUFSZ];
+#define MAXARGS 256
+char *args[MAXARGS];
+
+enum {DEFAULT, RED, GREEN, YELLOW, BLUE, CYAN, MAGENTA, WHITE};
+
+struct skit_t {
+ int nfiles;
+ int scroll;
+ int fsel;
+ char **fnames;
+ WINDOW *window;
+ char cwd[FILENAME_MAX];
+} skit;
+
+static int
+spcmp(const void *a, const void *b)
+{
+ int isdir1, isdir2, cmpdir;
+ const char *s1 = *(const char **) a;
+ const char *s2 = *(const char **) b;
+ isdir1 = strchr(s1, '/') != NULL;
+ isdir2 = strchr(s2, '/') != NULL;
+ cmpdir = isdir2 - isdir1;
+ /* FIXME: why doesn't `return cmpdir || strcoll(s1, s2)` work here? */
+ return cmpdir ? cmpdir : strcoll(s1, s2);
+}
+
+int
+ls(char *path, char ***namesp)
+{
+ DIR *dp;
+ struct dirent *ep;
+ struct stat statbuf;
+ char **names;
+ int i, n;
+
+ if((dp = opendir(path)) == NULL)
+ return -1;
+ n = -2; /* We don't want the entries "." and "..". */
+ while (readdir(dp)) n++;
+ rewinddir(dp);
+ names = (char **) malloc(n * sizeof(char *));
+ i = 0;
+ while ((ep = readdir(dp))) {
+ if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0)
+ continue;
+ /* FIXME: ANSI C doesn't have lstat(). How do we handle symlinks? */
+ (void) stat(ep->d_name, &statbuf);
+ names[i] = (char *) malloc(strlen(ep->d_name) + 2);
+ strcpy(names[i], ep->d_name);
+ if (S_ISDIR(statbuf.st_mode))
+ strcat(names[i], "/");
+ i++;
+ }
+ qsort(names, n, sizeof(char *), spcmp);
+ (void) closedir(dp);
+ *namesp = names;
+ return n;
+}
+
+static void
+sk_clean()
+{
+ endwin();
+}
+
+static void
+sk_init()
+{
+ setlocale(LC_ALL, "");
+ initscr();
+ cbreak(); /* Get one character at a time. */
+ noecho();
+ nonl(); /* No NL->CR/NL on output. */
+ intrflush(stdscr, FALSE);
+ keypad(stdscr, TRUE);
+ curs_set(FALSE); /* Hide blinking cursor. */
+ if (has_colors()) {
+ start_color();
+ init_pair(RED, COLOR_RED, COLOR_BLACK);
+ init_pair(GREEN, COLOR_GREEN, COLOR_BLACK);
+ init_pair(YELLOW, COLOR_YELLOW,COLOR_BLACK);
+ init_pair(BLUE, COLOR_BLUE, COLOR_BLACK);
+ init_pair(CYAN, COLOR_CYAN, COLOR_BLACK);
+ init_pair(MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
+ init_pair(WHITE, COLOR_WHITE, COLOR_BLACK);
+ }
+ atexit(sk_clean);
+}
+
+static void
+update_browser()
+{
+ int i, j;
+
+ for (i = 0, j = skit.scroll; i < LINES - 4 && j < skit.nfiles; i++, j++) {
+ if (j == skit.fsel)
+ wattr_on(skit.window, A_REVERSE, NULL);
+ (void) mvwhline(skit.window, i + 1, 1,
+ ' ', COLS - 2);
+ (void) mvwaddnstr(skit.window, i + 1, 1,
+ skit.fnames[j], COLS - 2);
+ if (j == skit.fsel)
+ wattr_off(skit.window, A_REVERSE, NULL);
+ }
+ wrefresh(skit.window);
+ /* C89 doesn't have snprintf(), but a buffer overrun will only occur here
+ * if the number of files reach 10 ^ (TXTBUFSZ / 2), which is unlikely. */
+ sprintf(TXTBUF, "% 10d/%d", skit.fsel + 1, skit.nfiles);
+ mvaddstr(LINES - 1, COLS - strlen(TXTBUF), TXTBUF);
+ refresh();
+}
+
+/* NOTE: The caller needs to write the new path to skit.cwd
+ * *before* calling this function. */
+static void
+cd()
+{
+ int i;
+
+ skit.fsel = 0;
+ skit.scroll = 0;
+ (void) chdir(skit.cwd);
+ (void) mvhline(0, 0, ' ', COLS);
+ (void) mvaddnstr(0, 0, skit.cwd, COLS);
+ for (i = 0; i < skit.nfiles; i++)
+ free(skit.fnames[i]);
+ if (skit.nfiles)
+ free(skit.fnames);
+ skit.nfiles = ls(skit.cwd, &skit.fnames);
+ (void) wclear(skit.window);
+ wborder(skit.window, 0, 0, 0, 0, 0, 0, 0, 0);
+ update_browser();
+ refresh();
+}
+
+static void
+spawn()
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ if (pid > 0) {
+ /* fork() succeeded. */
+ sk_clean();
+ (void) waitpid(pid, &status, 0);
+ sk_init();
+ doupdate();
+ }
+ else if (pid == 0) {
+ /* Child process. */
+ execvp(args[0], args);
+ }
+}
+
+int
+main()
+{
+ int ch;
+ char *program;
+
+ sk_init();
+ /* Avoid invalid free() calls in cd() by zeroing the tally. */
+ skit.nfiles = 0;
+ (void) getcwd(skit.cwd, FILENAME_MAX);
+ strcat(skit.cwd, "/");
+ skit.window = subwin(stdscr, LINES - 2, COLS, 1, 0);
+ cd();
+ while ((ch = getch()) != RVK_QUIT) switch (ch) {
+ case RVK_DOWN:
+ if (skit.fsel == skit.nfiles - 1)
+ skit.scroll = skit.fsel = 0;
+ else {
+ skit.fsel++;
+ if ((skit.fsel - skit.scroll) == (LINES - 4))
+ skit.scroll++;
+ }
+ update_browser();
+ break;
+ case RVK_UP:
+ if (skit.fsel == 0) {
+ skit.fsel = skit.nfiles - 1;
+ skit.scroll = skit.nfiles - LINES + 4;
+ if (skit.scroll < 0)
+ skit.scroll = 0;
+ }
+ else {
+ skit.fsel--;
+ if (skit.fsel < skit.scroll)
+ skit.scroll--;
+ }
+ update_browser();
+ break;
+ case RVK_JUMP_DOWN:
+ skit.fsel += RV_JUMP;
+ if (skit.fsel >= skit.nfiles)
+ skit.fsel = skit.nfiles - 1;
+ if (skit.nfiles > LINES - 4) {
+ skit.scroll += RV_JUMP;
+ if (skit.scroll > skit.nfiles - LINES + 4)
+ skit.scroll = skit.nfiles - LINES + 4;
+ }
+ update_browser();
+ break;
+ case RVK_JUMP_UP:
+ skit.fsel -= RV_JUMP;
+ if (skit.fsel < 0)
+ skit.fsel = 0;
+ skit.scroll -= RV_JUMP;
+ if (skit.scroll < 0)
+ skit.scroll = 0;
+ update_browser();
+ break;
+ case RVK_CD_DOWN:
+ if (strchr(skit.fnames[skit.fsel], '/') == NULL)
+ continue;
+ strcat(skit.cwd, skit.fnames[skit.fsel]);
+ cd();
+ break;
+ case RVK_CD_UP:
+ if (strlen(skit.cwd) == 1)
+ continue;
+ skit.cwd[strlen(skit.cwd) - 1] = '\0';
+ *(strrchr(skit.cwd, '/') + 1) = '\0';
+ cd();
+ break;
+ case RVK_HOME:
+ strcpy(skit.cwd, getenv("HOME"));
+ if (skit.cwd[strlen(skit.cwd) - 1] != '/')
+ strcat(skit.cwd, "/");
+ cd();
+ break;
+ case RVK_SHELL:
+ program = getenv("SHELL");
+ if (program) {
+ args[0] = program;
+ args[1] = NULL;
+ spawn();
+ }
+ break;
+ case RVK_EDIT:
+ if (strchr(skit.fnames[skit.fsel], '/') != NULL)
+ continue;
+ program = getenv("EDITOR");
+ if (program) {
+ args[0] = program;
+ args[1] = skit.fnames[skit.fsel];
+ args[2] = NULL;
+ spawn();
+ }
+ break;
+ default:
+ continue;
+ }
+ while (skit.nfiles--) free(skit.fnames[skit.nfiles]);
+ free(skit.fnames);
+ delwin(skit.window);
+ return 0;
+}