Added null-separation option for xargs to properly handle funky
filenames
This commit is contained in:
parent
66444fe971
commit
8f73ec7241
8
Makefile
8
Makefile
@ -1,16 +1,20 @@
|
|||||||
PREFIX=
|
PREFIX=
|
||||||
CC=cc
|
CC=cc
|
||||||
CFLAGS=-O0 -std=gnu99 -g
|
CFLAGS=-O0 -std=gnu99
|
||||||
LIBS=
|
LIBS=
|
||||||
NAME=bb
|
NAME=bb
|
||||||
|
G=
|
||||||
|
|
||||||
all: $(NAME)
|
all: $(NAME)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm $(NAME)
|
rm $(NAME)
|
||||||
|
|
||||||
|
config.h:
|
||||||
|
cp config.def.h config.h
|
||||||
|
|
||||||
$(NAME): $(NAME).c keys.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)
|
test: $(NAME)
|
||||||
./$(NAME) test.xml
|
./$(NAME) test.xml
|
||||||
|
63
README.md
Normal file
63
README.md
Normal file
@ -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, <space> 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.
|
18
bb.c
18
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;
|
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) {
|
while (firstselected) {
|
||||||
const char *p = firstselected->d_fullname;
|
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');
|
const char *p2 = strchr(p, '\n');
|
||||||
if (!p2) p2 = p + strlen(p);
|
if (!p2) p2 = p + strlen(p);
|
||||||
write(fd, p, p2 - p);
|
write(fd, p, p2 - p);
|
||||||
if (*p2 == '\n')
|
if (*p2 == '\n' && sep == '\n')
|
||||||
write(fd, "\\", 1);
|
write(fd, "\\", 1);
|
||||||
p = p2;
|
p = p2;
|
||||||
}
|
}
|
||||||
write(fd, "\n", 1);
|
write(fd, &sep, 1);
|
||||||
firstselected = firstselected->next;
|
firstselected = firstselected->next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -373,7 +373,7 @@ static void clear_selection(bb_state_t *state)
|
|||||||
state->nselected = 0;
|
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 *tmp = path;
|
||||||
char *original_path = calloc(strlen(tmp) + 1, 1);
|
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);
|
sig_t old_handler = signal(SIGINT, do_nothing);
|
||||||
child = run_cmd(NULL, &scriptinfd, bindings[i].command);
|
child = run_cmd(NULL, &scriptinfd, bindings[i].command);
|
||||||
if (!(bindings[i].flags & NO_FILES)) {
|
if (!(bindings[i].flags & NO_FILES)) {
|
||||||
|
char sep = (bindings[i].flags & NULL_SEP) ? '\0' : '\n';
|
||||||
if (state.nselected > 0) {
|
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) {
|
} else if (strcmp(state.files[state.cursor]->d_name, "..") != 0) {
|
||||||
write(scriptinfd, state.files[state.cursor]->d_name, state.files[state.cursor]->d_namlen);
|
write(scriptinfd, state.files[state.cursor]->d_name, state.files[state.cursor]->d_namlen);
|
||||||
}
|
}
|
||||||
@ -940,7 +941,7 @@ done:
|
|||||||
if (print_dir)
|
if (print_dir)
|
||||||
printf("%s\n", state.path);
|
printf("%s\n", state.path);
|
||||||
if (print_selection)
|
if (print_selection)
|
||||||
write_selection(STDOUT_FILENO, state.firstselected);
|
write_selection(STDOUT_FILENO, state.firstselected, sep);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -948,10 +949,13 @@ int main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
char _realpath[MAX_PATH+1];
|
char _realpath[MAX_PATH+1];
|
||||||
char *path = ".";
|
char *path = ".";
|
||||||
|
char sep = '\n';
|
||||||
int print_dir = 0, print_selection = 0;
|
int print_dir = 0, print_selection = 0;
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "-d") == 0) {
|
if (strcmp(argv[i], "-d") == 0) {
|
||||||
print_dir = 1;
|
print_dir = 1;
|
||||||
|
} else if (strcmp(argv[i], "-0") == 0) {
|
||||||
|
sep = '\0';
|
||||||
} else if (strcmp(argv[i], "-s") == 0) {
|
} else if (strcmp(argv[i], "-s") == 0) {
|
||||||
print_selection = 1;
|
print_selection = 1;
|
||||||
} else if (path[0]) {
|
} else if (path[0]) {
|
||||||
@ -962,7 +966,7 @@ int main(int argc, char *argv[])
|
|||||||
init_term();
|
init_term();
|
||||||
if (!realpath(path, _realpath))
|
if (!realpath(path, _realpath))
|
||||||
err("realpath failed");
|
err("realpath failed");
|
||||||
explore(_realpath, print_dir, print_selection);
|
explore(_realpath, print_dir, print_selection, sep);
|
||||||
done:
|
done:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
17
config.h
17
config.h
@ -7,7 +7,7 @@
|
|||||||
#define SCROLLOFF 5
|
#define SCROLLOFF 5
|
||||||
|
|
||||||
#define NO_FILES (1<<0)
|
#define NO_FILES (1<<0)
|
||||||
#define CD_TO_RESULT (1<<1)
|
#define NULL_SEP (1<<1)
|
||||||
#define REFRESH (1<<2)
|
#define REFRESH (1<<2)
|
||||||
#define CLEAR_SELECTION (1<<3)
|
#define CLEAR_SELECTION (1<<3)
|
||||||
#define ONSCREEN (1<<4)
|
#define ONSCREEN (1<<4)
|
||||||
@ -19,16 +19,17 @@ struct {
|
|||||||
} bindings[] = {
|
} bindings[] = {
|
||||||
// User-defined custom scripts go here:
|
// User-defined custom scripts go here:
|
||||||
{'L', "less"},
|
{'L', "less"},
|
||||||
{'D', "xargs rm -rf", CLEAR_SELECTION | REFRESH | ONSCREEN},
|
{'D', "xargs -0 rm -rf", CLEAR_SELECTION | REFRESH | ONSCREEN | NULL_SEP},
|
||||||
{'d', "xargs -I @ sh -c 'rm -rfi @ </dev/tty'", CLEAR_SELECTION | REFRESH | ONSCREEN},
|
{'d', "xargs -0 -I @ sh -c 'rm -rfi @ </dev/tty'", CLEAR_SELECTION | REFRESH | ONSCREEN | NULL_SEP},
|
||||||
{'c', "xargs -n1 -I @ cp @ @.copy", REFRESH | ONSCREEN},
|
{'m', "xargs -0 -I @ mv -i @ . </dev/tty", CLEAR_SELECTION | REFRESH | ONSCREEN | NULL_SEP},
|
||||||
{'m', "xargs -I @ mv -i @ . </dev/tty", CLEAR_SELECTION | REFRESH | ONSCREEN},
|
{'c', "xargs -0 -I @ cp -i @ . </dev/tty", CLEAR_SELECTION | REFRESH | ONSCREEN | NULL_SEP},
|
||||||
{'p', "xargs -I @ cp -i @ . </dev/tty", CLEAR_SELECTION | REFRESH | ONSCREEN},
|
{'C', "xargs -0 -n1 -I @ cp @ @.copy", REFRESH | ONSCREEN | NULL_SEP},
|
||||||
{'n', "touch \"`printf '\\033[33;1mNew file:\\033[0m ' >/dev/tty && head -n1 /dev/tty`\"", ONSCREEN | REFRESH | NO_FILES},
|
{'n', "touch \"`printf '\\033[33;1mNew file:\\033[0m ' >/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`\"", REFRESH},
|
||||||
{'>', "sh -c \"`printf '> ' >/dev/tty && head -n1 /dev/tty`\"", NO_FILES | 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 </dev/tty`\"'",
|
{'r', "xargs -0 -I @ -n1 sh -c 'mv \"@\" \"`printf \"\e[1mRename \e[1;33m%%s\e[0m: \" \"@\" >&2 && head -n1 </dev/tty`\"'",
|
||||||
REFRESH | CLEAR_SELECTION | ONSCREEN},
|
REFRESH | CLEAR_SELECTION | ONSCREEN | NULL_SEP},
|
||||||
|
|
||||||
// Hard-coded behaviors (these are just placeholders for the help):
|
// Hard-coded behaviors (these are just placeholders for the help):
|
||||||
{-1, "?\t\e[0;34mOpen help menu\e[0m"},
|
{-1, "?\t\e[0;34mOpen help menu\e[0m"},
|
||||||
|
Loading…
Reference in New Issue
Block a user