diff --git a/Makefile b/Makefile index 37ee948..376cac6 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,26 @@ NAME=bb PREFIX= CC ?= gcc +G ?= O ?= -O2 -CFLAGS=-std=c99 -D_XOPEN_SOURCE=500 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L +CFLAGS=-std=c99 -D_XOPEN_SOURCE=700 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L CWARN=-Wall -Wpedantic -Wextra -Wno-unknown-pragmas -Wno-missing-field-initializers\ -Wno-padded -Wsign-conversion -Wno-missing-noreturn -Wno-cast-qual -Wtype-limits #CFLAGS += -fsanitize=address -fno-omit-frame-pointer -G= + +CFILES=columns.c bterm.c +OBJFILES=$(CFILES:.c=.o) all: $(NAME) clean: - rm -f $(NAME) + rm -f $(NAME) $(OBJFILES) -$(NAME): $(NAME).c bterm.h bb.h columns.h - $(CC) $(NAME).c $(CFLAGS) $(CWARN) $(G) $(O) -o $@ +.c.o: + $(CC) -c $(CFLAGS) $(CWARN) $(G) $(O) -o $@ $< + +$(NAME): $(OBJFILES) $(NAME).c + $(CC) $(CFLAGS) $(CWARN) $(G) $(O) -o $@ $(OBJFILES) $(NAME).c install: $(NAME) @prefix="$(PREFIX)"; \ diff --git a/bb.c b/bb.c index 78c864e..33e5a04 100644 --- a/bb.c +++ b/bb.c @@ -6,9 +6,107 @@ * This file contains the main source code of `bb`. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "bb.h" +#include "columns.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); +static entry_t* load_entry(bb_t *bb, const char *path); +static inline int matches_cmd(const char *str, const char *cmd); +static void* memcheck(void *p); +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); +static void set_cursor(bb_t *bb, int i); +static void set_globs(bb_t *bb, const char *globs); +static void set_interleave(bb_t *bb, int interleave); +static void set_selected(bb_t *bb, entry_t *e, int selected); +static void set_scroll(bb_t *bb, int i); +static void set_sort(bb_t *bb, const char *sort); +static void set_title(bb_t *bb); +static void sort_files(bb_t *bb); +static char *trim(char *s); +static int try_free_entry(entry_t *e); +static void update_term_size(int sig); +static int wait_for_process(proc_t **proc); + +// Constants +static const char *T_ENTER_BBMODE = T_OFF(T_SHOW_CURSOR ";" T_WRAP) T_ON(T_ALT_SCREEN ";" T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR); +static const char *T_LEAVE_BBMODE = T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR ";" T_ALT_SCREEN) T_ON(T_SHOW_CURSOR ";" T_WRAP); +static const char *T_LEAVE_BBMODE_PARTIAL = T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR) T_ON(T_WRAP); +static const struct termios default_termios = { + .c_iflag = ICRNL, + .c_oflag = OPOST | ONLCR | NL0 | CR0 | TAB0 | BS0 | VT0 | FF0, + .c_lflag = ISIG | ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE, + .c_cflag = CS8 | CREAD, + .c_cc[VINTR] = '', + .c_cc[VQUIT] = '', + .c_cc[VERASE] = 127, + .c_cc[VKILL] = '', + .c_cc[VEOF] = '', + .c_cc[VSTART] = '', + .c_cc[VSTOP] = '', + .c_cc[VSUSP] = '', + .c_cc[VREPRINT] = '', + .c_cc[VWERASE] = '', + .c_cc[VLNEXT] = '', + .c_cc[VDISCARD] = '', + .c_cc[VMIN] = 1, + .c_cc[VTIME] = 0, +}; + +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]; static struct termios orig_termios, bb_termios; static FILE *tty_out = NULL, *tty_in = NULL; static struct winsize winsize = {0}; @@ -36,7 +134,7 @@ void bb_browse(bb_t *bb, const char *initial_path) * Check the bb command file and run any and all commands that have been * written to it. */ -void check_cmdfile(bb_t *bb) +static void check_cmdfile(bb_t *bb) { FILE *cmdfile = fopen(cmdfilename, "r"); if (!cmdfile) return; @@ -55,7 +153,7 @@ void check_cmdfile(bb_t *bb) /* * Clean up the terminal before going to the default signal handling behavior. */ -void cleanup_and_raise(int sig) +static void cleanup_and_raise(int sig) { cleanup(); int childsig = (sig == SIGTSTP || sig == SIGSTOP) ? sig : SIGHUP; @@ -71,7 +169,7 @@ void cleanup_and_raise(int sig) /* * Reset the screen and delete the cmdfile */ -void cleanup(void) +static void cleanup(void) { if (cmdfilename[0]) { unlink(cmdfilename); @@ -87,7 +185,7 @@ void cleanup(void) /* * Returns the color of a file listing, given its mode. */ -const char* color_of(mode_t mode) +static const char* color_of(mode_t mode) { if (S_ISDIR(mode)) return DIR_COLOR; else if (S_ISLNK(mode)) return LINK_COLOR; @@ -101,9 +199,9 @@ const char* color_of(mode_t mode) * like bb->sort */ #ifdef __APPLE__ -int compare_files(void *v, const void *v1, const void *v2) +static int compare_files(void *v, const void *v1, const void *v2) #else -int compare_files(const void *v1, const void *v2, void *v) +static int compare_files(const void *v1, const void *v2, void *v) #endif { #define COMPARE(a, b) if ((a) != (b)) { return sign*((a) < (b) ? 1 : -1); } @@ -168,7 +266,7 @@ int compare_files(const void *v1, const void *v2, void *v) * The color argument is what color to put back after the red. * Returns the number of bytes that were escaped. */ -int fputs_escaped(FILE *f, const char *str, const char *color) +static int fputs_escaped(FILE *f, const char *str, const char *color) { static const char *escapes = " abtnvfr e"; int escaped = 0; @@ -190,7 +288,7 @@ int fputs_escaped(FILE *f, const char *str, const char *color) * Wait until the user has pressed a key with an associated key binding and run * that binding. */ -void handle_next_key_binding(bb_t *bb) +static void handle_next_key_binding(bb_t *bb) { int key, mouse_x, mouse_y; binding_t *binding; @@ -253,7 +351,7 @@ void handle_next_key_binding(bb_t *bb) * Initialize the terminal files for /dev/tty and set up some desired * attributes like passing Ctrl-c as a key instead of interrupting */ -void init_term(void) +static void init_term(void) { if (tcsetattr(fileno(tty_out), TCSANOW, &bb_termios) == -1) err("Couldn't tcsetattr"); @@ -287,7 +385,7 @@ static int is_simple_bbcmd(const char *s) * Warning: this does not deduplicate entries, and it's best if there aren't * duplicate entries hanging around. */ -entry_t* load_entry(bb_t *bb, const char *path) +static entry_t* load_entry(bb_t *bb, const char *path) { struct stat linkedstat, filestat; if (!path || !path[0]) return NULL; @@ -353,7 +451,7 @@ static inline int matches_cmd(const char *str, const char *cmd) * Memory allocation failures are unrecoverable in bb, so this wrapper just * prints an error message and exits if that happens. */ -void* memcheck(void *p) +static void* memcheck(void *p) { if (!p) err("Allocation failure"); return p; @@ -363,7 +461,7 @@ void* memcheck(void *p) * Prepend `root` to relative paths, replace "~" with $HOME. * The normalized path is stored in `normalized`. */ -char *normalize_path(const char *root, const char *path, char *normalized) +static char *normalize_path(const char *root, const char *path, char *normalized) { char pbuf[PATH_MAX] = {0}; if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) { @@ -387,7 +485,7 @@ char *normalize_path(const char *root, const char *path, char *normalized) * Remove all the files currently stored in bb->files and if `bb->path` is * non-NULL, update `bb` with a listing of the files in `path` */ -int populate_files(bb_t *bb, const char *path) +static int populate_files(bb_t *bb, const char *path) { int samedir = path && strcmp(bb->path, path) == 0; int old_scroll = bb->scroll; @@ -501,7 +599,7 @@ int populate_files(bb_t *bb, const char *path) /* * Print the current key bindings */ -void print_bindings(int fd) +static void print_bindings(int fd) { char buf[1000], buf2[1024]; for (size_t i = 0; bindings[i].script && i < sizeof(bindings)/sizeof(bindings[0]); i++) { @@ -535,7 +633,7 @@ void print_bindings(int fd) * Run a bb internal command (e.g. "+refresh") and return an indicator of what * needs to happen next. */ -void run_bbcmd(bb_t *bb, const char *cmd) +static void run_bbcmd(bb_t *bb, const char *cmd) { while (*cmd == ' ' || *cmd == '\n') ++cmd; if (strncmp(cmd, "bbcmd ", strlen("bbcmd ")) == 0) cmd = &cmd[strlen("bbcmd ")]; @@ -733,7 +831,7 @@ void run_bbcmd(bb_t *bb, const char *cmd) * If `dirty` is false, then use terminal scrolling to move the file listing * around and only update the files that have changed. */ -void render(bb_t *bb) +static void render(bb_t *bb) { static int lastcursor = -1, lastscroll = -1; @@ -888,7 +986,7 @@ void render(bb_t *bb) /* * Close the /dev/tty terminals and restore some of the attributes. */ -void restore_term(const struct termios *term) +static void restore_term(const struct termios *term) { tcsetattr(fileno(tty_out), TCSANOW, term); fputs(T_LEAVE_BBMODE_PARTIAL, tty_out); @@ -900,7 +998,7 @@ void restore_term(const struct termios *term) * the script (or pass the cursor file if none are selected). * Return the exit status of the script. */ -int run_script(bb_t *bb, const char *cmd) +static int run_script(bb_t *bb, const char *cmd) { proc_t *proc = memcheck(calloc(1, sizeof(proc_t))); signal(SIGTTOU, SIG_IGN); @@ -943,7 +1041,7 @@ int run_script(bb_t *bb, const char *cmd) /* * Set the columns displayed by bb. */ -void set_columns(bb_t *bb, const char *cols) +static void set_columns(bb_t *bb, const char *cols) { strncpy(bb->columns, cols, MAX_COLS); setenv("BBCOLUMNS", bb->columns, 1); @@ -952,7 +1050,7 @@ void set_columns(bb_t *bb, const char *cols) /* * Set bb's file cursor to the given index (and adjust the scroll as necessary) */ -void set_cursor(bb_t *bb, int newcur) +static void set_cursor(bb_t *bb, int newcur) { int oldcur = bb->cursor; if (newcur > bb->nfiles - 1) newcur = bb->nfiles - 1; @@ -981,7 +1079,7 @@ void set_cursor(bb_t *bb, int newcur) /* * Set the glob pattern(s) used by bb. Patterns are ' ' delimited */ -void set_globs(bb_t *bb, const char *globs) +static void set_globs(bb_t *bb, const char *globs) { free(bb->globpats); bb->globpats = memcheck(strdup(globs)); @@ -992,7 +1090,7 @@ void set_globs(bb_t *bb, const char *globs) /* * Set whether or not bb should interleave directories and files. */ -void set_interleave(bb_t *bb, int interleave) +static void set_interleave(bb_t *bb, int interleave) { bb->interleave_dirs = interleave; if (interleave) setenv("BBINTERLEAVE", "interleave", 1); @@ -1003,7 +1101,7 @@ void set_interleave(bb_t *bb, int interleave) /* * Set bb's scroll to the given index (and adjust the cursor as necessary) */ -void set_scroll(bb_t *bb, int newscroll) +static void set_scroll(bb_t *bb, int newscroll) { int delta = newscroll - bb->scroll; if (bb->nfiles <= ONSCREEN) { @@ -1023,7 +1121,7 @@ void set_scroll(bb_t *bb, int newscroll) /* * Select or deselect a file. */ -void set_selected(bb_t *bb, entry_t *e, int selected) +static void set_selected(bb_t *bb, entry_t *e, int selected) { if (IS_SELECTED(e) == selected) return; @@ -1043,7 +1141,7 @@ void set_selected(bb_t *bb, entry_t *e, int selected) /* * Set the sorting method used by bb to display files. */ -void set_sort(bb_t *bb, const char *sort) +static void set_sort(bb_t *bb, const char *sort) { char sortbuf[strlen(sort)+1]; strcpy(sortbuf, sort); @@ -1065,7 +1163,7 @@ void set_sort(bb_t *bb, const char *sort) /* * Set the xwindow title property to: bb - current path */ -void set_title(bb_t *bb) +static void set_title(bb_t *bb) { char *home = getenv("HOME"); if (home && strncmp(bb->path, home, strlen(home)) == 0) @@ -1078,7 +1176,7 @@ void set_title(bb_t *bb) * If the given entry is not viewed or selected, remove it from the * hash, free it, and return 1. */ -int try_free_entry(entry_t *e) +static int try_free_entry(entry_t *e) { if (IS_SELECTED(e) || IS_VIEWED(e) || !IS_LOADED(e)) return 0; LL_REMOVE(e, hash); @@ -1089,7 +1187,7 @@ int try_free_entry(entry_t *e) /* * Sort the files in bb according to bb's settings. */ -void sort_files(bb_t *bb) +static void sort_files(bb_t *bb) { #ifdef __APPLE__ qsort_r(bb->files, (size_t)bb->nfiles, sizeof(entry_t*), bb, compare_files); @@ -1118,7 +1216,7 @@ static char *trim(char *s) /* * Hanlder for SIGWINCH events */ -void update_term_size(int sig) +static void update_term_size(int sig) { (void)sig; struct winsize oldsize = winsize; @@ -1129,7 +1227,7 @@ void update_term_size(int sig) /* * Wait for a process to either suspend or exit and return the status. */ -int wait_for_process(proc_t **proc) +static int wait_for_process(proc_t **proc) { tcsetpgrp(fileno(tty_out), (*proc)->pid); int status; diff --git a/bb.h b/bb.h index 946f323..e69d45e 100644 --- a/bb.h +++ b/bb.h @@ -5,21 +5,12 @@ * * This file contains definitions and customization for `bb`. */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#ifndef FILE_BB__H +#define FILE_BB__H #include "bterm.h" +#include "entry.h" +#include "columns.h" // Macros: #define BB_VERSION "0.27.0" @@ -35,12 +26,9 @@ #define MAX_BINDINGS 1024 // Configurable options: -#define BB_TIME_FMT " %T %D " #define SCROLLOFF MIN(5, (winsize.ws_row-4)/2) #define SORT_INDICATOR "↓" #define RSORT_INDICATOR "↑" -#define SELECTED_INDICATOR " \033[31;7m \033[0m" -#define NOT_SELECTED_INDICATOR " " // Colors (using ANSI escape sequences): #define TITLE_COLOR "\033[37;1m" #define NORMAL_COLOR "\033[37m" @@ -53,9 +41,6 @@ #define MAX(a,b) ((a) < (b) ? (b) : (a)) #define MIN(a,b) ((a) > (b) ? (b) : (a)) -#define IS_SELECTED(p) (((p)->selected.atme) != NULL) -#define IS_VIEWED(p) ((p)->index >= 0) -#define IS_LOADED(p) ((p)->hash.atme != NULL) #define LOWERCASE(c) ('A' <= (c) && (c) <= 'Z' ? ((c) + 'a' - 'A') : (c)) #define E_ISDIR(e) (S_ISDIR(S_ISLNK((e)->info.st_mode) ? (e)->linkedmode : (e)->info.st_mode)) #define ONSCREEN (winsize.ws_row - 3) @@ -111,41 +96,6 @@ typedef struct { char *description; } binding_t; -typedef enum { - COL_NONE = 0, - COL_NAME = 'n', - COL_SIZE = 's', - COL_PERM = 'p', - COL_MTIME = 'm', - COL_CTIME = 'c', - COL_ATIME = 'a', - COL_RANDOM = 'r', - COL_SELECTED = '*', -} column_e; - -/* entry_t uses intrusive linked lists. This means entries can only belong to - * one list at a time, in this case the list of selected entries. 'atme' is an - * indirect pointer to either the 'next' field of the previous list member, or - * the variable that points to the first list member. In other words, - * item->next->atme == &item->next and firstitem->atme == &firstitem. - */ -typedef struct entry_s { - struct { - struct entry_s *next, **atme; - } selected, hash; - char *name, *linkname; - struct stat info; - mode_t linkedmode; - int no_esc : 1; - int link_no_esc : 1; - int shufflepos; - int index; - char fullname[1]; - // ------- fullname must be last! -------------- - // When entries are allocated, extra space on the end is reserved to fill - // in fullname. -} entry_t; - typedef struct bb_s { entry_t *hash[HASH_SIZE]; entry_t **files; @@ -170,81 +120,11 @@ typedef struct proc_s { } running; } proc_t; -static binding_t bindings[MAX_BINDINGS]; - -#include "columns.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); -static entry_t* load_entry(bb_t *bb, const char *path); -static inline int matches_cmd(const char *str, const char *cmd); -static void* memcheck(void *p); -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); -static void set_cursor(bb_t *bb, int i); -static void set_globs(bb_t *bb, const char *globs); -static void set_interleave(bb_t *bb, int interleave); -static void set_selected(bb_t *bb, entry_t *e, int selected); -static void set_scroll(bb_t *bb, int i); -static void set_sort(bb_t *bb, const char *sort); -static void set_title(bb_t *bb); -static void sort_files(bb_t *bb); -static char *trim(char *s); -static int try_free_entry(entry_t *e); -static void update_term_size(int sig); -static int wait_for_process(proc_t **proc); - -// Constants -static const char *T_ENTER_BBMODE = T_OFF(T_SHOW_CURSOR ";" T_WRAP) T_ON(T_ALT_SCREEN ";" T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR); -static const char *T_LEAVE_BBMODE = T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR ";" T_ALT_SCREEN) T_ON(T_SHOW_CURSOR ";" T_WRAP); -static const char *T_LEAVE_BBMODE_PARTIAL = T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR) T_ON(T_WRAP); -static const struct termios default_termios = { - .c_iflag = ICRNL, - .c_oflag = OPOST | ONLCR | NL0 | CR0 | TAB0 | BS0 | VT0 | FF0, - .c_lflag = ISIG | ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE, - .c_cflag = CS8 | CREAD, - .c_cc[VINTR] = '', - .c_cc[VQUIT] = '', - .c_cc[VERASE] = 127, - .c_cc[VKILL] = '', - .c_cc[VEOF] = '', - .c_cc[VSTART] = '', - .c_cc[VSTOP] = '', - .c_cc[VSUSP] = '', - .c_cc[VREPRINT] = '', - .c_cc[VWERASE] = '', - .c_cc[VLNEXT] = '', - .c_cc[VDISCARD] = '', - .c_cc[VMIN] = 1, - .c_cc[VTIME] = 0, -}; - -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"; - // Hack to get TinyCC (TCC) compilation to work: // https://lists.nongnu.org/archive/html/tinycc-devel/2018-07/msg00000.html #ifdef __TINYC__ void * __dso_handle __attribute((visibility("hidden"))) = &__dso_handle; #endif +#endif // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 diff --git a/bterm.c b/bterm.c new file mode 100644 index 0000000..0a5d68f --- /dev/null +++ b/bterm.c @@ -0,0 +1,268 @@ +/* + * bterm.h + * Copyright 2019 Bruce Hill + * Released under the MIT License + * + * Implementations of some basic terminal stuff, like reading keys and some + * terminal escape sequences. + */ + +#include +#include +#include +#include + +#include "bterm.h" + +typedef struct { + int key; + const char *name; +} keyname_t; + +static keyname_t key_names[] = { + {KEY_SPACE, "Space"}, {KEY_BACKSPACE2, "Backspace"}, + {KEY_F1, "F1"}, {KEY_F2, "F2"}, {KEY_F3, "F3"}, {KEY_F4, "F4"}, {KEY_F5, "F5"}, + {KEY_F6, "F6"}, {KEY_F7, "F7"}, {KEY_F8, "F8"}, {KEY_F9, "F9"}, {KEY_F10, "F10"}, + {KEY_F11, "F11"}, {KEY_F12, "F12"}, + {KEY_INSERT, "Insert"}, {KEY_DELETE, "Delete"}, + {KEY_HOME, "Home"}, {KEY_END, "End"}, + {KEY_PGUP, "PgUp"}, {KEY_PGUP, "Page Up"}, + {KEY_PGDN, "PgDn"}, {KEY_PGDN, "Page Down"}, + {KEY_ARROW_UP, "Up"}, {KEY_ARROW_DOWN, "Down"}, {KEY_ARROW_LEFT, "Left"}, {KEY_ARROW_RIGHT, "Right"}, + {MOUSE_LEFT_PRESS, "Left press"}, {MOUSE_RIGHT_PRESS, "Right press"}, {MOUSE_MIDDLE_PRESS, "Middle press"}, + {MOUSE_LEFT_DRAG, "Left drag"}, {MOUSE_RIGHT_DRAG, "Right drag"}, {MOUSE_MIDDLE_DRAG, "Middle drag"}, + {MOUSE_LEFT_RELEASE, "Left click"}, {MOUSE_RIGHT_RELEASE, "Right click"}, {MOUSE_MIDDLE_RELEASE, "Middle click"}, + {MOUSE_LEFT_RELEASE, "Left up"}, {MOUSE_RIGHT_RELEASE, "Right up"}, {MOUSE_MIDDLE_RELEASE, "Middle up"}, + {MOUSE_LEFT_RELEASE, "Left release"}, {MOUSE_RIGHT_RELEASE, "Right release"}, {MOUSE_MIDDLE_RELEASE, "Middle release"}, + {MOUSE_LEFT_DOUBLE, "Double left click"}, {MOUSE_RIGHT_DOUBLE, "Double right click"}, {MOUSE_MIDDLE_DOUBLE, "Double middle click"}, + {MOUSE_WHEEL_RELEASE, "Mouse wheel up"}, {MOUSE_WHEEL_PRESS, "Mouse wheel down"}, + {KEY_TAB, "Tab"}, {KEY_ENTER, "Enter"}, {KEY_ENTER, "Return"}, + {KEY_CTRL_A, "Ctrl-a"}, {KEY_CTRL_B, "Ctrl-b"}, {KEY_CTRL_C, "Ctrl-c"}, + {KEY_CTRL_D, "Ctrl-d"}, {KEY_CTRL_E, "Ctrl-e"}, {KEY_CTRL_F, "Ctrl-f"}, + {KEY_CTRL_G, "Ctrl-g"}, {KEY_CTRL_H, "Ctrl-h"}, {KEY_CTRL_I, "Ctrl-i"}, + {KEY_CTRL_J, "Ctrl-j"}, {KEY_CTRL_K, "Ctrl-k"}, {KEY_CTRL_L, "Ctrl-l"}, + {KEY_CTRL_M, "Ctrl-m"}, {KEY_CTRL_N, "Ctrl-n"}, {KEY_CTRL_O, "Ctrl-o"}, + {KEY_CTRL_P, "Ctrl-p"}, {KEY_CTRL_Q, "Ctrl-q"}, {KEY_CTRL_R, "Ctrl-r"}, + {KEY_CTRL_S, "Ctrl-s"}, {KEY_CTRL_T, "Ctrl-t"}, {KEY_CTRL_U, "Ctrl-u"}, + {KEY_CTRL_V, "Ctrl-v"}, {KEY_CTRL_W, "Ctrl-w"}, {KEY_CTRL_X, "Ctrl-x"}, + {KEY_CTRL_Y, "Ctrl-y"}, {KEY_CTRL_Z, "Ctrl-z"}, + {KEY_ESC, "Esc"}, {KEY_ESC, "Escape"}, + {KEY_CTRL_TILDE, "Ctrl-~"}, {KEY_CTRL_BACKSLASH, "Ctrl-\\"}, + {KEY_CTRL_LSQ_BRACKET, "Ctrl-]"}, {KEY_CTRL_RSQ_BRACKET, "Ctrl-]"}, + {KEY_CTRL_UNDERSCORE, "Ctrl-_"}, {KEY_CTRL_SLASH, "Ctrl-/"}, + {KEY_CTRL_AT, "Ctrl-@"}, {KEY_CTRL_CARET, "Ctrl-^"}, + {KEY_CTRL_BACKTICK, "Ctrl-`"}, + {KEY_CTRL_2, "Ctrl-2"}, {KEY_CTRL_3, "Ctrl-3"}, {KEY_CTRL_4, "Ctrl-4"}, + {KEY_CTRL_5, "Ctrl-5"}, {KEY_CTRL_6, "Ctrl-6"}, {KEY_CTRL_7, "Ctrl-7"}, + {KEY_CTRL_5, "Ctrl-8"}, {KEY_CTRL_6, "Ctrl-9"}, + {':', "Colon"}, {',', "Comma"}, +}; + +static inline int nextchar(int fd) +{ + char c; + return read(fd, &c, 1) == 1 ? c : -1; +} + +static inline int nextnum(int fd, int c, int *n) +{ + for (*n = 0; '0' <= c && c <= '9'; c = nextchar(fd)) + *n = 10*(*n) + (c - '0'); + return c; +} + +/* + * Get one key of input from the given file. Returns -1 on failure. + * If mouse_x or mouse_y are non-null and a mouse event occurs, they will be + * set to the position of the mouse (0-indexed). + */ +int bgetkey(FILE *in, int *mouse_x, int *mouse_y) +{ + if (mouse_x) *mouse_x = -1; + if (mouse_y) *mouse_y = -1; + int fd = fileno(in); + int numcode = 0, modifiers = 0; + int c = nextchar(fd); + if (c == '\x1b') + goto escape; + + return c; + + escape: + c = nextchar(fd); + // Actual escape key: + if (c < 0) + return KEY_ESC; + + switch (c) { + case '\x1b': return KEY_ESC; + case '[': c = nextchar(fd); goto CSI_start; + case 'P': goto DCS; + case 'O': goto SS3; + default: return MOD_ALT | c; + } + + CSI_start: + if (c == -1) + return MOD_ALT | '['; + + switch (c) { + case 'A': return modifiers | KEY_ARROW_UP; + case 'B': return modifiers | KEY_ARROW_DOWN; + case 'C': return modifiers | KEY_ARROW_RIGHT; + case 'D': return modifiers | KEY_ARROW_LEFT; + case 'F': return modifiers | KEY_END; + case 'H': return modifiers | KEY_HOME; + case 'J': return numcode == 2 ? (MOD_SHIFT | KEY_HOME) : -1; + case 'K': return MOD_SHIFT | KEY_END; + case 'M': return MOD_CTRL | KEY_DELETE; + case 'P': return modifiers | (numcode == 1 ? KEY_F1 : KEY_DELETE); + case 'Q': return numcode == 1 ? (modifiers | KEY_F2) : -1; + case 'R': return numcode == 1 ? (modifiers | KEY_F3) : -1; + case 'S': return numcode == 1 ? (modifiers | KEY_F4) : -1; + case '~': + switch (numcode) { + case 1: return modifiers | KEY_HOME; + case 2: return modifiers | KEY_INSERT; + case 3: return modifiers | KEY_DELETE; + case 4: return modifiers | KEY_END; + case 5: return modifiers | KEY_PGUP; + case 6: return modifiers | KEY_PGDN; + case 7: return modifiers | KEY_HOME; + case 8: return modifiers | KEY_END; + case 10: return modifiers | KEY_F0; + case 11: return modifiers | KEY_F1; + case 12: return modifiers | KEY_F2; + case 13: return modifiers | KEY_F3; + case 14: return modifiers | KEY_F4; + case 15: return modifiers | KEY_F5; + case 17: return modifiers | KEY_F6; + case 18: return modifiers | KEY_F7; + case 19: return modifiers | KEY_F8; + case 20: return modifiers | KEY_F9; + case 21: return modifiers | KEY_F10; + case 23: return modifiers | KEY_F11; + case 24: return modifiers | KEY_F12; + } + return -1; + case '<': { // Mouse clicks + int buttons = 0, x = 0, y = 0; + c = nextnum(fd, nextchar(fd), &buttons); + if (c != ';') return -1; + c = nextnum(fd, nextchar(fd), &x); + if (c != ';') return -1; + c = nextnum(fd, nextchar(fd), &y); + if (c != 'm' && c != 'M') return -1; + + if (mouse_x) *mouse_x = x - 1; + if (mouse_y) *mouse_y = y - 1; + + if (buttons & 4) modifiers |= MOD_SHIFT; + if (buttons & 8) modifiers |= MOD_META; + if (buttons & 16) modifiers |= MOD_CTRL; + int key = -1; + switch (buttons & ~(4|8|16)) { + case 0: key = c == 'm' ? MOUSE_LEFT_RELEASE : MOUSE_LEFT_PRESS; break; + case 1: key = c == 'm' ? MOUSE_MIDDLE_RELEASE : MOUSE_MIDDLE_PRESS; break; + case 2: key = c == 'm' ? MOUSE_RIGHT_RELEASE : MOUSE_RIGHT_PRESS; break; + case 32: key = MOUSE_LEFT_DRAG; break; + case 33: key = MOUSE_MIDDLE_DRAG; break; + case 34: key = MOUSE_RIGHT_DRAG; break; + case 64: key = MOUSE_WHEEL_RELEASE; break; + case 65: key = MOUSE_WHEEL_PRESS; break; + default: return -1; + } + if (key == MOUSE_LEFT_RELEASE || key == MOUSE_RIGHT_RELEASE || key == MOUSE_MIDDLE_RELEASE) { + static int lastclick = -1; + static struct timespec lastclicktime = {0, 0}; + struct timespec clicktime; + clock_gettime(CLOCK_MONOTONIC, &clicktime); + if (key == lastclick) { + double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclicktime.tv_sec) + + 1e-6*(double)(clicktime.tv_nsec - lastclicktime.tv_nsec); + if (dt_ms < DOUBLECLICK_THRESHOLD) { + switch (key) { + case MOUSE_LEFT_RELEASE: key = MOUSE_LEFT_DOUBLE; break; + case MOUSE_RIGHT_RELEASE: key = MOUSE_RIGHT_DOUBLE; break; + case MOUSE_MIDDLE_RELEASE: key = MOUSE_MIDDLE_DOUBLE; break; + } + } + } + lastclicktime = clicktime; + lastclick = key; + } + return modifiers | key; + } + default: + if ('0' <= c && c <= '9') { + // Ps prefix + c = nextnum(fd, c, &numcode); + if (c == ';') { + c = nextnum(fd, nextchar(fd), &modifiers); + modifiers = (modifiers >> 1) << MOD_BITSHIFT; + } + goto CSI_start; + } + } + return -1; + + DCS: + return -1; + + SS3: + switch (nextchar(fd)) { + case 'P': return KEY_F1; + case 'Q': return KEY_F2; + case 'R': return KEY_F3; + case 'S': return KEY_F4; + default: break; + } + return -1; +} + +/* + * Populate `buf` with the name of a key. + */ +char *bkeyname(int key, char *buf) +{ + if (key & MOD_META) buf = stpcpy(buf, "Super-"); + if (key & MOD_CTRL) buf = stpcpy(buf, "Ctrl-"); + if (key & MOD_ALT) buf = stpcpy(buf, "Alt-"); + if (key & MOD_SHIFT) buf = stpcpy(buf, "Shift-"); + key &= ~(MOD_META | MOD_CTRL | MOD_ALT | MOD_SHIFT); + for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) { + if (key_names[i].key == key) { + return stpcpy(buf, key_names[i].name); + } + } + if (' ' < key && key <= '~') + return buf + sprintf(buf, "%c", key); + else + return buf + sprintf(buf, "\\x%02X", key); +} + +/* + * Return the key with the given name, if one exists, otherwise -1. + * (i.e. bkeywithname("Space") == ' ', bkeywithname("x") == 'x', bkeywithname("F1") == KEY_F1, bkeywithname("???") == -1) + */ +int bkeywithname(const char *name) +{ + int modifiers = 0; + static const struct { const char *prefix; int modifier; } modnames[] = { + {"Super-", MOD_META}, {"Ctrl-", MOD_CTRL}, {"Alt-", MOD_ALT}, {"Shift-", MOD_SHIFT} + }; + check_names: + for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) { + if (strcmp(key_names[i].name, name) == 0) + return modifiers | key_names[i].key; + } + for (size_t i = 0; i < sizeof(modnames)/sizeof(modnames[0]); i++) { + if (strncmp(name, modnames[i].prefix, strlen(modnames[i].prefix)) == 0) { + modifiers |= modnames[i].modifier; + name += strlen(modnames[i].prefix); + goto check_names; + } + } + return strlen(name) == 1 ? name[0] : -1; +} +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 diff --git a/bterm.h b/bterm.h index 3e42b27..d2fd963 100644 --- a/bterm.h +++ b/bterm.h @@ -3,8 +3,8 @@ * Copyright 2019 Bruce Hill * Released under the MIT License * - * Definitions of some basic terminal stuff, like reading keys - * and some terminal escape sequences. + * Definitions of some basic terminal stuff, like reading keys and some + * terminal escape sequences. */ #ifndef FILE__BTERM_H @@ -12,6 +12,7 @@ #include #include +#include // Maximum time in milliseconds between double clicks #define DOUBLECLICK_THRESHOLD 200 @@ -78,261 +79,9 @@ typedef enum { #define move_cursor(f, x, y) fprintf((f), "\033[%d;%dH", (int)(y)+1, (int)(x)+1) #define move_cursor_col(f, x) fprintf((f), "\033[%d`", (int)(x)+1) -typedef struct { - int key; - const char *name; -} keyname_t; - -static keyname_t key_names[] = { - {KEY_SPACE, "Space"}, {KEY_BACKSPACE2, "Backspace"}, - {KEY_F1, "F1"}, {KEY_F2, "F2"}, {KEY_F3, "F3"}, {KEY_F4, "F4"}, {KEY_F5, "F5"}, - {KEY_F6, "F6"}, {KEY_F7, "F7"}, {KEY_F8, "F8"}, {KEY_F9, "F9"}, {KEY_F10, "F10"}, - {KEY_F11, "F11"}, {KEY_F12, "F12"}, - {KEY_INSERT, "Insert"}, {KEY_DELETE, "Delete"}, - {KEY_HOME, "Home"}, {KEY_END, "End"}, - {KEY_PGUP, "PgUp"}, {KEY_PGUP, "Page Up"}, - {KEY_PGDN, "PgDn"}, {KEY_PGDN, "Page Down"}, - {KEY_ARROW_UP, "Up"}, {KEY_ARROW_DOWN, "Down"}, {KEY_ARROW_LEFT, "Left"}, {KEY_ARROW_RIGHT, "Right"}, - {MOUSE_LEFT_PRESS, "Left press"}, {MOUSE_RIGHT_PRESS, "Right press"}, {MOUSE_MIDDLE_PRESS, "Middle press"}, - {MOUSE_LEFT_DRAG, "Left drag"}, {MOUSE_RIGHT_DRAG, "Right drag"}, {MOUSE_MIDDLE_DRAG, "Middle drag"}, - {MOUSE_LEFT_RELEASE, "Left click"}, {MOUSE_RIGHT_RELEASE, "Right click"}, {MOUSE_MIDDLE_RELEASE, "Middle click"}, - {MOUSE_LEFT_RELEASE, "Left up"}, {MOUSE_RIGHT_RELEASE, "Right up"}, {MOUSE_MIDDLE_RELEASE, "Middle up"}, - {MOUSE_LEFT_RELEASE, "Left release"}, {MOUSE_RIGHT_RELEASE, "Right release"}, {MOUSE_MIDDLE_RELEASE, "Middle release"}, - {MOUSE_LEFT_DOUBLE, "Double left click"}, {MOUSE_RIGHT_DOUBLE, "Double right click"}, {MOUSE_MIDDLE_DOUBLE, "Double middle click"}, - {MOUSE_WHEEL_RELEASE, "Mouse wheel up"}, {MOUSE_WHEEL_PRESS, "Mouse wheel down"}, - {KEY_TAB, "Tab"}, {KEY_ENTER, "Enter"}, {KEY_ENTER, "Return"}, - {KEY_CTRL_A, "Ctrl-a"}, {KEY_CTRL_B, "Ctrl-b"}, {KEY_CTRL_C, "Ctrl-c"}, - {KEY_CTRL_D, "Ctrl-d"}, {KEY_CTRL_E, "Ctrl-e"}, {KEY_CTRL_F, "Ctrl-f"}, - {KEY_CTRL_G, "Ctrl-g"}, {KEY_CTRL_H, "Ctrl-h"}, {KEY_CTRL_I, "Ctrl-i"}, - {KEY_CTRL_J, "Ctrl-j"}, {KEY_CTRL_K, "Ctrl-k"}, {KEY_CTRL_L, "Ctrl-l"}, - {KEY_CTRL_M, "Ctrl-m"}, {KEY_CTRL_N, "Ctrl-n"}, {KEY_CTRL_O, "Ctrl-o"}, - {KEY_CTRL_P, "Ctrl-p"}, {KEY_CTRL_Q, "Ctrl-q"}, {KEY_CTRL_R, "Ctrl-r"}, - {KEY_CTRL_S, "Ctrl-s"}, {KEY_CTRL_T, "Ctrl-t"}, {KEY_CTRL_U, "Ctrl-u"}, - {KEY_CTRL_V, "Ctrl-v"}, {KEY_CTRL_W, "Ctrl-w"}, {KEY_CTRL_X, "Ctrl-x"}, - {KEY_CTRL_Y, "Ctrl-y"}, {KEY_CTRL_Z, "Ctrl-z"}, - {KEY_ESC, "Esc"}, {KEY_ESC, "Escape"}, - {KEY_CTRL_TILDE, "Ctrl-~"}, {KEY_CTRL_BACKSLASH, "Ctrl-\\"}, - {KEY_CTRL_LSQ_BRACKET, "Ctrl-]"}, {KEY_CTRL_RSQ_BRACKET, "Ctrl-]"}, - {KEY_CTRL_UNDERSCORE, "Ctrl-_"}, {KEY_CTRL_SLASH, "Ctrl-/"}, - {KEY_CTRL_AT, "Ctrl-@"}, {KEY_CTRL_CARET, "Ctrl-^"}, - {KEY_CTRL_BACKTICK, "Ctrl-`"}, - {KEY_CTRL_2, "Ctrl-2"}, {KEY_CTRL_3, "Ctrl-3"}, {KEY_CTRL_4, "Ctrl-4"}, - {KEY_CTRL_5, "Ctrl-5"}, {KEY_CTRL_6, "Ctrl-6"}, {KEY_CTRL_7, "Ctrl-7"}, - {KEY_CTRL_5, "Ctrl-8"}, {KEY_CTRL_6, "Ctrl-9"}, - {':', "Colon"}, {',', "Comma"}, -}; - int bgetkey(FILE *in, int *mouse_x, int *mouse_y); char *bkeyname(int key, char *buf); int bkeywithname(const char *name); -static inline int nextchar(int fd) -{ - char c; - return read(fd, &c, 1) == 1 ? c : -1; -} - -static inline int nextnum(int fd, int c, int *n) -{ - for (*n = 0; '0' <= c && c <= '9'; c = nextchar(fd)) - *n = 10*(*n) + (c - '0'); - return c; -} - -/* - * Get one key of input from the given file. Returns -1 on failure. - * If mouse_x or mouse_y are non-null and a mouse event occurs, they will be - * set to the position of the mouse (0-indexed). - */ -int bgetkey(FILE *in, int *mouse_x, int *mouse_y) -{ - if (mouse_x) *mouse_x = -1; - if (mouse_y) *mouse_y = -1; - int fd = fileno(in); - int numcode = 0, modifiers = 0; - int c = nextchar(fd); - if (c == '\x1b') - goto escape; - - return c; - - escape: - c = nextchar(fd); - // Actual escape key: - if (c < 0) - return KEY_ESC; - - switch (c) { - case '\x1b': return KEY_ESC; - case '[': c = nextchar(fd); goto CSI_start; - case 'P': goto DCS; - case 'O': goto SS3; - default: return MOD_ALT | c; - } - - CSI_start: - if (c == -1) - return MOD_ALT | '['; - - switch (c) { - case 'A': return modifiers | KEY_ARROW_UP; - case 'B': return modifiers | KEY_ARROW_DOWN; - case 'C': return modifiers | KEY_ARROW_RIGHT; - case 'D': return modifiers | KEY_ARROW_LEFT; - case 'F': return modifiers | KEY_END; - case 'H': return modifiers | KEY_HOME; - case 'J': return numcode == 2 ? (MOD_SHIFT | KEY_HOME) : -1; - case 'K': return MOD_SHIFT | KEY_END; - case 'M': return MOD_CTRL | KEY_DELETE; - case 'P': return modifiers | (numcode == 1 ? KEY_F1 : KEY_DELETE); - case 'Q': return numcode == 1 ? (modifiers | KEY_F2) : -1; - case 'R': return numcode == 1 ? (modifiers | KEY_F3) : -1; - case 'S': return numcode == 1 ? (modifiers | KEY_F4) : -1; - case '~': - switch (numcode) { - case 1: return modifiers | KEY_HOME; - case 2: return modifiers | KEY_INSERT; - case 3: return modifiers | KEY_DELETE; - case 4: return modifiers | KEY_END; - case 5: return modifiers | KEY_PGUP; - case 6: return modifiers | KEY_PGDN; - case 7: return modifiers | KEY_HOME; - case 8: return modifiers | KEY_END; - case 10: return modifiers | KEY_F0; - case 11: return modifiers | KEY_F1; - case 12: return modifiers | KEY_F2; - case 13: return modifiers | KEY_F3; - case 14: return modifiers | KEY_F4; - case 15: return modifiers | KEY_F5; - case 17: return modifiers | KEY_F6; - case 18: return modifiers | KEY_F7; - case 19: return modifiers | KEY_F8; - case 20: return modifiers | KEY_F9; - case 21: return modifiers | KEY_F10; - case 23: return modifiers | KEY_F11; - case 24: return modifiers | KEY_F12; - } - return -1; - case '<': { // Mouse clicks - int buttons = 0, x = 0, y = 0; - c = nextnum(fd, nextchar(fd), &buttons); - if (c != ';') return -1; - c = nextnum(fd, nextchar(fd), &x); - if (c != ';') return -1; - c = nextnum(fd, nextchar(fd), &y); - if (c != 'm' && c != 'M') return -1; - - if (mouse_x) *mouse_x = x - 1; - if (mouse_y) *mouse_y = y - 1; - - if (buttons & 4) modifiers |= MOD_SHIFT; - if (buttons & 8) modifiers |= MOD_META; - if (buttons & 16) modifiers |= MOD_CTRL; - int key = -1; - switch (buttons & ~(4|8|16)) { - case 0: key = c == 'm' ? MOUSE_LEFT_RELEASE : MOUSE_LEFT_PRESS; break; - case 1: key = c == 'm' ? MOUSE_MIDDLE_RELEASE : MOUSE_MIDDLE_PRESS; break; - case 2: key = c == 'm' ? MOUSE_RIGHT_RELEASE : MOUSE_RIGHT_PRESS; break; - case 32: key = MOUSE_LEFT_DRAG; break; - case 33: key = MOUSE_MIDDLE_DRAG; break; - case 34: key = MOUSE_RIGHT_DRAG; break; - case 64: key = MOUSE_WHEEL_RELEASE; break; - case 65: key = MOUSE_WHEEL_PRESS; break; - default: return -1; - } - if (key == MOUSE_LEFT_RELEASE || key == MOUSE_RIGHT_RELEASE || key == MOUSE_MIDDLE_RELEASE) { - static int lastclick = -1; - static struct timespec lastclicktime = {0, 0}; - struct timespec clicktime; - clock_gettime(CLOCK_MONOTONIC, &clicktime); - if (key == lastclick) { - double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclicktime.tv_sec) - + 1e-6*(double)(clicktime.tv_nsec - lastclicktime.tv_nsec); - if (dt_ms < DOUBLECLICK_THRESHOLD) { - switch (key) { - case MOUSE_LEFT_RELEASE: key = MOUSE_LEFT_DOUBLE; break; - case MOUSE_RIGHT_RELEASE: key = MOUSE_RIGHT_DOUBLE; break; - case MOUSE_MIDDLE_RELEASE: key = MOUSE_MIDDLE_DOUBLE; break; - } - } - } - lastclicktime = clicktime; - lastclick = key; - } - return modifiers | key; - } - default: - if ('0' <= c && c <= '9') { - // Ps prefix - c = nextnum(fd, c, &numcode); - if (c == ';') { - c = nextnum(fd, nextchar(fd), &modifiers); - modifiers = (modifiers >> 1) << MOD_BITSHIFT; - } - goto CSI_start; - } - } - return -1; - - DCS: - return -1; - - SS3: - switch (nextchar(fd)) { - case 'P': return KEY_F1; - case 'Q': return KEY_F2; - case 'R': return KEY_F3; - case 'S': return KEY_F4; - default: break; - } - return -1; -} - -/* - * Populate `buf` with the name of a key. - */ -char *bkeyname(int key, char *buf) -{ - if (key & MOD_META) buf = stpcpy(buf, "Super-"); - if (key & MOD_CTRL) buf = stpcpy(buf, "Ctrl-"); - if (key & MOD_ALT) buf = stpcpy(buf, "Alt-"); - if (key & MOD_SHIFT) buf = stpcpy(buf, "Shift-"); - key &= ~(MOD_META | MOD_CTRL | MOD_ALT | MOD_SHIFT); - for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) { - if (key_names[i].key == key) { - return stpcpy(buf, key_names[i].name); - } - } - if (' ' < key && key <= '~') - return buf + sprintf(buf, "%c", key); - else - return buf + sprintf(buf, "\\x%02X", key); -} - -/* - * Return the key with the given name, if one exists, otherwise -1. - * (i.e. bkeywithname("Space") == ' ', bkeywithname("x") == 'x', bkeywithname("F1") == KEY_F1, bkeywithname("???") == -1) - */ -int bkeywithname(const char *name) -{ - int modifiers = 0; - static const struct { const char *prefix; int modifier; } modnames[] = { - {"Super-", MOD_META}, {"Ctrl-", MOD_CTRL}, {"Alt-", MOD_ALT}, {"Shift-", MOD_SHIFT} - }; - check_names: - for (size_t i = 0; i < sizeof(key_names)/sizeof(key_names[0]); i++) { - if (strcmp(key_names[i].name, name) == 0) - return modifiers | key_names[i].key; - } - for (size_t i = 0; i < sizeof(modnames)/sizeof(modnames[0]); i++) { - if (strncmp(name, modnames[i].prefix, strlen(modnames[i].prefix)) == 0) { - modifiers |= modnames[i].modifier; - name += strlen(modnames[i].prefix); - goto check_names; - } - } - return strlen(name) == 1 ? name[0] : -1; -} - #endif // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 diff --git a/columns.c b/columns.c new file mode 100644 index 0000000..a729cd2 --- /dev/null +++ b/columns.c @@ -0,0 +1,161 @@ +/* + * 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/columns.h b/columns.h index 297a00a..3c9c599 100644 --- a/columns.h +++ b/columns.h @@ -1,154 +1,15 @@ /* - * This file contains logic for drawing columns. + * columns.h - This file contains definitions for bb column-drawing code. */ -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] = ' '; - } -} +#ifndef FILE_COLUMNS__H +#define FILE_COLUMNS__H -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; -} +#include "entry.h" -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); -} - -static void col_mreltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_mtime); - lpad(buf, width); -} - -static void col_areltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_atime); - lpad(buf, width); -} - -static void col_creltime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - timeago(buf, entry->info.st_ctime); - lpad(buf, width); -} - -static void col_mtime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, BB_TIME_FMT, localtime(&(entry->info.st_mtime))); -} - -static void col_atime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, BB_TIME_FMT, localtime(&(entry->info.st_atime))); -} - -static void col_ctime(entry_t *entry, const char *color, char *buf, int width) { - (void)color; - strftime(buf, (size_t)width, BB_TIME_FMT, localtime(&(entry->info.st_ctime))); -} - -static 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); -} - -static 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); -} - -static void col_random(entry_t *entry, const char *color, char *buf, int width) -{ - (void)color; - sprintf(buf, "%*d", width, entry->shufflepos); -} - -static 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]); -} - -static 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"); -} +#define TIME_FMT " %T %D " +#define SELECTED_INDICATOR " \033[31;7m \033[0m" +#define NOT_SELECTED_INDICATOR " " typedef struct { const char *name; @@ -156,16 +17,28 @@ typedef struct { unsigned int stretchy : 1; } column_t; -static 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}, -}; +typedef enum { + COL_NONE = 0, + COL_NAME = 'n', + COL_SIZE = 's', + COL_PERM = 'p', + COL_MTIME = 'm', + COL_CTIME = 'c', + COL_ATIME = 'a', + COL_RANDOM = 'r', + COL_SELECTED = '*', +} column_e; + +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); +void col_mtime(entry_t *entry, const char *color, char *buf, int width); +void col_atime(entry_t *entry, const char *color, char *buf, int width); +void col_ctime(entry_t *entry, const char *color, char *buf, int width); +void col_selected(entry_t *entry, const char *color, char *buf, int width); +void col_perm(entry_t *entry, const char *color, char *buf, int width); +void col_random(entry_t *entry, const char *color, char *buf, int width); +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 diff --git a/entry.h b/entry.h new file mode 100644 index 0000000..0574341 --- /dev/null +++ b/entry.h @@ -0,0 +1,38 @@ +/* + * entry.h - Define types for file entries. + */ +#ifndef FILE_ENTRY__H +#define FILE_ENTRY__H + +#include +#include +#include + +#define IS_SELECTED(p) (((p)->selected.atme) != NULL) +#define IS_VIEWED(p) ((p)->index >= 0) +#define IS_LOADED(p) ((p)->hash.atme != NULL) + +/* entry_t uses intrusive linked lists. This means entries can only belong to + * one list at a time, in this case the list of selected entries. 'atme' is an + * indirect pointer to either the 'next' field of the previous list member, or + * the variable that points to the first list member. In other words, + * item->next->atme == &item->next and firstitem->atme == &firstitem. + */ +typedef struct entry_s { + struct { + struct entry_s *next, **atme; + } selected, hash; + char *name, *linkname; + struct stat info; + mode_t linkedmode; + int no_esc : 1; + int link_no_esc : 1; + int shufflepos; + int index; + char fullname[1]; + // ------- fullname must be last! -------------- + // When entries are allocated, extra space on the end is reserved to fill + // in fullname. +} entry_t; + +#endif