aboutsummaryrefslogtreecommitdiff
path: root/bb.c
diff options
context:
space:
mode:
Diffstat (limited to 'bb.c')
-rw-r--r--bb.c1598
1 files changed, 676 insertions, 922 deletions
diff --git a/bb.c b/bb.c
index ad7777a..65dec89 100644
--- a/bb.c
+++ b/bb.c
@@ -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[])