/* ask - a simple command line asker * Copyright 2019 Bruce Hill * Released under the MIT License (see LICENSE for details) * Usage: ask [-Q|--quickpick] [-y|--yes] [-n|--no] [-P|--password] [[-H |--history=]name] * [-v|--version] [-h|--help] [-0|--read0] [-q |--query=initial] [[-p |--prompt=]prompt [options...]] * --password: password mode * --quickpick: quickpick mode (exit when only one option remains) * --version: print version and exit * --help: print usage and exit * --read0: read input delimited by NULL bytes instead of newlines * --query: initial value to pre-populate user input * --prompt: use the given prompt (displayed in bold) * --yes: append " [Y/n]" to the prompt, use quickpick mode, * --no: append " [y/N]" to the prompt, use quickpick mode, * --history: store the selected value in a history file, which can be browsed with * up/down arrow keys */ #include #include #include #include #include #include #include #include #include #include #include #include "bterm.h" #define ASK_VERSION "0.4" #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 "-\\|/" #define MAX_HISTORY 1000 static int password = 0, quickpick = 0, case_sensitive = 0, histindex = 0, nhistory = 0; static char histpath[PATH_MAX] = {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 (patt[p] == ' ') { if (str[s] == ' ') return lcs(str, patt, slen, plen, s+1, p+1, cache); else return lcs(str, patt, slen, plen, s, p+1, cache); } 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) { if (*patt == ' ') { ++patt; continue; } while (!EQ(*str, *patt)) { if (!*str) return 0; ++str; } ++str; ++patt; } return 1; } static int fputc_escaped(char c, FILE *out) { static const char *escapes = " abtnvfr e"; if (c > 0 && c <= '\x1b' && escapes[(int)c] != ' ') { // "\n", etc. fprintf(out, "\033[35m\\%c\033[37m", escapes[(int)c]); return 2; } else if (c >= 0 && !(' ' <= c && c <= '~')) { // "\x02", etc. fprintf(out, "\033[35m\\x%02X\033[37m", c); return 4; } else { fputc(c, out); return 1; } } static int draw_line(FILE *out, const char *option, const char *patt, int cursor) { static const char *dim = "\033[22;2m", *bold = "\033[22;1m", *normal = "\033[22m"; fputs(normal, out); const char *state = normal; const char *matchstyle = option ? bold : normal; if (!option) option = patt; size_t linelen = strlen(option), patlen = strlen(patt); int p = 0, run = 0; int *cache = calloc((linelen+1)*(patlen+1), sizeof(int)); int backtrack = 0, to_start = 0; for (int i = 0; i < (int)linelen; i++) { if (EQ(patt[p], option[i]) && run + lcs(option,patt,(int)linelen,(int)patlen,i,p,cache) >= lcs(option,patt,(int)linelen,(int)patlen,i+1,p,cache)) { if (state != matchstyle) { fputs(matchstyle, out); state = matchstyle; } int len = fputc_escaped(option[i], out); if (p >= cursor) backtrack += len; else to_start += len; ++run; ++p; } else if (patt[p] == ' ') { run = 0; ++p; --i; } else { run = 0; if (state != dim) { fputs(dim, out); state = dim; } int len = fputc_escaped(option[i], out); if (p >= cursor) backtrack += len; else to_start += len; } } if (backtrack) fprintf(out, "\033[0m\033[%dD", backtrack); free(cache); return to_start; } static void get_history(char **buf, size_t *cap, int index) { if (nhistory == 0) return; if (index == nhistory) { (*buf)[0] = '\0'; return; } FILE *f = fopen(histpath, "r"); char histline[256]; for (int i = 0; i < histindex; i++) { if (fgets(histline, sizeof(histline), f) == NULL) return; if (histline[strlen(histline)-1] != '\n') --i; } do { *buf = memcheck(realloc(*buf, (*cap += 100))); if (fgets(*buf, *cap, f) == NULL) return; } while ((*buf)[strlen(*buf)-1] != '\n'); (*buf)[strlen(*buf)-1] = '\0'; } /* * 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) { fprintf(out, "\033[K\033[0;1m%s\033[0m", prompt); size_t cap = initial ? strlen(initial) + 100 : 100; char *buf = memcheck(calloc(cap, 1)); char *picked = NULL; int b = 0, len = 0; if (initial) { strcpy(buf, initial); len = (int)strlen(initial); b = len; } int start = 0, backtrack = 0; for (;;) { case_sensitive = 0; for (const char *p = buf; *p; ++p) case_sensitive |= ('A' <= *p && *p <= 'Z'); int ncandidates = 0; picked = NULL; if (buf[0]) { 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 (backtrack) fprintf(out, "\033[%dD", backtrack); fputs("\033[K", out); 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); backtrack = 1; } else { backtrack = draw_line(out, picked, 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': goto finished; case KEY_CTRL_C: case KEY_ESC: cleanup: free(buf); buf = NULL; picked = NULL; goto finished; case KEY_CTRL_A: case KEY_HOME: b = 0; break; case KEY_CTRL_E: case KEY_END: 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_CTRL_D: if (len == 0) goto cleanup; // fallthrough case KEY_DELETE: 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; case KEY_ARROW_UP: if (nhistory > 0) { --histindex; if (histindex < 0) histindex = 0; get_history(&buf, &cap, histindex); len = strlen(buf); b = len; } break; case KEY_ARROW_DOWN: if (nhistory > 0) { ++histindex; if (histindex > nhistory) histindex = nhistory; get_history(&buf, &cap, histindex); len = strlen(buf); b = len; } break; case KEY_CTRL_Q: case KEY_CTRL_V: { int nextkey; while ((nextkey = bgetkey(in, NULL, NULL, -1)) < 0) ; 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)nextkey; buf[++len] = 0; break; } case KEY_CTRL_T: { if (len < 2 || b == 0) break; if (b < len) b++; char tmp = buf[b-1]; buf[b-1] = buf[b-2]; buf[b-2] = tmp; break; } default: if ((' ' <= key && key <= '~') || key == '\t') { 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; if (backtrack || prompt[0]) fprintf(out, "\033[%dD", backtrack + (int)strlen(prompt)); fputs("\033[0m\033[K", out); if (picked != buf && buf) free(buf); return picked; } static int cmp_len(const void *v1, const void *v2) { char *s1 = *(char**)v1, *s2 = *(char**)v2; size_t len1 = strlen(s1), len2 = strlen(s2); if (len1 != len2) return (int)(len1 - len2); return strcmp(s1, s2); } int main(int argc, char *argv[]) { int yes = 0, no = 0; char *prompt = NULL, *query = NULL, *histname = NULL; char **opts = NULL; char delim = '\n'; size_t linescap = 0, linecap = 0; int a; for (a = 1; a < argc; a++) { if (strcmp(argv[a], "-H") == 0) { histname = argv[++a]; continue; } else 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; case '0': delim = '\0'; break; case 'y': yes = 1; quickpick = 1; break; case 'n': no = 1; quickpick = 1; break; case 'p': if (a + 1 >= argc) goto help_flag; prompt = argv[++a]; break; case 'q': if (a + 1 >= argc) goto help_flag; query = argv[++a]; break; default: goto help_flag; } } } else if (strncmp(argv[a], "--read0", strlen("--read0")) == 0) { delim = '\0'; } else if (strncmp(argv[a], "--prompt=", strlen("--prompt=")) == 0) { prompt = &argv[a][strlen("--prompt=")]; } else if (strncmp(argv[a], "--query=", strlen("--query=")) == 0) { query = &argv[a][strlen("--query=")]; } else if (strcmp(argv[a], "--password") == 0) { password = 1; } else if (strncmp(argv[a], "--history=", strlen("--history=")) == 0) { histname = &argv[a][strlen("--history=")]; } else if (strcmp(argv[a], "--quickpick") == 0) { quickpick = 1; } else if (strcmp(argv[a], "--yes") == 0) { yes = 1; quickpick = 1; } else if (strcmp(argv[a], "--no") == 0) { no = 1; 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] " "[-y|--yes] [-n|--no] [-q |--query=query] [[-p |--prompt=]prompt [options...]]\n"); return 0; } else if (strcmp(argv[a], "--version") == 0) { version_flag: printf("ask %s\n", ASK_VERSION); return 0; } else break; } if (!prompt && a < argc) prompt = argv[a++]; int nopts = 0; char *line = NULL; struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; if (poll(&pfd, 1, 50) > 0) { while ((getdelim(&line, &linecap, delim, stdin)) >= 0) { if ((size_t)nopts >= linescap) opts = memcheck(realloc(opts, (linescap += 100)*sizeof(char*))); if (!line[0]) continue; if (line[strlen(line)-1] == '\n') line[strlen(line)-1] = '\0'; opts[nopts++] = memcheck(strdup(line)); } } while (a < argc) { if ((size_t)nopts >= linescap) opts = memcheck(realloc(opts, (linescap += 100)*sizeof(char*))); opts[nopts++] = argv[a++]; } if (yes || no) { if ((size_t)nopts + 4 >= linescap) opts = memcheck(realloc(opts, (linescap += 4)*sizeof(char*))); opts[nopts++] = "Y"; opts[nopts++] = "N"; } if (yes) { if (prompt) { char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char))); sprintf(p2, "%s [Y/n]", prompt); prompt = p2; } else { prompt = "[Y/n]"; } } else if (no) { if (prompt) { char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char))); sprintf(p2, "%s [y/N]", prompt); prompt = p2; } else { prompt = "[y/N]"; } } if (!prompt) prompt = "> "; 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; if (histname) { char *xdg_data = getenv("XDG_DATA_HOME"); if (xdg_data == NULL) { strcpy(histpath, getenv("HOME")); strcat(histpath, "/.local/share/ask/"); } else { strcpy(histpath, xdg_data); strcat(histpath, "/ask/"); } strcat(histpath, histname); for (char* p = strchr(histpath + 1, '/'); p; p = strchr(p + 1, '/')) { *p = '\0'; if (mkdir(histpath, 0777) == -1) { if (errno != EEXIST) { *p = '/'; printf("Error: could not create history directory at %s", histpath); return 1; } } *p = '/'; } char buf[1024]; FILE *f = fopen(histpath, "r"); if (f) { while (fgets(buf, sizeof(buf), f) != NULL) { size_t len = strlen(buf); if (len > 1 && buf[len-1] == '\n') ++nhistory; } histindex = nhistory; } } // Prefer shorter matches, but otherwise use alphabetic order qsort(opts, (size_t)nopts, sizeof(char*), cmp_len); char *output = get_input(tty_in, tty_out, prompt, query, nopts, opts); fflush(tty_out); tcsetattr(fileno(tty_out), TCSAFLUSH, &orig_termios); fclose(tty_out); tty_out = NULL; fclose(tty_in); tty_in = NULL; // This doesn't free memory, but it doesn't need to because // the program is exiting if (!output) return 1; if (yes) return strcmp(output, "N") == 0; if (no) return strcmp(output, "Y") != 0; fputs(output, stdout); if (histpath[0] && strlen(output) > 0) { FILE *f = fopen(histpath, "a"); fprintf(f, "%s\n", output); fclose(f); if (++nhistory > MAX_HISTORY) { if (fork() == 0) { char *args[] = {"sed", "-i", "1d", histpath, NULL}; execvp("sed", args); } } } return 0; } // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1