diff --git a/Makefile b/Makefile index 376cac6..6f01316 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ CWARN=-Wall -Wpedantic -Wextra -Wno-unknown-pragmas -Wno-missing-field-initializ -Wno-padded -Wsign-conversion -Wno-missing-noreturn -Wno-cast-qual -Wtype-limits #CFLAGS += -fsanitize=address -fno-omit-frame-pointer -CFILES=columns.c bterm.c +CFILES=draw.c bterm.c OBJFILES=$(CFILES:.c=.o) all: $(NAME) diff --git a/bb.c b/bb.c index 33e5a04..c753955 100644 --- a/bb.c +++ b/bb.c @@ -21,20 +21,18 @@ #include #include "bb.h" -#include "columns.h" +#include "draw.h" // Functions void bb_browse(bb_t *bb, const char *initial_path); static void check_cmdfile(bb_t *bb); static void cleanup(void); static void cleanup_and_raise(int sig); -static const char* color_of(mode_t mode); #ifdef __APPLE__ static int compare_files(void *v, const void *v1, const void *v2); #else static int compare_files(const void *v1, const void *v2, void *v); #endif -static int fputs_escaped(FILE *f, const char *str, const char *color); static void handle_next_key_binding(bb_t *bb); static void init_term(void); static int is_simple_bbcmd(const char *s); @@ -45,7 +43,6 @@ static char* normalize_path(const char *root, const char *path, char *pbuf); static int populate_files(bb_t *bb, const char *path); static void print_bindings(int fd); static void run_bbcmd(bb_t *bb, const char *cmd); -static void render(bb_t *bb); static void restore_term(const struct termios *term); static int run_script(bb_t *bb, const char *cmd); static void set_columns(bb_t *bb, const char *cols); @@ -90,20 +87,6 @@ static const struct termios default_termios = { static const char *description_str = "bb - an itty bitty console TUI file browser\n"; static const char *usage_str = "Usage: bb (-h/--help | -v/--version | -s | -d | -0 | +command)* [[--] directory]\n"; -column_t columns[255] = { - ['*'] = {.name = "*", .render = col_selected}, - ['n'] = {.name = "Name", .render = col_name, .stretchy = 1}, - ['s'] = {.name = " Size", .render = col_size}, - ['p'] = {.name = "Perm", .render = col_perm}, - ['m'] = {.name = " Modified", .render = col_mreltime}, - ['M'] = {.name = " Modified ", .render = col_mtime}, - ['a'] = {.name = " Accessed", .render = col_areltime}, - ['A'] = {.name = " Accessed ", .render = col_atime}, - ['c'] = {.name = " Created", .render = col_creltime}, - ['C'] = {.name = " Created ", .render = col_ctime}, - ['r'] = {.name = "Random", .render = col_random}, -}; - // Variables used within this file to track global state static binding_t bindings[MAX_BINDINGS]; @@ -111,8 +94,7 @@ static struct termios orig_termios, bb_termios; static FILE *tty_out = NULL, *tty_in = NULL; static struct winsize winsize = {0}; static char cmdfilename[PATH_MAX] = {0}; -static proc_t *running_procs = NULL; -static int dirty = 1; +static bb_t *current_bb = NULL; /* * Use bb to browse the filesystem. @@ -124,7 +106,7 @@ void bb_browse(bb_t *bb, const char *initial_path) run_script(bb, "bbstartup"); check_cmdfile(bb); while (!bb->should_quit) { - render(bb); + render(tty_out, bb); handle_next_key_binding(bb); } run_script(bb, "bbshutdown"); @@ -157,8 +139,12 @@ static void cleanup_and_raise(int sig) { cleanup(); int childsig = (sig == SIGTSTP || sig == SIGSTOP) ? sig : SIGHUP; - for (proc_t *p = running_procs; p; p = p->running.next) - kill(p->pid, childsig); + if (current_bb) { + for (proc_t *p = current_bb->running_procs; p; p = p->running.next) { + kill(p->pid, childsig); + LL_REMOVE(p, running); + } + } raise(sig); // This code will only ever be run if sig is SIGTSTP/SIGSTOP, otherwise, raise() won't return: init_term(); @@ -182,18 +168,6 @@ static void cleanup(void) } } -/* - * Returns the color of a file listing, given its mode. - */ -static const char* color_of(mode_t mode) -{ - if (S_ISDIR(mode)) return DIR_COLOR; - else if (S_ISLNK(mode)) return LINK_COLOR; - else if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) - return EXECUTABLE_COLOR; - else return NORMAL_COLOR; -} - /* * Used for sorting, this function compares files according to the sorting-related options, * like bb->sort @@ -261,29 +235,6 @@ static int compare_files(const void *v1, const void *v2, void *v) #undef COMPARE_TIME } -/* - * Print a string, but replacing bytes like '\n' with a red-colored "\n". - * The color argument is what color to put back after the red. - * Returns the number of bytes that were escaped. - */ -static int fputs_escaped(FILE *f, const char *str, const char *color) -{ - static const char *escapes = " abtnvfr e"; - int escaped = 0; - for (const char *c = str; *c; ++c) { - if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc. - fprintf(f, "\033[31m\\%c%s", escapes[(int)*c], color); - ++escaped; - } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc. - fprintf(f, "\033[31m\\x%02X%s", *c, color); - ++escaped; - } else { - fputc(*c, f); - } - } - return escaped; -} - /* * Wait until the user has pressed a key with an associated key binding and run * that binding. @@ -295,7 +246,7 @@ static void handle_next_key_binding(bb_t *bb) do { do { key = bgetkey(tty_in, &mouse_x, &mouse_y); - if (key == -1 && dirty) return; + if (key == -1 && bb->dirty) return; } while (key == -1); binding = NULL; @@ -309,10 +260,10 @@ static void handle_next_key_binding(bb_t *bb) char bbmousecol[2] = {0, 0}, bbclicked[PATH_MAX]; if (mouse_x != -1 && mouse_y != -1) { + int *colwidths = get_column_widths(bb->columns, winsize.ws_col-1); // Get bb column: for (int col = 0, x = 0; bb->columns[col]; col++, x++) { - if (!columns[(int)bb->columns[col]].name) continue; - x += (int)strlen(columns[(int)bb->columns[col]].name); + x += colwidths[col]; if (x >= mouse_x) { bbmousecol[0] = bb->columns[col]; break; @@ -522,7 +473,7 @@ static int populate_files(bb_t *bb, const char *path) setenv("BBPREVPATH", bb->prev_path, 1); } - dirty = 1; + bb->dirty = 1; strcpy(bb->path, pbuf); set_title(bb); @@ -710,10 +661,10 @@ static void run_bbcmd(bb_t *bb, const char *cmd) } } else if (matches_cmd(cmd, "fg:") || matches_cmd(cmd, "fg")) { // +fg: int nprocs = 0; - for (proc_t *p = running_procs; p; p = p->running.next) ++nprocs; + for (proc_t *p = bb->running_procs; p; p = p->running.next) ++nprocs; int fg = value ? nprocs - (int)strtol(value, NULL, 10) : 0; proc_t *child = NULL; - for (proc_t *p = running_procs; p && !child; p = p->running.next) { + for (proc_t *p = bb->running_procs; p && !child; p = p->running.next) { if (fg-- == 0) child = p; } if (!child) return; @@ -728,7 +679,7 @@ static void run_bbcmd(bb_t *bb, const char *cmd) signal(SIGTTOU, SIG_DFL); init_term(); set_title(bb); - dirty = 1; + bb->dirty = 1; } else if (matches_cmd(cmd, "glob:")) { // +glob: set_globs(bb, value[0] ? value : "*"); populate_files(bb, bb->path); @@ -826,163 +777,6 @@ static void run_bbcmd(bb_t *bb, const char *cmd) } } -/* - * Draw everything to the screen. - * If `dirty` is false, then use terminal scrolling to move the file listing - * around and only update the files that have changed. - */ -static void render(bb_t *bb) -{ - static int lastcursor = -1, lastscroll = -1; - - if (!dirty) { - // Use terminal scrolling: - if (lastscroll > bb->scroll) { - fprintf(tty_out, "\033[3;%dr\033[%dT\033[1;%dr", winsize.ws_row-1, lastscroll - bb->scroll, winsize.ws_row); - } else if (lastscroll < bb->scroll) { - fprintf(tty_out, "\033[3;%dr\033[%dS\033[1;%dr", winsize.ws_row-1, bb->scroll - lastscroll, winsize.ws_row); - } - } - - int colwidths[MAX_COLS] = {0}; - int space = winsize.ws_col - 1, nstretchy = 0; - for (int c = 0; bb->columns[c]; c++) { - column_t col = columns[(int)bb->columns[c]]; - if (!col.name) continue; - if (col.stretchy) { - ++nstretchy; - } else { - colwidths[c] = strlen(col.name) + 1; - space -= colwidths[c]; - } - if (c > 0) --space; - } - for (int c = 0; bb->columns[c]; c++) - if (columns[(int)bb->columns[c]].stretchy) - colwidths[c] = space / nstretchy; - - if (dirty) { - // Path - move_cursor(tty_out, 0, 0); - const char *color = TITLE_COLOR; - fputs(color, tty_out); - - char *home = getenv("HOME"); - if (home && strncmp(bb->path, home, strlen(home)) == 0) { - fputs("~", tty_out); - fputs_escaped(tty_out, bb->path + strlen(home), color); - } else { - fputs_escaped(tty_out, bb->path, color); - } - fprintf(tty_out, "\033[0;2m[%s]", bb->globpats); - fputs(" \033[K\033[0m", tty_out); - - static const char *help = "Press '?' to see key bindings "; - move_cursor(tty_out, MAX(0, winsize.ws_col - (int)strlen(help)), 0); - fputs(help, tty_out); - fputs("\033[K\033[0m", tty_out); - - // Columns - move_cursor(tty_out, 0, 1); - fputs("\033[0;44;30m\033[K", tty_out); - int x = 0; - for (int c = 0; bb->columns[c]; c++) { - column_t col = columns[(int)bb->columns[c]]; - if (!col.name) continue; - const char *title = col.name; - if (!title) title = ""; - move_cursor_col(tty_out, x); - if (c > 0) { - fputs("┃\033[K", tty_out); - x += 1; - } - const char *indicator = " "; - if (bb->columns[c] == bb->sort[1]) - indicator = bb->sort[0] == '-' ? RSORT_INDICATOR : SORT_INDICATOR; - move_cursor_col(tty_out, x); - fputs(indicator, tty_out); - fputs(title, tty_out); - x += colwidths[c]; - } - fputs(" \033[K\033[0m", tty_out); - } - - if (bb->nfiles == 0) { - move_cursor(tty_out, 0, 2); - fputs("\033[37;2m ...no files here... \033[0m\033[J", tty_out); - } else { - entry_t **files = bb->files; - for (int i = bb->scroll; i < bb->scroll + ONSCREEN && i < bb->nfiles; i++) { - if (!(dirty || i == bb->cursor || i == lastcursor || - i < lastscroll || i >= lastscroll + ONSCREEN)) { - continue; - } - - entry_t *entry = files[i]; - const char *color = i == bb->cursor ? - CURSOR_COLOR : color_of(entry->info.st_mode); - fputs(color, tty_out); - int x = 0, y = i - bb->scroll + 2; - move_cursor(tty_out, x, y); - for (int c = 0; bb->columns[c]; c++) { - column_t col = columns[(int)bb->columns[c]]; - if (!col.name) continue; - move_cursor_col(tty_out, x); - if (c > 0) { // Separator | - if (i == bb->cursor) fprintf(tty_out, "\033[2m┃\033[22m"); - else fprintf(tty_out, "\033[37;2m┃\033[22m%s", color); - x += 1; - } - char buf[PATH_MAX * 2] = {0}; - col.render(entry, color, buf, colwidths[c]); - fprintf(tty_out, "%s\033[K", buf); - x += colwidths[c]; - } - fputs("\033[0m", tty_out); - } - move_cursor(tty_out, 0, MIN(bb->nfiles - bb->scroll, ONSCREEN) + 2); - fputs("\033[J", tty_out); - } - - // Scrollbar: - if (bb->nfiles > ONSCREEN) { - int height = (ONSCREEN*ONSCREEN + (bb->nfiles-1))/bb->nfiles; - int start = 2 + (bb->scroll*ONSCREEN)/bb->nfiles; - for (int i = 2; i < 2 + ONSCREEN; i++) { - move_cursor(tty_out, winsize.ws_col-1, i); - fprintf(tty_out, "%s\033[0m", - (i >= start && i < start + height) ? SCROLLBAR_FG : SCROLLBAR_BG); - } - } - - // Bottom Line: - move_cursor(tty_out, winsize.ws_col/2, winsize.ws_row - 1); - fputs("\033[0m\033[K", tty_out); - int x = winsize.ws_col; - if (bb->selected) { // Number of selected files - int n = 0; - for (entry_t *s = bb->selected; s; s = s->selected.next) ++n; - x -= 14; - for (int k = n; k; k /= 10) x--; - move_cursor(tty_out, MAX(0, x), winsize.ws_row - 1); - fprintf(tty_out, "\033[41;30m %d Selected \033[0m", n); - } - int nprocs = 0; - for (proc_t *p = running_procs; p; p = p->running.next) ++nprocs; - if (nprocs > 0) { // Number of suspended processes - x -= 13; - for (int k = nprocs; k; k /= 10) x--; - move_cursor(tty_out, MAX(0, x), winsize.ws_row - 1); - fprintf(tty_out, "\033[44;30m %d Suspended \033[0m", nprocs); - } - move_cursor(tty_out, winsize.ws_col/2, winsize.ws_row - 1); - - lastcursor = bb->cursor; - lastscroll = bb->scroll; - fflush(tty_out); - dirty = 0; -} - /* * Close the /dev/tty terminals and restore some of the attributes. */ @@ -1032,9 +826,9 @@ static int run_script(bb_t *bb, const char *cmd) err("Failed to fork"); (void)setpgid(proc->pid, proc->pid); - LL_PREPEND(running_procs, proc, running); + LL_PREPEND(bb->running_procs, proc, running); int status = wait_for_process(&proc); - dirty = 1; + bb->dirty = 1; return status; } @@ -1095,7 +889,7 @@ static void set_interleave(bb_t *bb, int interleave) bb->interleave_dirs = interleave; if (interleave) setenv("BBINTERLEAVE", "interleave", 1); else unsetenv("BBINTERLEAVE"); - dirty = 1; + bb->dirty = 1; } /* @@ -1126,7 +920,7 @@ static void set_selected(bb_t *bb, entry_t *e, int selected) if (IS_SELECTED(e) == selected) return; if (bb->nfiles > 0 && e != bb->files[bb->cursor]) - dirty = 1; + bb->dirty = 1; if (selected) { LL_PREPEND(bb->selected, e, selected); @@ -1196,7 +990,7 @@ static void sort_files(bb_t *bb) #endif for (int i = 0; i < bb->nfiles; i++) bb->files[i]->index = i; - dirty = 1; + bb->dirty = 1; } /* @@ -1219,9 +1013,7 @@ static char *trim(char *s) static void update_term_size(int sig) { (void)sig; - struct winsize oldsize = winsize; ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize); - dirty |= (oldsize.ws_col != winsize.ws_col || oldsize.ws_row != winsize.ws_row); } /* @@ -1395,6 +1187,7 @@ int main(int argc, char *argv[]) .columns = "*smpn", .sort = "+n", }; + current_bb = &bb; set_globs(&bb, "*"); init_term(); bb_browse(&bb, full_initial_path); diff --git a/bb.h b/bb.h index e69d45e..5c53efd 100644 --- a/bb.h +++ b/bb.h @@ -10,7 +10,6 @@ #include "bterm.h" #include "entry.h" -#include "columns.h" // Macros: #define BB_VERSION "0.27.0" @@ -27,8 +26,6 @@ // Configurable options: #define SCROLLOFF MIN(5, (winsize.ws_row-4)/2) -#define SORT_INDICATOR "↓" -#define RSORT_INDICATOR "↑" // Colors (using ANSI escape sequences): #define TITLE_COLOR "\033[37;1m" #define NORMAL_COLOR "\033[37m" @@ -70,7 +67,7 @@ fputs(" Press any key to continue...\033[0m ", tty_out); \ fflush(tty_out); \ while (bgetkey(tty_in, NULL, NULL) == -1) usleep(100); \ - dirty = 1; \ + bb->dirty = 1; \ } while (0) #define LL_PREPEND(head, node, name) do { \ @@ -96,6 +93,14 @@ typedef struct { char *description; } binding_t; +// For keeping track of child processes +typedef struct proc_s { + pid_t pid; + struct { + struct proc_s *next, **atme; + } running; +} proc_t; + typedef struct bb_s { entry_t *hash[HASH_SIZE]; entry_t **files; @@ -110,16 +115,10 @@ typedef struct bb_s { char columns[MAX_COLS+1]; unsigned int interleave_dirs : 1; unsigned int should_quit : 1; + unsigned int dirty : 1; + proc_t *running_procs; } bb_t; -// For keeping track of child processes -typedef struct proc_s { - pid_t pid; - struct { - struct proc_s *next, **atme; - } running; -} proc_t; - // Hack to get TinyCC (TCC) compilation to work: // https://lists.nongnu.org/archive/html/tinycc-devel/2018-07/msg00000.html #ifdef __TINYC__ diff --git a/columns.c b/columns.c deleted file mode 100644 index a729cd2..0000000 --- a/columns.c +++ /dev/null @@ -1,161 +0,0 @@ -/* - * columns.c - This file contains logic for drawing columns. - */ - -#include -#include -#include -#include - -#include "columns.h" -#include "entry.h" - -#define E_ISDIR(e) (S_ISDIR(S_ISLNK((e)->info.st_mode) ? (e)->linkedmode : (e)->info.st_mode)) - -static void lpad(char *buf, int width) -{ - int len = strlen(buf); - if (len < width) { - int pad = width - len; - memmove(&buf[pad], buf, (size_t)len + 1); - while (pad > 0) buf[--pad] = ' '; - } -} - -static char* stpcpy_escaped(char *buf, const char *str, const char *color) -{ - static const char *escapes = " abtnvfr e"; - for (const char *c = str; *c; ++c) { - if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc. - buf += sprintf(buf, "\033[31m\\%c%s", escapes[(int)*c], color); - } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc. - buf += sprintf(buf, "\033[31m\\x%02X%s", *c, color); - } else { - *(buf++) = *c; - } - } - *buf = '\0'; - return buf; -} - -static void timeago(char *buf, time_t t) -{ - const int SECOND = 1; - const int MINUTE = 60 * SECOND; - const int HOUR = 60 * MINUTE; - const int DAY = 24 * HOUR; - const int MONTH = 30 * DAY; - const int YEAR = 365 * DAY; - - time_t now = time(0); - double delta = difftime(now, t); - - if (delta < 1.5) - sprintf(buf, "a second"); - else if (delta < 1 * MINUTE) - sprintf(buf, "%d seconds", (int)delta); - else if (delta < 2 * MINUTE) - sprintf(buf, "a minute"); - else if (delta < 1 * HOUR) - sprintf(buf, "%d minutes", (int)delta/MINUTE); - else if (delta < 2 * HOUR) - sprintf(buf, "an hour"); - else if (delta < 1 * DAY) - sprintf(buf, "%d hours", (int)delta/HOUR); - else if (delta < 2 * DAY) - sprintf(buf, "yesterday"); - else if (delta < 1 * MONTH) - sprintf(buf, "%d days", (int)delta/DAY); - else if (delta < 2 * MONTH) - sprintf(buf, "a month"); - else if (delta < 1 * YEAR) - sprintf(buf, "%d months", (int)delta/MONTH); - else if (delta < 2 * YEAR) - sprintf(buf, "a year"); - else - sprintf(buf, "%d years", (int)delta/YEAR); -} - -void col_mreltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_mtime); - lpad(buf, width); -} - -void col_areltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_atime); - lpad(buf, width); -} - -void col_creltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_ctime); - lpad(buf, width); -} - -void col_mtime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_mtime))); -} - -void col_atime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_atime))); -} - -void col_ctime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_ctime))); -} - -void col_selected(entry_t *entry, const char *color, char *buf, int width) { - (void)width; - buf = stpcpy(buf, IS_SELECTED(entry) ? SELECTED_INDICATOR : NOT_SELECTED_INDICATOR); - buf = stpcpy(buf, color); -} - -void col_perm(entry_t *entry, const char *color, char *buf, int width) { - (void)color; (void)width; - sprintf(buf, " %03o", entry->info.st_mode & 0777); -} - -void col_random(entry_t *entry, const char *color, char *buf, int width) -{ - (void)color; - sprintf(buf, "%*d", width, entry->shufflepos); -} - -void col_size(entry_t *entry, const char *color, char *buf, int width) -{ - (void)color; (void)width; - int j = 0; - const char* units = "BKMGTPEZY"; - double bytes = (double)entry->info.st_size; - while (bytes > 1024) { - bytes /= 1024; - j++; - } - sprintf(buf, " %6.*f%c ", j > 0 ? 1 : 0, bytes, units[j]); -} - -void col_name(entry_t *entry, const char *color, char *buf, int width) -{ - (void)width; - if (entry->no_esc) buf = stpcpy(buf, entry->name); - else buf = stpcpy_escaped(buf, entry->name, color); - - if (E_ISDIR(entry)) buf = stpcpy(buf, "/"); - - if (!entry->linkname) return; - - buf = stpcpy(buf, "\033[2m -> \033[3m"); - buf = stpcpy(buf, color); - if (entry->link_no_esc) buf = stpcpy(buf, entry->linkname); - else buf = stpcpy_escaped(buf, entry->linkname, color); - - if (S_ISDIR(entry->linkedmode)) - buf = stpcpy(buf, "/"); - - buf = stpcpy(buf, "\033[22;23m"); -} diff --git a/draw.c b/draw.c new file mode 100644 index 0000000..38e06bc --- /dev/null +++ b/draw.c @@ -0,0 +1,395 @@ +/* + * draw.c - This file contains logic for drawing columns. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "bterm.h" +#include "draw.h" +#include "entry.h" + +#define E_ISDIR(e) (S_ISDIR(S_ISLNK((e)->info.st_mode) ? (e)->linkedmode : (e)->info.st_mode)) + +column_t column_info[255] = { + ['*'] = {.name = "*", .render = col_selected}, + ['n'] = {.name = "Name", .render = col_name, .stretchy = 1}, + ['s'] = {.name = " Size", .render = col_size}, + ['p'] = {.name = "Perm", .render = col_perm}, + ['m'] = {.name = " Modified", .render = col_mreltime}, + ['M'] = {.name = " Modified ", .render = col_mtime}, + ['a'] = {.name = " Accessed", .render = col_areltime}, + ['A'] = {.name = " Accessed ", .render = col_atime}, + ['c'] = {.name = " Created", .render = col_creltime}, + ['C'] = {.name = " Created ", .render = col_ctime}, + ['r'] = {.name = "Random", .render = col_random}, +}; + +static void lpad(char *buf, int width) +{ + int len = strlen(buf); + if (len < width) { + int pad = width - len; + memmove(&buf[pad], buf, (size_t)len + 1); + while (pad > 0) buf[--pad] = ' '; + } +} + +static char* stpcpy_escaped(char *buf, const char *str, const char *color) +{ + static const char *escapes = " abtnvfr e"; + for (const char *c = str; *c; ++c) { + if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc. + buf += sprintf(buf, "\033[31m\\%c%s", escapes[(int)*c], color); + } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc. + buf += sprintf(buf, "\033[31m\\x%02X%s", *c, color); + } else { + *(buf++) = *c; + } + } + *buf = '\0'; + return buf; +} + +/* + * Print a string, but replacing bytes like '\n' with a red-colored "\n". + * The color argument is what color to put back after the red. + * Returns the number of bytes that were escaped. + */ +static int fputs_escaped(FILE *f, const char *str, const char *color) +{ + static const char *escapes = " abtnvfr e"; + int escaped = 0; + for (const char *c = str; *c; ++c) { + if (*c > 0 && *c <= '\x1b' && escapes[(int)*c] != ' ') { // "\n", etc. + fprintf(f, "\033[31m\\%c%s", escapes[(int)*c], color); + ++escaped; + } else if (*c >= 0 && !(' ' <= *c && *c <= '~')) { // "\x02", etc. + fprintf(f, "\033[31m\\x%02X%s", *c, color); + ++escaped; + } else { + fputc(*c, f); + } + } + return escaped; +} + +/* + * Returns the color of a file listing, given its mode. + */ +static const char* color_of(mode_t mode) +{ + if (S_ISDIR(mode)) return DIR_COLOR; + else if (S_ISLNK(mode)) return LINK_COLOR; + else if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) + return EXECUTABLE_COLOR; + else return NORMAL_COLOR; +} + +static void timeago(char *buf, time_t t) +{ + const int SECOND = 1; + const int MINUTE = 60 * SECOND; + const int HOUR = 60 * MINUTE; + const int DAY = 24 * HOUR; + const int MONTH = 30 * DAY; + const int YEAR = 365 * DAY; + + time_t now = time(0); + double delta = difftime(now, t); + + if (delta < 1.5) + sprintf(buf, "a second"); + else if (delta < 1 * MINUTE) + sprintf(buf, "%d seconds", (int)delta); + else if (delta < 2 * MINUTE) + sprintf(buf, "a minute"); + else if (delta < 1 * HOUR) + sprintf(buf, "%d minutes", (int)delta/MINUTE); + else if (delta < 2 * HOUR) + sprintf(buf, "an hour"); + else if (delta < 1 * DAY) + sprintf(buf, "%d hours", (int)delta/HOUR); + else if (delta < 2 * DAY) + sprintf(buf, "yesterday"); + else if (delta < 1 * MONTH) + sprintf(buf, "%d days", (int)delta/DAY); + else if (delta < 2 * MONTH) + sprintf(buf, "a month"); + else if (delta < 1 * YEAR) + sprintf(buf, "%d months", (int)delta/MONTH); + else if (delta < 2 * YEAR) + sprintf(buf, "a year"); + else + sprintf(buf, "%d years", (int)delta/YEAR); +} + +void col_mreltime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + timeago(buf, entry->info.st_mtime); + lpad(buf, width); +} + +void col_areltime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + timeago(buf, entry->info.st_atime); + lpad(buf, width); +} + +void col_creltime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + timeago(buf, entry->info.st_ctime); + lpad(buf, width); +} + +void col_mtime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_mtime))); +} + +void col_atime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_atime))); +} + +void col_ctime(entry_t *entry, const char *color, char *buf, int width) { + (void)color; + strftime(buf, (size_t)width, TIME_FMT, localtime(&(entry->info.st_ctime))); +} + +void col_selected(entry_t *entry, const char *color, char *buf, int width) { + (void)width; + buf = stpcpy(buf, IS_SELECTED(entry) ? SELECTED_INDICATOR : NOT_SELECTED_INDICATOR); + buf = stpcpy(buf, color); +} + +void col_perm(entry_t *entry, const char *color, char *buf, int width) { + (void)color; (void)width; + sprintf(buf, " %03o", entry->info.st_mode & 0777); +} + +void col_random(entry_t *entry, const char *color, char *buf, int width) +{ + (void)color; + sprintf(buf, "%*d", width, entry->shufflepos); +} + +void col_size(entry_t *entry, const char *color, char *buf, int width) +{ + (void)color; (void)width; + int j = 0; + const char* units = "BKMGTPEZY"; + double bytes = (double)entry->info.st_size; + while (bytes > 1024) { + bytes /= 1024; + j++; + } + sprintf(buf, " %6.*f%c ", j > 0 ? 1 : 0, bytes, units[j]); +} + +void col_name(entry_t *entry, const char *color, char *buf, int width) +{ + (void)width; + if (entry->no_esc) buf = stpcpy(buf, entry->name); + else buf = stpcpy_escaped(buf, entry->name, color); + + if (E_ISDIR(entry)) buf = stpcpy(buf, "/"); + + if (!entry->linkname) return; + + buf = stpcpy(buf, "\033[2m -> \033[3m"); + buf = stpcpy(buf, color); + if (entry->link_no_esc) buf = stpcpy(buf, entry->linkname); + else buf = stpcpy_escaped(buf, entry->linkname, color); + + if (S_ISDIR(entry->linkedmode)) + buf = stpcpy(buf, "/"); + + buf = stpcpy(buf, "\033[22;23m"); +} + +int *get_column_widths(char columns[], int width) +{ + // TODO: maybe memoize + static int colwidths[16] = {0}; + int space = width, nstretchy = 0; + for (int c = 0; columns[c]; c++) { + column_t col = column_info[(int)columns[c]]; + if (!col.name) continue; + if (col.stretchy) { + ++nstretchy; + } else { + colwidths[c] = strlen(col.name) + 1; + space -= colwidths[c]; + } + if (c > 0) --space; + } + for (int c = 0; columns[c]; c++) + if (column_info[(int)columns[c]].stretchy) + colwidths[c] = space / nstretchy; + return colwidths; +} + +void draw_column_labels(FILE *out, char columns[], char *sort, int width) +{ + int *colwidths = get_column_widths(columns, width); + fputs("\033[0;44;30m\033[K", out); + int x = 0; + for (int c = 0; columns[c]; c++) { + column_t col = column_info[(int)columns[c]]; + if (!col.name) continue; + const char *title = col.name; + move_cursor_col(out, x); + if (c > 0) { + fputs("┃\033[K", out); + x += 1; + } + const char *indicator = " "; + if (columns[c] == sort[1]) + indicator = sort[0] == '-' ? RSORT_INDICATOR : SORT_INDICATOR; + move_cursor_col(out, x); + fputs(indicator, out); + if (title) fputs(title, out); + x += colwidths[c]; + } + fputs(" \033[K\033[0m", out); +} + +void draw_row(FILE *out, char columns[], entry_t *entry, const char *color, int width) +{ + int *colwidths = get_column_widths(columns, width); + fputs(color, out); + int x = 0; + for (int c = 0; columns[c]; c++) { + column_t col = column_info[(int)columns[c]]; + if (!col.name) continue; + move_cursor_col(out, x); + if (c > 0) { // Separator | + fprintf(out, "\033[37;2m┃\033[22m%s", color); + x += 1; + } + char buf[PATH_MAX * 2] = {0}; + col.render(entry, color, buf, colwidths[c]); + fprintf(out, "%s\033[K", buf); + x += colwidths[c]; + } + fputs("\033[0m", out); +} + +/* + * Draw everything to the screen. + * If `dirty` is false, then use terminal scrolling to move the file listing + * around and only update the files that have changed. + */ +void render(FILE *out, bb_t *bb) +{ + static int lastcursor = -1, lastscroll = -1; + static struct winsize oldsize = {0}; + + struct winsize winsize; + ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize); + + bb->dirty |= (winsize.ws_row != oldsize.ws_row) || (winsize.ws_col != oldsize.ws_col); + + if (!bb->dirty) { + // Use terminal scrolling: + if (lastscroll > bb->scroll) { + fprintf(out, "\033[3;%dr\033[%dT\033[1;%dr", winsize.ws_row-1, lastscroll - bb->scroll, winsize.ws_row); + } else if (lastscroll < bb->scroll) { + fprintf(out, "\033[3;%dr\033[%dS\033[1;%dr", winsize.ws_row-1, bb->scroll - lastscroll, winsize.ws_row); + } + } + + if (bb->dirty) { + // Path + move_cursor(out, 0, 0); + const char *color = TITLE_COLOR; + fputs(color, out); + + char *home = getenv("HOME"); + if (home && strncmp(bb->path, home, strlen(home)) == 0) { + fputs("~", out); + fputs_escaped(out, bb->path + strlen(home), color); + } else { + fputs_escaped(out, bb->path, color); + } + fprintf(out, "\033[0;2m[%s]", bb->globpats); + fputs(" \033[K\033[0m", out); + + static const char *help = "Press '?' to see key bindings "; + move_cursor(out, MAX(0, winsize.ws_col - (int)strlen(help)), 0); + fputs(help, out); + fputs("\033[K\033[0m", out); + + // Columns + move_cursor(out, 0, 1); + fputs("\033[0;44;30m\033[K", out); + draw_column_labels(out, bb->columns, bb->sort, winsize.ws_col-1); + } + + if (bb->nfiles == 0) { + move_cursor(out, 0, 2); + fputs("\033[37;2m ...no files here... \033[0m\033[J", out); + } else { + entry_t **files = bb->files; + for (int i = bb->scroll; i < bb->scroll + ONSCREEN && i < bb->nfiles; i++) { + if (!(bb->dirty || i == bb->cursor || i == lastcursor || + i < lastscroll || i >= lastscroll + ONSCREEN)) { + continue; + } + + entry_t *entry = files[i]; + const char *color = i == bb->cursor ? + CURSOR_COLOR : color_of(entry->info.st_mode); + int x = 0, y = i - bb->scroll + 2; + move_cursor(out, x, y); + draw_row(out, bb->columns, entry, color, winsize.ws_col-1); + } + move_cursor(out, 0, MIN(bb->nfiles - bb->scroll, ONSCREEN) + 2); + fputs("\033[J", out); + } + + // Scrollbar: + if (bb->nfiles > ONSCREEN) { + int height = (ONSCREEN*ONSCREEN + (bb->nfiles-1))/bb->nfiles; + int start = 2 + (bb->scroll*ONSCREEN)/bb->nfiles; + for (int i = 2; i < 2 + ONSCREEN; i++) { + move_cursor(out, winsize.ws_col-1, i); + fprintf(out, "%s\033[0m", + (i >= start && i < start + height) ? SCROLLBAR_FG : SCROLLBAR_BG); + } + } + + // Bottom Line: + move_cursor(out, winsize.ws_col/2, winsize.ws_row - 1); + fputs("\033[0m\033[K", out); + int x = winsize.ws_col; + if (bb->selected) { // Number of selected files + int n = 0; + for (entry_t *s = bb->selected; s; s = s->selected.next) ++n; + x -= 14; + for (int k = n; k; k /= 10) x--; + move_cursor(out, MAX(0, x), winsize.ws_row - 1); + fprintf(out, "\033[41;30m %d Selected \033[0m", n); + } + int nprocs = 0; + for (proc_t *p = bb->running_procs; p; p = p->running.next) ++nprocs; + if (nprocs > 0) { // Number of suspended processes + x -= 13; + for (int k = nprocs; k; k /= 10) x--; + move_cursor(out, MAX(0, x), winsize.ws_row - 1); + fprintf(out, "\033[44;30m %d Suspended \033[0m", nprocs); + } + move_cursor(out, winsize.ws_col/2, winsize.ws_row - 1); + + lastcursor = bb->cursor; + lastscroll = bb->scroll; + fflush(out); + bb->dirty = 0; +} +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 diff --git a/columns.h b/draw.h similarity index 74% rename from columns.h rename to draw.h index 3c9c599..8ed1de0 100644 --- a/columns.h +++ b/draw.h @@ -1,15 +1,20 @@ /* - * columns.h - This file contains definitions for bb column-drawing code. + * draw.h - This file contains definitions for bb column-drawing code. */ #ifndef FILE_COLUMNS__H #define FILE_COLUMNS__H +#include + +#include "bb.h" #include "entry.h" #define TIME_FMT " %T %D " #define SELECTED_INDICATOR " \033[31;7m \033[0m" #define NOT_SELECTED_INDICATOR " " +#define SORT_INDICATOR "↓" +#define RSORT_INDICATOR "↑" typedef struct { const char *name; @@ -29,6 +34,11 @@ typedef enum { COL_SELECTED = '*', } column_e; +void draw_column_labels(FILE *out, char columns[], char *sort, int width); +void draw_row(FILE *out, char columns[], entry_t *entry, const char *color, int width); +int *get_column_widths(char columns[], int width); +void render(FILE *out, bb_t *bb); + void col_mreltime(entry_t *entry, const char *color, char *buf, int width); void col_areltime(entry_t *entry, const char *color, char *buf, int width); void col_creltime(entry_t *entry, const char *color, char *buf, int width); @@ -42,3 +52,4 @@ void col_size(entry_t *entry, const char *color, char *buf, int width); void col_name(entry_t *entry, const char *color, char *buf, int width); #endif +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1