diff --git a/Makefile b/Makefile index d07f1b4..324207c 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,7 @@ CC=gcc CFLAGS=-O0 -std=gnu99 -D_XOPEN_SOURCE=500 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L CWARN= -Wall -Wpedantic -Wno-unknown-pragmas -fsanitize=address -fno-omit-frame-pointer G=-g -PICK= -ASK= -ASKECHO= +PICKER= ifeq ($(shell uname),Darwin) CFLAGS += -D_DARWIN_C_SOURCE @@ -16,24 +14,17 @@ endif ifneq (, $(shell which ask)) ifeq (, $(ASKECHO)$(ASK)) -ASKECHO="ask --prompt=\"" prompt "\" --query=\"" initial "\"" +CFLAGS += -D'ASKECHO(prompt,initial)="ask --prompt=\"" prompt "\" --query=\"" initial "\""' endif -ifeq (, $(PICK)) -PICK="ask --prompt=\"" prompt "\" --query=\"" initial "\"" +ifeq (, $(PICKER)) +PICKER=ask endif endif -ifneq (, $(ASKECHO)) -CFLAGS += -D'ASKECHO(prompt,initial)=$(ASKECHO)' +ifneq (, $(PICKER)) +CFLAGS += -D'PICK(prompt, initial)="$(PICKER) --prompt=\"" prompt "\" --query=\"" initial "\""' endif -ifneq (, $(ASK)) -CFLAGS += -D'ASK(var,prompt,initial)=$(ASK)' -endif - -ifneq (, $(PICK)) -CFLAGS += -D'PICK(prompt, initial)=$(PICK)' -endif all: $(NAME) diff --git a/README.md b/README.md index cc67312..b961f2a 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,81 @@ - Without any build dependencies other than the C standard library (no ncurses) - A good proof-of-concept for making a TUI from scratch -The core idea behind `bb` is that almost all actions are performed by piping -selected files to external scripts, rather than having the file manager itself -try to anticipate and hard-code every possible user action. Unix tools are very -good at doing file management, the thing that `bb` adds is immediate visual -feedback and rapid navigation. +## Building +No dependencies, just: -For example, instead of using `ls`, then `rm` and typing out file names, you can -just open `bb`, scroll through the list of files, select the ones you want to -delete, and hit `D`. The `D` key's behavior is defined in a single line of code -in `config.h` as passing the selected files as arguments to `rm -rf "$@"`. -That's it! If you want to add a mapping to upload files to your server, you can -just add a binding for `scp user@example.com:~/ "$@"`. Want to zip files? Add -a mapping for `read -p "Archive: " name && zip "$name" "$@"` or, if you have -some complicated one-time task, you can just hit `>` to drop to a shell and run -commands with the selected files available in `$@` (or use `|` to run a quick -one-liner command that gets the selected files piped as input). +`make` +`sudo make install` + +## Usage + +Run `bb` to launch the file browser. `bb` also has the flags: + +- `-d`: when `bb` exits successfully, print the directory `bb` was browsing. +- `-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. + +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, `` to toggle +selection, `d` to delete, `c` to copy, `M` to move, `r` to rename, `n` to +create a new file, `N` to create a new directory, `:` to run a command with the +selected files in `$@`, and `|` to pipe files to a command. Pressing `Ctrl-c` +will cause `bb` to exit with a failure status and without printing anything. + +## Using bb to Change Directory +Applications cannot change the shell's working directory on their own, but you +can define a shell function that uses the shell's builtin `cd` function on the +output of `bb -d` (print directory on exit). For bash (sh, zsh, etc.), you can +put the following function in your `~/.profile` (or `~/.bashrc`, `~/.zshrc`, +etc.): + + function bcd() { cd "$(bb -d "$@" || pwd)"; } + +For [fish](https://fishshell.com/) (v3.0.0+), you can put this in your +`~/.config/fish/functions/`: + + function bcd; cd (bb -d $argv || pwd); end + +In both versions, `|| pwd` means the directory will not change if `bb` exits +with failure (e.g. by pressing `Ctrl-c`). + +## Launching bb with a Keyboard Shortcut +Using a keyboard shortcut to launch `bb` from the shell is something that is +handled by your shell. Here are some examples for binding `Ctrl-b` to launch +`bb` and change directory to `bb`'s directory (using the `bcd` function defined +above). For sh and bash, put this in your `~/.profile`: + + bind '"\C-b":"bcd\n"' + +For fish, put this in your `~/.config/fish/functions/fish_user_key_bindings.fish`: + + bind \cB 'bcd; commandline -f repaint' + +# bb's Philosophy +The core idea behind `bb` is that `bb` is a file **browser**, not a file +**manager**, which means `bb` uses shell scripts to perform all modifications +to the filesystem (passing selected files as arguments), rather than +reinventing the wheel by hard-coding operations like `rm`, `mv`, `cp`, `touch`, +and so on. Shell scripts can be bound to keypresses in config.h (the user's +version of [the defaults in config.def.h](config.def.h)). For example, `D` is +bound to `rm -rf "$@"`, which means selecting `file_foo` and `dir_baz`, then +pressing `D` will cause `bb` to run the shell command `rm -rf file_foo dir_baz`. + +`bb` comes with a bunch of pre-defined bindings for basic actions in +[config.def.h](config.def.h) (within `bb`, press `?` to see descriptions of the +bindings), but it's very easy to add new bindings for whatever custom scripts +you want to run, just add a new entry in `bindings` in config.h with the form +`{{keys...}, "", ""}` The description is shown when +pressing `?` within `bb`. + +## User Input in Scripts +If you need user input in a script, you can just use the `read` shell function +like so: `read -p "Archive: " name && zip "$name" "$@"` However, `read` doesn't +support a lot of functionality (e.g. using the arrow keys), so I would recommnd +using [ask](https://bitbucket.org/spilt/ask) instead. If you have `ask` +isntalled, making `bb` will automatically detect it and the default key +bindings will use it instead of `read`. ## API `bb` also exposes an API so that programs can modify `bb`'s internal state. @@ -44,18 +103,6 @@ code for ncurses. I hope anyone checking out this project can see it as a great example of how you can build a full TUI without ncurses or any external libraries as long as you're willing to hand-write a few escape sequences. -## Building - -`make` -`sudo make install` - -## Usage - -Just run `bb` to launch the file browser. Press `?` for a full list of -available key bindings. In short: `h`/`j`/`k`/`l` or arrow keys for navigation, -`q` to quit, `` to toggle selection, `d` to delete, `c` to copy, `M` to -move, `r` to rename, `n` to create a new file, `N` to create a new directory, -and `|` to pipe files to a command. ## Hacking diff --git a/bb.c b/bb.c index 991f7df..dedf90d 100644 --- a/bb.c +++ b/bb.c @@ -1042,9 +1042,12 @@ bb_result_t execute_cmd(bb_t *bb, const char *cmd) if (!lastslash) return BB_INVALID; *lastslash = '\0'; // Split in two cd_to(bb, pbuf); - if (e->index >= 0) + e = load_entry(bb, lastslash+1); + if (!e) return BB_INVALID; + if (IS_VIEWED(e)) { set_cursor(bb, e->index); - if (!IS_VIEWED(e) && !IS_SELECTED(e)) + return BB_OK; + } else if (!IS_SELECTED(e)) remove_entry(e); return BB_OK; } @@ -1320,8 +1323,6 @@ void bb_browse(bb_t *bb, const char *path) goto redraw; } move_cursor(tty_out, 0, termheight-1); - if (binding->flags & NORMAL_TERM) - fputs(T_OFF(T_ALT_SCREEN), tty_out); fputs(T_ON(T_SHOW_CURSOR), tty_out); close_term(); run_cmd_on_selection(bb, binding->command); diff --git a/config.def.h b/config.def.h index cd187cf..77fdd26 100644 --- a/config.def.h +++ b/config.def.h @@ -1,19 +1,22 @@ /* - BB Key Bindings + BB Configuration, Startup Commands, and Key Bindings - User-defined key bindings go in config.h, which is created by running `make` + User customization goes in config.h, which is created by running `make` (config.def.h is for keeping the defaults around, just in case) - The basic structure is: - - - (for the help menu) - (whether to run in a full terminal window or silently, etc.) + This file contains: + - Global options, like which colors are used + - Column formatting (width and title) + - Startup commands + - User key bindings - When the scripts are run, the following values are provided as 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 name of the file under the cursor + $@ (the list of arguments): the full paths of the selected files, or if + no files are selected, the full path of the file under the cursor + $BBCURSOR: the full path of the file under the cursor + $BBSELECTED: "1" if any files are selected, 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) @@ -40,65 +43,33 @@ spread: Spread the selection state at the cursor toggle: Toggle the selection status of - 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. - - *Note: for numeric-based commands (like scroll), the number can be either + 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" -// Configurable options: -#define KEY_DELAY 50 -#define SCROLLOFF MIN(5, (termheight-4)/2) - -#define CMDFILE_FORMAT "/tmp/bb.XXXXXX" - -#define SORT_INDICATOR "↓" -#define RSORT_INDICATOR "↑" -#define SELECTED_INDICATOR " \033[31;7m \033[0m" -#define NOT_SELECTED_INDICATOR " " - -#define TITLE_COLOR "\033[37;1m" -#define NORMAL_COLOR "\033[37m" -#define CURSOR_COLOR "\033[43;30;1m" -#define LINK_COLOR "\033[35m" -#define DIR_COLOR "\033[34m" -#define EXECUTABLE_COLOR "\033[31m" - -#define PAUSE " read -n1 -p '\033[2mPress any key to continue...\033[0m\033[?25l'" - -#ifndef ASK -#ifdef ASKECHO -#define ASK(var, prompt, initial) var "=\"$(" ASKECHO(prompt, initial) ")\"" -#else -#define ASK(var, prompt, initial) "read -p \"" prompt "\" " var -#endif -#endif - -#ifndef ASKECHO -#define ASKECHO(prompt, initial) ASK("REPLY", prompt, initial) " && echo \"$REPLY\"" -#endif - -#ifndef PICK -#define PICK(prompt, initial) "true && " ASKECHO(prompt, initial) -#endif - -#define NORMAL_TERM (1<<0) - +// Constants: #define MAX_REBINDINGS 8 +// Types: typedef struct { int keys[MAX_REBINDINGS+1]; const char *command; const char *description; - int flags; } binding_t; typedef struct { @@ -106,23 +77,68 @@ typedef struct { const char *name; } column_t; +// Configurable options: +#define KEY_DELAY 50 +#define SCROLLOFF MIN(5, (termheight-4)/2) +#define CMDFILE_FORMAT "/tmp/bb.XXXXXX" +#define SORT_INDICATOR "↓" +#define RSORT_INDICATOR "↑" +#define SELECTED_INDICATOR " \033[31;7m \033[0m" +#define NOT_SELECTED_INDICATOR " " +// Colors (using ANSI escape sequences): +#define TITLE_COLOR "\033[37;1m" +#define NORMAL_COLOR "\033[37m" +#define CURSOR_COLOR "\033[43;30;1m" +#define LINK_COLOR "\033[35m" +#define DIR_COLOR "\033[34m" +#define EXECUTABLE_COLOR "\033[31m" + +// Some handy macros for common shell script behaviors: +#define PAUSE " read -n1 -p '\033[2mPress any key to continue...\033[0m\033[?25l'" + +// Bold text: +#define B(s) "\033[1m" s "\033[22m" + +// Macros for getting user input: +#ifndef ASK +#ifdef ASKECHO +#define ASK(var, prompt, initial) var "=\"$(" ASKECHO(prompt, initial) ")\"" +#else +#define ASK(var, prompt, initial) "read -p \"" prompt "\" " var " /dev/tty" +#endif +#endif + +#ifndef ASKECHO +#define ASKECHO(prompt, initial) ASK("REPLY", prompt, initial) " && echo \"$REPLY\"" +#endif + +// Get user input to choose an option (piped in). If you want to use +// a fuzzy finder like fzy or fzf, then this should be something like: +// "fzy --prompt=\"" prompt "\" --query=\"" initial "\"" +#ifndef PICK +#define PICK(prompt, initial) "grep -i -m1 \"^$(" ASKECHO(prompt, initial) " | 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" +#endif + + // These commands will run at startup (before command-line arguments) extern const char *startupcmds[]; extern const column_t columns[128]; +extern binding_t bindings[]; -const char *startupcmds[] = { - ////////////////////////////////////////////// - // User-defined startup commands can go here - ////////////////////////////////////////////// - // Set some default marks: - "+mark:0", "+mark:~=~", "+mark:h=~", "+mark:/=/", "+mark:c=~/.config", - "+mark:l=~/.local", "+mark:s=", - // Default column and sorting options: - "+sort:+n", "+col:*smpn", "+..", - NULL, // NULL-terminated array -}; - -// Column widths: +// Column widths and titles: const column_t columns[128] = { ['*'] = {2, "*"}, ['a'] = {21, " Accessed"}, @@ -134,17 +150,29 @@ const column_t columns[128] = { ['s'] = {9, "Size"}, }; -extern binding_t bindings[]; -#define B(s) "\033[1m" s "\033[22m" +// This is a list of commands that runs when `bb` launches: +const char *startupcmds[] = { + // Set some default marks: + "+mark:0", "+mark:~=~", "+mark:h=~", "+mark:/=/", "+mark:c=~/.config", + "+mark:l=~/.local", "+mark:s=", + // 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,...}, "