/* ask - a simple command line asker * Copyright 2019 Bruce Hill * Released under the MIT License (see LICENSE for details) * Usage: ask [-q|--quickpick] [-p|--password] [-v|--version] [-h|--help] [--initial=initial] [prompt [options...]] * --password: password mode * --quickpick: quickpick mode (exit when only one option remains) * --version: print version and exit * --help: print usage and exit * --initial: initial value to pre-populate user input */ #include #include #include #include #include #include #include #include #include "bterm.h" #define LOWERCASE(c) ('A' <= (c) && (c) <= 'Z' ? ((c) + 'a' - 'A') : (c)) #define EQ(a, b) (case_sensitive ? (a) == (b) : LOWERCASE(a) == LOWERCASE(b)) #define PASSWORD "-\\|/" static int password = 0, quickpick = 0, case_sensitive = 0; static inline void *memcheck(void *p) { if (!p) { fprintf(stderr, "Memory allocation failure\n"); exit(1); } return p; } /* Return length of the longest substring of patt[p:] that occurs within * str[s:], while still leaving enough space for the rest of `patt` to occur * within the rest of `str`. If there is no valid solution, return -1. */ static int lcs(const char *str, const char *patt, int slen, int plen, int s, int p, int *cache) { if (!patt[p]) return 0; if (!str[s]) return -1; if (cache[s*plen + p]) return cache[s*plen + p]; if (!EQ(str[s], patt[p])) return lcs(str, patt, slen, plen, s+1, p, cache); // Run starting here int run = 1; while (str[s+run] && patt[p+run] && EQ(str[s+run], patt[p+run]) && lcs(str, patt, slen, plen, s+run, p+run, cache) >= 0) { ++run; } if (run == 0) run = -1; cache[s*plen + p] = run; return run; } static int matches(const char *str, const char *patt) { while (*patt) { while (!EQ(*str, *patt)) { if (!*str) return 0; ++str; } ++str; ++patt; } return 1; } static void draw_line(FILE *out, const char *line, const char *patt, int cursor) { size_t linelen = strlen(line), patlen = strlen(patt); int dim = 0; int p = 0; int run = 0; int *cache = calloc((linelen+1)*(patlen+1), sizeof(int)); if (cursor >= (int)patlen) fputs("\0337", out); for (int i = 0; i < (int)linelen; i++) { if (EQ(patt[p], line[i]) && run + lcs(line,patt,(int)linelen,(int)patlen,i,p,cache) >= lcs(line,patt,(int)linelen,(int)patlen,i+1,p,cache)) { if (dim) { fputs("\033[22m", out); dim = 0; } fputc(patt[p], out); ++run; ++p; if (cursor == p) fputs("\0337", out); } else { run = 0; if (!dim) { fputs("\033[2m", out); dim = 1; } fputc(line[i], out); } } fputs("\0338", out); } /* * A basic fuzzy matcher and line inputter */ static char *get_input(FILE *in, FILE *out, const char *prompt, const char *initial, int nopts, char **opts) { size_t cap = initial ? strlen(initial) + 100 : 100; char *buf = memcheck(calloc(cap, 1)); char *picked = NULL; int b = 0, len = 0; if (initial) { if (!strcpy(buf, initial)) return NULL; len = (int)strlen(initial); b = len; fputs(initial, out); } int start = 0; // Show cursor and set to blinking line: while (1) { fputs("\033[G\033[K\033[0m", out); if (prompt) { fputs("\033[1G\033[K\033[37;1m", out); fputs(prompt, out); fputs("\033[0m", out); } case_sensitive = 0; for (const char *p = buf; *p; ++p) case_sensitive |= ('A' <= *p && *p <= 'Z'); int ncandidates = 0; picked = NULL; for (int i = 0; i < nopts; i++) { int j = (start + i) % nopts; if (matches(opts[j], buf)) { ++ncandidates; if (!picked) { picked = opts[j]; start = j; } } } if (quickpick && ncandidates == 1 && picked) goto finished; if (password) { if (picked) fputs("\033[0;32m", out); else if (nopts > 0) fputs("\033[0;31m", out); else fputs("\033[0;2m", out); fputc((PASSWORD)[strlen(buf) % strlen(PASSWORD)], out); fputs("\033[0m", out); } else { if (picked) { draw_line(out, picked, buf, b); } else { if (nopts > 0) fprintf(out, "\033[0;31m%s\033[0m", buf); else fprintf(out, "\033[0m%s\033[0m", buf); if (b < (int)strlen(buf)) fprintf(out, "\033[%dD", (int)strlen(buf) - b); } } fflush(out); int key; skip_redraw: key = bgetkey(in, NULL, NULL, -1); switch (key) { case -1: case -2: case -3: goto skip_redraw; case '\r': // TODO: support backslash-enter goto finished; case KEY_CTRL_C: case KEY_ESC: free(buf); buf = NULL; picked = NULL; goto finished; case KEY_CTRL_A: if (b > 0) b = 0; break; case KEY_CTRL_E: if (b < len) b = len; break; case KEY_CTRL_U: { int to_clear = b; if (to_clear) { memmove(buf, buf+b, (size_t)(len-b)); buf[len -= b] = 0; b = 0; } break; } case KEY_CTRL_K: if (b < len) buf[len = b] = 0; break; case KEY_CTRL_N: if (picked) { for (int i = 1; i < nopts; i++) { int j = (start + i) % nopts; if (matches(opts[j], buf)) { start = j; break; } } } break; case KEY_CTRL_P: if (picked) { for (int i = nopts-1; i > 0; i--) { int j = (start + i) % nopts; if (matches(opts[j], buf)) { start = j; break; } } } break; case KEY_BACKSPACE: case KEY_BACKSPACE2: if (b > 0) { --b; memmove(buf+b, buf+b+1, (size_t)(len-b)); buf[--len] = 0; } break; case KEY_DELETE: case KEY_CTRL_D: if (b < len) { memmove(buf+b, buf+b+1, (size_t)(len-b)); buf[--len] = 0; } break; case KEY_ARROW_LEFT: case KEY_CTRL_B: if (b > 0) --b; break; case KEY_ARROW_RIGHT: case KEY_CTRL_F: if (b < len) ++b; break; default: if (' ' <= key && key <= '~') { if (len + 1 >= (int)cap) buf = memcheck(realloc(buf, (cap += 100))); if (b < len) memmove(buf+b+1, buf+b, (size_t)(len-b+1)); buf[b++] = (char)key; buf[++len] = 0; } break; } } finished: if (picked) picked = memcheck(strdup(picked)); else picked = buf; return picked; } int main(int argc, char *argv[]) { FILE *tty_in = fopen("/dev/tty", "r"); FILE *tty_out = fopen("/dev/tty", "w"); struct termios orig_termios, bb_termios; tcgetattr(fileno(tty_out), &orig_termios); cfmakeraw(&bb_termios); if (tcsetattr(fileno(tty_out), TCSAFLUSH, &bb_termios) == -1) return 1; char *prompt = NULL, *initial = NULL; char **lines = NULL; size_t linescap = 0, linecap = 0; int nlines = 0; char *line = NULL; struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; if (poll(&pfd, 1, 50) > 0) { while ((getline(&line, &linecap, stdin)) >= 0) { if ((size_t)nlines >= linescap) lines = memcheck(realloc(lines, (linescap += 100)*sizeof(char*))); if (!line[0]) continue; if (line[strlen(line)-1] == '\n') line[strlen(line)-1] = '\0'; lines[nlines++] = memcheck(strdup(line)); } } int a = 1; while (a < argc) { if (argv[a][0] == '-' && argv[a][1] != '-') { for (char *p = &argv[a][1]; *p; p++) { switch (*p) { case 'p': password = 1; break; case 'q': quickpick = 1; break; case 'h': goto help_flag; case 'v': goto version_flag; } } } else if (strncmp(argv[a], "--initial=", strlen("--initial=")) == 0) { initial = &argv[a][strlen("--initial=")]; } else if (strcmp(argv[a], "--password") == 0) { password = 1; } else if (strcmp(argv[a], "--quickpick") == 0) { quickpick = 1; } else if (strcmp(argv[a], "--help") == 0) { help_flag: printf("ask - A simple command line input tool.\n" "Usage: ask [-q|--quickpick] [-p|--password] [-v|--version] [-h|--help] [--initial=initial] [prompt [options...]]\n"); return 0; } else if (strcmp(argv[a], "--version") == 0) { version_flag: printf("ask v0.1\n"); return 0; } else break; ++a; } if (!prompt && a < argc) prompt = argv[a++]; if (!prompt) prompt = "> "; while (a < argc) { if ((size_t)nlines >= linescap) lines = memcheck(realloc(lines, (linescap += 100)*sizeof(char*))); lines[nlines++] = argv[a++]; } char *output = get_input(tty_in, tty_out, prompt, initial, nlines, lines); fputs("\033[G\033[K\033[0m", tty_out); fflush(tty_out); tcsetattr(fileno(tty_out), TCSAFLUSH, &orig_termios); fclose(tty_out); tty_out = NULL; fclose(tty_in); tty_in = NULL; if (!output) return 1; puts(output); free(output); // This doesn't free the memory in lines, but it doesn't need to because // the program is exiting return 0; } // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1