aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--API.md69
-rw-r--r--Makefile49
-rw-r--r--README.md6
-rw-r--r--bb.c218
-rwxr-xr-xbbstartup.sh31
-rw-r--r--bindings.bb165
-rw-r--r--config.def.h247
7 files changed, 476 insertions, 309 deletions
diff --git a/API.md b/API.md
new file mode 100644
index 0000000..df4da71
--- /dev/null
+++ b/API.md
@@ -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.
diff --git a/Makefile b/Makefile
index e3ce213..2d46459 100644
--- a/Makefile
+++ b/Makefile
@@ -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'
diff --git a/README.md b/README.md
index d509b42..9cb9e5a 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/bb.c b/bb.c
index 32f3706..ad7777a 100644
--- a/bb.c
+++ b/bb.c
@@ -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