code / bb

Lines2.7K C1.8K Shell331 YAML273 Markdown197 make44
(1.2K lines)
1 //
2 // bb.c
3 // Copyright 2020 Bruce Hill
4 // Released under the MIT license with the Commons Clause
5 //
6 // This file contains the main source code of `bb` the Bitty Browser.
7 //
9 #include <ctype.h>
10 #include <err.h>
11 #include <fcntl.h>
12 #include <glob.h>
13 #include <limits.h>
14 #include <signal.h>
15 #include <stdarg.h>
16 #include <stdio.h>
17 #include <stdlib.h>
18 #include <string.h>
19 #include <sys/errno.h>
20 #include <sys/ioctl.h>
21 #include <sys/stat.h>
22 #include <sys/wait.h>
23 #include <termios.h>
24 #include <unistd.h>
26 #include "draw.h"
27 #include "terminal.h"
28 #include "types.h"
29 #include "utils.h"
31 #ifndef BB_NAME
32 #define BB_NAME "bb"
33 #endif
35 #define BB_VERSION "0.31.0"
36 #define MAX_BINDINGS 1024
37 #define SCROLLOFF MIN(5, (winsize.ws_row - 4) / 2)
38 #define ONSCREEN (winsize.ws_row - 3)
40 #define LOG(...) \
41 do { \
42 FILE *f = fopen("log.txt", "a"); \
43 fprintf(f, __VA_ARGS__); \
44 fclose(f); \
45 } while (0)
47 // Functions
48 void bb_browse(bb_t *bb, int argc, char *argv[]);
49 static void check_cmdfile(bb_t *bb);
50 static void cleanup(void);
51 static void cleanup_and_raise(int sig);
52 static int compare_files(const void *v1, const void *v2);
53 __attribute__((format(printf, 2, 3))) void flash_warn(bb_t *bb, const char *fmt, ...);
54 static void handle_next_key_binding(bb_t *bb);
55 static void init_term(void);
56 static int is_simple_bbcmd(const char *s);
57 static entry_t *load_entry(bb_t *bb, const char *path);
58 static int matches_cmd(const char *str, const char *cmd);
59 static char *normalize_path(const char *path, char *pbuf);
60 static int populate_files(bb_t *bb, const char *path);
61 static void print_bindings(FILE *f);
62 static void run_bbcmd(bb_t *bb, const char *cmd);
63 static void restore_term(const struct termios *term);
64 static int run_script(bb_t *bb, const char *cmd);
65 static void set_columns(bb_t *bb, const char *cols);
66 static void set_cursor(bb_t *bb, int i);
67 static void set_globs(bb_t *bb, const char *globs);
68 static void set_interleave(bb_t *bb, int interleave);
69 static void set_selected(bb_t *bb, entry_t *e, int selected);
70 static void set_scroll(bb_t *bb, int i);
71 static void set_sort(bb_t *bb, const char *sort);
72 static void set_title(bb_t *bb);
73 static void sort_files(bb_t *bb);
74 static char *trim(char *s);
75 static int try_free_entry(entry_t *e);
76 static void update_term_size(int sig);
77 static int wait_for_process(proc_t **proc);
79 // Constants
80 static const char *T_ENTER_BBMODE =
81 T_OFF(T_SHOW_CURSOR ";" T_WRAP) T_ON(T_ALT_SCREEN ";" T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR);
82 static const char *T_LEAVE_BBMODE =
83 T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR ";" T_ALT_SCREEN) T_ON(T_SHOW_CURSOR ";" T_WRAP);
84 static const char *T_LEAVE_BBMODE_PARTIAL = T_OFF(T_MOUSE_XY ";" T_MOUSE_CELL ";" T_MOUSE_SGR) T_ON(T_WRAP);
85 static const char *description_str = BB_NAME " - an itty bitty console TUI file browser\n";
86 static const char *usage_str =
87 "Usage: " BB_NAME " (-h/--help | -v/--version | -s | -d | -0 | +command)* [[--] directory]\n";
89 // Variables used within this file to track global state
90 static binding_t bindings[MAX_BINDINGS];
91 static struct termios orig_termios, bb_termios;
92 static FILE *tty_out = NULL, *tty_in = NULL;
93 static struct winsize winsize = {0};
94 static char cmdfilename[PATH_MAX] = {0};
95 static bb_t *current_bb = NULL;
97 // Redirect stderr/stdout to these files during execution, and dump them on exit
98 typedef struct {
99 int orig_fd, dup_fd, tmp_fd;
100 const char *name;
101 char filename[PATH_MAX];
102 } outbuf_t;
103 outbuf_t output_buffers[] = {
104 {.name = "stdout", .orig_fd = STDOUT_FILENO, .dup_fd = -1, .tmp_fd = -1},
105 {.name = "stderr", .orig_fd = STDERR_FILENO, .dup_fd = -1, .tmp_fd = -1},
109 // Use bb to browse the filesystem.
111 void bb_browse(bb_t *bb, int argc, char *argv[]) {
112 const char *initial_path;
113 if (argc >= 3 && streq(argv[argc - 2], "--")) {
114 initial_path = argv[argc - 1];
115 argc -= 2;
116 } else if (argc >= 2 && argv[argc - 1][0] != '-' && argv[argc - 1][0] != '+') {
117 initial_path = argv[argc - 1];
118 argc -= 1;
119 } else {
120 initial_path = ".";
123 char full_initial_path[PATH_MAX];
124 normalize_path(initial_path, full_initial_path);
126 struct stat path_stat;
127 const char *goto_file = NULL;
128 nonnegative(stat(full_initial_path, &path_stat), "Could not find initial path: \"%s\"", initial_path);
129 if (!S_ISDIR(path_stat.st_mode)) {
130 char *slash = strrchr(full_initial_path, '/');
131 *slash = '\0';
132 goto_file = slash + 1;
135 if (populate_files(bb, full_initial_path))
136 errx(EXIT_FAILURE, "Could not find initial path: \"%s\"", full_initial_path);
138 // Emergency fallback:
139 bindings[0].key = KEY_CTRL_C;
140 bindings[0].script = check_strdup("kill -INT $PPID");
141 bindings[0].description = check_strdup("Kill the bb process");
142 system("bbstartup");
144 FILE *cmdfile = fopen(cmdfilename, "a");
145 if (goto_file) fprintf(cmdfile, "%cgoto:%s", '\0', goto_file);
146 for (int i = 0; i < argc; i++) {
147 if (argv[i][0] == '+') {
148 char *cmd = argv[i] + 1;
149 char *colon = strchr(cmd, ':');
150 if (colon && !colon[1]) {
151 for (++i; i < argc; i++)
152 fprintf(cmdfile, "%c%s%s", '\0', cmd, argv[i]);
153 } else {
154 fprintf(cmdfile, "%c%s", '\0', cmd);
158 fclose(cmdfile);
160 check_cmdfile(bb);
161 while (!bb->should_quit) {
162 render(tty_out, bb);
163 handle_next_key_binding(bb);
165 system("bbshutdown");
166 check_cmdfile(bb);
170 // Check the bb command file and run any and all commands that have been
171 // written to it.
173 static void check_cmdfile(bb_t *bb) {
174 FILE *cmdfile = fopen(cmdfilename, "r");
175 if (!cmdfile) return;
176 char *cmd = NULL;
177 size_t space = 0;
178 while (getdelim(&cmd, &space, '\0', cmdfile) >= 0) {
179 if (!cmd[0]) continue;
180 run_bbcmd(bb, cmd);
181 if (bb->should_quit) break;
183 delete (&cmd);
184 fclose(cmdfile);
185 unlink(cmdfilename);
189 // Clean up the terminal before going to the default signal handling behavior.
191 static void cleanup_and_raise(int sig) {
192 cleanup();
193 int childsig = (sig == SIGTSTP || sig == SIGSTOP) ? sig : SIGHUP;
194 if (current_bb) {
195 for (proc_t *p = current_bb->running_procs; p; p = p->running.next) {
196 kill(p->pid, childsig);
197 LL_REMOVE(p, running);
200 raise(sig);
201 // This code will only ever be run if sig is SIGTSTP/SIGSTOP, otherwise, raise() won't return:
202 init_term();
203 struct sigaction sa = {.sa_handler = &cleanup_and_raise, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
204 sigaction(sig, &sa, NULL);
208 // Reset the screen, delete the cmdfile, and print the stdout/stderr buffers
210 static void cleanup(void) {
211 if (cmdfilename[0]) {
212 unlink(cmdfilename);
213 cmdfilename[0] = '\0';
215 if (tty_out) {
216 fputs(T_LEAVE_BBMODE, tty_out);
217 fflush(tty_out);
218 tcsetattr(fileno(tty_out), TCSANOW, &orig_termios);
220 FOREACH(outbuf_t *, ob, output_buffers) {
221 if (ob->tmp_fd == -1) continue;
222 fflush(ob->orig_fd == STDOUT_FILENO ? stdout : stderr);
223 dup2(ob->dup_fd, ob->orig_fd);
224 lseek(ob->tmp_fd, 0, SEEK_SET);
225 char buf[256];
226 for (ssize_t len; (len = read(ob->tmp_fd, buf, LEN(buf))) > 0;)
227 write(ob->orig_fd, buf, len);
228 close(ob->tmp_fd);
229 ob->tmp_fd = ob->dup_fd = -1;
230 unlink(ob->filename);
235 // Used for sorting, this function compares files according to the sorting-related options,
236 // like bb->sort
238 static int compare_files(const void *v1, const void *v2) {
239 #define COMPARE(a, b) \
240 if ((a) != (b)) { \
241 return sign * ((a) < (b) ? 1 : -1); \
243 #define COMPARE_TIME(t1, t2) COMPARE((t1).tv_sec, (t2).tv_sec) COMPARE((t1).tv_nsec, (t2).tv_nsec)
244 bb_t *bb = current_bb;
245 const entry_t *e1 = *((const entry_t **)v1), *e2 = *((const entry_t **)v2);
247 int sign = 1;
248 if (!bb->interleave_dirs) {
249 COMPARE(E_ISDIR(e1), E_ISDIR(e2));
252 for (char *sort = bb->sort + 1; *sort; sort += 2) {
253 sign = sort[-1] == '-' ? -1 : 1;
254 switch (*sort) {
255 case COL_SELECTED: COMPARE(IS_SELECTED(e1), IS_SELECTED(e2)); break;
256 case COL_NAME: {
257 // This sorting method is not identical to strverscmp(). Notably, bb's sort
258 // will order: [0, 1, 9, 00, 01, 09, 10, 000, 010] instead of strverscmp()'s
259 // order: [000, 00, 01, 010, 09, 0, 1, 9, 10]. I believe bb's sort is consistent
260 // with how people want their files grouped: all files padded to n digits
261 // will be grouped together, and files with the same padding will be sorted
262 // ordinally. This version also does case-insensitivity by lowercasing words,
263 // so the following characters come before all letters: [\]^_`
264 const char *n1 = e1->name, *n2 = e2->name;
265 while (*n1 && *n2) {
266 char c1 = tolower(*n1), c2 = tolower(*n2);
267 if ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9') {
268 long i1 = strtol(n1, (char **)&n1, 10);
269 long i2 = strtol(n2, (char **)&n2, 10);
270 // Shorter numbers always go before longer. In practice, I assume
271 // filenames padded to the same number of digits should be grouped
272 // together, instead of
273 // [1.png, 0001.png, 2.png, 0002.png, 3.png], it makes more sense to have:
274 // [1.png, 2.png, 3.png, 0001.png, 0002.png]
275 COMPARE((n2 - e2->name), (n1 - e1->name));
276 COMPARE(i2, i1);
277 } else {
278 COMPARE(c2, c1);
279 ++n1;
280 ++n2;
283 COMPARE(tolower(*n2), tolower(*n1));
284 break;
286 case COL_PERM: COMPARE((e1->info.st_mode & 0x3FF), (e2->info.st_mode & 0x3FF)); break;
287 case COL_SIZE: COMPARE(e1->info.st_size, e2->info.st_size); break;
288 case COL_MTIME: COMPARE_TIME(get_mtime(e1->info), get_mtime(e2->info)); break;
289 case COL_CTIME: COMPARE_TIME(get_ctime(e1->info), get_ctime(e2->info)); break;
290 case COL_ATIME: COMPARE_TIME(get_atime(e1->info), get_atime(e2->info)); break;
291 case COL_RANDOM: COMPARE(e2->shufflepos, e1->shufflepos); break;
292 default: break;
295 return 0;
296 #undef COMPARE
297 #undef COMPARE_TIME
301 // Flash a warning message at the bottom of the screen.
303 void flash_warn(bb_t *bb, const char *fmt, ...) {
304 move_cursor(tty_out, 0, winsize.ws_row - 1);
305 fputs("\033[41;33;1m", tty_out);
306 va_list args;
307 va_start(args, fmt);
308 vfprintf(tty_out, fmt, args);
309 va_end(args);
310 fputs(" Press any key to continue...\033[0m ", tty_out);
311 fflush(tty_out);
312 while (bgetkey(tty_in, NULL, NULL) == -1)
313 usleep(100);
314 bb->dirty = 1;
318 // Wait until the user has pressed a key with an associated key binding and run
319 // that binding.
321 static void handle_next_key_binding(bb_t *bb) {
322 int key, mouse_x, mouse_y;
323 binding_t *binding;
324 do {
325 do {
326 struct winsize prevsize = winsize;
327 key = bgetkey(tty_in, &mouse_x, &mouse_y);
328 // Window size changed while waiting for keypress:
329 if (winsize.ws_row != prevsize.ws_row || winsize.ws_col != prevsize.ws_col) bb->dirty = 1;
330 if (key == -1 && bb->dirty) return;
331 } while (key == -1);
333 binding = NULL;
334 FOREACH(binding_t *, b, bindings) {
335 if (key == b->key) {
336 binding = b;
337 break;
340 } while (!binding);
342 char bbmousecol[2] = {0, 0}, bbclicked[PATH_MAX];
343 if (mouse_x != -1 && mouse_y != -1) {
344 int *colwidths = get_column_widths(bb->columns, winsize.ws_col - 1);
345 // Get bb column:
346 for (int col = 0, x = 0; bb->columns[col]; col++, x++) {
347 x += colwidths[col];
348 if (x >= mouse_x) {
349 bbmousecol[0] = bb->columns[col];
350 break;
353 if (mouse_y == 1) {
354 strcpy(bbclicked, "<column label>");
355 } else if (2 <= mouse_y && mouse_y <= winsize.ws_row - 2 && bb->scroll + (mouse_y - 2) <= bb->nfiles - 1) {
356 strcpy(bbclicked, bb->files[bb->scroll + (mouse_y - 2)]->fullname);
357 } else {
358 bbclicked[0] = '\0';
360 setenv("BBMOUSECOL", bbmousecol, 1);
361 setenv("BBCLICKED", bbclicked, 1);
364 if (is_simple_bbcmd(binding->script)) {
365 run_bbcmd(bb, binding->script);
366 } else {
367 move_cursor(tty_out, 0, winsize.ws_row - 1);
368 fputs("\033[K", tty_out);
369 restore_term(&orig_termios);
370 run_script(bb, binding->script);
371 for (entry_t *next, *e = bb->selected; e; e = next) {
372 next = e->selected.next;
373 struct stat buf;
374 if (stat(e->fullname, &buf) != 0) set_selected(bb, e, 0);
376 init_term();
377 set_title(bb);
378 check_cmdfile(bb);
380 if (mouse_x != -1 && mouse_y != -1) {
381 setenv("BBMOUSECOL", "", 1);
382 setenv("BBCLICKED", "", 1);
387 // Initialize the terminal files for /dev/tty and set up some desired
388 // attributes like passing Ctrl-c as a key instead of interrupting
390 static void init_term(void) {
391 nonnegative(tcsetattr(fileno(tty_out), TCSANOW, &bb_termios));
392 update_term_size(0);
393 // Initiate mouse tracking and disable text wrapping:
394 fputs(T_ENTER_BBMODE, tty_out);
395 fflush(tty_out);
399 // Return whether or not 's' is a simple bb command that doesn't need
400 // a full shell instance (e.g. "bbcmd cd:.." or "bbcmd move:+1").
402 static int is_simple_bbcmd(const char *s) {
403 if (!s) return 0;
404 while (*s == ' ')
405 ++s;
406 if (strncmp(s, "bbcmd ", strlen("bbcmd ")) != 0) return 0;
407 const char *special = ";$&<>|\n*?\\\"'";
408 for (const char *p = special; *p; ++p) {
409 if (strchr(s, *p)) return 0;
411 return 1;
415 // Load a file's info into an entry_t and return it (if found).
416 // The returned entry must be free()ed by the caller.
417 // Warning: this does not deduplicate entries, and it's best if there aren't
418 // duplicate entries hanging around.
420 static entry_t *load_entry(bb_t *bb, const char *path) {
421 struct stat linkedstat, filestat;
422 if (!path || !path[0]) return NULL;
423 if (lstat(path, &filestat) == -1) return NULL;
424 char pbuf[PATH_MAX];
425 if (path[0] == '/') strcpy(pbuf, path);
426 else sprintf(pbuf, "%s%s", bb->path, path);
427 if (pbuf[strlen(pbuf) - 1] == '/' && pbuf[1]) pbuf[strlen(pbuf) - 1] = '\0';
429 // Check for pre-existing:
430 for (entry_t *e = bb->hash[(int)filestat.st_ino & HASH_MASK]; e; e = e->hash.next) {
431 if (e->info.st_ino == filestat.st_ino
432 && e->info.st_dev == filestat.st_dev
433 // Need to check filename in case of hard links
434 && streq(pbuf, e->fullname))
435 return e;
438 ssize_t linkpathlen = -1;
439 char linkbuf[PATH_MAX];
440 if (S_ISLNK(filestat.st_mode)) {
441 linkpathlen = nonnegative(readlink(pbuf, linkbuf, sizeof(linkbuf)), "Couldn't read link: '%s'", pbuf);
442 linkbuf[linkpathlen] = '\0';
443 while (linkpathlen > 0 && linkbuf[linkpathlen - 1] == '/')
444 linkbuf[--linkpathlen] = '\0';
445 if (stat(pbuf, &linkedstat) == -1) memset(&linkedstat, 0, sizeof(linkedstat));
447 size_t pathlen = strlen(pbuf);
448 size_t entry_size = sizeof(entry_t) + (pathlen + 1) + (size_t)(linkpathlen + 1);
449 entry_t *entry = new_bytes(entry_size);
450 char *end = stpcpy(entry->fullname, pbuf);
451 if (linkpathlen >= 0) entry->linkname = strcpy(end + 1, linkbuf);
452 if (streq(entry->fullname, "/")) {
453 entry->name = entry->fullname;
454 } else {
455 if (strncmp(entry->fullname, bb->path, strlen(bb->path)) == 0) entry->name = entry->fullname + strlen(bb->path);
456 else entry->name = strrchr(entry->fullname, '/') + 1; // Last path component
458 if (S_ISLNK(filestat.st_mode)) entry->linkedmode = linkedstat.st_mode;
459 entry->info = filestat;
460 LL_PREPEND(bb->hash[(int)filestat.st_ino & HASH_MASK], entry, hash);
461 entry->index = -1;
462 bb->hash[(int)filestat.st_ino & HASH_MASK] = entry;
463 return entry;
467 // Return whether a string matches a command
468 // e.g. matches_cmd("sel:x", "select:") == 1, matches_cmd("q", "quit") == 1
470 static int matches_cmd(const char *str, const char *cmd) {
471 if ((strchr(cmd, ':') == NULL) != (strchr(str, ':') == NULL)) return 0;
472 while (*str == *cmd && *cmd && *cmd != ':')
473 ++str, ++cmd;
474 return *str == '\0' || *str == ':';
478 // Prepend `./` to relative paths, replace "~" with $HOME.
479 // The normalized path is stored in `normalized`.
481 static char *normalize_path(const char *path, char *normalized) {
482 char pbuf[PATH_MAX] = {0};
483 if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) {
484 char *home;
485 if (!(home = getenv("HOME"))) return NULL;
486 strcpy(pbuf, home);
487 ++path;
488 } else if (path[0] != '/') {
489 strcpy(pbuf, "./");
491 strcat(pbuf, path);
492 if (realpath(pbuf, normalized) == NULL) {
493 strcpy(normalized, pbuf); // TODO: normalize better?
494 return NULL;
496 return normalized;
500 // Remove all the files currently stored in bb->files and if `bb->path` is
501 // non-NULL, update `bb` with a listing of the files in `path`
503 static int populate_files(bb_t *bb, const char *path) {
504 int clear_future_history = 0;
505 if (path == NULL)
507 else if (streq(path, "-")) {
508 if (!bb->history) return -1;
509 if (bb->history->prev) bb->history = bb->history->prev;
510 path = bb->history->path;
511 } else if (streq(path, "+")) {
512 if (!bb->history) return -1;
513 if (bb->history->next) bb->history = bb->history->next;
514 path = bb->history->path;
515 } else clear_future_history = 1;
517 int samedir = path && streq(bb->path, path);
518 int old_scroll = bb->scroll;
519 int old_cursor = bb->cursor;
520 char old_selected[PATH_MAX] = "";
521 if (samedir && bb->nfiles > 0) strcpy(old_selected, bb->files[bb->cursor]->fullname);
523 char pbuf[PATH_MAX] = {0}, prev[PATH_MAX] = {0};
524 strcpy(prev, bb->path);
525 if (path != NULL) {
526 if (!normalize_path(path, pbuf)) flash_warn(bb, "Could not normalize path: \"%s\"", path);
527 if (pbuf[strlen(pbuf) - 1] != '/') strcat(pbuf, "/");
528 if (chdir(pbuf)) {
529 flash_warn(bb, "Could not cd to: \"%s\"", pbuf);
530 return -1;
534 if (clear_future_history && !samedir) {
535 for (bb_history_t *next, *h = bb->history ? bb->history->next : NULL; h; h = next) {
536 next = h->next;
537 delete (&h);
540 bb_history_t *h = new (bb_history_t);
541 strcpy(h->path, pbuf);
542 h->prev = bb->history;
543 if (bb->history) bb->history->next = h;
544 bb->history = h;
547 bb->dirty = 1;
548 strcpy(bb->path, pbuf);
549 set_title(bb);
551 // Clear old files (if any)
552 if (bb->files) {
553 for (int i = 0; i < bb->nfiles; i++) {
554 bb->files[i]->index = -1;
555 try_free_entry(bb->files[i]);
556 bb->files[i] = NULL;
558 delete (&bb->files);
560 bb->nfiles = 0;
561 bb->cursor = 0;
562 bb->scroll = 0;
564 if (!bb->path[0]) return 0;
566 size_t space = 0;
567 glob_t globbuf = {0};
568 char *pat, *tmpglob = check_strdup(bb->globpats);
569 while ((pat = strsep(&tmpglob, " ")) != NULL)
570 glob(pat, GLOB_NOSORT | GLOB_APPEND, NULL, &globbuf);
571 delete (&tmpglob);
572 for (size_t i = 0; i < globbuf.gl_pathc; i++) {
573 // Don't normalize path so we can get "." and ".."
574 entry_t *entry = load_entry(bb, globbuf.gl_pathv[i]);
575 if (!entry) {
576 flash_warn(bb, "Failed to load entry: '%s'", globbuf.gl_pathv[i]);
577 continue;
579 entry->index = bb->nfiles;
580 if ((size_t)bb->nfiles + 1 > space) bb->files = grow(bb->files, space += 100);
581 bb->files[bb->nfiles++] = entry;
583 globfree(&globbuf);
585 // RNG is seeded with a hash of all the inodes in the current dir
586 // This hash algorithm is based on Python's frozenset hashing
587 unsigned long seed = (unsigned long)bb->nfiles * 1927868237UL;
588 for (int i = 0; i < bb->nfiles; i++)
589 seed ^= ((bb->files[i]->info.st_ino ^ 89869747UL) ^ (bb->files[i]->info.st_ino << 16)) * 3644798167UL;
590 srand((unsigned int)seed);
591 for (int i = 0; i < bb->nfiles; i++) {
592 int j = rand() % (i + 1); // This introduces some RNG bias, but it's not important here
593 bb->files[i]->shufflepos = bb->files[j]->shufflepos;
594 bb->files[j]->shufflepos = i;
597 sort_files(bb);
598 if (samedir) {
599 set_scroll(bb, old_scroll);
600 bb->cursor = old_cursor > bb->nfiles - 1 ? bb->nfiles - 1 : old_cursor;
601 if (old_selected[0]) {
602 entry_t *e = load_entry(bb, old_selected);
603 if (e) set_cursor(bb, e->index);
605 } else {
606 entry_t *p = load_entry(bb, prev);
607 if (p) {
608 if (IS_VIEWED(p)) set_cursor(bb, p->index);
609 else try_free_entry(p);
612 return 0;
616 // Print the current key bindings
618 static void print_bindings(FILE *f) {
619 FOREACH(binding_t *, b, bindings) {
620 if (!b->description) break;
621 if (b->key == -1) {
622 const char *label = b->description;
623 fprintf(f, "\n\033[33;1;4m\033[%dG%s\033[0m\n", (winsize.ws_col - (int)strlen(label)) / 2, label);
624 continue;
626 char buf[1000];
627 char *p = buf;
628 for (binding_t *next = b;
629 next < &bindings[LEN(bindings)] && next->script && streq(b->description, next->description); next++) {
630 if (next > b) p = stpcpy(p, ", ");
631 p = bkeyname(next->key, p);
632 b = next;
634 *p = '\0';
635 fprintf(f, "\033[1m\033[%dG%s\033[0m", winsize.ws_col / 2 - 1 - (int)strlen(buf), buf);
636 fprintf(f, "\033[1m\033[%dG\033[34m%s\033[0m", winsize.ws_col / 2 + 1, b->description);
637 fprintf(f, "\033[0m\n");
639 fprintf(f, "\n");
643 // Run a bb internal command (e.g. "+refresh") and return an indicator of what
644 // needs to happen next.
646 static void run_bbcmd(bb_t *bb, const char *cmd) {
647 while (*cmd == ' ' || *cmd == '\n')
648 ++cmd;
649 if (strncmp(cmd, "bbcmd ", strlen("bbcmd ")) == 0) cmd = &cmd[strlen("bbcmd ")];
650 const char *value = strchr(cmd, ':');
651 if (value) ++value;
652 if (matches_cmd(cmd, "bind:")) { // +bind:<keys>:<script>
653 char *value_copy = check_strdup(value);
654 char *keys = trim(value_copy);
655 if (!keys[0]) {
656 delete (&value_copy);
657 return;
659 char *script = strchr(keys + 1, ':');
660 if (!script) {
661 delete (&value_copy);
662 flash_warn(bb, "No script provided.");
663 return;
665 *script = '\0';
666 script = trim(script + 1);
667 char *description;
668 if (script[0] == '#') {
669 description = trim(strsep(&script, "\n") + 1);
670 if (!script) script = (char *)"";
671 else script = trim(script);
672 } else description = script;
673 for (char *key; (key = strsep(&keys, ","));) {
674 int keyval;
675 if (streq(key, "Section")) {
676 keyval = -1;
677 } else {
678 keyval = bkeywithname(key);
679 if (keyval == -1) continue;
680 // Delete existing bindings for this key (if any):
681 FOREACH(binding_t *, b, bindings) {
682 if (b->key == keyval) {
683 delete ((char **)&b->description);
684 delete ((char **)&b->script);
685 int i = (int)(b - bindings);
686 memmove(&bindings[i], &bindings[i + 1], sizeof(binding_t) * (LEN(bindings) - i - 1));
687 memset(&bindings[LEN(bindings) - 1], 0, sizeof(binding_t));
691 // Append binding:
692 FOREACH(binding_t *, b, bindings) {
693 if (b->script) continue;
694 b->key = keyval;
695 if (is_simple_bbcmd(script)) b->script = check_strdup(script);
696 else nonnegative(asprintf((char **)&b->script, "set -e\n%s", script), "Could not copy script");
697 b->description = check_strdup(description);
698 break;
701 delete (&value_copy);
702 } else if (matches_cmd(cmd, "cd:")) { // +cd:
703 if (populate_files(bb, value)) flash_warn(bb, "Could not open directory: \"%s\"", value);
704 } else if (matches_cmd(cmd, "columns:")) { // +columns:
705 set_columns(bb, value);
706 } else if (matches_cmd(cmd, "deselect")) { // +deselect
707 while (bb->selected)
708 set_selected(bb, bb->selected, 0);
709 } else if (matches_cmd(cmd, "deselect:")) { // +deselect:<file>
710 char pbuf[PATH_MAX];
711 normalize_path(value, pbuf);
712 entry_t *e = load_entry(bb, pbuf);
713 if (e) {
714 set_selected(bb, e, 0);
715 return;
717 // Filename may no longer exist:
718 for (e = bb->selected; e; e = e->selected.next) {
719 if (streq(e->fullname, pbuf)) {
720 set_selected(bb, e, 0);
721 return;
724 } else if (matches_cmd(cmd, "fg:") || matches_cmd(cmd, "fg")) { // +fg:
725 int nprocs = 0;
726 for (proc_t *p = bb->running_procs; p; p = p->running.next)
727 ++nprocs;
728 int fg = value ? nprocs - (int)strtol(value, NULL, 10) : 0;
729 proc_t *child = NULL;
730 for (proc_t *p = bb->running_procs; p && !child; p = p->running.next) {
731 if (fg-- == 0) child = p;
733 if (!child) return;
734 move_cursor(tty_out, 0, winsize.ws_row - 1);
735 fputs("\033[K", tty_out);
736 restore_term(&orig_termios);
737 signal(SIGTTOU, SIG_IGN);
738 nonnegative(tcsetpgrp(fileno(tty_out), child->pid));
739 kill(-(child->pid), SIGCONT);
740 wait_for_process(&child);
741 signal(SIGTTOU, SIG_DFL);
742 init_term();
743 set_title(bb);
744 bb->dirty = 1;
745 } else if (matches_cmd(cmd, "glob:")) { // +glob:
746 set_globs(bb, value[0] ? value : "*");
747 populate_files(bb, bb->path);
748 } else if (matches_cmd(cmd, "goto:") || matches_cmd(cmd, "goto")) { // +goto:
749 if (!value && !bb->selected) return;
750 entry_t *e = load_entry(bb, value ? value : bb->selected->fullname);
751 if (!e) {
752 flash_warn(bb, "Could not find file to go to: \"%s\"", value);
753 return;
755 char pbuf[PATH_MAX];
756 strcpy(pbuf, e->fullname);
757 char *lastslash = strrchr(pbuf, '/');
758 if (!lastslash) errx(EXIT_FAILURE, "No slash found in filename: %s", pbuf);
759 *lastslash = '\0'; // Split in two
760 try_free_entry(e);
761 // Move to dir and reselect
762 populate_files(bb, pbuf);
763 e = load_entry(bb, lastslash + 1);
764 if (!e) {
765 flash_warn(bb, "Could not find file again: \"%s\"", lastslash + 1);
767 if (IS_VIEWED(e)) set_cursor(bb, e->index);
768 else try_free_entry(e);
769 } else if (matches_cmd(cmd, "help")) { // +help
770 FILE *p = popen("less -rfKX >/dev/tty", "w");
771 print_bindings(p);
772 pclose(p);
773 bb->dirty = 1;
774 } else if (matches_cmd(cmd, "interleave:") || matches_cmd(cmd, "interleave")) { // +interleave
775 bb->interleave_dirs = value ? (value[0] == '1') : !bb->interleave_dirs;
776 set_interleave(bb, bb->interleave_dirs);
777 sort_files(bb);
778 } else if (matches_cmd(cmd, "move:")) { // +move:
779 int oldcur, isdelta, n;
780 move:
781 if (bb->nfiles == 0) return;
782 oldcur = bb->cursor;
783 isdelta = value[0] == '-' || value[0] == '+';
784 n = (int)strtol(value, (char **)&value, 10);
785 if (*value == '%') n = (n * (value[1] == 'n' ? bb->nfiles : winsize.ws_row)) / 100;
786 if (isdelta) set_cursor(bb, bb->cursor + n);
787 else set_cursor(bb, n);
788 if (matches_cmd(cmd, "spread:")) { // +spread:
789 int sel = IS_SELECTED(bb->files[oldcur]);
790 for (int i = bb->cursor; i != oldcur; i += (oldcur > i ? 1 : -1))
791 set_selected(bb, bb->files[i], sel);
793 } else if (matches_cmd(cmd, "quit")) { // +quit
794 bb->should_quit = 1;
795 } else if (matches_cmd(cmd, "refresh")) { // +refresh
796 populate_files(bb, bb->path);
797 } else if (matches_cmd(cmd, "scroll:")) { // +scroll:
798 // TODO: figure out the best version of this
799 int isdelta = value[0] == '+' || value[0] == '-';
800 int n = (int)strtol(value, (char **)&value, 10);
801 if (*value == '%') n = (n * (value[1] == 'n' ? bb->nfiles : winsize.ws_row)) / 100;
802 if (isdelta) set_scroll(bb, bb->scroll + n);
803 else set_scroll(bb, n);
804 } else if (matches_cmd(cmd, "select")) { // +select
805 for (int i = 0; i < bb->nfiles; i++)
806 set_selected(bb, bb->files[i], 1);
807 } else if (matches_cmd(cmd, "select:")) { // +select:<file>
808 entry_t *e = load_entry(bb, value);
809 if (e) set_selected(bb, e, 1);
810 else flash_warn(bb, "Could not find file to select: \"%s\"", value);
811 } else if (matches_cmd(cmd, "sort:")) { // +sort:
812 set_sort(bb, value);
813 sort_files(bb);
814 } else if (matches_cmd(cmd, "spread:")) { // +spread:
815 goto move;
816 } else if (matches_cmd(cmd, "toggle")) { // +toggle
817 for (int i = 0; i < bb->nfiles; i++)
818 set_selected(bb, bb->files[i], !IS_SELECTED(bb->files[i]));
819 } else if (matches_cmd(cmd, "toggle:")) { // +toggle:<file>
820 entry_t *e = load_entry(bb, value);
821 if (e) set_selected(bb, e, !IS_SELECTED(e));
822 else flash_warn(bb, "Could not find file to toggle: \"%s\"", value);
823 } else {
824 flash_warn(bb, "Invalid bb command: %s", cmd);
829 // Close the /dev/tty terminals and restore some of the attributes.
831 static void restore_term(const struct termios *term) {
832 tcsetattr(fileno(tty_out), TCSANOW, term);
833 fputs(T_LEAVE_BBMODE_PARTIAL, tty_out);
834 fflush(tty_out);
838 // Run a shell script with the selected files passed as sequential arguments to
839 // the script (or pass the cursor file if none are selected).
840 // Return the exit status of the script.
842 static int run_script(bb_t *bb, const char *cmd) {
843 proc_t *proc = new (proc_t);
844 signal(SIGTTOU, SIG_IGN);
845 if ((proc->pid = nonnegative(fork())) == 0) {
846 pid_t pgrp = getpid();
847 (void)setpgid(0, pgrp);
848 nonnegative(tcsetpgrp(STDIN_FILENO, pgrp));
849 const char **args = new (char * [4 + (size_t)bb->nselected + 1]);
850 int i = 0;
851 args[i++] = "sh";
852 args[i++] = "-c";
853 args[i++] = (char *)cmd;
854 args[i++] = "--"; // ensure files like "-i" are not interpreted as flags for sh
855 // bb->selected is in most-recent order, so populate args in reverse to make sure
856 // that $1 is the first selected, etc.
857 i += bb->nselected;
858 for (entry_t *e = bb->selected; e; e = e->selected.next)
859 args[--i] = e->fullname;
861 setenv("BB", bb->nfiles ? bb->files[bb->cursor]->fullname : "", 1);
863 dup2(fileno(tty_out), STDOUT_FILENO);
864 dup2(fileno(tty_out), STDERR_FILENO);
865 dup2(fileno(tty_in), STDIN_FILENO);
866 execvp(args[0], (char **)args);
867 err(EXIT_FAILURE, "Failed to execute command: '%s'", cmd);
868 return -1;
871 (void)setpgid(getpid(), getpid());
872 LL_PREPEND(bb->running_procs, proc, running);
873 int status = wait_for_process(&proc);
874 bb->dirty = 1;
875 return status;
879 // Set the columns displayed by bb.
881 static void set_columns(bb_t *bb, const char *cols) {
882 strncpy(bb->columns, cols, MAX_COLS);
883 setenv("BBCOLUMNS", bb->columns, 1);
887 // Set bb's file cursor to the given index (and adjust the scroll as necessary)
889 static void set_cursor(bb_t *bb, int newcur) {
890 int oldcur = bb->cursor;
891 if (newcur > bb->nfiles - 1) newcur = bb->nfiles - 1;
892 if (newcur < 0) newcur = 0;
893 bb->cursor = newcur;
894 if (bb->nfiles <= ONSCREEN) {
895 bb->scroll = 0;
896 return;
899 if (oldcur < bb->cursor) {
900 if (bb->scroll > bb->cursor) bb->scroll = MAX(0, bb->cursor);
901 else if (bb->scroll < bb->cursor - ONSCREEN + 1 + SCROLLOFF)
902 bb->scroll = MIN(bb->nfiles - 1 - ONSCREEN + 1, bb->scroll + (newcur - oldcur));
903 } else {
904 if (bb->scroll > bb->cursor - SCROLLOFF)
905 bb->scroll = MAX(0, bb->scroll + (newcur - oldcur)); // bb->cursor - SCROLLOFF);
906 else if (bb->scroll < bb->cursor - ONSCREEN + 1)
907 bb->scroll = MIN(bb->cursor - ONSCREEN + 1, bb->nfiles - 1 - ONSCREEN + 1);
912 // Set the glob pattern(s) used by bb. Patterns are ' ' delimited
914 static void set_globs(bb_t *bb, const char *globs) {
915 delete (&bb->globpats);
916 bb->globpats = check_strdup(globs);
917 setenv("BBGLOB", bb->globpats, 1);
921 // Set whether or not bb should interleave directories and files.
923 static void set_interleave(bb_t *bb, int interleave) {
924 bb->interleave_dirs = interleave;
925 if (interleave) setenv("BBINTERLEAVE", "interleave", 1);
926 else unsetenv("BBINTERLEAVE");
927 bb->dirty = 1;
931 // Set bb's scroll to the given index (and adjust the cursor as necessary)
933 static void set_scroll(bb_t *bb, int newscroll) {
934 int delta = newscroll - bb->scroll;
935 if (bb->nfiles <= ONSCREEN) {
936 newscroll = 0;
937 } else {
938 if (newscroll > bb->nfiles - 1 - ONSCREEN + 1) newscroll = bb->nfiles - 1 - ONSCREEN + 1;
939 if (newscroll < 0) newscroll = 0;
942 bb->scroll = newscroll;
943 bb->cursor += delta;
944 if (bb->cursor > bb->nfiles - 1) bb->cursor = bb->nfiles - 1;
945 if (bb->cursor < 0) bb->cursor = 0;
949 // Select or deselect a file.
951 static void set_selected(bb_t *bb, entry_t *e, int selected) {
952 if (IS_SELECTED(e) == selected) return;
954 if (bb->nfiles > 0 && e != bb->files[bb->cursor]) bb->dirty = 1;
956 if (selected) {
957 LL_PREPEND(bb->selected, e, selected);
958 ++bb->nselected;
959 } else {
960 LL_REMOVE(e, selected);
961 try_free_entry(e);
962 --bb->nselected;
967 // Set the sorting method used by bb to display files.
969 static void set_sort(bb_t *bb, const char *sort) {
970 char sortbuf[MAX_SORT + 1];
971 strncpy(sortbuf, sort, MAX_SORT);
972 for (char *s = sortbuf; s[0] && s[1]; s += 2) {
973 char *found = strchr(bb->sort, s[1]);
974 if (found) {
975 if (*s == '~') *s = found[-1] == '+' && found == &bb->sort[1] ? '-' : '+';
976 memmove(found - 1, found + 1, strlen(found + 1) + 1);
977 } else if (*s == '~') *s = '+';
979 size_t len = MIN(MAX_SORT, strlen(sort));
980 memmove(bb->sort + len, bb->sort, MAX_SORT + 1 - len);
981 memmove(bb->sort, sortbuf, len);
982 setenv("BBSORT", bb->sort, 1);
986 // Set the xwindow title property to: bb - current path
988 static void set_title(bb_t *bb) {
989 char *home = getenv("HOME");
990 if (home && strncmp(bb->path, home, strlen(home)) == 0)
991 fprintf(tty_out, "\033]2;" BB_NAME ": ~%s\007", bb->path + strlen(home));
992 else fprintf(tty_out, "\033]2;" BB_NAME ": %s\007", bb->path);
996 // If the given entry is not viewed or selected, remove it from the
997 // hash, free it, and return 1.
999 static int try_free_entry(entry_t *e) {
1000 if (IS_SELECTED(e) || IS_VIEWED(e) || !IS_LOADED(e)) return 0;
1001 LL_REMOVE(e, hash);
1002 delete (&e);
1003 return 1;
1007 // Sort the files in bb according to bb's settings.
1009 static void sort_files(bb_t *bb) {
1010 qsort(bb->files, (size_t)bb->nfiles, sizeof(entry_t *), compare_files);
1011 for (int i = 0; i < bb->nfiles; i++)
1012 bb->files[i]->index = i;
1013 bb->dirty = 1;
1017 // Trim trailing whitespace by inserting '\0' and return a pointer to after the
1018 // first non-whitespace char
1020 static char *trim(char *s) {
1021 if (!s) return NULL;
1022 while (*s == ' ' || *s == '\n')
1023 ++s;
1024 char *end;
1025 for (end = &s[strlen(s) - 1]; end >= s && (*end == ' ' || *end == '\n'); end--)
1026 *end = '\0';
1027 return s;
1031 // Hanlder for SIGWINCH events
1033 static void update_term_size(int sig) {
1034 (void)sig;
1035 ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize);
1039 // Wait for a process to either suspend or exit and return the status.
1041 static int wait_for_process(proc_t **proc) {
1042 tcsetpgrp(fileno(tty_out), (*proc)->pid);
1043 int status;
1044 while (*proc) {
1045 if (waitpid((*proc)->pid, &status, WUNTRACED) < 0) continue;
1046 if (WIFEXITED(status) || WIFSIGNALED(status)) {
1047 LL_REMOVE((*proc), running);
1048 delete (proc);
1049 } else if (WIFSTOPPED(status)) break;
1051 nonnegative(tcsetpgrp(fileno(tty_out), getpid()));
1052 signal(SIGTTOU, SIG_DFL);
1053 return status;
1056 int main(int argc, char *argv[]) {
1057 char sep = '\n';
1058 int print_dir = 0, print_selection = 0;
1059 for (int i = 1; i < argc; i++) {
1060 // Commands are processed below, after flags have been parsed
1061 if (argv[i][0] == '+') {
1062 char *colon = strchr(argv[i], ':');
1063 if (colon && !colon[1]) break;
1064 } else if (streq(argv[i], "--")) {
1065 break;
1066 } else if (streq(argv[i], "--help")) {
1067 help:
1068 printf("%s%s", description_str, usage_str);
1069 return 0;
1070 } else if (streq(argv[i], "--version")) {
1071 version:
1072 printf(BB_NAME " " BB_VERSION "\n");
1073 return 0;
1074 } else if (argv[i][0] == '-' && argv[i][1] != '-') {
1075 for (char *c = &argv[i][1]; *c; c++) {
1076 switch (*c) {
1077 case 'h': goto help;
1078 case 'v': goto version;
1079 case 'd': print_dir = 1; break;
1080 case '0': sep = '\0'; break;
1081 case 's': print_selection = 1; break;
1082 default: printf("Unknown command line argument: -%c\n%s", *c, usage_str); return 1;
1085 } else if (i < argc - 1) {
1086 printf("Unknown command line argument: \"%s\"\n%s", argv[i], usage_str);
1087 return 1;
1091 struct sigaction sa_winch = {.sa_handler = &update_term_size};
1092 sigaction(SIGWINCH, &sa_winch, NULL);
1093 update_term_size(0);
1094 // Wait 100us at a time for terminal to initialize if necessary
1095 while (winsize.ws_row == 0)
1096 usleep(100);
1098 // Set up environment variables
1099 // Default values
1100 setenv("TMPDIR", "/tmp", 0);
1101 sprintf(cmdfilename, "%s/" BB_NAME ".cmd.XXXXXX", getenv("TMPDIR"));
1102 int cmdfd =
1103 nonnegative(mkostemp(cmdfilename, O_APPEND), "Couldn't create " BB_NAME " command file: '%s'", cmdfilename);
1104 close(cmdfd);
1105 setenv("BBCMD", cmdfilename, 1);
1106 char xdg_config_home[PATH_MAX], xdg_data_home[PATH_MAX];
1107 sprintf(xdg_config_home, "%s/.config", getenv("HOME"));
1108 setenv("XDG_CONFIG_HOME", xdg_config_home, 0);
1109 sprintf(xdg_data_home, "%s/.local/share", getenv("HOME"));
1110 setenv("XDG_DATA_HOME", xdg_data_home, 0);
1111 setenv("sysconfdir", "/etc", 0);
1113 char *newpath;
1114 static char bbpath[PATH_MAX];
1115 // Hacky fix to allow `bb` to be run out of its build directory:
1116 if (strncmp(argv[0], "./", 2) == 0) {
1117 nonnull(realpath(argv[0], bbpath), "Could not resolve path: %s", bbpath);
1118 char *slash = strrchr(bbpath, '/');
1119 if (!slash) errx(EXIT_FAILURE, "No slash found in real path: %s", bbpath);
1120 *slash = '\0';
1121 setenv("BBPATH", bbpath, 1);
1123 if (getenv("BBPATH")) {
1124 nonnegative(asprintf(&newpath, "%s/" BB_NAME ":%s/scripts:%s", getenv("XDG_CONFIG_HOME"), getenv("BBPATH"),
1125 getenv("PATH")),
1126 "Could not allocate memory for PATH");
1127 } else {
1128 nonnegative(asprintf(&newpath, "%s/" BB_NAME ":%s/" BB_NAME ":%s", getenv("XDG_CONFIG_HOME"),
1129 getenv("sysconfdir"), getenv("PATH")),
1130 "Could not allocate memory for PATH");
1132 setenv("PATH", newpath, 1);
1134 setenv("SHELL", "bash", 0);
1135 setenv("EDITOR", "nano", 0);
1136 char *curdepth = getenv("BBDEPTH");
1137 int depth = curdepth ? atoi(curdepth) : 0;
1138 char depthstr[16];
1139 sprintf(depthstr, "%d", depth + 1);
1140 setenv("BBDEPTH", depthstr, 1);
1142 atexit(cleanup);
1144 FOREACH(outbuf_t *, ob, output_buffers) {
1145 sprintf(ob->filename, "%s/" BB_NAME ".%s.XXXXXX", getenv("TMPDIR"), ob->name);
1146 ob->tmp_fd = nonnegative(mkostemp(ob->filename, O_RDWR), "Couldn't create error file");
1147 ob->dup_fd = nonnegative(dup(ob->orig_fd));
1148 nonnegative(dup2(ob->tmp_fd, ob->orig_fd), "Couldn't redirect error output");
1151 tty_in = nonnull(fopen("/dev/tty", "r"));
1152 tty_out = nonnull(fopen("/dev/tty", "w"));
1153 nonnegative(tcgetattr(fileno(tty_out), &orig_termios));
1154 memcpy(&bb_termios, &orig_termios, sizeof(bb_termios));
1155 cfmakeraw(&bb_termios);
1156 bb_termios.c_cc[VMIN] = 0;
1157 bb_termios.c_cc[VTIME] = 1;
1159 int signals[] = {SIGTERM, SIGINT, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGSEGV, SIGTSTP};
1160 struct sigaction sa = {.sa_handler = &cleanup_and_raise, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
1161 for (size_t i = 0; i < LEN(signals); i++)
1162 sigaction(signals[i], &sa, NULL);
1164 bb_t bb = {
1165 .columns = "*smpn",
1166 .sort = "+n",
1167 .history = NULL,
1169 current_bb = &bb;
1170 set_globs(&bb, "*");
1171 init_term();
1172 bb_browse(&bb, argc, argv);
1173 cleanup(); // Optional, but this allows us to write directly to stdout instead of the buffer
1175 if (bb.selected && print_selection) {
1176 for (entry_t *e = bb.selected; e; e = e->selected.next) {
1177 write(STDOUT_FILENO, e->fullname, strlen(e->fullname));
1178 write(STDOUT_FILENO, &sep, 1);
1182 if (print_dir) printf("%s\n", bb.path);
1184 // Cleanup:
1185 populate_files(&bb, NULL);
1186 while (bb.selected)
1187 set_selected(&bb, bb.selected, 0);
1188 delete (&bb.globpats);
1189 for (bb_history_t *next; bb.history; bb.history = next) {
1190 next = bb.history->next;
1191 delete (&bb.history);
1193 return 0;
1196 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0