diff --git a/Makefile b/Makefile index eda68a0..5dcf77a 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ PREFIX= CC=cc -CFLAGS=-O0 -std=gnu99 -Wall -Wpedantic -Weverything -Wno-missing-field-initializers -Wno-padded -Wno-missing-noreturn -Wno-sign-conversion -Wno-cast-qual -Wno-sign-compare -Wno-shorten-64-to-32 +CFLAGS=-O0 -std=gnu99 -Wall -Wpedantic -Weverything -Wno-missing-field-initializers -Wno-padded -Wno-missing-noreturn -Wno-cast-qual LIBS= NAME=bb -G= +G=-g all: $(NAME) diff --git a/bb.c b/bb.c index a87db6d..275ac88 100644 --- a/bb.c +++ b/bb.c @@ -24,25 +24,33 @@ #define MAX_PATH 4096 #define KEY_DELAY 50 +#define SCROLLOFF MIN(5, (termheight-4)/2) #define CMDFILE_FORMAT "/tmp/bb.XXXXXX" #define MAX(a,b) ((a) < (b) ? (b) : (a)) #define MIN(a,b) ((a) > (b) ? (b) : (a)) #define writez(fd, str) write(fd, str, strlen(str)) #define IS_SELECTED(p) (((p)->atme) != NULL) +// This uses the intrusive linked list offsets +//#define PREV_STATE(s) ((s) == firststate ? NULL : (bb_state_t*)((s)->atme - offsetof(bb_state_t, next))) +#define PREV_STATE(s) ((s) == firststate ? NULL : (bb_state_t*)((long)(s)->atme + (long)(s) - (long)(&(s)->atme))) +#define LLREMOVE(e) do { \ + (e)->next->atme = (e)->atme; \ + *((e)->atme) = (e)->next; \ + (e)->next = NULL; \ + (e)->atme = NULL; \ +} while (0) #define alt_screen() writez(termfd, "\033[?1049h") #define default_screen() writez(termfd, "\033[?1049l") #define hide_cursor() writez(termfd, "\033[?25l"); #define show_cursor() writez(termfd, "\033[?25h"); -#define queue_select(state, name) do {\ - char *__name = (name); \ - (state)->to_select = realloc((state)->to_select, strlen(__name)+1); \ - strcpy((state)->to_select, __name); \ -} while (0) #define err(...) do { \ - if (termfd) close_term(); \ + if (termfd) { \ + default_screen(); \ + close_term(); \ + } \ fprintf(stderr, __VA_ARGS__); \ if (errno) \ fprintf(stderr, "\n%s", strerror(errno)); \ @@ -50,13 +58,12 @@ cleanup_and_exit(1); \ } while (0) -extern binding_t bindings[]; -static struct termios orig_termios; -static int termfd = 0; -static int width, height; -static int mouse_x, mouse_y; -static char *cmdfilename = NULL; +// This bit toggles 'A' (0) vs 'a' (1) +#define SORT_DESCENDING 32 +#define IS_REVERSED(method) (!((method) & SORT_DESCENDING)) +#define DESCENDING(method) ((method) | SORT_DESCENDING) +// Types typedef enum { SORT_NONE = 0, SORT_NAME = 'n', @@ -76,14 +83,17 @@ typedef enum { RSORT_RANDOM = 'R', } sortmethod_t; -// This bit toggles 'A' (0) vs 'a' (1) -#define SORT_DESCENDING 32 -#define IS_REVERSED(method) (!((method) & SORT_DESCENDING)) -#define DESCENDING(method) ((method) | SORT_DESCENDING) - +/* Both entry_t and bb_state_t use intrusive linked lists. This means they can + * only belong to one list at a time, in this case the list of selected entries + * and the list of states, respectively. 'atme' is an indirect pointer to + * either the 'next' field of the previous list member, or the variable that + * points to the first list member. In other words, item->next->atme == &item->next + * and firstitem->atme == &firstitem. + */ typedef struct entry_s { struct entry_s *next, **atme; - int visible : 1, d_isdir : 1; + int visible; + int d_isdir : 1; ino_t d_ino; __uint16_t d_reclen; __uint8_t d_type; @@ -92,46 +102,117 @@ typedef struct entry_s { char d_fullname[1]; } entry_t; -typedef struct { +typedef struct bb_state_s { + struct bb_state_s *next, **atme; + entry_t **files; char path[MAX_PATH]; char *to_select; - entry_t *firstselected, **files; - size_t nselected, nfiles; + int nfiles; int scroll, cursor; int showhidden; - struct timespec lastclick; char columns[16]; char sort; } bb_state_t; -static void update_term_size(int sig) +// Functions +static bb_state_t *new_state(bb_state_t *template); +static void delete_state(bb_state_t *s); +static void update_term_size(int sig); +static void init_term(void); +static void cleanup_and_exit(int sig); +static void close_term(void); +static void* memcheck(void *p); +static int run_cmd_on_selection(bb_state_t *s, const char *cmd); +static void term_move(int x, int y); +static int write_escaped(int fd, const char *str, size_t n, const char *reset_color); +static void render(bb_state_t *s, int lazy); +static int compare_files(void *r, const void *v1, const void *v2); +static int find_file(bb_state_t *s, const char *name); +static void write_selection(int fd, char sep); +static void clear_selection(void); +static int select_file(entry_t *e); +static int deselect_file(entry_t *e); +static void set_cursor(bb_state_t *state, int i); +static void set_scroll(bb_state_t *state, int i); +static void populate_files(bb_state_t *s, const char *path); +static void sort_files(bb_state_t *state); +static entry_t *explore(const char *path); +static void print_bindings(int verbose); + +// Global variables +extern binding_t bindings[]; +static struct termios orig_termios; +static int termfd = 0; +static int termwidth, termheight; +static int mouse_x, mouse_y; +static char *cmdfilename = NULL; +static const int colsizew = 7, coldatew = 19, colpermw = 4; +static int colnamew; +static struct timespec lastclick = {0, 0}; +static entry_t *firstselected = NULL; +static bb_state_t *firststate = NULL; + + +bb_state_t *new_state(bb_state_t *template) +{ + bb_state_t *s = memcheck(calloc(1, sizeof(bb_state_t))); + if (template) { + populate_files(s, template->path); + s->cursor = template->cursor; + s->scroll = template->scroll; + s->showhidden = template->showhidden; + strcpy(s->columns, template->columns); + s->sort = template->sort; + sort_files(s); + } else { + s->sort = 'n'; + strncpy(s->columns, "smpn", sizeof(s->columns)); + } + return s; +} + +void delete_state(bb_state_t *s) +{ + if (s->files) { + for (int i = 0; i < s->nfiles; i++) { + entry_t *e = s->files[i]; + --e->visible; + if (!IS_SELECTED(e)) + free(e); + } + free(s->files); + } + if (s->to_select) + free(s->to_select); + + LLREMOVE(s); + memset(s, 'X', sizeof(bb_state_t)); // Just to be safe + free(s); + if (!firststate) + cleanup_and_exit(0); +} + +void update_term_size(int sig) { (void)sig; struct winsize sz = {0}; ioctl(termfd, TIOCGWINSZ, &sz); - width = sz.ws_col; - height = sz.ws_row; + termwidth = sz.ws_col; + termheight = sz.ws_row; } -static inline int clamped(int x, int low, int high) -{ - if (x < low) return low; - if (x > high) return high; - return x; -} - -static void init_term() +void init_term(void) { termfd = open("/dev/tty", O_RDWR); tcgetattr(termfd, &orig_termios); struct termios tios; memcpy(&tios, &orig_termios, sizeof(tios)); - tios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP + tios.c_iflag &= (unsigned long)~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - tios.c_oflag &= ~OPOST; - tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); - tios.c_cflag &= ~(CSIZE | PARENB); - tios.c_cflag |= CS8; + tios.c_oflag &= (unsigned long)~OPOST; + tios.c_lflag &= (unsigned long)~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tios.c_cflag &= (unsigned long)~(CSIZE | PARENB); + tios.c_cflag |= (unsigned long)CS8; tios.c_cc[VMIN] = 0; tios.c_cc[VTIME] = 0; tcsetattr(termfd, TCSAFLUSH, &tios); @@ -141,7 +222,7 @@ static void init_term() writez(termfd, "\033[?1000h\033[?1002h\033[?1015h\033[?1006h"); } -static void cleanup_and_exit(int sig) +void cleanup_and_exit(int sig) { (void)sig; if (termfd) { @@ -155,7 +236,7 @@ static void cleanup_and_exit(int sig) exit(1); } -static void close_term() +void close_term(void) { signal(SIGWINCH, SIG_IGN); @@ -167,13 +248,13 @@ static void close_term() termfd = 0; } -static void* memcheck(void *p) +void* memcheck(void *p) { if (!p) err("Allocation failure"); return p; } -static int run_cmd_on_selection(bb_state_t *state, const char *cmd) +int run_cmd_on_selection(bb_state_t *s, const char *cmd) { pid_t child; sig_t old_handler = signal(SIGINT, SIG_IGN); @@ -181,28 +262,25 @@ static int run_cmd_on_selection(bb_state_t *state, const char *cmd) if ((child = fork()) == 0) { signal(SIGINT, SIG_DFL); // TODO: is there a max number of args? Should this be batched? - char **args = memcheck(calloc(MAX(1, state->nselected) + 5, sizeof(char*))); - int i = 0; + size_t space = 32; + char **args = memcheck(calloc(space, sizeof(char*))); + size_t i = 0; args[i++] = "sh"; args[i++] = "-c"; args[i++] = (char*)cmd; args[i++] = "--"; - entry_t *first = state->firstselected ? state->firstselected : state->files[state->cursor]; + entry_t *first = firstselected ? firstselected : s->files[s->cursor]; for (entry_t *e = first; e; e = e->next) { + if (i >= space) { + space += 100; + args = memcheck(realloc(args, space*sizeof(char*))); + } args[i++] = e->d_name; } args[i] = NULL; - char bb_depth_str[64] = {0}; - { // Set environment variable to track shell nesting - char *depthstr = getenv("BB_DEPTH"); - int depth = depthstr ? atoi(depthstr) : 0; - snprintf(bb_depth_str, sizeof(bb_depth_str), "%d", depth + 1); - setenv("BB_DEPTH", bb_depth_str, 1); - setenv("BBCMD", cmdfilename, 1); - setenv("BBCURSOR", state->files[state->cursor]->d_name, 1); - setenv("BBFULLCURSOR", state->files[state->cursor]->d_fullname, 1); - } + setenv("BBCURSOR", s->files[s->cursor]->d_name, 1); + setenv("BBFULLCURSOR", s->files[s->cursor]->d_fullname, 1); execvp("sh", args); err("Failed to execute command: '%s'", cmd); @@ -219,22 +297,22 @@ static int run_cmd_on_selection(bb_state_t *state, const char *cmd) return status; } -static void term_move(int x, int y) +void term_move(int x, int y) { static char buf[32] = {0}; int len = snprintf(buf, sizeof(buf), "\033[%d;%dH", y+1, x+1); if (len > 0) - write(termfd, buf, len); + write(termfd, buf, (size_t)len); } -static int write_escaped(int fd, const char *str, size_t n, const char *reset_color) +int write_escaped(int fd, const char *str, size_t n, const char *reset_color) { // Returns number of *visible* characters written, not including coloring // escapes['\n'] == 'n', etc. static const char *escapes = " abtnvfr e"; char buf[5]; int ret = 0; - int backlog = 0; + size_t backlog = 0; for (size_t i = 0; i < n; i++) { int escapelen = 0; if (str[i] <= '\x1b' && escapes[(int)str[i]] != ' ') @@ -253,7 +331,7 @@ static int write_escaped(int fd, const char *str, size_t n, const char *reset_co backlog = 0; } writez(fd, "\033[31m"); - write(fd, buf, escapelen); + write(fd, buf, (size_t)escapelen); writez(fd, reset_color); } if (backlog > 0) @@ -261,9 +339,7 @@ static int write_escaped(int fd, const char *str, size_t n, const char *reset_co return ret; } -static const int sizewidth = 7, datewidth = 19, permwidth = 4; -static int namewidth; -static void render(bb_state_t *state, int lazy) +void render(bb_state_t *s, int lazy) { static int lastcursor = -1, lastscroll = -1; char buf[64]; @@ -272,25 +348,25 @@ static void render(bb_state_t *state, int lazy) if (lazy) { // Use terminal scrolling: - if (lastscroll > state->scroll) { - int n = sprintf(buf, "\033[3;%dr\033[%dT\033[1;%dr", height-1, lastscroll - state->scroll, height); - write(termfd, buf, n); - } else if (lastscroll < state->scroll) { - int n = sprintf(buf, "\033[3;%dr\033[%dS\033[1;%dr", height-1, state->scroll - lastscroll, height); - write(termfd, buf, n); + if (lastscroll > s->scroll) { + int n = sprintf(buf, "\033[3;%dr\033[%dT\033[1;%dr", termheight-1, lastscroll - s->scroll, termheight); + write(termfd, buf, (size_t)n); + } else if (lastscroll < s->scroll) { + int n = sprintf(buf, "\033[3;%dr\033[%dS\033[1;%dr", termheight-1, s->scroll - lastscroll, termheight); + write(termfd, buf, (size_t)n); } } - namewidth = width - 1; - for (char *col = state->columns; *col; ++col) { + colnamew = termwidth - 1; + for (char *col = s->columns; *col; ++col) { switch (*col) { case 's': - namewidth -= sizewidth + 3; + colnamew -= colsizew + 3; break; case 'm': case 'c': case 'a': - namewidth -= datewidth + 3; + colnamew -= coldatew + 3; break; case 'p': - namewidth -= permwidth + 3; + colnamew -= colpermw + 3; break; } } @@ -298,75 +374,74 @@ static void render(bb_state_t *state, int lazy) if (!lazy) { // Path term_move(0,0); - writez(termfd, "\033[0;2;37m "); - write_escaped(termfd, state->path, strlen(state->path), "\033[0;2;37m"); + char tabnum[] = "1)"; + for (bb_state_t *si = firststate; si; si = si->next) { + const char *color = si == s ? "\033[0;1;37m" : "\033[0;2;37m"; + writez(termfd, color); + write(termfd, tabnum, 2); + ++tabnum[0]; + // TODO error check + char *title = si == s ? si->path : strrchr(si->path, '/') + 1; + write_escaped(termfd, title, strlen(title), color); + writez(termfd, " "); + } writez(termfd, "\033[K\033[0m"); // Columns term_move(0,1); writez(termfd, " \033[0;44;30m"); - for (char *col = state->columns; *col; ++col) { + for (char *col = s->columns; *col; ++col) { const char *colname; int colwidth = 0; switch (*col) { case 's': - colname = " Size"; colwidth = sizewidth; + colname = " Size"; colwidth = colsizew; break; case 'p': - colname = "Per"; colwidth = permwidth; + colname = "Per"; colwidth = colpermw; break; case 'm': - colname = " Modified"; colwidth = datewidth; + colname = " Modified"; colwidth = coldatew; break; case 'a': - colname = " Accessed"; colwidth = datewidth; + colname = " Accessed"; colwidth = coldatew; break; case 'c': - colname = " Created"; colwidth = datewidth; + colname = " Created"; colwidth = coldatew; break; case 'n': - colname = "Name"; colwidth = namewidth; + colname = "Name"; colwidth = colnamew; break; default: continue; } - if (col != state->columns) writez(termfd, " │ "); - writez(termfd, DESCENDING(state->sort) == *col ? (IS_REVERSED(state->sort) ? "▲" : "▼") : " "); + if (col != s->columns) writez(termfd, " │ "); + writez(termfd, DESCENDING(s->sort) == *col ? (IS_REVERSED(s->sort) ? "▲" : "▼") : " "); for (ssize_t i = writez(termfd, colname); i < colwidth-1; i++) write(termfd, " ", 1); } writez(termfd, "\033[0m"); } - if (state->nselected > 0) { - int len = snprintf(buf, sizeof(buf), "%lu selected ", state->nselected); - if (strlen(state->path) + 1 + len < width) { - term_move(width-len, 0); - writez(termfd, "\033[0;1;30;47m"); - write(termfd, buf, len); - writez(termfd, "\033[0m"); - } - } - - entry_t **files = state->files; + entry_t **files = s->files; static const char *NORMAL_COLOR = "\033[0m"; static const char *CURSOR_COLOR = "\033[0;30;43m"; static const char *LINKDIR_COLOR = "\033[0;36m"; static const char *DIR_COLOR = "\033[0;34m"; static const char *LINK_COLOR = "\033[0;33m"; - for (int i = state->scroll; i < state->scroll + height - 3; i++) { + for (int i = s->scroll; i < s->scroll + termheight - 3; i++) { if (lazy) { - if (i == state->cursor || i == lastcursor) + if (i == s->cursor || i == lastcursor) goto do_render; - if (i < lastscroll || i >= lastscroll + height - 3) + if (i < lastscroll || i >= lastscroll + termheight - 3) goto do_render; continue; } do_render:; - int y = i - state->scroll + 2; + int y = i - s->scroll + 2; term_move(0, y); - if (i >= state->nfiles) { + if (i >= s->nfiles) { writez(termfd, "\033[K"); continue; } @@ -383,7 +458,7 @@ static void render(bb_state_t *state, int lazy) } const char *color; - if (i == state->cursor) + if (i == s->cursor) color = CURSOR_COLOR; else if (entry->d_isdir && entry->d_type == DT_LNK) color = LINKDIR_COLOR; @@ -395,8 +470,8 @@ static void render(bb_state_t *state, int lazy) color = NORMAL_COLOR; writez(termfd, color); - for (char *col = state->columns; *col; ++col) { - if (col != state->columns) writez(termfd, " │ "); + for (char *col = s->columns; *col; ++col) { + if (col != s->columns) writez(termfd, " │ "); switch (*col) { case 's': { int j = 0; @@ -449,13 +524,13 @@ static void render(bb_state_t *state, int lazy) err("readlink() failed"); writez(termfd, "\033[2m -> "); wrote += 4; - wrote += write_escaped(termfd, linkpath, pathlen, color); + wrote += write_escaped(termfd, linkpath, (size_t)pathlen, color); if (entry->d_isdir) { writez(termfd, "/"); ++wrote; } } - while (wrote++ < namewidth - 1) + while (wrote++ < colnamew - 1) write(termfd, " ", 1); break; } @@ -465,15 +540,16 @@ static void render(bb_state_t *state, int lazy) } static const char *help = "Press '?' to see key bindings "; - term_move(0, height - 1); + term_move(0, termheight - 1); writez(termfd, "\033[K"); - term_move(MAX(0, width - (int)strlen(help)), height - 1); + term_move(MAX(0, termwidth - (int)strlen(help)), termheight - 1); writez(termfd, help); - lastcursor = state->cursor; - lastscroll = state->scroll; + lastcursor = s->cursor; + lastscroll = s->scroll; + // TODO: show selection and dotfile setting and anything else? } -static int compare_files(void *r, const void *v1, const void *v2) +int compare_files(void *r, const void *v1, const void *v2) { char sort = *((char *)r); int sign = IS_REVERSED(sort) ? -1 : 1; @@ -529,118 +605,169 @@ static int compare_files(void *r, const void *v1, const void *v2) return 0; } -static entry_t *find_file(bb_state_t *state, const char *name) +int find_file(bb_state_t *s, const char *name) { - for (int i = 0; i < state->nfiles; i++) { - entry_t *e = state->files[i]; + for (int i = 0; i < s->nfiles; i++) { + entry_t *e = s->files[i]; if (strcmp(name[0] == '/' ? e->d_fullname : e->d_name, name) == 0) - return e; + return i; } - return NULL; + return -1; } -static void write_selection(int fd, entry_t *firstselected, char sep) +void write_selection(int fd, char sep) { - while (firstselected) { - const char *p = firstselected->d_fullname; + for (entry_t *e = firstselected; e; e = e->next) { + const char *p = e->d_fullname; while (*p) { const char *p2 = strchr(p, '\n'); if (!p2) p2 = p + strlen(p); - write(fd, p, p2 - p); + write(fd, p, (size_t)(p2 - p)); if (*p2 == '\n' && sep == '\n') write(fd, "\\", 1); p = p2; } write(fd, &sep, 1); - firstselected = firstselected->next; } } -static void clear_selection(bb_state_t *state) +void clear_selection(void) { - entry_t **tofree = memcheck(calloc(state->nselected, sizeof(entry_t*))); - int i = 0; - for (entry_t *e = state->firstselected; e; e = e->next) { - if (!e->visible) tofree[i++] = e; - *e->atme = NULL; - e->atme = NULL; + for (entry_t *next, *e = firstselected; e; e = next) { + next = e->next; + if (!e->visible) + free(e); } - while (i) free(tofree[--i]); - free(tofree); - state->nselected = 0; + firstselected = NULL; } -static int select_file(bb_state_t *state, entry_t *e) +int select_file(entry_t *e) { if (IS_SELECTED(e)) return 0; if (strcmp(e->d_name, "..") == 0) return 0; - if (state->firstselected) - state->firstselected->atme = &e->next; - e->next = state->firstselected; - e->atme = &state->firstselected; - state->firstselected = e; - ++state->nselected; + if (firstselected) + firstselected->atme = &e->next; + e->next = firstselected; + e->atme = &firstselected; + firstselected = e; return 1; } -static int deselect_file(bb_state_t *state, entry_t *e) +int deselect_file(entry_t *e) { if (!IS_SELECTED(e)) return 0; - if (e->next) e->next->atme = e->atme; - *(e->atme) = e->next; - e->next = NULL; e->atme = NULL; - --state->nselected; + LLREMOVE(e); return 1; } -static void populate_files(bb_state_t *state) +void set_cursor(bb_state_t *state, int newcur) { + if (newcur > state->nfiles - 1) newcur = state->nfiles - 1; + if (newcur < 0) newcur = 0; + state->cursor = newcur; + if (state->nfiles <= termheight - 4) + return; + + if (newcur < state->scroll + SCROLLOFF) + state->scroll = newcur - SCROLLOFF; + else if (newcur > state->scroll + (termheight-4) - SCROLLOFF) + state->scroll = newcur - (termheight-4) + SCROLLOFF; + int max_scroll = state->nfiles - (termheight-4) - 1; + if (max_scroll < 0) max_scroll = 0; + if (state->scroll > max_scroll) state->scroll = max_scroll; + if (state->scroll < 0) state->scroll = 0; +} + +void set_scroll(bb_state_t *state, int newscroll) +{ + newscroll = MIN(newscroll, state->nfiles - (termheight-4) - 1); + newscroll = MAX(newscroll, 0); + state->scroll = newscroll; + + int delta = newscroll - state->scroll; + int oldcur = state->cursor; + if (state->nfiles < termheight - 4) { + newscroll = 0; + } else { + if (state->cursor > newscroll + (termheight-4) - SCROLLOFF && state->scroll < state->nfiles - (termheight-4) - 1) + state->cursor = newscroll + (termheight-4) - SCROLLOFF; + else if (state->cursor < newscroll + SCROLLOFF && state->scroll > 0) + state->cursor = newscroll + SCROLLOFF; + } + state->scroll = newscroll; + if (abs(state->cursor - oldcur) < abs(delta)) + state->cursor += delta - (state->cursor - oldcur); + if (state->cursor > state->nfiles - 1) state->cursor = state->nfiles - 1; + if (state->cursor < 0) state->cursor = 0; +} + +void populate_files(bb_state_t *s, const char *path) +{ + if (!path) err("No path given"); + ino_t old_inode = 0; // Clear old files (if any) - if (state->files) { - for (int i = 0; i < state->nfiles; i++) { - entry_t *e = state->files[i]; - e->visible = 0; + if (s->files) { + old_inode = s->files[s->cursor]->d_ino; + for (int i = 0; i < s->nfiles; i++) { + entry_t *e = s->files[i]; + --e->visible; if (!IS_SELECTED(e)) free(e); } - free(state->files); + free(s->files); + s->files = NULL; + } + s->nfiles = 0; + s->cursor = 0; + if (strcmp(path, s->path) != 0) { + s->scroll = 0; + strcpy(s->path, path); } - state->files = NULL; - state->nfiles = 0; // Hash inode -> entry_t with linear probing - size_t hashsize = 2 * state->nselected; - entry_t **selecthash = memcheck(calloc(hashsize, sizeof(entry_t*))); - for (entry_t *p = state->firstselected; p; p = p->next) { - int probe = ((int)p->d_ino) % hashsize; - while (selecthash[probe]) - probe = (probe + 1) % hashsize; - selecthash[probe] = p; + int nselected = 0; + for (entry_t *p = firstselected; p; p = p->next) ++nselected; + int hashsize = 2 * nselected; + entry_t **selecthash = NULL; + if (nselected > 0) { + selecthash = memcheck(calloc((size_t)hashsize, sizeof(entry_t*))); + for (entry_t *p = firstselected; p; p = p->next) { + int probe = ((int)p->d_ino) % hashsize; + while (selecthash[probe]) + probe = (probe + 1) % hashsize; + selecthash[probe] = p; + } } - DIR *dir = opendir(state->path); + DIR *dir = opendir(s->path); if (!dir) - err("Couldn't open dir: %s", state->path); - struct dirent *dp; - size_t pathlen = strlen(state->path); + err("Couldn't open dir: %s", s->path); + size_t pathlen = strlen(s->path); size_t filecap = 0; - while ((dp = readdir(dir)) != NULL) { + for (struct dirent *dp; (dp = readdir(dir)) != NULL; ) { if (dp->d_name[0] == '.' && dp->d_name[1] == '\0') continue; - if (!state->showhidden && dp->d_name[0] == '.' && !(dp->d_name[1] == '.' && dp->d_name[2] == '\0')) + if (!s->showhidden && dp->d_name[0] == '.' && !(dp->d_name[1] == '.' && dp->d_name[2] == '\0')) continue; + if ((size_t)s->nfiles >= filecap) { + filecap += 100; + s->files = memcheck(realloc(s->files, filecap*sizeof(entry_t*))); + } + // Hashed lookup from selected: - if (state->nselected > 0) { + if (nselected > 0) { for (int probe = ((int)dp->d_ino) % hashsize; selecthash[probe]; probe = (probe + 1) % hashsize) { if (selecthash[probe]->d_ino == dp->d_ino) { - selecthash[probe]->visible = 1; - state->files[state->nfiles++] = selecthash[probe]; + ++selecthash[probe]->visible; + s->files[s->nfiles++] = selecthash[probe]; goto next_file; } } } + entry_t *entry = memcheck(malloc(sizeof(entry_t) + pathlen + dp->d_namlen + 2)); - strncpy(entry->d_fullname, state->path, pathlen); + if (pathlen > MAX_PATH) err("Path is too big"); + strncpy(entry->d_fullname, s->path, pathlen); entry->d_fullname[pathlen] = '/'; entry->d_name = &entry->d_fullname[pathlen + 1]; strncpy(entry->d_name, dp->d_name, dp->d_namlen + 1); @@ -648,7 +775,7 @@ static void populate_files(bb_state_t *state) entry->d_reclen = dp->d_reclen; entry->d_type = dp->d_type; entry->d_isdir = dp->d_type == DT_DIR; - entry->visible = 1; + ++entry->visible; if (!entry->d_isdir && entry->d_type == DT_LNK) { struct stat statbuf; if (stat(entry->d_fullname, &statbuf) == 0) @@ -656,78 +783,80 @@ static void populate_files(bb_state_t *state) } entry->d_namlen = dp->d_namlen; entry->next = NULL; entry->atme = NULL; - - if (state->nfiles >= filecap) { - filecap += 100; - state->files = memcheck(realloc(state->files, filecap*sizeof(entry_t*))); - } - state->files[state->nfiles++] = entry; + s->files[s->nfiles++] = entry; next_file:; } closedir(dir); free(selecthash); - if (state->nfiles == 0) err("No files found (not even '..')"); + if (s->nfiles == 0) err("No files found (not even '..')"); + + if (old_inode) { + for (int i = 0; i < s->nfiles; i++) { + if (s->files[i]->d_ino == old_inode) { + set_cursor(s, i); + break; + } + } + } } -static int explore(bb_state_t *state) +void sort_files(bb_state_t *state) { - long cmdpos = 0; - if (chdir(state->path) != 0) - err("Couldn't chdir into '%s'", state->path); - refresh:; - if (!getwd(state->path)) - err("Couldn't get working directory"); - - populate_files(state); - - state->cursor = 0; - state->scroll = 0; - - sort_files: - qsort_r(&state->files[1], state->nfiles-1, sizeof(entry_t*), &state->sort, compare_files); + ino_t cursor_inode = state->files[state->cursor]->d_ino; + qsort_r(&state->files[1], (size_t)(state->nfiles-1), sizeof(entry_t*), &state->sort, compare_files); if (DESCENDING(state->sort) == SORT_RANDOM) { entry_t **files = &state->files[1]; - size_t ndirs = 0, nents = state->nfiles - 1; + int ndirs = 0, nents = state->nfiles - 1; for (int i = 0; i < nents; i++) { if (state->files[i]->d_isdir) ++ndirs; else break; } - for (size_t i = 0; i < ndirs - 1; i++) { - //size_t j = i + rand() / (RAND_MAX / (ndirs - i) + 1); - size_t j = i + rand() / (RAND_MAX / (ndirs - 1 - i)); + for (int i = 0; i < ndirs - 1; i++) { + int j = i + rand() / (RAND_MAX / (ndirs - 1 - i)); entry_t *tmp = files[j]; files[j] = files[i]; files[i] = tmp; } - for (size_t i = ndirs; i < nents - 1; i++) { - //size_t j = i + rand() / (RAND_MAX / (nents - i) + 1); - size_t j = i + rand() / (RAND_MAX / (nents - 1 - i)); + for (int i = ndirs; i < nents - 1; i++) { + int j = i + rand() / (RAND_MAX / (nents - 1 - i)); entry_t *tmp = files[j]; files[j] = files[i]; files[i] = tmp; } } - - // Put the cursor on the first *real* file if one exists - if (state->cursor == 0 && state->nfiles > 1) - ++state->cursor; - - if (state->to_select) { - char *sel = state->to_select; - for (int i = 0; i < state->nfiles; i++) { - if (strcmp(sel, sel[0] == '/' ? state->files[i]->d_fullname : state->files[i]->d_name) == 0) { - state->cursor = i; - if (state->nfiles > height - 4) - state->scroll = MAX(0, i - MIN(SCROLLOFF, (height-4)/2)); - break; - } + for (int i = 0; i < state->nfiles; i++) { + if (state->files[i]->d_ino == cursor_inode) { + set_cursor(state, i); + break; } - free(state->to_select); - state->to_select = NULL; } +} - int lastwidth = width, lastheight = height; - int scrolloff = MIN(SCROLLOFF, (height-4)/2); +entry_t *explore(const char *path) +{ + static long cmdpos = 0; + + init_term(); + alt_screen(); + hide_cursor(); + + bb_state_t *state = new_state(NULL); + if (!firststate) { + firststate = state; + state->atme = &firststate; + } + { + char *real = realpath(path, NULL); + if (!real || chdir(real)) + err("Not a valid path: %s\n", path); + populate_files(state, real); + free(real); // estate + } + sort_files(state); + + refresh:; + + int lastwidth = termwidth, lastheight = termheight; int lazy = 0; redraw: @@ -735,9 +864,8 @@ static int explore(bb_state_t *state) lazy = 1; next_input: - if (width != lastwidth || height != lastheight) { - scrolloff = MIN(SCROLLOFF, (height-4)/2); - lastwidth = width; lastheight = height; + if (termwidth != lastwidth || termheight != lastheight) { + lastwidth = termwidth; lastheight = termheight; lazy = 0; goto redraw; } @@ -765,8 +893,8 @@ static int explore(bb_state_t *state) if (value) ++value; switch (cmd[0]) { case 'r': // refresh - queue_select(state, state->files[state->cursor]->d_name); - cleanup_cmd(); + populate_files(state, state->path); + sort_files(state); goto refresh; case 'q': // quit cleanup_cmd(); @@ -780,44 +908,38 @@ static int explore(bb_state_t *state) case 'c': case 'C': case 'a': case 'A': case 'r': case 'R': state->sort = *value; - queue_select(state, state->files[state->cursor]->d_name); + sort_files(state); cleanup_cmd(); - goto sort_files; + lazy = 0; + goto refresh; } goto next_cmd; case 'c': { // scroll: + // TODO: figure out the best version of this int isdelta = value[0] == '+' || value[0] == '-'; - long n = strtol(value, &value, 10); + int n = (int)strtol(value, &value, 10); if (*value == '%') - n = (n * (value[1] == 'n' ? state->nfiles : height)) / 100; - if (isdelta) { - state->cursor += n; - if (state->nfiles > height-4) - state->scroll += n; - } else { - state->cursor = (int)n; - if (state->nfiles > height-4) - state->scroll = (int)n; - } - if (state->nfiles > height-4) - state->scroll = clamped(state->scroll, 0, state->nfiles-1 - (height-4)); - state->cursor = clamped(state->cursor, 0, state->nfiles-1); - break; + n = (n * (value[1] == 'n' ? state->nfiles : termheight)) / 100; + if (isdelta) + set_scroll(state, state->scroll + n); + else + set_scroll(state, n); + goto next_cmd; } - case 'p': + case 'p': // spread: goto move; case '\0': case 'e': // select: lazy = 0; if (strcmp(value, "*") == 0) { for (int i = 0; i < state->nfiles; i++) - select_file(state, state->files[i]); + select_file(state->files[i]); } else { - entry_t *e = find_file(state, value); - if (e) select_file(state, e); + int f = find_file(state, value); + if (f >= 0) select_file(state->files[f]); } - break; + goto next_cmd; } case 'c': switch (cmd[1]) { @@ -826,18 +948,23 @@ static int explore(bb_state_t *state) if (!rpbuf) continue; if (strcmp(rpbuf, state->path) == 0) { free(rpbuf); - continue; + goto next_cmd; } - if (chdir(rpbuf) == 0) { - if (strcmp(value, "..") == 0) - queue_select(state, state->path); - strcpy(state->path, rpbuf); - free(rpbuf); - cleanup_cmd(); - goto refresh; - } else { + if (chdir(rpbuf)) { free(rpbuf); + goto next_cmd; } + char *oldpath = memcheck(strdup(state->path)); + populate_files(state, rpbuf); + sort_files(state); + free(rpbuf); + if (strcmp(value, "..") == 0) { + int f = find_file(state, oldpath); + if (f >= 0) set_cursor(state, f); + } + free(oldpath); + cleanup_cmd(); + goto refresh; } case 'o': // cols: for (char *col = value, *dst = state->columns; @@ -845,41 +972,110 @@ static int explore(bb_state_t *state) *(dst++) = DESCENDING(*col); *dst = '\0'; } + break; + case 'l': { // closetab: + if (!value) value = "+0"; + bb_state_t *from = (value[0] == '+' || value[0] == '-') ? state : firststate; + int n = (int)strtol(value, &value, 10); + bb_state_t *s; + if (n < 0) { + for (s = from; n++; s = PREV_STATE(s)) + if (!s) goto next_cmd; + } else { + for (s = from; n--; s = s->next) + if (!s) goto next_cmd; + } + if (state == s) { + if (s->next) + state = s->next; + else if (PREV_STATE(s)) + state = PREV_STATE(s); + if (chdir(state->path)) + err("Could not cd to '%s'", state->path); + } + delete_state(s); + goto next_cmd; + } } - case 't': { // toggle: - lazy = 0; - entry_t *e = find_file(state, value); - if (e) { - if (IS_SELECTED(e)) deselect_file(state, e); - else select_file(state, e); - } - break; + case 'n': { // newtab + bb_state_t *ns = new_state(state); + if (state->next) + state->next->atme = &ns->next; + ns->next = state->next; + ns->atme = &state->next; + state->next = ns; + state = ns; + if (chdir(ns->path)) + err("Could not cd to '%s'", ns->path); + goto refresh; } - case 'd': { // deselect: - lazy = 0; - if (strcmp(value, "*") == 0) { - clear_selection(state); - } else { - entry_t *e = find_file(state, value); - if (e) select_file(state, e); + case 't': { // toggle:, tab: + if (cmd[1] == 'o') { // toggle: + lazy = 0; + int f = find_file(state, value); + if (f >= 0) { + entry_t *e = state->files[f]; + if (IS_SELECTED(e)) deselect_file(e); + else select_file(e); + } + goto next_cmd; + } else if (cmd[1] == 'a') { // tab: + bb_state_t *from = (value[0] == '+' || value[0] == '-') ? state : firststate; + int n = (int)strtol(value, &value, 10); + bb_state_t *s = from; + if (n < 0) { + while (s && n++) s = PREV_STATE(s); + } else { + while (s && n--) s = s->next; + } + if (!s || s == state) + goto next_cmd; + + state = s; + if (chdir(s->path)) + err("Could not cd to '%s'", s->path); + goto refresh; + } + } + case 'd': { // deselect:, dots: + if (cmd[1] == 'o') { // dots: + int requested = value ? (value[0] == 'y') : state->showhidden ^ 1; + if (requested == state->showhidden) + goto next_cmd; + state->showhidden = requested; + populate_files(state, state->path); + sort_files(state); + goto refresh; + } else if (value) { // deselect: + lazy = 0; + if (strcmp(value, "*") == 0) { + clear_selection(); + } else { + int f = find_file(state, value); + if (f >= 0) + select_file(state->files[f]); + } + goto next_cmd; } } case 'g': { // goto: - for (int i = 0; i < state->nfiles; i++) { - if (strcmp(value[0] == '/' ? - state->files[i]->d_fullname : state->files[i]->d_name, - value) == 0) { - state->cursor = i; - goto next_cmd; - } + int f = find_file(state, value); + if (f >= 0) { + set_cursor(state, f); + goto next_cmd; } char *lastslash = strrchr(value, '/'); if (!lastslash) goto next_cmd; *lastslash = '\0'; // Split in two - if (chdir(value) != 0) goto next_cmd; - strcpy(state->path, value); - if (lastslash[1]) - queue_select(state, lastslash+1); + char *real = realpath(path, NULL); + if (!real || chdir(real)) + err("Not a valid path: %s\n", path); + populate_files(state, real); + free(real); // estate + if (lastslash[1]) { + f = find_file(state, value); + if (f >= 0) set_cursor(state, f); + } cleanup_cmd(); goto refresh; } @@ -887,39 +1083,22 @@ static int explore(bb_state_t *state) move:; int oldcur = state->cursor; int isdelta = value[0] == '-' || value[0] == '+'; - long n = strtol(value, &value, 10); + int n = (int)strtol(value, &value, 10); if (*value == '%') - n = (n * (value[1] == 'n' ? state->nfiles : height)) / 100; - if (isdelta) state->cursor += n; - else state->cursor = n; - - state->cursor = clamped(state->cursor, 0, state->nfiles-1); - int delta = state->cursor - oldcur; - - if (state->nfiles > height-4) { - if (delta > 0) { - if (state->cursor >= state->scroll + (height-4) - scrolloff) - state->scroll += delta; - } else if (delta < 0) { - if (state->cursor <= state->scroll + scrolloff) - state->scroll += delta; - } - //int target = clamped(state->scroll, state->cursor - (height-4) + scrolloff, state->cursor - scrolloff); - //state->scroll += (delta > 0 ? 1 : -1)*MIN(abs(target-state->scroll), abs((int)delta)); - //state->scroll = target; - state->scroll = clamped(state->scroll, state->cursor - (height-4) + 1, state->cursor); - state->scroll = clamped(state->scroll, 0, state->nfiles-1 - (height-4)); - } + n = (n * (value[1] == 'n' ? state->nfiles : termheight)) / 100; + if (isdelta) set_cursor(state, state->cursor + n); + else set_cursor(state, n); if (cmd[0] == 's') { // spread: int sel = IS_SELECTED(state->files[oldcur]); for (int i = state->cursor; i != oldcur; i += (oldcur > i ? 1 : -1)) { if (sel && !IS_SELECTED(state->files[i])) - select_file(state, state->files[i]); + select_file(state->files[i]); else if (!sel && IS_SELECTED(state->files[i])) - deselect_file(state, state->files[i]); + deselect_file(state->files[i]); } lazy &= abs(oldcur - state->cursor) <= 1; } + goto next_cmd; } default: break; } @@ -940,27 +1119,28 @@ static int explore(bb_state_t *state) case KEY_MOUSE_LEFT: { struct timespec clicktime; clock_gettime(CLOCK_MONOTONIC, &clicktime); - double dt_ms = 1e3*(double)(clicktime.tv_sec - state->lastclick.tv_sec); - dt_ms += 1e-6*(double)(clicktime.tv_nsec - state->lastclick.tv_nsec); - state->lastclick = 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; if (mouse_y == 1) { // Sort column: int x = 1; for (char *col = state->columns; *col; ++col) { if (col != state->columns) x += 3; switch (*col) { - case 's': x += sizewidth; break; - case 'p': x += permwidth; break; + case 's': x += colsizew; break; + case 'p': x += colpermw; break; case 'm': case 'a': case 'c': - x += datewidth; break; - case 'n': x += namewidth; break; + x += coldatew; break; + case 'n': x += colnamew; break; } if (x >= mouse_x) { if (DESCENDING(state->sort) == *col) state->sort ^= SORT_DESCENDING; else state->sort = *col; - goto sort_files; + sort_files(state); + goto refresh; } } goto next_input; @@ -968,12 +1148,12 @@ static int explore(bb_state_t *state) int clicked = state->scroll + (mouse_y - 2); if (mouse_x == 0) { if (IS_SELECTED(state->files[clicked])) - deselect_file(state, state->files[clicked]); + deselect_file(state->files[clicked]); else - select_file(state, state->files[clicked]); + select_file(state->files[clicked]); goto redraw; } - state->cursor = clicked; + set_cursor(state, clicked); if (dt_ms <= 200) key = KEY_MOUSE_DOUBLE_LEFT; goto user_bindings; @@ -983,7 +1163,7 @@ static int explore(bb_state_t *state) case KEY_CTRL_C: cleanup_and_exit(SIGINT); - return 1; + goto quit; // Unreachable case KEY_CTRL_Z: default_screen(); @@ -996,17 +1176,12 @@ static int explore(bb_state_t *state) lazy = 0; goto redraw; - case '.': - state->showhidden ^= 1; - queue_select(state, state->files[state->cursor]->d_name); - goto refresh; - case KEY_CTRL_H: { - term_move(0,height-1); + term_move(0,termheight-1); writez(termfd, "\033[K\033[33;1mPress any key...\033[0m"); while ((key = term_getkey(termfd, &mouse_x, &mouse_y, 1000)) == -1) ; - term_move(0,height-1); + term_move(0,termheight-1); writez(termfd, "\033[K\033[1m<\033[33m"); const char *name = keyname(key); char buf[32] = {(char)key}; @@ -1052,7 +1227,7 @@ static int explore(bb_state_t *state) run_binding: if (cmdpos != 0) err("Command file still open"); - term_move(0, height-1); + term_move(0, termheight-1); //writez(termfd, "\033[K"); if (binding->flags & NORMAL_TERM) { default_screen(); @@ -1071,10 +1246,14 @@ static int explore(bb_state_t *state) goto next_input; quit: - return 0; + + default_screen(); + show_cursor(); + close_term(); + return firstselected; } -static void print_bindings(int verbose) +void print_bindings(int verbose) { struct winsize sz = {0}; ioctl(STDOUT_FILENO, TIOCGWINSZ, &sz); @@ -1112,15 +1291,10 @@ static void print_bindings(int verbose) int main(int argc, char *argv[]) { - bb_state_t state; - memset(&state, 0, sizeof(bb_state_t)); - clock_gettime(CLOCK_MONOTONIC, &state.lastclick); - state.sort = 'n'; - strncpy(state.columns, "smpn", sizeof(state.columns)); - - char *initial_path = NULL; + char *initial_path = NULL, *depthstr; char sep = '\n'; int print_dir = 0, print_selection = 0; + int cmd_args = 0; for (int i = 1; i < argc; i++) { if (argv[i][0] == '+') { @@ -1142,6 +1316,13 @@ int main(int argc, char *argv[]) if (!mktemp(cmdfilename)) err("Couldn't create tmpfile\n"); cmdfile = fopen(cmdfilename, "a"); if (!cmdfile) err("Couldn't create cmdfile: '%s'\n", cmdfilename); + // Set up environment variables + depthstr = getenv("BB_DEPTH"); + int depth = depthstr ? atoi(depthstr) : 0; + if (asprintf(&depthstr, "%d", depth + 1) < 0) + err("Allocation failure"); + setenv("BB_DEPTH", depthstr, 1); + setenv("BBCMD", cmdfilename, 1); } else if (cmd_args > 0) { char *parent_bbcmd = getenv("BBCMD"); if (!parent_bbcmd || parent_bbcmd[0] == '\0') { @@ -1200,10 +1381,6 @@ int main(int argc, char *argv[]) return 0; } - char *real = realpath(initial_path, NULL); - if (!real) err("Not a valid file: %s\n", initial_path); - strcpy(state.path, initial_path); - signal(SIGTERM, cleanup_and_exit); signal(SIGINT, cleanup_and_exit); signal(SIGXCPU, cleanup_and_exit); @@ -1211,26 +1388,19 @@ int main(int argc, char *argv[]) signal(SIGVTALRM, cleanup_and_exit); signal(SIGPROF, cleanup_and_exit); - init_term(); - alt_screen(); - hide_cursor(); - int ret = explore(&state); - default_screen(); - show_cursor(); - close_term(); + char *real = realpath(initial_path, NULL); + if (!real || chdir(real)) err("Not a valid path: %s\n", initial_path); + explore(real); + free(real); - unlink(cmdfilename); - - if (ret == 0) { - if (print_dir) - printf("%s\n", state.path); - if (print_selection) - write_selection(STDOUT_FILENO, state.firstselected, sep); - } else if (print_dir) { + if (firstselected && print_selection) { + write_selection(STDOUT_FILENO, sep); + } + if (print_dir) { printf("%s\n", initial_path); } - return ret; + return 0; } // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 diff --git a/keys.h b/keys.h index c219ae9..cdacaf8 100644 --- a/keys.h +++ b/keys.h @@ -229,6 +229,8 @@ const char *keyname(int key) case KEY_CTRL_H: return "Ctrl-h"; case KEY_CTRL_R: return "Ctrl-r"; case KEY_CTRL_U: return "Ctrl-u"; + case KEY_CTRL_T: return "Ctrl-t"; + case KEY_CTRL_W: return "Ctrl-w"; case KEY_PGDN: return "PgDn"; case KEY_PGUP: return "PgUp"; case KEY_MOUSE_WHEEL_DOWN: return "Scroll down";