From 8f73ec72411aee6f3493295b558190635a12ef28 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Wed, 22 May 2019 01:39:15 -0700 Subject: [PATCH] Added null-separation option for xargs to properly handle funky filenames --- Makefile | 8 +++++-- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ bb.c | 18 +++++++++------- config.h | 17 ++++++++------- 4 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 README.md diff --git a/Makefile b/Makefile index dc5845e..5efd253 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,20 @@ PREFIX= CC=cc -CFLAGS=-O0 -std=gnu99 -g +CFLAGS=-O0 -std=gnu99 LIBS= NAME=bb +G= all: $(NAME) clean: rm $(NAME) +config.h: + cp config.def.h config.h + $(NAME): $(NAME).c keys.h config.h - $(CC) $(NAME).c $(LIBS) $(CFLAGS) -o $(NAME) + $(CC) $(NAME).c $(LIBS) $(CFLAGS) $(G) -o $(NAME) test: $(NAME) ./$(NAME) test.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcc619b --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# bb - A bitty browser for command line file management + +`bb` is a TUI console file browser that is: + +- Extremely lightweight (currently around 1.2K lines of code) +- Highly interoperable with unix pipelines +- Highly customizable and hackable +- Without any build dependencies other than the C standard library (no ncurses) +- A good proof-of-concept for making a TUI without using any libraries + +The core idea behind `bb` is that almost all actions are performed by piping +selected files to external scripts, rather than hard-coding actions. Unix +tools are very good at doing file management, the thing that `bb` adds is +immediate visual feedback and rapid navigation. + +For example, normally on the command line, if you wanted to manually delete a +handful of files, you would first get a listing of the files with `ls`, then +type `rm` followed by typing out the names of the files (with some tab +autocompletion). With `bb`, you can just launch `bb`, see all the files +immediately, select the ones you want with a few keystrokes, and press `D` to +delete them (or `d` for deleting with confirmation). The `D` key's behavior +is defined in a single line of code in `config.h` to pipe the selected files +to `xargs -0 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 `xargs scp user@example.com` or, +if you have some complicated one-time task, you can just hit `|` and type in +any arbitrary command and have the selected files piped to it. + +## Zero Dependencies + +There's a lot of TUI libraries out there like ncurses and termbox, but +essentially all they do is write ANSI escape sequences to the terminal. `bb` +does all of that by itself, just using basic calls to `write()`, with no +external libraries beyond the C standard library. Since `bb` only has to +support the terminal functionality that it uses itself, `bb`'s entire source +code is less than half the size of the source code for an extremely compact +library like termbox, and less than *half a percent* of the size of the source +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 + +If you want to customize `bb`, you can add or change the key bindings by +editing `config.h` and recompiling. In [suckless](https://suckless.org/) style, +customizing means editing source code, and compilation is extremely fast. +Key character constants are in `keys.h` and the rest of the code is in `bb.c`. + +## License + +`bb` is released under the MIT license. See the `LICENSE` file for full details. diff --git a/bb.c b/bb.c index 7670743..633cc3a 100644 --- a/bb.c +++ b/bb.c @@ -342,7 +342,7 @@ static int compare_date(void *r, const void *v1, const void *v2) return -(info1.st_mtimespec.tv_sec - info2.st_mtimespec.tv_sec)*sign; } -static void write_selection(int fd, entry_t *firstselected) +static void write_selection(int fd, entry_t *firstselected, char sep) { while (firstselected) { const char *p = firstselected->d_fullname; @@ -350,11 +350,11 @@ static void write_selection(int fd, entry_t *firstselected) const char *p2 = strchr(p, '\n'); if (!p2) p2 = p + strlen(p); write(fd, p, p2 - p); - if (*p2 == '\n') + if (*p2 == '\n' && sep == '\n') write(fd, "\\", 1); p = p2; } - write(fd, "\n", 1); + write(fd, &sep, 1); firstselected = firstselected->next; } } @@ -373,7 +373,7 @@ static void clear_selection(bb_state_t *state) state->nselected = 0; } -static void explore(char *path, int print_dir, int print_selection) +static void explore(char *path, int print_dir, int print_selection, char sep) { char *tmp = path; char *original_path = calloc(strlen(tmp) + 1, 1); @@ -902,8 +902,9 @@ static void explore(char *path, int print_dir, int print_selection) sig_t old_handler = signal(SIGINT, do_nothing); child = run_cmd(NULL, &scriptinfd, bindings[i].command); if (!(bindings[i].flags & NO_FILES)) { + char sep = (bindings[i].flags & NULL_SEP) ? '\0' : '\n'; if (state.nselected > 0) { - write_selection(scriptinfd, state.firstselected); + write_selection(scriptinfd, state.firstselected, sep); } else if (strcmp(state.files[state.cursor]->d_name, "..") != 0) { write(scriptinfd, state.files[state.cursor]->d_name, state.files[state.cursor]->d_namlen); } @@ -940,7 +941,7 @@ done: if (print_dir) printf("%s\n", state.path); if (print_selection) - write_selection(STDOUT_FILENO, state.firstselected); + write_selection(STDOUT_FILENO, state.firstselected, sep); return; } @@ -948,10 +949,13 @@ int main(int argc, char *argv[]) { char _realpath[MAX_PATH+1]; char *path = "."; + char sep = '\n'; int print_dir = 0, print_selection = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-d") == 0) { print_dir = 1; + } else if (strcmp(argv[i], "-0") == 0) { + sep = '\0'; } else if (strcmp(argv[i], "-s") == 0) { print_selection = 1; } else if (path[0]) { @@ -962,7 +966,7 @@ int main(int argc, char *argv[]) init_term(); if (!realpath(path, _realpath)) err("realpath failed"); - explore(_realpath, print_dir, print_selection); + explore(_realpath, print_dir, print_selection, sep); done: return 0; } diff --git a/config.h b/config.h index ff6ff89..2d308ed 100644 --- a/config.h +++ b/config.h @@ -7,7 +7,7 @@ #define SCROLLOFF 5 #define NO_FILES (1<<0) -#define CD_TO_RESULT (1<<1) +#define NULL_SEP (1<<1) #define REFRESH (1<<2) #define CLEAR_SELECTION (1<<3) #define ONSCREEN (1<<4) @@ -19,16 +19,17 @@ struct { } bindings[] = { // User-defined custom scripts go here: {'L', "less"}, - {'D', "xargs rm -rf", CLEAR_SELECTION | REFRESH | ONSCREEN}, - {'d', "xargs -I @ sh -c 'rm -rfi @ /dev/tty && head -n1 /dev/tty`\"", ONSCREEN | REFRESH | NO_FILES}, + {'N', "mkdir \"`printf '\\033[33;1mNew dir:\\033[0m ' >/dev/tty && head -n1 /dev/tty`\"", ONSCREEN | REFRESH | NO_FILES}, {'|', "sh -c \"`printf '> ' >/dev/tty && head -n1 /dev/tty`\"", REFRESH}, {'>', "sh -c \"`printf '> ' >/dev/tty && head -n1 /dev/tty`\"", NO_FILES | REFRESH}, - {'r', "xargs -I @ -n1 sh -c 'mv \"@\" \"`printf \"\e[1mRename \e[1;33m%%s\e[0m: \" \"@\" >&2 && head -n1 &2 && head -n1