From 5f0e1bf0bed5e835fbe6b4af97701e30ebff598d Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Wed, 22 May 2019 14:33:14 -0700 Subject: [PATCH] Overhaul to use arguments instead of piping for commands --- README.md | 32 +++++++++++++----------- bb.c | 70 +++++++++++++++++++++++++++++++++++----------------- config.def.h | 34 ++++++++++++------------- 3 files changed, 82 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 42fe42d..e678d87 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,23 @@ - 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 +- 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 hard-coding actions. Unix -tools are very good at doing file management, the thing that `bb` adds is -immediate visual feedback and rapid navigation. +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. -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. +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 piping 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 -0 scp user@example.com`. Want to zip files? Add a mapping for +`xargs -0 zip "$(printf 'Zip file: ' >/dev/tty && head -n1 /dev/tty)"` 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 @@ -58,6 +57,9 @@ 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`. +If you want to get user input during a command (e.g. to get the name of a new file), +you can use `/dev/tty` like so: `touch "$(printf 'New file: ' >/dev/tty && head -n1 /dev/tty)"` + ## License `bb` is released under the MIT license. See the `LICENSE` file for full details. diff --git a/bb.c b/bb.c index 827707a..fa7c946 100644 --- a/bb.c +++ b/bb.c @@ -124,6 +124,49 @@ static void err(const char *msg, ...) _exit(1); } +static int run_cmd_on_selection(bb_state_t *state, const char *cmd) +{ + pid_t child; + sig_t old_handler = signal(SIGINT, do_nothing); + + if ((child = fork()) == 0) { + // TODO: is there a max number of args? Should this be batched? + char **const args = calloc(MAX(1, state->nselected) + 4, sizeof(char*)); + int i = 0; + args[i++] = "sh"; + args[i++] = "-c"; + args[i++] = (char*)cmd; + args[i++] = "--"; + entry_t *first = state->firstselected ? state->firstselected : state->files[state->cursor]; + for (entry_t *e = first; e; e = e->next) { + args[i++] = e->d_name; + } + args[i] = NULL; + + { // Set environment variable to track shell nesting + char *depthstr = getenv("BB_DEPTH"); + int depth = depthstr ? atoi(depthstr) : 0; + char buf[64] = {0}; + snprintf(buf, sizeof(buf), "BB_DEPTH=%d", depth + 1); + putenv(buf); + } + + execvp("sh", args); + free(args); + err("Failed to execute command: '%s'", cmd); + _exit(0); + return -1; + } + + if (child == -1) + err("Failed to fork"); + int status; + waitpid(child, &status, 0); + signal(SIGINT, old_handler); + return status; +} + + static pid_t run_cmd(int *child_out, int *child_in, const char *cmd, ...) { int child_outfds[2], child_infds[2]; @@ -885,40 +928,23 @@ static void explore(char *path, int print_dir, int print_selection, char sep) for (int i = 0; bindings[i].key > 0; i++) { if (key == bindings[i].key) { term_move(0, height-1); + writez(termfd, "\e[K"); struct termios cur_tios; if (!(bindings[i].flags & ONSCREEN)) { close_term(); } else { tcgetattr(termfd, &cur_tios); - struct termios tios; - memcpy(&tios, &orig_termios, sizeof(tios)); - tcsetattr(termfd, TCSAFLUSH, &tios); - // Show cursor - writez(termfd, "\e[?25h"); + tcsetattr(termfd, TCSAFLUSH, &orig_termios); + writez(termfd, "\e[?25h"); // Show cursor } - int scriptinfd; - pid_t child; - 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, 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); - } - } - close(scriptinfd); - waitpid(child, NULL, 0); - signal(SIGINT, old_handler); + run_cmd_on_selection(&state, bindings[i].command); if (!(bindings[i].flags & ONSCREEN)) { init_term(); } else { tcsetattr(termfd, TCSAFLUSH, &cur_tios); - // hide cursor - writez(termfd, "\e[?25l"); + writez(termfd, "\e[?25l"); // Hide cursor } if (bindings[i].flags & CLEAR_SELECTION) diff --git a/config.def.h b/config.def.h index 2d308ed..e0ab01a 100644 --- a/config.def.h +++ b/config.def.h @@ -4,13 +4,13 @@ #include "keys.h" #define PROG_FUZZY "fzf" +#define PIPE_SELECTION_TO " printf '%s\\n' \"$@\" | " +#define AND_PAUSE " && read -n1 -p '\n\e[2m...press any key to continue...\e[0m\e[?25l'" #define SCROLLOFF 5 -#define NO_FILES (1<<0) -#define NULL_SEP (1<<1) -#define REFRESH (1<<2) -#define CLEAR_SELECTION (1<<3) -#define ONSCREEN (1<<4) +#define REFRESH (1<<0) +#define CLEAR_SELECTION (1<<1) +#define ONSCREEN (1<<2) struct { int key; @@ -18,18 +18,18 @@ struct { int flags; } bindings[] = { // User-defined custom scripts go here: - {'L', "less"}, - {'D', "xargs -0 rm -rf", CLEAR_SELECTION | REFRESH | ONSCREEN | NULL_SEP}, - {'d', "xargs -0 -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 -0 -I @ -n1 sh -c 'mv \"@\" \"`printf \"\e[1mRename \e[1;33m%%s\e[0m: \" \"@\" >&2 && head -n1 \e[0m \" cmd && " PIPE_SELECTION_TO "$SHELL -c \"$cmd\"" AND_PAUSE, REFRESH}, + {'>', "$SHELL", REFRESH}, + {'r', "for f; do read -p \"Rename $f: \" renamed && mv \"$f\" \"$renamed\"; done", + REFRESH | CLEAR_SELECTION | ONSCREEN}, // Hard-coded behaviors (these are just placeholders for the help): {-1, "?\t\e[0;34mOpen help menu\e[0m"},