/* * Bitty Browser (bb) * Copyright 2019 Bruce Hill * Released under the MIT license */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #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\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" " 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 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 */ 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; } /* * 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; } 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) { 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; } signal(SIGWINCH, SIG_DFL); } /* * Close safely in a way that doesn't gunk up the terminal. */ void cleanup_and_exit(int sig) { static volatile sig_atomic_t error_in_progress = 0; if (error_in_progress) raise(sig); error_in_progress = 1; cleanup(); signal(sig, SIG_DFL); raise(sig); } /* * Close the terminal, reset the screen, and delete the cmdfile */ void cleanup(void) { if (cmdfilename) { unlink(cmdfilename); free(cmdfilename); cmdfilename = NULL; } if (tty_out) fputs(T_OFF(T_ALT_SCREEN) T_ON(T_SHOW_CURSOR), tty_out); restore_term(&orig_termios); } /* * 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) { if (S_ISDIR(mode)) return DIR_COLOR; else if (S_ISLNK(mode)) return LINK_COLOR; else if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) return EXECUTABLE_COLOR; else return NORMAL_COLOR; } /* * 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, "") == 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 */ #ifdef __APPLE__ int compare_files(void *v, const void *v1, const void *v2) #else int compare_files(const void *v1, const void *v2, void *v) #endif { #define COMPARE(a, b) if ((a) != (b)) { return sign*((a) < (b) ? 1 : -1); } #define COMPARE_TIME(t1, t2) COMPARE((t1).tv_sec, (t2).tv_sec) COMPARE((t1).tv_nsec, (t2).tv_nsec) bb_t *bb = (bb_t*)v; const entry_t *e1 = *((const entry_t**)v1), *e2 = *((const entry_t**)v2); int sign = 1; if (!bb->interleave_dirs) { COMPARE(E_ISDIR(e1), E_ISDIR(e2)); } for (char *sort = bb->sort + 1; *sort; sort += 2) { sign = sort[-1] == '-' ? -1 : 1; switch (*sort) { case COL_SELECTED: COMPARE(IS_SELECTED(e1), IS_SELECTED(e2)); break; case COL_NAME: { /* This sorting method is not identical to strverscmp(). Notably, bb's sort * will order: [0, 1, 9, 00, 01, 09, 10, 000, 010] instead of strverscmp()'s * order: [000, 00, 01, 010, 09, 0, 1, 9, 10]. I believe bb's sort is consistent * with how people want their files grouped: all files padded to n digits * will be grouped together, and files with the same padding will be sorted * ordinally. This version also does case-insensitivity by lowercasing words, * so the following characters come before all letters: [\]^_` */ const char *n1 = e1->name, *n2 = e2->name; while (*n1 && *n2) { char c1 = LOWERCASE(*n1), c2 = LOWERCASE(*n2); if ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9') { long i1 = strtol(n1, (char**)&n1, 10); long i2 = strtol(n2, (char**)&n2, 10); // Shorter numbers always go before longer. In practice, I assume // filenames padded to the same number of digits should be grouped // together, instead of // [1.png, 0001.png, 2.png, 0002.png, 3.png], it makes more sense to have: // [1.png, 2.png, 3.png, 0001.png, 0002.png] COMPARE((n2 - e2->name), (n1 - e1->name)); COMPARE(i2, i1); } else { COMPARE(c2, c1); ++n1; ++n2; } } COMPARE(LOWERCASE(*n2), LOWERCASE(*n1)); break; } case COL_PERM: COMPARE((e1->info.st_mode & 0x3FF), (e2->info.st_mode & 0x3FF)); break; case COL_SIZE: COMPARE(e1->info.st_size, e2->info.st_size); break; case COL_MTIME: COMPARE_TIME(mtime(e1->info), mtime(e2->info)); break; case COL_CTIME: COMPARE_TIME(ctime(e1->info), ctime(e2->info)); break; case COL_ATIME: COMPARE_TIME(atime(e1->info), atime(e2->info)); break; case COL_RANDOM: COMPARE(e1->shufflepos, e2->shufflepos); break; } } return 0; #undef COMPARE #undef COMPARE_TIME } /* * 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 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; } /* * Load a file's info into an entry_t and return it (if found). * The returned entry must be free()ed by the caller. * Warning: this does not deduplicate entries, and it's best if there aren't * duplicate entries hanging around. */ entry_t* load_entry(bb_t *bb, const char *path, int clear_dots) { struct stat linkedstat, filestat; if (!path || !path[0]) return NULL; if (lstat(path, &filestat) == -1) return NULL; char pbuf[PATH_MAX]; normalize_path(bb->path, path, pbuf, clear_dots); if (pbuf[strlen(pbuf)-1] == '/' && pbuf[1]) pbuf[strlen(pbuf)-1] = '\0'; // Check for pre-existing: for (entry_t *e = bb->hash[(int)filestat.st_ino & HASH_MASK]; e; e = e->hash.next) { if (e->info.st_ino == filestat.st_ino && e->info.st_dev == filestat.st_dev // Need to check filename in case of hard links && strcmp(pbuf, e->fullname) == 0) return e; } ssize_t linkpathlen = -1; char linkbuf[PATH_MAX]; if (S_ISLNK(filestat.st_mode)) { linkpathlen = readlink(pbuf, linkbuf, sizeof(linkbuf)); if (linkpathlen < 0) err("Couldn't read link: '%s'", pbuf); linkbuf[linkpathlen] = '\0'; while (linkpathlen > 0 && linkbuf[linkpathlen-1] == '/') linkbuf[--linkpathlen] = '\0'; if (stat(pbuf, &linkedstat) == -1) memset(&linkedstat, 0, sizeof(linkedstat)); } size_t pathlen = strlen(pbuf); size_t entry_size = sizeof(entry_t) + (pathlen + 1) + (size_t)(linkpathlen + 1); entry_t *entry = memcheck(calloc(entry_size, 1)); char *end = stpcpy(entry->fullname, pbuf); if (linkpathlen >= 0) entry->linkname = strcpy(end + 1, linkbuf); if (strcmp(entry->fullname, "/") == 0) { entry->name = entry->fullname; } else { entry->name = strrchr(entry->fullname, '/'); if (!entry->name) err("No slash found in '%s' from '%s'", entry->fullname, path); ++entry->name; } if (S_ISLNK(filestat.st_mode)) entry->linkedmode = linkedstat.st_mode; entry->info = filestat; if (bb->hash[(int)filestat.st_ino & HASH_MASK]) bb->hash[(int)filestat.st_ino & HASH_MASK]->hash.atme = &entry->hash.next; entry->hash.next = bb->hash[(int)filestat.st_ino & HASH_MASK]; entry->hash.atme = &bb->hash[(int)filestat.st_ino & HASH_MASK]; entry->index = -1; bb->hash[(int)filestat.st_ino & HASH_MASK] = entry; return entry; } /* * 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; } /* * Prepend `root` to relative paths, replace "~" with $HOME. * If clear_dots is 1, then also replace "/foo/." with "/foo" and replace "/foo/baz/.." with "/foo". * The normalized path is stored in `normalized`. */ void normalize_path(const char *root, const char *path, char *normalized, int clear_dots) { if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) { char *home; if (!(home = getenv("HOME"))) return; strcpy(normalized, home); ++path; } else if (path[0] == '/') { normalized[0] = '\0'; } else { strcpy(normalized, root); if (root[strlen(root)-1] != '/') err("No trailing slash on root"); } strcat(normalized, path); if (clear_dots) { char *src = normalized, *dest = normalized; while (*src) { while (src[0] == '/' && src[1] == '.') { // Replace "foo/./?" with "foo/?" if (src[2] == '/' || src[2] == '\0') { src += 2; } else if (src[2] == '.' && (src[3] == '/' || src[3] == '\0')) { // Replace "foo/baz/../asdf" with "foo/asdf" src += 3; *dest = '\0'; if (dest[-1] == '/') dest[-1] = '\0'; while (dest > normalized && *dest != '/') dest--; } else break; } *(dest++) = *(src++); } *dest = '\0'; } } /* * 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, "") == 0) { strcpy(pbuf, path); } else if (strcmp(path, "..") == 0 && strcmp(bb->path, "") == 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, "") != 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` */ void populate_files(bb_t *bb, int samedir) { bb->dirty = 1; // Clear old files (if any) if (bb->files) { for (int i = 0; i < bb->nfiles; i++) { bb->files[i]->index = -1; try_free_entry(bb->files[i]); bb->files[i] = NULL; } free(bb->files); bb->files = NULL; } int old_scroll = bb->scroll, old_cursor = bb->cursor; bb->nfiles = 0; bb->cursor = 0; bb->scroll = 0; if (!bb->path[0]) return; size_t space = 0; if (strcmp(bb->path, "") == 0) { for (entry_t *e = bb->firstselected; e; e = e->selected.next) { if ((size_t)bb->nfiles + 1 > space) bb->files = memcheck(realloc(bb->files, (space += 100)*sizeof(void*))); e->index = bb->nfiles; bb->files[bb->nfiles++] = e; } } else { DIR *dir = opendir(bb->path); if (!dir) err("Couldn't open dir: %s", bb->path); for (struct dirent *dp; (dp = readdir(dir)) != NULL; ) { if (dp->d_name[0] == '.') { if (dp->d_name[1] == '.' && dp->d_name[2] == '\0') { if (!bb->show_dotdot || strcmp(bb->path, "/") == 0) continue; } else if (dp->d_name[1] == '\0') { if (!bb->show_dot) continue; } else if (!bb->show_dotfiles) continue; } if ((size_t)bb->nfiles + 1 > space) bb->files = memcheck(realloc(bb->files, (space += 100)*sizeof(void*))); // Don't normalize path so we can get "." and ".." entry_t *entry = load_entry(bb, dp->d_name, 0); if (!entry) err("Failed to load entry: '%s'", dp->d_name); entry->index = bb->nfiles; bb->files[bb->nfiles++] = entry; } closedir(dir); } for (int i = 0; i < bb->nfiles; i++) { int j = rand() / (RAND_MAX / (i + 1)); // This is not optimal, but doesn't need to be bb->files[i]->shufflepos = bb->files[j]->shufflepos; bb->files[j]->shufflepos = i; } sort_files(bb); if (samedir) { set_cursor(bb, old_cursor); set_scroll(bb, old_scroll); } } /* * Run a bb internal command (e.g. "+refresh") and return an indicator of what * needs to happen next. */ bb_result_t process_cmd(bb_t *bb, const char *cmd) { if (cmd[0] == '+') ++cmd; else if (strncmp(cmd, "bb +", 4) == 0) cmd = &cmd[4]; const char *value = strchr(cmd, ':'); if (value) ++value; #define set_bool(target) do { if (!value) { target = !target; } else { target = value[0] == '1'; } } while (0) switch (cmd[0]) { case '.': { // +..:, +.: if (cmd[1] == '.') // +..: set_bool(bb->show_dotdot); else // +.: set_bool(bb->show_dot); populate_files(bb, 1); return BB_OK; } case 'b': { // +bind::