diff options
| -rw-r--r-- | API.md | 69 | ||||
| -rw-r--r-- | Makefile | 49 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | bb.c | 218 | ||||
| -rwxr-xr-x | bbstartup.sh | 31 | ||||
| -rw-r--r-- | bindings.bb | 165 | ||||
| -rw-r--r-- | config.def.h | 247 |
7 files changed, 476 insertions, 309 deletions
@@ -0,0 +1,69 @@ +# BB's API +In `bb`, all interaction (more or less) occurs through binding keypresses +(and mouse events) to shell scripts. These shell scripts can perform external +actions (like moving files around) or internal actions (like changing the +directory `bb` is displaying). When a shell script runs, `bb` creates a +temporary file, and scripts may write commands to this file to modify `bb`'s +internal state. + +## Helper Functions +- `bb`: used for modifying `bb`'s internal state (see BB Commands). +- `ask`: get user input in a standardized and customizable way. The first + argument is a variable where the value is stored. The second argument is + a prompt. A third optional argument can provide a default value (may be + ignored). +- `ask1`: get a single character of user input. The first argument is a variable + where the input will be stored and the second argument is a prompt. +- `pause`: Display a "press any key to continue" message and wait for a keypress. +- `confirm`: Display a "Is this okay? [y/N]" prompt and exit with failure if + the user does not press 'y'. +- `spin`: Display a spinning icon while a slow command executes in the background. + (e.g. `spin sleep 5`). + +## Environment Variables +For startup commands and key bindings, the following values are provided as +environment variables: + +- `$@` (the list of arguments): the full paths of the selected files +- `$BBCURSOR`: the full path of the file under the cursor +- `$BBDOTFILES`: "1" if files beginning with "." are visible in bb, otherwise "" +- `$BB_DEPTH`: the number of `bb` instances deep (in case you want to run a + shell and have that shell print something special in the prompt) +- `$BBCMD`: a file to which `bb` commands can be written (used internally) + +## BB Internal State Commands +In order to modify bb's internal state, you can call `bb +cmd`, where "cmd" +is one of the following commands (or a unique prefix of one): + +- `.:[01]` Whether to show "." in each directory +- `..:[01]` Whether to show ".." in each directory +- `cd:<path>` Navigate to <path> +- `columns:<columns>` Change which columns are visible, and in what order +- `deselect:<filename>` Deselect <filename> +- `dotfiles:[01]` Whether dotfiles are visible +- `goto:<filename>` Move the cursor to <filename> (changing directory if needed) +- `interleave:[01]` Whether or not directories should be interleaved with files in the display +- `move:<num*>` Move the cursor a numeric amount +- `quit` Quit bb +- `refresh` Refresh the file listing +- `scroll:<num*>` Scroll the view a numeric amount +- `select:<filename>` Select <filename> +- `sort:([+-]method)+` Set sorting method (+: normal, -: reverse), additional methods act as tiebreaker +- `spread:<num*>` Spread the selection state at the cursor +- `toggle:<filename>` Toggle the selection status of <filename> + +For any of these commands (e.g. `+select`), an empty parameter followed by +additional arguments (e.g. `bb +select: <file1> <file2> ...`) is equivalent to +repeating the command with each argument (e.g. `bb +select:<file1> ++select:<file2> ...`). + +Note: for numeric-based commands (like scroll), the number can be either an +absolute value or a relative value (starting with `+` or `-`), and/or a percent +(ending with `%`). Scrolling and moving, `%` means percent of screen height, +and `%n` means percent of number of files (e.g. `+50%` means half a screen +height down, and `100%n` means the last file) + +Internally, `bb` will write the commands (NUL terminated) to a file whose path +is in`$BBCMD` and read the file when `bb` resumes. These commands can also be +passed to bb at startup, and will run immediately. E.g. `bb '+col:n' +'+sort:+r' .` will launch `bb` only showing the name column, randomly sorted. @@ -22,21 +22,21 @@ ifeq (, $(PICKER)) PICKER=$(shell sh -c "(which fzy >/dev/null 2>/dev/null && echo 'fzy') || (which fzf >/dev/null 2>/dev/null && echo 'fzf') || (which pick >/dev/null 2>/dev/null && echo 'pick') || (which ask >/dev/null 2>/dev/null && echo 'ask')") endif ifneq (, $(PICKER)) - PICKER_FLAG=-D"PICK(prompt)=\"$(PICKER)\"" + PICKER_FLAG=-D"PICK=\"$(PICKER) --prompt=\\\"$$1\\\"\"" ifeq ($(shell which $(PICKER)),$(shell which fzy 2>/dev/null || echo '<none>')) - PICKER_FLAG=-D'PICK(prompt)="{ printf \"\\033[3A\" >/dev/tty; fzy --lines=3 --prompt=\"\033[1m" prompt "\033[0m\"; }"' + PICKER_FLAG=-D'PICK="printf \"\\033[3A\" >/dev/tty; fzy --lines=3 --prompt=\"\033[1m$$1\033[0m\""' endif ifeq ($(shell which $(PICKER)),$(shell which fzf 2>/dev/null || echo '<none>')) - PICKER_FLAG=-D'PICK(prompt)="{ printf \"\\033[3A\" >/dev/tty; fzf --height=4 --prompt=\"" prompt "\"; }"' + PICKER_FLAG=-D'PICK="printf \"\\033[3A\" >/dev/tty; fzf --height=4 --prompt=\"$$1\""' endif ifeq ($(shell which $(PICKER)),$(shell which ask 2>/dev/null || echo '<none>')) - PICKER_FLAG=-D'PICK(prompt)="ask --prompt=\"" prompt "\""' + PICKER_FLAG=-D'PICK="/usr/bin/env ask --prompt=\"$$1\""' endif ifeq ($(shell which $(PICKER)),$(shell which pick 2>/dev/null || echo '<none>')) - PICKER_FLAG=-D'PICK(prompt)="pick"' + PICKER_FLAG=-D'PICK="pick"' endif ifeq ($(shell which $(PICKER)),$(shell which dmenu 2>/dev/null || echo '<none>')) - PICKER_FLAG=-D'PICK(prompt)="dmenu -i -l 10 -p \"" prompt "\""' + PICKER_FLAG=-D'PICK="dmenu -i -l 10 -p \"$$1\""' endif endif CFLAGS += $(PICKER_FLAG) @@ -44,11 +44,11 @@ CFLAGS += $(PICKER_FLAG) ifneq (, $(ASKER)) PERCENT := % ifeq ($(shell which $(ASKER)),$(shell which ask 2>/dev/null || echo '<none>')) - CFLAGS += -D'ASK(var, prompt, initial)=var "=\"$$(ask --history=bb."STRINGIFY(__COUNTER__)".hist --prompt=\"" prompt "\" --query=\"" initial "\")\""' - CFLAGS += -D'CONFIRM(action, files)=" { printf \"$(PERCENT)s\\n\" \""B(action)"\" \""files"\" | more; ask -n \"Is that okay?\"; } "' + CFLAGS += -D'ASK="eval \"$$1=\\$$(/usr/bin/env ask --history=bb.hist --prompt=\\\"$$2\\\" --query=\\\"$$3\\\")\""' + CFLAGS += -D'CONFIRM="/usr/bin/env ask -n \"Is that okay?\""' endif ifeq ($(shell which $(ASKER)),$(shell which dmenu 2>/dev/null || echo '<none>')) - CFLAGS += -D'ASK(var, prompt, initial)=var "=\"$$(printf \"" initial "\" | dmenu -p \"" prompt "\")\""' + CFLAGS += -D'ASK="eval \"$$1=\\$$(echo \"$$3\" | dmenu -p \"$$2\")\""' endif endif @@ -65,24 +65,27 @@ $(NAME): $(NAME).c bterm.h config.h install: $(NAME) @prefix="$(PREFIX)"; \ - if test -z $$prefix; then \ - read -p $$'\033[1mWhere do you want to install? (default: /usr/local) \033[0m' prefix; \ + if [ ! "$$prefix" ]; then \ + printf '\033[1mWhere do you want to install? (default: /usr/local) \033[0m'; \ + read prefix; \ fi; \ - if test -z $$prefix; then \ - prefix="/usr/local"; \ - fi; \ - mkdir -pv $$prefix/bin $$prefix/share/man/man1 \ - && cp -v $(NAME) $$prefix/bin/ \ - && cp -v $(NAME).1 $$prefix/share/man/man1/ + [ ! "$$prefix" ] && prefix="/usr/local"; \ + [ ! "$$sysconfdir" ] && sysconfdir=/etc; \ + mkdir -m 700 -pv "$$prefix/bin" "$$prefix/share/man/man1" "$$sysconfdir/bb" \ + && cp -v $(NAME) "$$prefix/bin/" \ + && cp -v $(NAME).1 "$$prefix/share/man/man1/" \ + && cp -v bbstartup.sh bindings.bb "$$sysconfdir/bb/" uninstall: @prefix="$(PREFIX)"; \ - if test -z $$prefix; then \ - read -p $$'\033[1mWhere do you want to uninstall from? (default: /usr/local) \033[0m' prefix; \ - fi; \ - if test -z $$prefix; then \ - prefix="/usr/local"; \ + if [ ! "$$prefix" ]; then \ + printf '\033[1mWhere do you want to uninstall from? (default: /usr/local) \033[0m'; \ + read prefix; \ fi; \ + [ ! "$$prefix" ] && prefix="/usr/local"; \ + [ ! "$$sysconfdir" ] && sysconfdir=/etc; \ + [ ! "$$XDG_CONFIG_HOME" ] && XDG_CONFIG_HOME=~/.config; \ echo "Deleting..."; \ - rm -rvf $$prefix/bin/$(NAME) $$prefix/share/man/man1/$(NAME).1 + rm -rvf "$$prefix/bin/$(NAME)" "$$prefix/share/man/man1/$(NAME).1" "$$sysconfdir/bb"; \ + printf '\033[1mIf you created any config files in $$XDG_CONFIG_HOME/bb, you may want to delete them manually.\033[0m' @@ -16,6 +16,10 @@ No dependencies besides `make` and a C compiler, just: make sudo make install +To run `bb`, it's expected that you have some basic unix tools: +`basename`, `cat`, `cp`, `echo`, `find`, `grep`, `mkdir`, `more`, `mv`, +`printf`, `read`, `rm`, `sed`, `sh`, `sleep`, `touch`, `tput`, `tr`, `wait`. + ## Usage Run `bb` to launch the file browser. `bb` also has the flags: @@ -25,6 +29,8 @@ Run `bb` to launch the file browser. `bb` also has the flags: - `-s`: when `bb` exits successfully, print the files that were selected. - `-0`: use NULL-terminated strings instead of newline-separated strings with the `-s` flag. +- `-h`: print usage +- `-v`: print version Within `bb`, press `?` for a full list of available key bindings. In short: `h`/`j`/`k`/`l` or arrow keys for navigation, `q` to quit, `<space>` to toggle @@ -24,7 +24,7 @@ #include "config.h" #include "bterm.h" -#define BB_VERSION "0.15.2" +#define BB_VERSION "0.16.0" #ifndef PATH_MAX #define PATH_MAX 4096 @@ -134,6 +134,8 @@ 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); @@ -149,7 +151,6 @@ static void update_term_size(int sig); // Config options extern binding_t bindings[]; -extern const char *startupcmds[]; extern const column_t columns[128]; // Constants @@ -162,13 +163,75 @@ static const char *bbcmdfn = "bb() {\n" " for arg; do\n" " shift;\n" " if echo \"$arg\" | grep \"^+[^:]*:$\" >/dev/null 2>/dev/null; then\n" -" if test $# -gt 0; then printf \"$arg%s\\0\" \"$@\" >> $BBCMD\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 \"$arg\\0\" >> $BBCMD\n" +" printf \"%s\\0\" \"$arg\" >> $BBCMD\n" " done\n" -"}\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 static struct termios orig_termios, bb_termios; @@ -299,6 +362,10 @@ void* memcheck(void *p) */ 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) { @@ -309,9 +376,6 @@ int run_script(bb_t *bb, const char *cmd) size_t i = 0; args[i++] = SH; args[i++] = "-c"; - char *fullcmd = calloc(strlen(cmd) + strlen(bbcmdfn) + 1, sizeof(char)); - strcpy(fullcmd, bbcmdfn); - strcat(fullcmd, cmd); 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) { @@ -874,6 +938,38 @@ 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}; @@ -986,10 +1082,11 @@ void populate_files(bb_t *bb, int samedir) */ bb_result_t process_cmd(bb_t *bb, const char *cmd) { - char *value = strchr(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) - if (cmd[0] == '+') ++cmd; switch (cmd[0]) { case '.': { // +..:, +.: if (cmd[1] == '.') // +..: @@ -999,6 +1096,44 @@ bb_result_t process_cmd(bb_t *bb, const char *cmd) populate_files(bb, 1); return BB_OK; } + case 'b': { // +bind:<keys>:<script> + if (!value || !value[0]) + return BB_INVALID; + char *value_copy = memcheck(strdup(value)); + char *keys = trim(value_copy); + if (!keys[0]) { free(value_copy); return BB_OK; } + char *script = strchr(keys+1, ':'); + if (!script) { free(value_copy); return BB_INVALID; } + *script = '\0'; + script = trim(script + 1); + char *description; + if (script[0] == '#') { + description = trim(strsep(&script, "\n") + 1); + if (!script) script = ""; + else script = trim(script); + } else description = script; + for (char *key; (key = strsep(&keys, ",")); ) { + int is_section = strcmp(key, "Section") == 0; + int keyval = strlen(key) == 1 ? key[0] : bkeywithname(key); + if (keyval == -1 && !is_section) continue; + for (int i = 0; i < sizeof(bindings)/sizeof(bindings[0]); i++) { + if (bindings[i].key && (bindings[i].key != keyval || is_section)) + continue; + binding_t binding = {keyval, memcheck(strdup(script)), + memcheck(strdup(description))}; + if (bindings[i].key == keyval) { + free(bindings[i].description); + free(bindings[i].script); + for (; i + 1 < sizeof(bindings)/sizeof(bindings[0]) && bindings[i+1].key; i++) + bindings[i] = bindings[i+1]; + } + bindings[i] = binding; + break; + } + } + free(value_copy); + return BB_OK; + } case 'c': { // +cd:, +columns: switch (cmd[1]) { case 'd': { // +cd: @@ -1097,7 +1232,7 @@ bb_result_t process_cmd(bb_t *bb, const char *cmd) if (!bb->nfiles) return BB_INVALID; oldcur = bb->cursor; isdelta = value[0] == '-' || value[0] == '+'; - n = (int)strtol(value, &value, 10); + n = (int)strtol(value, (char**)&value, 10); if (*value == '%') n = (n * (value[1] == 'n' ? bb->nfiles : termheight)) / 100; if (isdelta) set_cursor(bb, bb->cursor + n); @@ -1121,7 +1256,7 @@ bb_result_t process_cmd(bb_t *bb, const char *cmd) if (!value) return BB_INVALID; // TODO: figure out the best version of this int isdelta = value[0] == '+' || value[0] == '-'; - int n = (int)strtol(value, &value, 10); + int n = (int)strtol(value, (char**)&value, 10); if (*value == '%') n = (n * (value[1] == 'n' ? bb->nfiles : termheight)) / 100; if (isdelta) @@ -1172,15 +1307,17 @@ void bb_browse(bb_t *bb, const char *path) bb->scroll = 0; bb->cursor = 0; - for (int i = 0; startupcmds[i]; i++) { - if (startupcmds[i][0] == '+') - process_cmd(bb, startupcmds[i] + 1); - else - run_script(bb, startupcmds[i]); - if (bb->should_quit) - goto quit; - } - + 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; @@ -1290,20 +1427,10 @@ void bb_browse(bb_t *bb, const char *path) // Search user-defined key bindings from config.h: binding_t *binding; user_bindings: - for (int i = 0; bindings[i].keys[0] >= 0; i++) { - for (int j = 0; bindings[i].keys[j]; j++) { - if (key == bindings[i].keys[j]) { - // Move to front optimization: - if (i > 2) { - binding_t tmp; - tmp = bindings[0]; - bindings[0] = bindings[i]; - bindings[i] = tmp; - i = 0; - } - binding = &bindings[i]; - goto run_binding; - } + 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 @@ -1312,8 +1439,8 @@ void bb_browse(bb_t *bb, const char *path) run_binding: if (cmdpos != 0) err("Command file still open"); - if (binding->script[0] == '+') { - process_cmd(bb, binding->script + 1); + 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); @@ -1341,17 +1468,19 @@ void bb_browse(bb_t *bb, const char *path) void print_bindings(int fd) { char buf[1000], buf2[1024]; - for (int i = 0; bindings[i].keys[0] >= 0; i++) { - if (bindings[i].keys[0] == 0) { + 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 = 0; bindings[i].keys[j]; j++) { - if (j > 0) p = stpcpy(p, ", "); - int key = bindings[i].keys[j]; + 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); @@ -1363,10 +1492,11 @@ void print_bindings(int fd) *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[0m\033[%dG\033[34m%s\033[0m", termwidth/2 + 1, - bindings[i].description ? bindings[i].description : bindings[i].script); + 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); } diff --git a/bbstartup.sh b/bbstartup.sh new file mode 100755 index 0000000..014bad5 --- /dev/null +++ b/bbstartup.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# This file contains the script that is run when bb launches +# See API.md for details on bb's API. + +# Default XDG values +[ ! "$XDG_CONFIG_HOME" ] && XDG_CONFIG_HOME=~/.config +[ ! "$sysconfdir" ] && sysconfdir=/etc + +# Create some default marks: +mkdir -p "$XDG_CONFIG_HOME/bb/marks" +mark() { + ln -sT "$XDG_CONFIG_HOME/bb/marks/$1" "$2" 2>/dev/null +} +mark home ~ +mark root / +mark config "$XDG_CONFIG_HOME" +mark marks "$XDG_CONFIG_HOME/bb/marks" + +# Set viewing options +bb +sort:+n +bb +col:'*smpn' + +# Load key bindings +# first check ~/.config/bb/bindings.bb, then /etc/xdg/bb/bindings.bb, then ./bindings.bb +if [ -e "$XDG_CONFIG_HOME/bb/bindings.bb" ]; then + cat "$XDG_CONFIG_HOME/bb/bindings.bb" +elif [ -e "$sysconfdir/xdg/bb/bindings.bb" ]; then + cat "$sysconfdir/xdg/bb/bindings.bb" +elif [ -e bindings.bb ]; then + cat bindings.bb +fi | sed -e '/^#/d' -e "s/^[^ ]/$(printf '\034')+bind:\\0/" | tr '\034' '\0' >> "$BBCMD" diff --git a/bindings.bb b/bindings.bb new file mode 100644 index 0000000..0ef25f8 --- /dev/null +++ b/bindings.bb @@ -0,0 +1,165 @@ +# This file defines the key bindings for bb +# The format is: <key>(,<key>)*:[ ]*#<description>(\n[ ]+script)+ +Section: BB Commands +?,F1: # Show Help menu + bb +help +q,Q: # Quit + bb +quit + +Section: File Navigation +j,Down: # Next file + bb +move:+1 +k,Up: # Previous file + bb +move:-1 +h,Left: # Parent directory + bb +cd:.. +l,Right: # Enter directory + [ -d "$BBCURSOR" ] && bb +cd:"$BBCURSOR" +Ctrl-f: # Search for file + bb +goto:"$( + if [ $BBDOTFILES ]; then + find -mindepth 1; + else find -mindepth 1 ! -path '*/.*'; + fi | pick "Find: " + )" +/: # Pick a file + bb +goto:"$( + if [ $BBDOTFILES ]; then find -mindepth 1 -maxdepth 1; + else find -mindepth 1 -maxdepth 1 ! -path '*/.*'; fi | pick "Pick: " + )" +Ctrl-g: # Go to directory + ask goto "Go to directory: " && bb +cd:"$goto" +m: # Mark this directory + ask mark "Mark: " && ln -s "$PWD" ~/.config/bb/marks/"$mark" +': # Go to a marked directory + mark="$(ls ~/.config/bb/marks | pick "Jump to: ")" && + bb +cd:"$(readlink -f ~/.config/bb/marks/"$mark")" +-,Backspace: # Go to previous directory + [ $BBPREVPATH ] && bb +cd:"$BBPREVPATH" +;: # Show selected files + bb +cd:'<selection>' +0: # Go to intitial directory + bb +cd:"$BBINITIALPATH" +g,Home: # Go to first file + bb +move:0 +G,End: # Go to last file + bb +move:100%n +PgDn: # Page down + bb +scroll:+100% +PgUp: # Page up + bb +scroll:-100% +Ctrl-d: # Half page down + bb +scroll:+50% +Ctrl-u: # Half page up + bb +scroll:-50% +Mouse wheel down: # Scroll down + bb +scroll:+3 +Mouse wheel up: # Scroll up + bb +scroll:-3 + +Section: File Selection +v,V,Space: # Toggle selection at cursor + bb +toggle +Escape: # Clear selection + bb +deselect: "$@" +Ctrl-s: # Save the selection + [ $# -gt 0 ] && ask savename "Save selection as: " && printf '%s\0' "$@" > ~/.config/bb/"$savename" +Ctrl-o: # Open a saved selection + loadpath="$(find ~/.config/bb -maxdepth 1 -type f | pick "Load selection: ")" && + [ -e "$loadpath" ] && bb +deselect: "$@" && + while IFS= read -r -d $'\0'; do bb +select:"$REPLY"; done < "$loadpath" +J: # Spread selection down + bb +spread:+1 +K: # Spread selection up + bb +spread:-1 +Ctrl-a: # Select all files here + if [ $BBDOTFILES ]; then find -mindepth 1 -maxdepth 1 -print0; + else find -mindepth 1 -maxdepth 1 ! -path '*/.*' -print0; fi | bb +sel: + +Section: File Actions +Enter: # Open file/directory + if [ -d "$BBCURSOR ]; then bb +cd:"$BBCURSOR"; + elif file -bi "$BBCURSOR" | grep -q '^\(text/\|inode/empty\)'; then $EDITOR "$BBCURSOR"; + else open "$BBCURSOR"; fi +e: # Edit file in $EDITOR + $EDITOR "$BBCURSOR" || pause +d: # Delete a file + printf "\033[1mDeleting \033[33m$BBCURSOR\033[0;1m...\033[0m " && confirm && + rm -rf "$BBCURSOR" && bb +refresh && bb +deselect:"$BBCURSOR" +D: # Delete all selected files + [ $# -gt 0 ] && printf "\033[1mDeleting the following:\n \033[33m$(printf ' %s\n' "$@")\033[0m" | more && + confirm && rm -rf "$@" && bb +refresh && bb +deselect: "$@" +Ctrl-v: # Move files here + printf "\033[1mMoving the following to here:\n \033[33m$(printf ' %s\n' "$@")\033[0m" | more && + confirm && spin mv -i "$@" . && bb +refresh && bb +deselect "$@" && + for f; do bb +sel:"$(basename "$f")"; done || + pause +c: # Copy a file + printf "\033[1mCreating copy of \033[33m$BBCURSOR\033[0;1m...\033[0m " && confirm && cp -ri "$BBCURSOR" "$BBCURSOR.copy" && bb +refresh +C: # Copy all selected files here + [ $# -gt 0 ] && printf "\033[1mCopying the following to here:\n \033[33m$(printf ' %s\n' "$@")\033[0m" | more && + confirm && + for f; do if [ "./$(basename "$f")" -ef "$f" ]; then + spin cp -ri "$f" "$f.copy"; + else spin cp -ri "$f" .; fi; done; bb +refresh +Ctrl-n: # New file/directory + case "$(printf '%s\n' File Directory | pick "Create new: ")" in + File) + ask name "New File: " || exit + touch "$name" + ;; + Directory) + ask name "New Directory: " || exit + mkdir "$name" + ;; + *) exit + ;; + esac && bb +goto:"$name" +refresh || pause +p: # Page through a file with $PAGER + $PAGER "$BBCURSOR" +|: # Pipe selected files to a command + ask cmd '|' && printf '%s\n' "$@" | sh -c "$BBSHELLFUNC$cmd"; bb +r; pause +:: # Run a command + ask cmd ':' && sh -c "$BBSHELLFUNC$cmd" -- "$@"; bb +r; pause +>: # Open a shell + tput rmcup >/dev/tty; $SHELL; bb +r +r,F2: # Rename a file + ask newname "Rename \033[33m$(basename "$BBCURSOR")\033[39m: " "$(basename "$BBCURSOR")" || exit + r="$(dirname "$BBCURSOR")/$newname" || exit + [ "$r" != "$BBCURSOR" ] && mv -i "$BBCURSOR" "$r" && bb +refresh && + while [ $# -gt 0 ]; do "$1" = "$BBCURSOR" && bb +deselect:"$BBCURSOR" +select:"$r"; shift; done && + bb +goto:"$r" +R: # Rename all selected files + bb +refresh; + for f; do + ask newname "Rename \033[33m$(basename "$f")\033[39m: " "$(basename "$f")" || break; + r="$(dirname "$f")/$newname"; + [ "$r" != "$f" ] && mv -i "$f" "$r" && bb "+deselect:$f" "+select:$r"; + [ "$f" = "$BBCURSOR" ] && bb +goto:"$r"; + done +Ctrl-r: # Regex rename files + command -v rename >/dev/null || { echo 'The `rename` command is not installed. Please install it to use this key binding.'; pause; exit; }; + ask patt "Replace pattern: " && ask rep "Replacement: " && + printf "\033[1mRenaming:\n\033[33m$(if [ $# -gt 0 ]; then rename -nv "$patt" "$rep" "$@"; else rename -nv "$patt" "$rep" *; fi)\033[0m" | more && + confirm && + if [ $# -gt 0 ]; then rename -i "$patt" "$rep" "$@"; else rename -i "$patt" "$rep" *; fi; + bb +deselect: "$@"; + bb +refresh + +Section: Viewing Options +s: # Sort by... + ask1 sort "Sort (n)ame (s)ize (m)odification (c)reation (a)ccess (r)andom (p)ermissions: " && + bb +sort:"~$sort" +refresh +#: # Set columns + ask columns "Set columns (*)selected (a)ccessed (c)reated (m)odified (n)ame (p)ermissions (r)andom (s)ize: " && + bb +col:"$columns" +.: # Toggle dotfile visibility + bb +dotfiles +i: # Toggle interleaving files and directories + bb +interleave +F5: # Refresh view + bb +refresh +Ctrl-b: # Bind a key to a script + ask1 key "Press key to bind..." && echo && ask script "Bind script: " && bb +bind:"$(printf "$key:$script")" + +Section: User Bindings diff --git a/config.def.h b/config.def.h index f4ae750..f8f431e 100644 --- a/config.def.h +++ b/config.def.h @@ -7,66 +7,15 @@ This file contains: - Global options, like which colors are used - Column formatting (width and title) - - Startup commands - - User key bindings - - For startup commands and key bindings, the following values are provided as - environment variables: - - $@ (the list of arguments): the full paths of the selected files - $BBCURSOR: the full path of the file under the cursor - $BBDOTFILES: "1" if files beginning with "." are visible in bb, otherwise "" - $BB_DEPTH: the number of `bb` instances deep (in case you want to run a - shell and have that shell print something special in the prompt) - $BBCMD: a file to which `bb` commands can be written (used internally) - - In order to modify bb's internal state, you can call `bb +cmd`, where "cmd" - is one of the following commands (or a unique prefix of one): - - .:[01] Whether to show "." in each directory - ..:[01] Whether to show ".." in each directory - cd:<path> Navigate to <path> - columns:<columns> Change which columns are visible, and in what order - deselect:<filename> Deselect <filename> - dotfiles:[01] Whether dotfiles are visible - goto:<filename> Move the cursor to <filename> (changing directory if needed) - interleave:[01] Whether or not directories should be interleaved with files in the display - move:<num*> Move the cursor a numeric amount - quit Quit bb - refresh Refresh the file listing - scroll:<num*> Scroll the view a numeric amount - select:<filename> Select <filename> - sort:([+-]method)+ Set sorting method (+: normal, -: reverse), additional methods act as tiebreaker - spread:<num*> Spread the selection state at the cursor - toggle:<filename> Toggle the selection status of <filename> - - Note: for numeric-based commands (like scroll), the number can be either - an absolute value or a relative value (starting with '+' or '-'), and/or - a percent (ending with '%'). Scrolling and moving, '%' means percent of - screen height, and '%n' means percent of number of files (e.g. +50% means - half a screen height down, and 100%n means the last file) - - Internally, bb will write the commands (NUL terminated) to $BBCMD, if - $BBCMD is set, and read the file when file browsing resumes. These commands - can also be passed to bb at startup, and will run immediately. - E.g. `bb '+col:n' '+sort:+r' .` will launch `bb` only showing the name column, randomly sorted. - - As a shorthand and performance optimization, commands that don't rely on any - shell variables or scripting can be written as "+move:+1" instead of "bb '+move:+1'", - which is a bit faster because internally it avoids writing to and reading from - the $BBCMD file. */ #include "bterm.h" -// Constants: -#define MAX_REBINDINGS 8 - // Types: typedef struct { - int keys[MAX_REBINDINGS+1]; - const char *script; - const char *description; + int key; + char *script; + char *description; } binding_t; typedef struct { @@ -93,58 +42,9 @@ typedef struct { #define SH "sh" #endif -// Used for STRINGIFY(__COUNTER__) to embed the line number as a string -// (as in "ask --history=bb."STRINGIFY(__COUNTER__)") -#define STRINGIFY2(x) #x -#define STRINGIFY(x) STRINGIFY2(x) - -// Some handy macros for common shell script behaviors: -// Bold text: -#define B(s) "\033[1m" s "\033[22m" - -// Macro for getting user input: -#ifndef ASK -#define ASK(var, prompt, initial) " read -p \"" B(prompt) "\" " var " </dev/tty >/dev/tty " -#endif - -#ifndef ASK1 -#define ASK1(var, prompt) " { printf \""prompt"\"; stty -icanon -echo; "var"=$(dd bs=1 count=1 2> /dev/null); stty icanon echo; } " -#endif - -#define PAUSE ASK1("REPLY", "\033[2mPress any key to continue...\033[0m\033[?25l") - -// Macro for picking from a list of options: -#ifndef PICK -#define PICK(prompt) " { "ASK("query", prompt)" && awk '{print length, $1}' | sort -n | cut -d' ' -f2- | "\ - "grep -i -m1 \"$(echo \"$query\" | sed 's;.;[^/&]*[&];g')\"; } " -#endif - -// Display a spinning indicator if command takes longer than 10ms: -#ifndef SPIN -#define SPIN(cmd) "{ { " cmd "; } & " \ - "pid=$!; "\ - "spinner='-\\|/'; "\ - "sleep 0.01; "\ - "while kill -0 $pid 2>/dev/null; do "\ - " printf '%c\\033[D' \"$spinner\" >/dev/tty; "\ - " spinner=\"$(echo $spinner | sed 's/\\(.\\)\\(.*\\)/\\2\\1/')\"; "\ - " sleep 0.1; "\ - "done; "\ - "wait $pid; }" -#endif - -#ifndef CONFIRM -#define CONFIRM(action, files) " { printf '%s\\n' \""B(action)"\" \""files"\" | more; " \ - ASK1("REPLY", B("Is that okay? [y/N] "))"; [ \"$REPLY\" = 'y' ]; } " -#endif - -#define SECTION(name) {{0}, NULL, name} - - // These commands will run at startup (before command-line arguments) -extern const char *startupcmds[]; extern const column_t columns[128]; -extern binding_t bindings[]; +extern binding_t bindings[1024]; // Column widths and titles: const column_t columns[128] = { @@ -158,20 +58,6 @@ const column_t columns[128] = { ['s'] = {9, " Size"}, }; -// This is a list of commands that runs when `bb` launches: -const char *startupcmds[] = { - // Set some default marks: - "mkdir -p ~/.config/bb/marks", - "ln -sT ~/.config/bb/marks ~/.config/bb/marks/marks 2>/dev/null", - "ln -sT ~ ~/.config/bb/marks/home 2>/dev/null", - "ln -sT / ~/.config/bb/marks/root 2>/dev/null", - "ln -sT ~/.config ~/.config/bb/marks/config 2>/dev/null", - "ln -sT ~/.local ~/.config/bb/marks/local 2>/dev/null", - // Default column and sorting options: - "+sort:+n", "+col:*smpn", - NULL, // NULL-terminated array -}; - /****************************************************************************** * These are all the key bindings for bb. * The format is: {{keys,...}, "<script>", "<description>"} @@ -183,129 +69,6 @@ const char *startupcmds[] = { * `vim -c 'set t_ti= t_te=' "$@"` to prevent momentarily seeing the shell * after editing. *****************************************************************************/ -binding_t bindings[] = { - SECTION("Key Bindings"), - {{'?', KEY_F1}, "+help", B("Help")" menu"}, - {{'q', 'Q'}, "+quit", B("Quit")}, - - SECTION("Navigation"), - {{'j', KEY_ARROW_DOWN}, "+move:+1", B("Next")" file"}, - {{'k', KEY_ARROW_UP}, "+move:-1", B("Previous")" file"}, - {{'h', KEY_ARROW_LEFT}, "+cd:..", B("Parent")" directory"}, - {{'l', KEY_ARROW_RIGHT}, "[ -d \"$BBCURSOR\" ] && bb \"+cd:$BBCURSOR\"", B("Enter")" a directory"}, - {{KEY_CTRL_F}, "bb \"+goto:$(if [ $BBDOTFILES ]; then find -mindepth 1; else find -mindepth 1 ! -path '*/.*'; fi " - "| "PICK("Find: ")")\"", B("Search")" for file"}, - {{'/'}, "bb \"+goto:$(if [ $BBDOTFILES ]; then find -mindepth 1 -maxdepth 1; else find -mindepth 1 -maxdepth 1 ! -path '*/.*'; fi " - "| "PICK("Pick: ")")\"", B("Pick")" file"}, - {{KEY_CTRL_G}, ASK("goto", "Go to directory: ", "")" && bb +cd:\"$goto\"", B("Go to")" directory"}, - {{'m'}, ASK("mark", "Mark: ", "")" && ln -s \"$PWD\" ~/.config/bb/marks/\"$mark\"", B("Mark")" this directory"}, - {{'\''}, "mark=\"$(ls ~/.config/bb/marks | " PICK("Jump to: ") ")\" " - "&& bb +cd:\"$(readlink -f ~/.config/bb/marks/\"$mark\")\"", - "Go to a "B("marked")" directory"}, - {{'-', KEY_BACKSPACE, KEY_BACKSPACE2}, - "[ $BBPREVPATH ] && bb +cd:\"$BBPREVPATH\"", "Go to "B("previous")" directory"}, - {{';'}, "bb +cd:'<selection>'", "Show "B("selected files")}, - {{'0'}, "bb +cd:\"$BBINITIALPATH\"", "Go to "B("initial directory")}, - {{'g', KEY_HOME}, "+move:0", "Go to "B("first")" file"}, - {{'G', KEY_END}, "+move:100%n", "Go to "B("last")" file"}, - {{KEY_PGDN}, "+scroll:+100%", B("Page down")}, - {{KEY_PGUP}, "+scroll:-100%", B("Page up")}, - {{KEY_CTRL_D}, "+scroll:+50%", B("Half page down")}, - {{KEY_CTRL_U}, "+scroll:-50%", B("Half page up")}, - {{KEY_MOUSE_WHEEL_DOWN}, "+scroll:+3", B("Scroll down")}, - {{KEY_MOUSE_WHEEL_UP}, "+scroll:-3", B("Scroll up")}, - - SECTION("File Selection"), - {{' ','v','V'}, "+toggle", B("Toggle")" selection at cursor"}, - {{KEY_ESC}, "bb +deselect: \"$@\"", B("Clear")" selection"}, - {{KEY_CTRL_S}, "[ $# -gt 0 ] && "ASK("savename", "Save selection as: ", "") " && printf '%s\\0' \"$@\" > ~/.config/bb/\"$savename\"", - B("Save")" the selection"}, - {{KEY_CTRL_O}, "loadpath=\"$(find ~/.config/bb -maxdepth 1 -type f | " PICK("Load selection: ") ")\" " - "&& [ -e \"$loadpath\" ] && bb +deselect:'*' " - "&& while IFS= read -r -d $'\\0'; do bb +select:\"$REPLY\"; done < \"$loadpath\"", - B("Open")" a saved selection"}, - {{'J'}, "+spread:+1", B("Spread")" selection down"}, - {{'K'}, "+spread:-1", B("Spread")" selection up"}, - {{KEY_CTRL_A}, - "if [ $BBDOTFILES ]; then find -mindepth 1 -maxdepth 1 -print0; " - "else find -mindepth 1 -maxdepth 1 ! -path '*/.*' -print0; fi | bb +sel:", - B("Select all")" files here"}, - - SECTION("Actions"), - {{'\r', KEY_MOUSE_DOUBLE_LEFT}, - "if [ -d \"$BBCURSOR\" ]; then bb \"+cd:$BBCURSOR\"; " -#ifdef __APPLE__ - "elif file -bI \"$BBCURSOR\" | grep -q '^\\(text/\\|inode/empty\\)'; then $EDITOR \"$BBCURSOR\"; " - "else open \"$BBCURSOR\"; fi", -#else - "elif file -bi \"$BBCURSOR\" | grep -q '^\\(text/\\|inode/empty\\)'; then $EDITOR \"$BBCURSOR\"; " - "else xdg-open \"$BBCURSOR\"; fi", -#endif - B("Open")" file/directory"}, - {{'e'}, "$EDITOR \"$BBCURSOR\" || "PAUSE, B("Edit")" file in $EDITOR"}, - {{'E'}, "[ $# -gt 0 ] && $EDITOR \"$@\" || "PAUSE, B("Edit")" selected files in $EDITOR"}, - {{'d'}, CONFIRM("The following will be deleted:", "$BBCURSOR") " && rm -rf \"$BBCURSOR\" && bb +refresh && bb +deselect: \"$BBCURSOR\"", - B("Delete")" a file"}, - {{'D', KEY_DELETE}, - "[ $# -gt 0 ] && "CONFIRM("The following will be deleted:", "$@") " && rm -rf \"$@\" && bb +refresh && bb +deselect: \"$@\"", - B("Delete")" selected files"}, - {{KEY_CTRL_V}, "[ $# -gt 0 ] && " - CONFIRM("The following will be moved here:", "$@") " && { " - SPIN("mv -i \"$@\" . && bb +refresh && bb +deselect: \"$@\" && for f; do bb \"+sel:$(basename \"$f\")\"; done")" || "PAUSE"; }", - B("Move")" files here"}, - {{'c'}, CONFIRM("Copy file:", "$BBCURSOR")" && cp -ri \"$BBCURSOR\" \"$BBCURSOR.copy\" && bb +refresh", - B("Copy")" a file"}, - {{'C'}, "[ $# -gt 0 ] && "CONFIRM("The following will be copied here:", "$@") - " && for f; do if [ \"./$(basename \"$f\")\" -ef \"$f\" ]; then " - SPIN("cp -ri \"$f\" \"$(basename \"$f\").copy\"")"; " - "else "SPIN("cp -ri \"$f\" .")"; fi; done; bb +refresh", - B("Copy")" the selected files here"}, - {{KEY_CTRL_N}, "type=\"$(printf '%s\\n' File Directory | "PICK("Create new: ")")\" " - "&& "ASK("name", "New $type: ", "")" && " - "{ if [ $type = File ]; then touch \"$name\"; else mkdir \"$name\"; fi " - "&& bb \"+goto:$name\" +r || "PAUSE"; }", B("New")" file/directory"}, - {{'p'}, "$PAGER \"$BBCURSOR\"", B("Page")" through a file in $PAGER"}, - {{'P'}, "[ $# -gt 0 ] && $PAGER \"$@\"", B("Page")" through selected files in $PAGER"}, - {{'|'}, ASK("cmd", "|", "") " && printf '%s\\n' \"$@\" | "SH" -c \"$BBSHELLFUNC$cmd\"; " PAUSE "; bb +r", - B("Pipe")" selected files to a command"}, - {{':'}, ASK("cmd", ":", "")" && "SH" -c \"$BBSHELLFUNC$cmd\" -- \"$@\"; " PAUSE "; bb +refresh", - B("Run")" a command"}, - {{'>'}, "tput rmcup >/dev/tty; $SHELL; bb +r", "Open a "B("shell")}, - {{'r', KEY_F2}, - ASK("newname", "Rename \033[33m$(basename \"$BBCURSOR\")\033[39m: ", "$(basename \"$BBCURSOR\")")" && " - "r=\"$(dirname \"$BBCURSOR\")/$newname\" && " - "[ \"$r\" != \"$BBCURSOR\" ] && mv -i \"$BBCURSOR\" \"$r\" && bb +refresh && " - "while [ $# -gt 0 ]; do [ \"$1\" = \"$BBCURSOR\" ] && bb \"+deselect:$BBCURSOR\" \"+select:$r\"; shift; done && " - "bb +goto:\"$r\"", - B("Rename")" a file"}, - {{'R'}, - "bb +refresh; " - "for f; do " - " "ASK("newname", "Rename \033[33m$(basename \"$f\")\033[39m: ", "$(basename \"$f\")")" || break; " - " r=\"$(dirname \"$f\")/$newname\"; " - " [ \"$r\" != \"$f\" ] && mv -i \"$f\" \"$r\" && bb \"+deselect:$f\" \"+select:$r\"; " - " [ \"$f\" = \"$BBCURSOR\" ] && bb +goto:\"$r\"; " - "done", B("Rename")" selected files"}, - {{KEY_CTRL_R}, - "command -v rename >/dev/null || { echo 'The `rename` command is not installed. Please install it to use this key binding.'; "PAUSE"; exit; }; " - ASK("patt", "Replace pattern: ", "")" && "ASK("rep", "Replacement: ", "")" && " - CONFIRM("Renaming:", "$(if [ $# -gt 0 ]; then rename -nv \"$patt\" \"$rep\" \"$@\"; else rename -nv \"$patt\" \"$rep\" *; fi)")" && " - "if [ $# -gt 0 ]; then rename -i \"$patt\" \"$rep\" \"$@\"; else rename -i \"$patt\" \"$rep\" *; fi; bb +refresh", - B("Regex rename")" files"}, - - SECTION("File Display"), - {{'s'}, - ASK1("sort", B("Sort (n)ame (s)ize (m)odification (c)reation (a)ccess (r)andom (p)ermissions: ")) - " && bb +sort:\"~$sort\" +refresh", - B("Sort")" by..."}, - {{'#'}, ASK("columns", "Set columns (*)selected (a)ccessed (c)reated (m)odified (n)ame (p)ermissions (r)andom (s)ize: ", "") - " && bb +col:\"$columns\"", - "Set "B("columns")}, - {{'.'}, "+dotfiles", "Toggle "B("dotfile")" visibility"}, - {{'i'}, "+interleave", "Toggle "B("interleaving")" files and directories"}, - {{KEY_F5}, "+refresh", B("Refresh")}, - - {{-1}} // Array must be -1-terminated -}; +binding_t bindings[1024]; // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 |
