commit e36b0fb8558c588613060549d9c1e2d7f7eeb34c
Author: rani <clagv.randomgames@gmail.com>
Date: Tue Dec 30 23:38:22 2025 +0000
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..37cbb01
--- /dev/null
+++ b/Makefile
@@ -00 +121 @@
+BIN = cscroll
+SRC = src/dir.c src/ui.c src/main.c
+OBJ = ${SRC:.c=.o}
+
+CC ?= cc
+
+CFLAGS += -Iinclude -Wall -Wextra -O2
+LDFLAGS += -lncurses
+
+all: ${BIN}
+
+${BIN}: ${OBJ}
+ ${CC} ${CFLAGS} ${OBJ} -o $@ ${LDFLAGS}
+
+src/.c.o:
+ ${CC} -c ${CFLAGS} $<
+
+clean:
+ rm -f ${BIN} ${OBJ}
+
+.PHONY: all clean
diff --git a/include/dir.h b/include/dir.h
index 38f14d1..132915d 100644
--- a/include/dir.h
+++ b/include/dir.h
@@ -4914 +4921 @@ typedef struct {
typedef struct {
size_t len;
cvector(dirent_t) entries;
+
+ size_t longest_uname;
+ size_t longest_gname;
+ size_t longest_size;
} dir_t;
int dir_list(const char * path, dir_t * dir);
void dir_free(dir_t * dir);
+const char * dir_get_cwd(void);
char dirent_crepr(const dirent_t * de); // character representing dir entry
char dirent_creprl(const dirent_t * de); // like above, but for the link
char dirent_longcrepr(const dirent_t * de); // like above, but for long mode
bool dirent_isexec(const dirent_t * de);
const char * dirent_prettymode(const dirent_t * de);
+int dir_cd_back(const char * cwd);
+int dir_cd(const char * cwd, const char * next);
#endif /* DIR_H */
diff --git a/include/ui.h b/include/ui.h
new file mode 100644
index 0000000..a59aacd
--- /dev/null
+++ b/include/ui.h
@@ -00 +137 @@
+#ifndef UI_H
+#define UI_H
+
+#include <ncurses.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "dir.h"
+
+enum ui_color {
+ COLOR_FILE = 1,
+ COLOR_DIR,
+ COLOR_FIFO,
+ COLOR_LINK,
+ COLOR_BLOCK,
+ COLOR_CHAR,
+ COLOR_SOCKET,
+ COLOR_UNKNOWN,
+ COLOR_EXEC,
+
+ RED,
+ YELLOW,
+ MAGENTA,
+ WHITE,
+
+};
+
+void ui_init(void);
+void ui_deinit(void);
+void ui_set_title(const char * title);
+void ui_set_status(const char * status);
+void ui_erase(void);
+void ui_refresh(void);
+void ui_print_dir(const dir_t * dir, size_t cursor, bool longmode);
+void ui_print_cursor(size_t cursor, size_t total);
+
+#endif /* UI_H */
diff --git a/src/dir.c b/src/dir.c
index 7e68625..247e502 100644
--- a/src/dir.c
+++ b/src/dir.c
@@ -116 +117 @@
#include "dir.h"
#include "cvector.h"
+#define CWDSZ (PATH_MAX + 1)
#define LINKNAMESZ PATH_MAX
static void dir_entry(int dirfd, const char * name, dirent_t * dirent);
@@ -2211 +2323 @@ static void free_dirent(void * p) {
free(de->gname);
free(de->linkname);
}
+static size_t ilen(size_t i, int base) {
+ if (i == 0) return 1;
+ size_t r = 0;
+ while (i != 0) {
+ r++;
+ i /= base;
+ }
+ return r;
+}
int dir_list(const char * path, dir_t * dir) {
dir->len = 0;
dir->entries = NULL;
cvector_init(dir->entries, 1, free_dirent);
+ dir->longest_uname = 0;
+ dir->longest_gname = 0;
+ dir->longest_size = 0;
struct dirent * de;
DIR * dp = opendir(path);
@@ -436 +5615 @@ int dir_list(const char * path, dir_t * dir) {
dir_entry(dirfd(dp), de->d_name, &dirent);
cvector_push_back(dir->entries, dirent);
dir->len++;
+
+ size_t uname_len = 0;
+ size_t gname_len = 0;
+ size_t size_len = ilen(dirent.size, 10);
+ if (dirent.uname) uname_len = strlen(dirent.uname);
+ if (dirent.gname) gname_len = strlen(dirent.gname);
+ if (uname_len > dir->longest_uname) dir->longest_uname = uname_len;
+ if (gname_len > dir->longest_gname) dir->longest_gname = gname_len;
+ if (size_len > dir->longest_size) dir->longest_size = size_len;
}
closedir(dp);
@@ -1923 +21445 @@ const char * dirent_prettymode(const dirent_t * de) {
return s;
}
+
+const char * dir_get_cwd(void) {
+ static char cwd[CWDSZ];
+
+ getcwd(cwd, CWDSZ);
+ return cwd;
+}
+
+int dir_cd_back(const char * cwd) {
+ if (!*cwd) return -1;
+ if (!strcmp(cwd, "/")) return 0;
+
+ char * newwd = strdup(cwd);
+ char * p = strrchr(newwd, '/');
+ *p = 0;
+ if (*newwd == 0) {
+ *newwd = '/';
+ *(newwd + 1) = 0;
+ }
+ int ret = chdir(newwd);
+ free(newwd);
+
+ if (ret < 0) return -1;
+ return 0;
+}
+
+int dir_cd(const char * cwd, const char * next) {
+ size_t cwdlen = strlen(cwd);
+ size_t nextlen = strlen(next);
+
+ // extra byte for '/' character
+ char * newwd = malloc(cwdlen + nextlen + 1 + 1);
+ strcpy(newwd, cwd);
+ newwd[cwdlen] = '/';
+ strcpy(newwd + cwdlen + 1, next);
+
+ int ret = chdir(newwd);
+ free(newwd);
+
+ if (ret < 0) return -1;
+ return 0;
+}
diff --git a/src/main.c b/src/main.c
index 7b4a140..8948789 100644
--- a/src/main.c
+++ b/src/main.c
@@ -124 +171 @@
#include <stdio.h>
#include <stddef.h>
+#include <ncurses.h>
+#include "ui.h"
#include "dir.h"
int main(int argc, char ** argv) {
+ ui_init();
+
+ const char * cwd = dir_get_cwd();
+
dir_t dir;
- dir_list(argv[1], &dir);
-
- for (size_t i = 0; i < dir.len; i++) {
- dirent_t * de = &dir.entries[i];
- char c = dirent_crepr(de);
- if (!c) c = ' ';
- const char * pmode = dirent_prettymode(de);
- printf("%s %s %s %lu %ld %s%c", pmode, de->uname, de->gname, de->size, de->mtime, de->name, c);
- if (de->linkname) {
- c = dirent_creprl(de);
- if (!c) c = ' ';
- printf(" -> %s%c\n", de->linkname, c);
- } else putchar('\n');
+ dir_list(cwd, &dir);
+
+ size_t cursor = 0;
+ ui_set_status("");
+ for (;;) {
+ ui_erase();
+
+ ui_set_title(cwd);
+
+ ui_print_dir(&dir, cursor, true);
+ ui_print_cursor(cursor, dir.len);
+
+ ui_refresh();
+
+ dirent_t * cur_de = &dir.entries[cursor];
+ switch (getch()) {
+ case KEY_UP:
+ if (cursor > 0) cursor--;
+ break;
+ case KEY_DOWN:
+ if (cursor < dir.len - 1) cursor++;
+ break;
+ case KEY_LEFT:
+ if (dir_cd_back(cwd) >= 0) {
+ cursor = 0;
+ cwd = dir_get_cwd();
+ dir_free(&dir);
+ dir_list(cwd, &dir);
+ }
+ break;
+ case KEY_RIGHT:
+ if (cur_de->type == DE_DIR) {
+ if (dir_cd(cwd, cur_de->name) >= 0) {
+ cursor = 0;
+ cwd = dir_get_cwd();
+ dir_free(&dir);
+ dir_list(cwd, &dir);
+ }
+ }
+ break;
+ case KEY_HOME:
+ case 'g':
+ cursor = 0;
+ break;
+ case KEY_END:
+ case 'G':
+ cursor = dir.len - 1;
+ break;
+ case 'q':
+ goto finished;
+ default: break;
+ }
}
+finished:
dir_free(&dir);
+ ui_deinit();
}
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..8100059
--- /dev/null
+++ b/src/ui.c
@@ -00 +1204 @@
+#include <ncurses.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <locale.h>
+#include <time.h>
+
+#include "ui.h"
+#include "dir.h"
+
+static WINDOW * titlewin = NULL;
+static WINDOW * statuswin = NULL;
+static WINDOW * filewin = NULL;
+
+static void win_set(WINDOW * win, const char * str, int attrs);
+static int ui_dirent_color(const dirent_t * de);
+static void ui_print_dirent(const dirent_t * de, size_t pos, bool selected, bool longmode, const dir_t * dir);
+static void ui_wlpadstr(WINDOW * win, const char * s, size_t len);
+static void ui_get_first_last(size_t n_dirents, size_t cursor, size_t * first, size_t * last);
+
+void ui_init(void) {
+ setlocale(LC_CTYPE, "");
+
+ initscr();
+ keypad(stdscr, TRUE);
+ curs_set(0);
+ noecho();
+
+ titlewin = newwin(1, COLS, 0, 0);
+ statuswin = newwin(1, COLS, 1, 0);
+ filewin = newwin(LINES - 2, COLS, 2, 0);
+
+ clear();
+ refresh();
+
+ use_default_colors();
+ start_color();
+
+ init_pair(COLOR_FILE, -1, -1);
+ init_pair(COLOR_DIR, COLOR_BLUE, -1);
+ init_pair(COLOR_FIFO, COLOR_YELLOW, -1);
+ init_pair(COLOR_LINK, COLOR_CYAN, -1);
+ init_pair(COLOR_BLOCK, COLOR_YELLOW, -1);
+ init_pair(COLOR_CHAR, COLOR_YELLOW, -1);
+ init_pair(COLOR_SOCKET, COLOR_MAGENTA, -1);
+ init_pair(COLOR_UNKNOWN, COLOR_RED, -1);
+ init_pair(COLOR_EXEC, COLOR_GREEN, -1);
+
+ init_pair(RED, COLOR_RED, -1);
+ init_pair(YELLOW, COLOR_YELLOW, -1);
+ init_pair(MAGENTA, COLOR_MAGENTA, -1);
+ init_pair(WHITE, -1, -1);
+}
+
+void ui_deinit(void) {
+ endwin();
+}
+
+void win_set(WINDOW * win, const char * str, int attrs) {
+ werase(win);
+ wattron(win, attrs);
+ waddstr(win, str);
+ wattroff(win, attrs);
+}
+
+void ui_set_title(const char * title) {
+ win_set(titlewin, title, 0);
+}
+
+void ui_set_status(const char * status) {
+ win_set(statuswin, status, 0);
+}
+
+void ui_erase(void) {
+ werase(filewin);
+}
+
+void ui_refresh(void) {
+ wnoutrefresh(titlewin);
+ wnoutrefresh(statuswin);
+ wnoutrefresh(filewin);
+ doupdate();
+}
+
+static void ui_wlpadstr(WINDOW * win, const char * s, size_t len) {
+ size_t slen = strlen(s);
+
+ for (size_t i = slen; i < len; i++) {
+ waddch(win, ' ');
+ }
+
+ waddstr(win, s);
+}
+
+void ui_print_dirent(const dirent_t * de, size_t pos, bool selected, bool longmode, const dir_t * dir) {
+ static const enum ui_color mode_colors[] = {
+ ['r'] = RED, ['w'] = MAGENTA, ['x'] = COLOR_EXEC,
+ ['s'] = YELLOW, ['S'] = YELLOW, ['t'] = RED,
+ ['T'] = RED, ['-'] = WHITE, ['?'] = RED,
+
+ ['.'] = WHITE,
+ ['d'] = COLOR_DIR, ['b'] = COLOR_BLOCK, ['c'] = COLOR_CHAR,
+ ['l'] = COLOR_LINK, ['|'] = COLOR_FIFO, ['='] = COLOR_SOCKET,
+ };
+
+ static char stime[128];
+ static char isbuf[40]; // should be big enough to hold any integer
+
+ wmove(filewin, pos, 0);
+
+ if (longmode) {
+ const char * pmode = dirent_prettymode(de);
+ for (const char * p = pmode; *p; p++) {
+ int color = COLOR_PAIR(mode_colors[(unsigned)*p]);
+ if (*p == '-' || *p == '.') color |= A_DIM;
+ wattron(filewin, color);
+ waddch(filewin, *p);
+ wattroff(filewin, color);
+ }
+
+ if (de->uname) ui_wlpadstr(filewin, de->uname, dir->longest_uname + 1);
+ else waddstr(filewin, " ?");
+
+ if (de->gname) ui_wlpadstr(filewin, de->gname, dir->longest_gname + 1);
+ else waddstr(filewin, " ?");
+
+ snprintf(isbuf, sizeof(isbuf), "%lu", de->size);
+ ui_wlpadstr(filewin, isbuf, dir->longest_size + 1);
+
+ strftime(stime, sizeof(stime), "%b %d %H:%M %Y", localtime(&de->mtime));
+ wprintw(filewin, " %s ", stime);
+ }
+
+ int color = COLOR_PAIR(ui_dirent_color(de));
+ if (selected) color |= A_REVERSE;
+ wattron(filewin, color);
+ waddstr(filewin, de->name);
+ wattroff(filewin, color);
+ char c = dirent_crepr(de);
+ if (c) waddch(filewin, c);
+}
+
+static int ui_dirent_color(const dirent_t * de) {
+ int color;
+ switch (de->type) {
+ case DE_FILE: color = COLOR_FILE; break;
+ case DE_DIR: color = COLOR_DIR; break;
+ case DE_FIFO: color = COLOR_FIFO; break;
+ case DE_LINK: color = COLOR_LINK; break;
+ case DE_BLOCK: color = COLOR_BLOCK; break;
+ case DE_CHAR: color = COLOR_CHAR; break;
+ case DE_SOCKET: color = COLOR_SOCKET; break;
+ case DE_UNKNOWN: color = COLOR_UNKNOWN; break;
+ default: color = WHITE; break;
+ }
+
+ if (de->type == DE_FILE && dirent_isexec(de)) {
+ color = COLOR_EXEC;
+ }
+
+ return color;
+}
+
+void ui_print_dir(const dir_t * dir, size_t cursor, bool longmode) {
+ size_t first;
+ size_t last;
+ ui_get_first_last(dir->len, cursor, &first, &last);
+
+ for (size_t i = first; i < last; i++) {
+ size_t pos = i - first;
+ ui_print_dirent(&dir->entries[i], pos, cursor == i, longmode, dir);
+ }
+}
+
+void ui_print_cursor(size_t cursor, size_t total) {
+ size_t lines, cols;
+ getmaxyx(filewin, lines, cols);
+ if (total < lines - 2) wmove(filewin, total + 1, 0);
+ else wmove(filewin, lines - 1, 0);
+
+ wprintw(filewin, "%zu/%zu", cursor + 1, total);
+}
+
+static void ui_get_first_last(size_t dir_len, size_t cursor, size_t * first, size_t * last) {
+ size_t lines, cols;
+ getmaxyx(filewin, lines, cols);
+ lines -= 2;
+ // try to keep cursor in the middle of the window
+ if (dir_len < lines) {
+ *first = 0;
+ *last = dir_len;
+ } else {
+ size_t off = lines / 2;
+ if (cursor < off) {
+ *first = 0;
+ if (dir_len > lines) *last = lines;
+ else *last = dir_len;
+ } else {
+ *first = cursor - off;
+ if (dir_len > cursor + off) *last = cursor + off;
+ else *last = dir_len;
+ }
+ }
+}