diff options
Diffstat (limited to 'bb.c')
| -rw-r--r-- | bb.c | 1598 |
1 files changed, 676 insertions, 922 deletions
@@ -2,318 +2,220 @@ * Bitty Browser (bb) * Copyright 2019 Bruce Hill * Released under the MIT license + * + * This file contains the main source code of `bb`. */ -#include <dirent.h> -#include <fcntl.h> -#include <limits.h> -#include <poll.h> -#include <signal.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/dir.h> -#include <sys/errno.h> -#include <sys/ioctl.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <sys/wait.h> -#include <termios.h> -#include <time.h> -#include <unistd.h> - -#include "config.h" -#include "bterm.h" - -#define BB_VERSION "0.16.0" - -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif - -#define MAX_COLS 12 -#define MAX_SORT (2*MAX_COLS) -#define HASH_SIZE 1024 -#define HASH_MASK (HASH_SIZE - 1) -#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 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 (termheight - 3) -#ifdef __APPLE__ -#define mtime(s) (s).st_mtimespec -#define atime(s) (s).st_atimespec -#define ctime(s) (s).st_ctimespec -#else -#define mtime(s) (s).st_mtim -#define atime(s) (s).st_atim -#define ctime(s) (s).st_ctim -#endif +#include "bb.h" -#define err(...) do { \ - cleanup(); \ - fprintf(stderr, __VA_ARGS__); \ - if (errno) fprintf(stderr, "\n%s", strerror(errno)); \ - fprintf(stderr, "\n"); \ - exit(EXIT_FAILURE); \ -} while (0) - -// Types -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 { - struct entry_s *next, **atme; -} llnode_t; - -typedef struct entry_s { - llnode_t 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! -------------- -} entry_t; - -typedef struct bb_s { - entry_t *hash[HASH_SIZE]; - entry_t **files; - entry_t *firstselected; - char path[PATH_MAX]; - char prev_path[PATH_MAX]; - int nfiles; - int scroll, cursor; - - char sort[MAX_SORT+1]; - char columns[MAX_COLS+1]; - unsigned int dirty : 1; - unsigned int show_dotdot : 1; - unsigned int show_dot : 1; - unsigned int show_dotfiles : 1; - unsigned int interleave_dirs : 1; - unsigned int should_quit : 1; -} bb_t; - -typedef enum { BB_OK = 0, BB_INVALID, BB_QUIT } bb_result_t; - -// Functions -static void bb_browse(bb_t *bb, const char *path); -static int cd_to(bb_t *bb, const char *path); -static const char* color_of(mode_t mode); -static void cleanup(void); -static void cleanup_and_exit(int sig); -static void restore_term(struct termios *term); -#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 init_term(void); -static entry_t* load_entry(bb_t *bb, const char *path, int clear_dots); -static void* memcheck(void *p); -static void normalize_path(const char *root, const char *path, char *pbuf, int clear_dots); -static int is_simple_bbcmd(const char *s); -static char *trim(char *s); -static void populate_files(bb_t *bb, int samedir); -static void print_bindings(int fd); -static bb_result_t process_cmd(bb_t *bb, const char *cmd); -static void render(bb_t *bb); -static int run_script(bb_t *bb, const char *cmd); -static void set_cursor(bb_t *bb, int i); -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 sort_files(bb_t *bb); -static int try_free_entry(entry_t *e); -static void update_term_size(int sig); - -// Config options -extern binding_t bindings[]; -extern const column_t columns[128]; - -// 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 char *bbcmdfn = "bb() {\n" -" if test $# -eq 0; then cat >> $BBCMD; return; fi\n" -" for arg; do\n" -" shift;\n" -" if echo \"$arg\" | grep \"^+[^:]*:$\" >/dev/null 2>/dev/null; then\n" -" if test $# -gt 0; then printf \"%s\\0\" \"$arg\" \"$@\" >> $BBCMD\n" -" else sed \"s/\\([^\\x00]\\+\\)/$arg\\1/g\" >> $BBCMD; fi\n" -" return\n" -" fi\n" -" printf \"%s\\0\" \"$arg\" >> $BBCMD\n" -" done\n" -"}\n" -"ask() {\n" -#ifdef ASK -ASK ";\n" -#else -" printf \"\033[1m%s\033[0m\" \"$2\" >/dev/tty;\n" -" read $1 </dev/tty >/dev/tty\n" -#endif -"}\n" -"ask1() {\n" -#ifdef ASK1 -ASK1 ";\n" -#else -" printf \"\033[?25l\" >/dev/tty;\n" -" printf \"\033[1m%s\033[0m\" \"$2\" >/dev/tty;\n" -" stty -icanon -echo >/dev/tty;\n" -" eval \"$1=\\$(dd bs=1 count=1 2>/dev/null </dev/tty)\";\n" -" stty icanon echo >/dev/tty;\n" -" printf \"\033[?25h\" >/dev/tty;\n" -#endif -"}\n" -"confirm() {\n" -#ifdef CONFIRM -CONFIRM ";\n" -#else -" ask1 REPLY \"\033[1mIs that okay? [y/N] \" && [ \"$REPLY\" = 'y' ];\n" -#endif -"}\n" -"pause() {\n" -#ifdef PAUSE -PAUSE ";\n" -#else -" ask1 REPLY \"\033[2mPress any key to continue...\033[0m\";" -#endif -"}\n" -"pick() {\n" -#ifdef PICK -PICK ";\n" -#else -" ask query \"$1\" && awk '{print length, $1}' | sort -n | cut -d' ' -f2- |\n" -" grep -i -m1 \"$(echo \"$query\" | sed 's;.;[^/&]*[&];g')\";\n" -#endif -"}\n" -"spin() {\n" -#ifdef SPIN -SPIN ";\n" -#else -" eval \"$@\" &\n" -" pid=$!;\n" -" spinner='-\\|/';\n" -" sleep 0.01;\n" -" while kill -0 $pid 2>/dev/null; do\n" -" printf '%c\\033[D' \"$spinner\" >/dev/tty;\n" -" spinner=\"$(echo $spinner | sed 's/\\(.\\)\\(.*\\)/\\2\\1/')\";\n" -" sleep 0.1;\n" -" done;\n" -" wait $pid;\n" -#endif -"}\n" -#ifdef SH -"alias sh=" SH";\n" -#endif -; - -// Global variables +// Variables used within this file to track global state static struct termios orig_termios, bb_termios; -static 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 FILE *tty_out = NULL, *tty_in = NULL; static int termwidth, termheight; static int mouse_x, mouse_y; static char *cmdfilename = NULL; static struct timespec lastclick = {0, 0}; - /* - * Hanlder for SIGWINCH events + * Use bb to browse a path. */ -void update_term_size(int sig) +void bb_browse(bb_t *bb, const char *path) { - (void)sig; - struct winsize sz = {0}; - ioctl(fileno(tty_in), TIOCGWINSZ, &sz); - termwidth = sz.ws_col; - termheight = sz.ws_row; -} + static long cmdpos = 0; + int lastwidth = termwidth, lastheight = termheight; + int check_cmds = 1; -/* - * 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 int first_time = 1; - tty_in = fopen("/dev/tty", "r"); - tty_out = fopen("/dev/tty", "w"); - if (first_time) { - tcgetattr(fileno(tty_out), &orig_termios); - first_time = 0; + cd_to(bb, path); + bb->scroll = 0; + bb->cursor = 0; + + const char *runstartup = +"[ ! \"$XDG_CONFIG_HOME\" ] && XDG_CONFIG_HOME=~/.config;\n" +"[ ! \"$sysconfdir\" ] && sysconfdir=/etc;\n" +"if [ -e \"$XDG_CONFIG_HOME/bb/bbstartup.sh\" ]; then\n" +" . \"$XDG_CONFIG_HOME/bb/bbstartup.sh\";\n" +"elif [ -e \"$sysconfdir/xdg/bb/bbstartup.sh\" ]; then\n" +" . \"$sysconfdir/xdg/bb/bbstartup.sh\";\n" +"elif [ -e \"./bbstartup.sh\" ]; then\n" +" . \"./bbstartup.sh\";\n" +"fi\n"; + run_script(bb, runstartup); + init_term(); + goto force_check_cmds; + + redraw: + render(bb); + bb->dirty = 0; + + next_input: + if (termwidth != lastwidth || termheight != lastheight) { + lastwidth = termwidth; lastheight = termheight; + bb->dirty = 1; + goto redraw; + } + + if (check_cmds) { + FILE *cmdfile; + force_check_cmds: + cmdfile = fopen(cmdfilename, "r"); + if (!cmdfile) { + if (bb->dirty) goto redraw; + goto get_keyboard_input; + } + + if (cmdpos) + fseek(cmdfile, cmdpos, SEEK_SET); + + char *cmd = NULL; + size_t space = 0; + while (cmdfile && getdelim(&cmd, &space, '\0', cmdfile) >= 0) { + cmdpos = ftell(cmdfile); + if (!cmd[0]) continue; + process_cmd(bb, cmd); + if (bb->should_quit) { + free(cmd); + fclose(cmdfile); + goto quit; + } + } + free(cmd); + fclose(cmdfile); + unlink(cmdfilename); + cmdpos = 0; + check_cmds = 0; + goto redraw; + } + + int key; + get_keyboard_input: + key = bgetkey(tty_in, &mouse_x, &mouse_y); + switch (key) { + case KEY_MOUSE_LEFT: { + struct timespec clicktime; + clock_gettime(CLOCK_MONOTONIC, &clicktime); + double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclick.tv_sec); + dt_ms += 1e-6*(double)(clicktime.tv_nsec - lastclick.tv_nsec); + lastclick = clicktime; + // Get column: + char column[3] = "+?"; + for (int col = 0, x = 0; bb->columns[col]; col++) { + if (col > 0) x += 1; + x += columns[(int)bb->columns[col]].width; + if (x >= mouse_x) { + column[1] = bb->columns[col]; + break; + } + } + if (mouse_y == 1) { + char *pos; + if ((pos = strstr(bb->sort, column)) && pos == bb->sort) + column[0] = '-'; + set_sort(bb, column); + sort_files(bb); + goto redraw; + } else if (2 <= mouse_y && mouse_y <= termheight - 2) { + int clicked = bb->scroll + (mouse_y - 2); + if (clicked > bb->nfiles - 1) goto next_input; + if (column[1] == COL_SELECTED) { + set_selected(bb, bb->files[clicked], !IS_SELECTED(bb->files[clicked])); + bb->dirty = 1; + goto redraw; + } + set_cursor(bb, clicked); + if (dt_ms <= 200) { + key = KEY_MOUSE_DOUBLE_LEFT; + goto user_bindings; + } + goto redraw; + } + break; + } + + case KEY_CTRL_C: + cleanup_and_exit(SIGINT); + + case KEY_CTRL_Z: + fputs(T_LEAVE_BBMODE, tty_out); + restore_term(&orig_termios); + raise(SIGTSTP); + init_term(); + bb->dirty = 1; + goto redraw; + + case -1: + goto next_input; + + default: { + // Search user-defined key bindings from config.h: + binding_t *binding; + user_bindings: + for (int i = 0; bindings[i].key != 0 && i < sizeof(bindings)/sizeof(bindings[0]); i++) { + if (key == bindings[i].key) { + binding = &bindings[i]; + goto run_binding; + } + } + // Nothing matched + goto next_input; + + run_binding: + if (cmdpos != 0) + err("Command file still open"); + if (is_simple_bbcmd(binding->script)) { + process_cmd(bb, binding->script); + } else { + move_cursor(tty_out, 0, termheight-1); + fputs(T_ON(T_SHOW_CURSOR), tty_out); + restore_term(&default_termios); + run_script(bb, binding->script); + init_term(); + check_cmds = 1; + } + if (bb->should_quit) goto quit; + goto redraw; + } + } + goto next_input; + + quit: + if (tty_out) { + fputs(T_LEAVE_BBMODE, tty_out); + cleanup(); } - memcpy(&bb_termios, &orig_termios, sizeof(bb_termios)); - cfmakeraw(&bb_termios); - bb_termios.c_cc[VMIN] = 0; - bb_termios.c_cc[VTIME] = 1; - if (tcsetattr(fileno(tty_out), TCSAFLUSH, &bb_termios) == -1) - err("Couldn't tcsetattr"); - update_term_size(0); - signal(SIGWINCH, update_term_size); - // Initiate mouse tracking and disable text wrapping: - fputs(T_ENTER_BBMODE, tty_out); } -/* - * Close the /dev/tty terminals and restore some of the attributes. - */ -void restore_term(struct termios *term) +int cd_to(bb_t *bb, const char *path) { - if (tty_out) { - tcsetattr(fileno(tty_out), TCSAFLUSH, term); - fputs(T_LEAVE_BBMODE_PARTIAL, tty_out); - fflush(tty_out); - fclose(tty_out); - tty_out = NULL; - fclose(tty_in); - tty_in = NULL; + char pbuf[PATH_MAX], prev[PATH_MAX] = {0}; + strcpy(prev, bb->path); + if (strcmp(path, "<selection>") == 0) { + strcpy(pbuf, path); + } else if (strcmp(path, "..") == 0 && strcmp(bb->path, "<selection>") == 0) { + if (!bb->prev_path[0]) return -1; + strcpy(pbuf, bb->prev_path); + if (chdir(pbuf)) return -1; + } else { + normalize_path(bb->path, path, pbuf, 1); + if (pbuf[strlen(pbuf)-1] != '/') + strcat(pbuf, "/"); + if (chdir(pbuf)) return -1; } - signal(SIGWINCH, SIG_DFL); + + if (strcmp(bb->path, "<selection>") != 0) { + strcpy(bb->prev_path, prev); + setenv("BBPREVPATH", bb->prev_path, 1); + } + + strcpy(bb->path, pbuf); + populate_files(bb, 0); + if (prev[0]) { + entry_t *p = load_entry(bb, prev, 0); + if (p) { + if (IS_VIEWED(p)) set_cursor(bb, p->index); + else try_free_entry(p); + } + } + return 0; } /* @@ -346,94 +248,6 @@ void cleanup(void) } /* - * Memory allocation failures are unrecoverable in bb, so this wrapper just - * prints an error message and exits if that happens. - */ -void* memcheck(void *p) -{ - if (!p) err("Allocation failure"); - return p; -} - -/* - * Run a shell script with the selected files passed as sequential arguments to - * 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) -{ - char *fullcmd = calloc(strlen(cmd) + strlen(bbcmdfn) + 1, sizeof(char)); - strcpy(fullcmd, bbcmdfn); - strcat(fullcmd, cmd); - - pid_t child; - void (*old_handler)(int) = signal(SIGINT, SIG_IGN); - if ((child = fork()) == 0) { - signal(SIGINT, SIG_DFL); - // TODO: is there a max number of args? Should this be batched? - size_t space = 32; - char **args = memcheck(calloc(space, sizeof(char*))); - size_t i = 0; - args[i++] = SH; - args[i++] = "-c"; - args[i++] = fullcmd; - args[i++] = "--"; // ensure files like "-i" are not interpreted as flags for sh - for (entry_t *e = bb->firstselected; e; e = e->selected.next) { - if (i >= space) - args = memcheck(realloc(args, (space += 100)*sizeof(char*))); - args[i++] = e->fullname; - } - args[i] = NULL; - - setenv("BBDOTFILES", bb->show_dotfiles ? "1" : "", 1); - setenv("BBCURSOR", bb->nfiles ? bb->files[bb->cursor]->fullname : "", 1); - setenv("BBSHELLFUNC", bbcmdfn, 1); - - int ttyout, ttyin; - ttyout = open("/dev/tty", O_RDWR); - ttyin = open("/dev/tty", O_RDONLY); - dup2(ttyout, STDOUT_FILENO); - dup2(ttyin, STDIN_FILENO); - execvp(SH, args); - err("Failed to execute command: '%s'", cmd); - return -1; - } - - if (child == -1) - err("Failed to fork"); - - int status; - waitpid(child, &status, 0); - signal(SIGINT, old_handler); - - bb->dirty = 1; - return status; -} - -/* - * 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. - */ -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. */ const char* color_of(mode_t mode) @@ -446,225 +260,6 @@ const char* color_of(mode_t mode) } /* - * Set the sorting method used by bb to display files. - */ -void set_sort(bb_t *bb, const char *sort) -{ - char sortbuf[strlen(sort)+1]; - strcpy(sortbuf, sort); - for (char *s = sortbuf; s[0] && s[1]; s += 2) { - char *found; - if ((found = strchr(bb->sort, s[1]))) { - if (*s == '~') - *s = found[-1] == '+' && found == &bb->sort[1] ? '-' : '+'; - memmove(found-1, found+1, strlen(found+1)+1); - } else if (*s == '~') - *s = '+'; - } - size_t len = MIN(MAX_SORT, strlen(sort)); - memmove(bb->sort + len, bb->sort, MAX_SORT+1 - len); - memmove(bb->sort, sortbuf, len); -} - -/* - * Draw everything to the screen. - * If bb->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 int lastcursor = -1, lastscroll = -1; - char buf[64]; - if (lastcursor == -1 || lastscroll == -1) - bb->dirty = 1; - - if (!bb->dirty) { - // Use terminal scrolling: - if (lastscroll > bb->scroll) { - fprintf(tty_out, "\033[3;%dr\033[%dT\033[1;%dr", termheight-1, lastscroll - bb->scroll, termheight); - } else if (lastscroll < bb->scroll) { - fprintf(tty_out, "\033[3;%dr\033[%dS\033[1;%dr", termheight-1, bb->scroll - lastscroll, termheight); - } - } - - if (bb->dirty) { - // Path - move_cursor(tty_out, 0, 0); - const char *color = TITLE_COLOR; - fputs(color, tty_out); - fputs_escaped(tty_out, bb->path, color); - fputs(" \033[K\033[0m", tty_out); - - static const char *help = "Press '?' to see key bindings "; - move_cursor(tty_out, MAX(0, termwidth - (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 col = 0; bb->columns[col]; col++) { - const char *title = columns[(int)bb->columns[col]].name; - if (!title) title = ""; - move_cursor(tty_out, x, 1); - if (col > 0) { - fputs("│\033[K", tty_out); - x += 1; - } - const char *indicator = " "; - if (bb->columns[col] == bb->sort[1]) - indicator = bb->sort[0] == '-' ? RSORT_INDICATOR : SORT_INDICATOR; - move_cursor(tty_out, x, 1); - fputs(indicator, tty_out); - fputs(title, tty_out); - x += columns[(int)bb->columns[col]].width; - } - fputs(" \033[K\033[0m", tty_out); - } - - entry_t **files = bb->files; - for (int i = bb->scroll; i < bb->scroll + ONSCREEN; i++) { - if (!bb->dirty) { - if (i == bb->cursor || i == lastcursor) - goto do_render; - if (i < lastscroll || i >= lastscroll + ONSCREEN) - goto do_render; - continue; - } - - int y; - do_render: - y = i - bb->scroll + 2; - move_cursor(tty_out, 0, y); - - if (i == bb->scroll && bb->nfiles == 0) { - const char *s = "...no files here..."; - fprintf(tty_out, "\033[37;2m%s\033[0m\033[K\033[J", s); - break; - } - - if (i >= bb->nfiles) { - fputs("\033[J", tty_out); - break; - } - - entry_t *entry = files[i]; - if (i == bb->cursor) fputs(CURSOR_COLOR, tty_out); - - int use_fullname = strcmp(bb->path, "<selection>") == 0; - int x = 0; - for (int col = 0; bb->columns[col]; col++) { - fprintf(tty_out, "\033[%d;%dH\033[K", y+1, x+1); - if (col > 0) { - if (i == bb->cursor) fputs("│", tty_out); - else fputs("\033[37;2m│\033[22m", tty_out); - fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); - x += 1; - } - move_cursor(tty_out, x, y); - switch (bb->columns[col]) { - case COL_SELECTED: - fputs(IS_SELECTED(entry) ? SELECTED_INDICATOR : NOT_SELECTED_INDICATOR, tty_out); - fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); - break; - - case COL_RANDOM: { - double k = (double)entry->shufflepos/(double)bb->nfiles; - int color = (int)(k*232 + (1.-k)*255); - fprintf(tty_out, "\033[48;5;%dm \033[0m%s", color, - i == bb->cursor ? CURSOR_COLOR : "\033[0m"); - break; - } - - case COL_SIZE: { - int j = 0; - const char* units = "BKMGTPEZY"; - double bytes = (double)entry->info.st_size; - while (bytes > 1024) { - bytes /= 1024; - j++; - } - fprintf(tty_out, " %6.*f%c ", j > 0 ? 1 : 0, bytes, units[j]); - break; - } - - case COL_MTIME: - strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_mtime))); - fputs(buf, tty_out); - break; - - case COL_CTIME: - strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_ctime))); - fputs(buf, tty_out); - break; - - case COL_ATIME: - strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_atime))); - fputs(buf, tty_out); - break; - - case COL_PERM: - fprintf(tty_out, " %03o", entry->info.st_mode & 0777); - break; - - case COL_NAME: { - char color[128]; - strcpy(color, color_of(entry->info.st_mode)); - if (i == bb->cursor) strcat(color, CURSOR_COLOR); - fputs(color, tty_out); - - char *name = use_fullname ? entry->fullname : entry->name; - if (entry->no_esc) fputs(name, tty_out); - else entry->no_esc |= !fputs_escaped(tty_out, name, color); - - if (E_ISDIR(entry)) fputs("/", tty_out); - - if (entry->linkname) { - if (i != bb->cursor) - fputs("\033[37m", tty_out); - fputs("\033[2m -> \033[3m", tty_out); - strcpy(color, color_of(entry->linkedmode)); - if (i == bb->cursor) strcat(color, CURSOR_COLOR); - strcat(color, "\033[3;2m"); - fputs(color, tty_out); - if (entry->link_no_esc) fputs(entry->linkname, tty_out); - else entry->link_no_esc |= !fputs_escaped(tty_out, entry->linkname, color); - - if (S_ISDIR(entry->linkedmode)) - fputs("/", tty_out); - - fputs("\033[22;23m", tty_out); - } - fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); - fputs("\033[K", tty_out); - break; - } - default: break; - } - x += columns[(int)bb->columns[col]].width; - } - fputs(" \033[K\033[0m", tty_out); // Reset color and attributes - } - - if (bb->firstselected) { - int n = 0; - for (entry_t *s = bb->firstselected; s; s = s->selected.next) ++n; - int x = termwidth - 14; - for (int k = n; k; k /= 10) x--; - move_cursor(tty_out, MAX(0, x), termheight - 1); - fprintf(tty_out, "\033[41;30m %d Selected \033[0m", n); - } else { - move_cursor(tty_out, termwidth/2, termheight - 1); - fputs("\033[0m\033[K", tty_out); - } - - lastcursor = bb->cursor; - lastscroll = bb->scroll; - fflush(tty_out); -} - -/* * Used for sorting, this function compares files according to the sorting-related options, * like bb->sort */ @@ -732,80 +327,69 @@ int compare_files(const void *v1, const void *v2, void *v) } /* - * Select or deselect a file. + * 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. */ -void set_selected(bb_t *bb, entry_t *e, int selected) +int fputs_escaped(FILE *f, const char *str, const char *color) { - if (IS_SELECTED(e) == selected) return; - - if (bb->nfiles > 0 && e != bb->files[bb->cursor]) - bb->dirty = 1; - - if (selected) { - if (bb->firstselected) - bb->firstselected->selected.atme = &e->selected.next; - e->selected.next = bb->firstselected; - e->selected.atme = &bb->firstselected; - bb->firstselected = e; - } else { - if (bb->nfiles > 0 && e != bb->files[bb->cursor]) - bb->dirty = 1; - if (e->selected.next) - e->selected.next->selected.atme = e->selected.atme; - *(e->selected.atme) = e->selected.next; - e->selected.next = NULL; - e->selected.atme = NULL; - try_free_entry(e); + 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; } /* - * Set bb's file cursor to the given index (and adjust the scroll as necessary) + * Initialize the terminal files for /dev/tty and set up some desired + * attributes like passing Ctrl-c as a key instead of interrupting */ -void set_cursor(bb_t *bb, int newcur) +void init_term(void) { - int oldcur = bb->cursor; - if (newcur > bb->nfiles - 1) newcur = bb->nfiles - 1; - if (newcur < 0) newcur = 0; - bb->cursor = newcur; - if (bb->nfiles <= ONSCREEN) { - bb->scroll = 0; - return; - } - - if (oldcur < bb->cursor) { - if (bb->scroll > bb->cursor) - bb->scroll = MAX(0, bb->cursor); - else if (bb->scroll < bb->cursor - ONSCREEN + 1 + SCROLLOFF) - bb->scroll = MIN(bb->nfiles - 1 - ONSCREEN + 1, - bb->scroll + (newcur - oldcur)); - } else { - if (bb->scroll > bb->cursor - SCROLLOFF) - bb->scroll = MAX(0, bb->scroll + (newcur - oldcur));//bb->cursor - SCROLLOFF); - else if (bb->scroll < bb->cursor - ONSCREEN + 1) - bb->scroll = MIN(bb->cursor - ONSCREEN + 1, - bb->nfiles - 1 - ONSCREEN + 1); + static int first_time = 1; + tty_in = fopen("/dev/tty", "r"); + tty_out = fopen("/dev/tty", "w"); + if (first_time) { + tcgetattr(fileno(tty_out), &orig_termios); + first_time = 0; } + memcpy(&bb_termios, &orig_termios, sizeof(bb_termios)); + cfmakeraw(&bb_termios); + bb_termios.c_cc[VMIN] = 0; + bb_termios.c_cc[VTIME] = 1; + if (tcsetattr(fileno(tty_out), TCSAFLUSH, &bb_termios) == -1) + err("Couldn't tcsetattr"); + update_term_size(0); + signal(SIGWINCH, update_term_size); + // Initiate mouse tracking and disable text wrapping: + fputs(T_ENTER_BBMODE, tty_out); } -/* - * Set bb's scroll to the given index (and adjust the cursor as necessary) +/* + * Return whether or not 's' is a simple bb command that doesn't need + * a full shell instance (e.g. "bb +cd:.." or "bb +move:+1"). */ -void set_scroll(bb_t *bb, int newscroll) +static int is_simple_bbcmd(const char *s) { - int delta = newscroll - bb->scroll; - if (bb->nfiles <= ONSCREEN) { - newscroll = 0; - } else { - if (newscroll > bb->nfiles - 1 - ONSCREEN + 1) - newscroll = bb->nfiles - 1 - ONSCREEN + 1; - if (newscroll < 0) newscroll = 0; + if (!s) return 0; + while (*s == ' ') ++s; + if (s[0] != '+' && strncmp(s, "bb +", 4) != 0) + return 0; + const char *special = ";$&<>|\n*?\\\"'"; + for (const char *p = special; *p; ++p) { + if (strchr(s, *p)) + return 0; } - - bb->scroll = newscroll; - bb->cursor += delta; - if (bb->cursor > bb->nfiles - 1) bb->cursor = bb->nfiles - 1; - if (bb->cursor < 0) bb->cursor = 0; + return 1; } /* @@ -867,34 +451,13 @@ entry_t* load_entry(bb_t *bb, const char *path, int clear_dots) } /* - * 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) -{ - if (IS_SELECTED(e) || IS_VIEWED(e)) return 0; - if (e->hash.next) - e->hash.next->hash.atme = e->hash.atme; - *(e->hash.atme) = e->hash.next; - e->hash.atme = NULL; - e->hash.next = NULL; - free(e); - return 1; -} - -/* - * Sort the files in bb according to bb's settings. + * Memory allocation failures are unrecoverable in bb, so this wrapper just + * prints an error message and exits if that happens. */ -void sort_files(bb_t *bb) +void* memcheck(void *p) { -#ifdef __APPLE__ - qsort_r(bb->files, (size_t)bb->nfiles, sizeof(entry_t*), bb, compare_files); -#else - qsort_r(bb->files, (size_t)bb->nfiles, sizeof(entry_t*), compare_files, bb); -#endif - for (int i = 0; i < bb->nfiles; i++) - bb->files[i]->index = i; - bb->dirty = 1; + if (!p) err("Allocation failure"); + return p; } /* @@ -938,72 +501,6 @@ void normalize_path(const char *root, const char *path, char *normalized, int cl } } -/* - * Return whether or not 's' is a simple bb command that doesn't need - * a full shell instance (e.g. "bb +cd:.." or "bb +move:+1"). - */ -static int is_simple_bbcmd(const char *s) -{ - if (!s) return 0; - while (*s == ' ') ++s; - if (s[0] != '+' && strncmp(s, "bb +", 4) != 0) - return 0; - const char *special = ";$&<>|\n*?\\\"'"; - for (const char *p = special; *p; ++p) { - if (strchr(s, *p)) - return 0; - } - return 1; -} - -/* - * Trim trailing whitespace by inserting '\0' and return a pointer to after the - * first non-whitespace char - */ -static char *trim(char *s) -{ - if (!s) return NULL; - while (*s == ' ' || *s == '\n') ++s; - char *end; - for (end = &s[strlen(s)-1]; end >= s && (*end == ' ' || *end == '\n'); end--) - *end = '\0'; - return s; -} - -int cd_to(bb_t *bb, const char *path) -{ - char pbuf[PATH_MAX], prev[PATH_MAX] = {0}; - strcpy(prev, bb->path); - if (strcmp(path, "<selection>") == 0) { - strcpy(pbuf, path); - } else if (strcmp(path, "..") == 0 && strcmp(bb->path, "<selection>") == 0) { - if (!bb->prev_path[0]) return -1; - strcpy(pbuf, bb->prev_path); - if (chdir(pbuf)) return -1; - } else { - normalize_path(bb->path, path, pbuf, 1); - if (pbuf[strlen(pbuf)-1] != '/') - strcat(pbuf, "/"); - if (chdir(pbuf)) return -1; - } - - if (strcmp(bb->path, "<selection>") != 0) { - strcpy(bb->prev_path, prev); - setenv("BBPREVPATH", bb->prev_path, 1); - } - - strcpy(bb->path, pbuf); - populate_files(bb, 0); - if (prev[0]) { - entry_t *p = load_entry(bb, prev, 0); - if (p) { - if (IS_VIEWED(p)) set_cursor(bb, p->index); - else try_free_entry(p); - } - } - return 0; -} - /* * 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` @@ -1077,6 +574,45 @@ void populate_files(bb_t *bb, int samedir) } /* + * Print the current key bindings + */ +void print_bindings(int fd) +{ + char buf[1000], buf2[1024]; + for (int i = 0; bindings[i].key != 0 && i < sizeof(bindings)/sizeof(bindings[0]); i++) { + if (bindings[i].key == -1) { + const char *label = bindings[i].description; + sprintf(buf, "\n\033[33;1;4m\033[%dG%s\033[0m\n", (termwidth-(int)strlen(label))/2, label); + write(fd, buf, strlen(buf)); + continue; + } + int to_skip = -1; + char *p = buf; + for (int j = i; bindings[j].key && strcmp(bindings[j].description, bindings[i].description) == 0; j++) { + if (j > i) p = stpcpy(p, ", "); + ++to_skip; + int key = bindings[j].key; + const char *name = bkeyname(key); + if (name) + p = stpcpy(p, name); + else if (' ' <= key && key <= '~') + p += sprintf(p, "%c", (char)key); + else + p += sprintf(p, "\033[31m\\x%02X", key); + } + *p = '\0'; + sprintf(buf2, "\033[1m\033[%dG%s\033[0m", termwidth/2 - 1 - (int)strlen(buf), buf); + write(fd, buf2, strlen(buf2)); + sprintf(buf2, "\033[1m\033[%dG\033[34m%s\033[0m", termwidth/2 + 1, + bindings[i].description); + write(fd, buf2, strlen(buf2)); + write(fd, "\033[0m\n", strlen("\033[0m\n")); + i += to_skip; + } + write(fd, "\n", 1); +} + +/* * Run a bb internal command (e.g. "+refresh") and return an indicator of what * needs to happen next. */ @@ -1295,210 +831,428 @@ bb_result_t process_cmd(bb_t *bb, const char *cmd) } /* - * Use bb to browse a path. + * Draw everything to the screen. + * If bb->dirty is false, then use terminal scrolling to move the file listing + * around and only update the files that have changed. */ -void bb_browse(bb_t *bb, const char *path) +void render(bb_t *bb) { - static long cmdpos = 0; - int lastwidth = termwidth, lastheight = termheight; - int check_cmds = 1; + static int lastcursor = -1, lastscroll = -1; + char buf[64]; + if (lastcursor == -1 || lastscroll == -1) + bb->dirty = 1; - cd_to(bb, path); - bb->scroll = 0; - bb->cursor = 0; + if (!bb->dirty) { + // Use terminal scrolling: + if (lastscroll > bb->scroll) { + fprintf(tty_out, "\033[3;%dr\033[%dT\033[1;%dr", termheight-1, lastscroll - bb->scroll, termheight); + } else if (lastscroll < bb->scroll) { + fprintf(tty_out, "\033[3;%dr\033[%dS\033[1;%dr", termheight-1, bb->scroll - lastscroll, termheight); + } + } - const char *runstartup = -"[ ! \"$XDG_CONFIG_HOME\" ] && XDG_CONFIG_HOME=~/.config;\n" -"[ ! \"$sysconfdir\" ] && sysconfdir=/etc;\n" -"if [ -e \"$XDG_CONFIG_HOME/bb/bbstartup.sh\" ]; then\n" -" . \"$XDG_CONFIG_HOME/bb/bbstartup.sh\";\n" -"elif [ -e \"$sysconfdir/xdg/bb/bbstartup.sh\" ]; then\n" -" . \"$sysconfdir/xdg/bb/bbstartup.sh\";\n" -"elif [ -e \"./bbstartup.sh\" ]; then\n" -" . \"./bbstartup.sh\";\n" -"fi\n"; - run_script(bb, runstartup); - init_term(); - goto force_check_cmds; + if (bb->dirty) { + // Path + move_cursor(tty_out, 0, 0); + const char *color = TITLE_COLOR; + fputs(color, tty_out); + fputs_escaped(tty_out, bb->path, color); + fputs(" \033[K\033[0m", tty_out); - redraw: - render(bb); - bb->dirty = 0; + static const char *help = "Press '?' to see key bindings "; + move_cursor(tty_out, MAX(0, termwidth - (int)strlen(help)), 0); + fputs(help, tty_out); + fputs("\033[K\033[0m", tty_out); - next_input: - if (termwidth != lastwidth || termheight != lastheight) { - lastwidth = termwidth; lastheight = termheight; - bb->dirty = 1; - goto redraw; + // Columns + move_cursor(tty_out, 0, 1); + fputs("\033[0;44;30m\033[K", tty_out); + int x = 0; + for (int col = 0; bb->columns[col]; col++) { + const char *title = columns[(int)bb->columns[col]].name; + if (!title) title = ""; + move_cursor(tty_out, x, 1); + if (col > 0) { + fputs("│\033[K", tty_out); + x += 1; + } + const char *indicator = " "; + if (bb->columns[col] == bb->sort[1]) + indicator = bb->sort[0] == '-' ? RSORT_INDICATOR : SORT_INDICATOR; + move_cursor(tty_out, x, 1); + fputs(indicator, tty_out); + fputs(title, tty_out); + x += columns[(int)bb->columns[col]].width; + } + fputs(" \033[K\033[0m", tty_out); } - if (check_cmds) { - FILE *cmdfile; - force_check_cmds: - cmdfile = fopen(cmdfilename, "r"); - if (!cmdfile) { - if (bb->dirty) goto redraw; - goto get_keyboard_input; + entry_t **files = bb->files; + for (int i = bb->scroll; i < bb->scroll + ONSCREEN; i++) { + if (!bb->dirty) { + if (i == bb->cursor || i == lastcursor) + goto do_render; + if (i < lastscroll || i >= lastscroll + ONSCREEN) + goto do_render; + continue; } - if (cmdpos) - fseek(cmdfile, cmdpos, SEEK_SET); + int y; + do_render: + y = i - bb->scroll + 2; + move_cursor(tty_out, 0, y); - char *cmd = NULL; - size_t space = 0; - while (cmdfile && getdelim(&cmd, &space, '\0', cmdfile) >= 0) { - cmdpos = ftell(cmdfile); - if (!cmd[0]) continue; - process_cmd(bb, cmd); - if (bb->should_quit) { - free(cmd); - fclose(cmdfile); - goto quit; - } + if (i == bb->scroll && bb->nfiles == 0) { + const char *s = "...no files here..."; + fprintf(tty_out, "\033[37;2m%s\033[0m\033[K\033[J", s); + break; } - free(cmd); - fclose(cmdfile); - unlink(cmdfilename); - cmdpos = 0; - check_cmds = 0; - goto redraw; - } - int key; - get_keyboard_input: - key = bgetkey(tty_in, &mouse_x, &mouse_y); - switch (key) { - case KEY_MOUSE_LEFT: { - struct timespec clicktime; - clock_gettime(CLOCK_MONOTONIC, &clicktime); - double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclick.tv_sec); - dt_ms += 1e-6*(double)(clicktime.tv_nsec - lastclick.tv_nsec); - lastclick = clicktime; - // Get column: - char column[3] = "+?"; - for (int col = 0, x = 0; bb->columns[col]; col++) { - if (col > 0) x += 1; - x += columns[(int)bb->columns[col]].width; - if (x >= mouse_x) { - column[1] = bb->columns[col]; - break; - } - } - if (mouse_y == 1) { - char *pos; - if ((pos = strstr(bb->sort, column)) && pos == bb->sort) - column[0] = '-'; - set_sort(bb, column); - sort_files(bb); - goto redraw; - } else if (2 <= mouse_y && mouse_y <= termheight - 2) { - int clicked = bb->scroll + (mouse_y - 2); - if (clicked > bb->nfiles - 1) goto next_input; - if (column[1] == COL_SELECTED) { - set_selected(bb, bb->files[clicked], !IS_SELECTED(bb->files[clicked])); - bb->dirty = 1; - goto redraw; - } - set_cursor(bb, clicked); - if (dt_ms <= 200) { - key = KEY_MOUSE_DOUBLE_LEFT; - goto user_bindings; - } - goto redraw; - } + if (i >= bb->nfiles) { + fputs("\033[J", tty_out); break; } - case KEY_CTRL_C: - cleanup_and_exit(SIGINT); + entry_t *entry = files[i]; + if (i == bb->cursor) fputs(CURSOR_COLOR, tty_out); - case KEY_CTRL_Z: - fputs(T_LEAVE_BBMODE, tty_out); - restore_term(&orig_termios); - raise(SIGTSTP); - init_term(); - bb->dirty = 1; - goto redraw; + int use_fullname = strcmp(bb->path, "<selection>") == 0; + int x = 0; + for (int col = 0; bb->columns[col]; col++) { + fprintf(tty_out, "\033[%d;%dH\033[K", y+1, x+1); + if (col > 0) { + if (i == bb->cursor) fputs("│", tty_out); + else fputs("\033[37;2m│\033[22m", tty_out); + fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); + x += 1; + } + move_cursor(tty_out, x, y); + switch (bb->columns[col]) { + case COL_SELECTED: + fputs(IS_SELECTED(entry) ? SELECTED_INDICATOR : NOT_SELECTED_INDICATOR, tty_out); + fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); + break; - case -1: - goto next_input; + case COL_RANDOM: { + double k = (double)entry->shufflepos/(double)bb->nfiles; + int color = (int)(k*232 + (1.-k)*255); + fprintf(tty_out, "\033[48;5;%dm \033[0m%s", color, + i == bb->cursor ? CURSOR_COLOR : "\033[0m"); + break; + } - default: { - // Search user-defined key bindings from config.h: - binding_t *binding; - user_bindings: - for (int i = 0; bindings[i].key != 0 && i < sizeof(bindings)/sizeof(bindings[0]); i++) { - if (key == bindings[i].key) { - binding = &bindings[i]; - goto run_binding; + case COL_SIZE: { + int j = 0; + const char* units = "BKMGTPEZY"; + double bytes = (double)entry->info.st_size; + while (bytes > 1024) { + bytes /= 1024; + j++; + } + fprintf(tty_out, " %6.*f%c ", j > 0 ? 1 : 0, bytes, units[j]); + break; } - } - // Nothing matched - goto next_input; - run_binding: - if (cmdpos != 0) - err("Command file still open"); - if (is_simple_bbcmd(binding->script)) { - process_cmd(bb, binding->script); - } else { - move_cursor(tty_out, 0, termheight-1); - fputs(T_ON(T_SHOW_CURSOR), tty_out); - restore_term(&default_termios); - run_script(bb, binding->script); - init_term(); - check_cmds = 1; + case COL_MTIME: + strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_mtime))); + fputs(buf, tty_out); + break; + + case COL_CTIME: + strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_ctime))); + fputs(buf, tty_out); + break; + + case COL_ATIME: + strftime(buf, sizeof(buf), " %I:%M%p %b %e %Y ", localtime(&(entry->info.st_atime))); + fputs(buf, tty_out); + break; + + case COL_PERM: + fprintf(tty_out, " %03o", entry->info.st_mode & 0777); + break; + + case COL_NAME: { + char color[128]; + strcpy(color, color_of(entry->info.st_mode)); + if (i == bb->cursor) strcat(color, CURSOR_COLOR); + fputs(color, tty_out); + + char *name = use_fullname ? entry->fullname : entry->name; + if (entry->no_esc) fputs(name, tty_out); + else entry->no_esc |= !fputs_escaped(tty_out, name, color); + + if (E_ISDIR(entry)) fputs("/", tty_out); + + if (entry->linkname) { + if (i != bb->cursor) + fputs("\033[37m", tty_out); + fputs("\033[2m -> \033[3m", tty_out); + strcpy(color, color_of(entry->linkedmode)); + if (i == bb->cursor) strcat(color, CURSOR_COLOR); + strcat(color, "\033[3;2m"); + fputs(color, tty_out); + if (entry->link_no_esc) fputs(entry->linkname, tty_out); + else entry->link_no_esc |= !fputs_escaped(tty_out, entry->linkname, color); + + if (S_ISDIR(entry->linkedmode)) + fputs("/", tty_out); + + fputs("\033[22;23m", tty_out); + } + fputs(i == bb->cursor ? CURSOR_COLOR : "\033[0m", tty_out); + fputs("\033[K", tty_out); + break; + } + default: break; } - if (bb->should_quit) goto quit; - goto redraw; + x += columns[(int)bb->columns[col]].width; } + fputs(" \033[K\033[0m", tty_out); // Reset color and attributes } - goto next_input; - quit: + if (bb->firstselected) { + int n = 0; + for (entry_t *s = bb->firstselected; s; s = s->selected.next) ++n; + int x = termwidth - 14; + for (int k = n; k; k /= 10) x--; + move_cursor(tty_out, MAX(0, x), termheight - 1); + fprintf(tty_out, "\033[41;30m %d Selected \033[0m", n); + } else { + move_cursor(tty_out, termwidth/2, termheight - 1); + fputs("\033[0m\033[K", tty_out); + } + + lastcursor = bb->cursor; + lastscroll = bb->scroll; + fflush(tty_out); +} + +/* + * Close the /dev/tty terminals and restore some of the attributes. + */ +void restore_term(const struct termios *term) +{ if (tty_out) { - fputs(T_LEAVE_BBMODE, tty_out); - cleanup(); + tcsetattr(fileno(tty_out), TCSAFLUSH, term); + fputs(T_LEAVE_BBMODE_PARTIAL, tty_out); + fflush(tty_out); + fclose(tty_out); + tty_out = NULL; + fclose(tty_in); + tty_in = NULL; } + signal(SIGWINCH, SIG_DFL); } /* - * Print the current key bindings + * Run a shell script with the selected files passed as sequential arguments to + * the script (or pass the cursor file if none are selected). + * Return the exit status of the script. */ -void print_bindings(int fd) +int run_script(bb_t *bb, const char *cmd) { - char buf[1000], buf2[1024]; - for (int i = 0; bindings[i].key != 0 && i < sizeof(bindings)/sizeof(bindings[0]); i++) { - if (bindings[i].key == -1) { - const char *label = bindings[i].description; - sprintf(buf, "\n\033[33;1;4m\033[%dG%s\033[0m\n", (termwidth-(int)strlen(label))/2, label); - write(fd, buf, strlen(buf)); - continue; - } - int to_skip = -1; - char *p = buf; - for (int j = i; bindings[j].key && strcmp(bindings[j].description, bindings[i].description) == 0; j++) { - if (j > i) p = stpcpy(p, ", "); - ++to_skip; - int key = bindings[j].key; - const char *name = bkeyname(key); - if (name) - p = stpcpy(p, name); - else if (' ' <= key && key <= '~') - p += sprintf(p, "%c", (char)key); - else - p += sprintf(p, "\033[31m\\x%02X", key); + char *fullcmd = calloc(strlen(cmd) + strlen(bbcmdfn) + 1, sizeof(char)); + strcpy(fullcmd, bbcmdfn); + strcat(fullcmd, cmd); + + pid_t child; + void (*old_handler)(int) = signal(SIGINT, SIG_IGN); + if ((child = fork()) == 0) { + signal(SIGINT, SIG_DFL); + // TODO: is there a max number of args? Should this be batched? + size_t space = 32; + char **args = memcheck(calloc(space, sizeof(char*))); + size_t i = 0; + args[i++] = SH; + args[i++] = "-c"; + args[i++] = fullcmd; + args[i++] = "--"; // ensure files like "-i" are not interpreted as flags for sh + for (entry_t *e = bb->firstselected; e; e = e->selected.next) { + if (i >= space) + args = memcheck(realloc(args, (space += 100)*sizeof(char*))); + args[i++] = e->fullname; } - *p = '\0'; - sprintf(buf2, "\033[1m\033[%dG%s\033[0m", termwidth/2 - 1 - (int)strlen(buf), buf); - write(fd, buf2, strlen(buf2)); - sprintf(buf2, "\033[1m\033[%dG\033[34m%s\033[0m", termwidth/2 + 1, - bindings[i].description); - write(fd, buf2, strlen(buf2)); - write(fd, "\033[0m\n", strlen("\033[0m\n")); - i += to_skip; + args[i] = NULL; + + setenv("BBDOTFILES", bb->show_dotfiles ? "1" : "", 1); + setenv("BBCURSOR", bb->nfiles ? bb->files[bb->cursor]->fullname : "", 1); + setenv("BBSHELLFUNC", bbcmdfn, 1); + + int ttyout, ttyin; + ttyout = open("/dev/tty", O_RDWR); + ttyin = open("/dev/tty", O_RDONLY); + dup2(ttyout, STDOUT_FILENO); + dup2(ttyin, STDIN_FILENO); + execvp(SH, args); + err("Failed to execute command: '%s'", cmd); + return -1; } - write(fd, "\n", 1); + + if (child == -1) + err("Failed to fork"); + + int status; + waitpid(child, &status, 0); + signal(SIGINT, old_handler); + + bb->dirty = 1; + return status; +} + +/* + * Set bb's file cursor to the given index (and adjust the scroll as necessary) + */ +void set_cursor(bb_t *bb, int newcur) +{ + int oldcur = bb->cursor; + if (newcur > bb->nfiles - 1) newcur = bb->nfiles - 1; + if (newcur < 0) newcur = 0; + bb->cursor = newcur; + if (bb->nfiles <= ONSCREEN) { + bb->scroll = 0; + return; + } + + if (oldcur < bb->cursor) { + if (bb->scroll > bb->cursor) + bb->scroll = MAX(0, bb->cursor); + else if (bb->scroll < bb->cursor - ONSCREEN + 1 + SCROLLOFF) + bb->scroll = MIN(bb->nfiles - 1 - ONSCREEN + 1, + bb->scroll + (newcur - oldcur)); + } else { + if (bb->scroll > bb->cursor - SCROLLOFF) + bb->scroll = MAX(0, bb->scroll + (newcur - oldcur));//bb->cursor - SCROLLOFF); + else if (bb->scroll < bb->cursor - ONSCREEN + 1) + bb->scroll = MIN(bb->cursor - ONSCREEN + 1, + bb->nfiles - 1 - ONSCREEN + 1); + } +} + +/* + * Set bb's scroll to the given index (and adjust the cursor as necessary) + */ +void set_scroll(bb_t *bb, int newscroll) +{ + int delta = newscroll - bb->scroll; + if (bb->nfiles <= ONSCREEN) { + newscroll = 0; + } else { + if (newscroll > bb->nfiles - 1 - ONSCREEN + 1) + newscroll = bb->nfiles - 1 - ONSCREEN + 1; + if (newscroll < 0) newscroll = 0; + } + + bb->scroll = newscroll; + bb->cursor += delta; + if (bb->cursor > bb->nfiles - 1) bb->cursor = bb->nfiles - 1; + if (bb->cursor < 0) bb->cursor = 0; +} + +/* + * Select or deselect a file. + */ +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]) + bb->dirty = 1; + + if (selected) { + if (bb->firstselected) + bb->firstselected->selected.atme = &e->selected.next; + e->selected.next = bb->firstselected; + e->selected.atme = &bb->firstselected; + bb->firstselected = e; + } else { + if (bb->nfiles > 0 && e != bb->files[bb->cursor]) + bb->dirty = 1; + if (e->selected.next) + e->selected.next->selected.atme = e->selected.atme; + *(e->selected.atme) = e->selected.next; + e->selected.next = NULL; + e->selected.atme = NULL; + try_free_entry(e); + } +} + +/* + * Set the sorting method used by bb to display files. + */ +void set_sort(bb_t *bb, const char *sort) +{ + char sortbuf[strlen(sort)+1]; + strcpy(sortbuf, sort); + for (char *s = sortbuf; s[0] && s[1]; s += 2) { + char *found; + if ((found = strchr(bb->sort, s[1]))) { + if (*s == '~') + *s = found[-1] == '+' && found == &bb->sort[1] ? '-' : '+'; + memmove(found-1, found+1, strlen(found+1)+1); + } else if (*s == '~') + *s = '+'; + } + size_t len = MIN(MAX_SORT, strlen(sort)); + memmove(bb->sort + len, bb->sort, MAX_SORT+1 - len); + memmove(bb->sort, sortbuf, len); +} + +/* + * 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) +{ + if (IS_SELECTED(e) || IS_VIEWED(e)) return 0; + if (e->hash.next) + e->hash.next->hash.atme = e->hash.atme; + *(e->hash.atme) = e->hash.next; + e->hash.atme = NULL; + e->hash.next = NULL; + free(e); + return 1; +} + +/* + * Sort the files in bb according to bb's settings. + */ +void sort_files(bb_t *bb) +{ +#ifdef __APPLE__ + qsort_r(bb->files, (size_t)bb->nfiles, sizeof(entry_t*), bb, compare_files); +#else + qsort_r(bb->files, (size_t)bb->nfiles, sizeof(entry_t*), compare_files, bb); +#endif + for (int i = 0; i < bb->nfiles; i++) + bb->files[i]->index = i; + bb->dirty = 1; +} + +/* + * Trim trailing whitespace by inserting '\0' and return a pointer to after the + * first non-whitespace char + */ +static char *trim(char *s) +{ + if (!s) return NULL; + while (*s == ' ' || *s == '\n') ++s; + char *end; + for (end = &s[strlen(s)-1]; end >= s && (*end == ' ' || *end == '\n'); end--) + *end = '\0'; + return s; +} + +/* + * Hanlder for SIGWINCH events + */ +void update_term_size(int sig) +{ + (void)sig; + struct winsize sz = {0}; + ioctl(fileno(tty_in), TIOCGWINSZ, &sz); + termwidth = sz.ws_col; + termheight = sz.ws_row; } int main(int argc, char *argv[]) |
