1 /* ask - a simple command line asker
2 * Copyright 2019 Bruce Hill
3 * Released under the MIT License (see LICENSE for details)
4 * Usage: ask [-Q|--quickpick] [-y|--yes] [-n|--no] [-P|--password] [[-H |--history=]name]
5 * [-v|--version] [-h|--help] [-0|--read0] [-q |--query=initial] [[-p |--prompt=]prompt [options...]]
6 * --password: password mode
7 * --quickpick: quickpick mode (exit when only one option remains)
8 * --version: print version and exit
9 * --help: print usage and exit
10 * --read0: read input delimited by NULL bytes instead of newlines
11 * --query: initial value to pre-populate user input
12 * --prompt: use the given prompt (displayed in bold)
13 * --yes: append " [Y/n]" to the prompt, use quickpick mode,
14 * --no: append " [y/N]" to the prompt, use quickpick mode,
15 * --history: store the selected value in a history file, which can be browsed with
25 #include <sys/types.h>
32 #define ASK_VERSION "0.4"
33 #define LOWERCASE(c) ('A' <= (c) && (c) <= 'Z' ? ((c) + 'a' - 'A') : (c))
34 #define EQ(a, b) (case_sensitive ? (a) == (b) : LOWERCASE(a) == LOWERCASE(b))
35 #define PASSWORD "-\\|/"
36 #define MAX_HISTORY 1000
38 static int password = 0, quickpick = 0, case_sensitive = 0, histindex = 0, nhistory = 0;
39 static char histpath[PATH_MAX] = {0};
41 static inline void *memcheck(void *p)
44 fprintf(stderr, "Memory allocation failure\n");
50 /* Return length of the longest substring of patt[p:] that occurs within
51 * str[s:], while still leaving enough space for the rest of `patt` to occur
52 * within the rest of `str`. If there is no valid solution, return -1.
54 static int lcs(const char *str, const char *patt, int slen, int plen, int s, int p, int *cache)
56 if (!patt[p]) return 0;
57 if (!str[s]) return -1;
59 if (str[s] == ' ') return lcs(str, patt, slen, plen, s+1, p+1, cache);
60 else return lcs(str, patt, slen, plen, s, p+1, cache);
62 if (cache[s*plen + p]) return cache[s*plen + p];
63 if (!EQ(str[s], patt[p])) return lcs(str, patt, slen, plen, s+1, p, cache);
66 while (str[s+run] && patt[p+run] && EQ(str[s+run], patt[p+run])
67 && lcs(str, patt, slen, plen, s+run, p+run, cache) >= 0) {
70 if (run == 0) run = -1;
71 cache[s*plen + p] = run;
75 static int matches(const char *str, const char *patt)
82 while (!EQ(*str, *patt)) {
92 static int fputc_escaped(char c, FILE *out)
94 static const char *escapes = " abtnvfr e";
95 if (c > 0 && c <= '\x1b' && escapes[(int)c] != ' ') { // "\n", etc.
96 fprintf(out, "\033[35m\\%c\033[37m", escapes[(int)c]);
98 } else if (c >= 0 && !(' ' <= c && c <= '~')) { // "\x02", etc.
99 fprintf(out, "\033[35m\\x%02X\033[37m", c);
107 static int draw_line(FILE *out, const char *option, const char *patt, int cursor)
109 static const char *dim = "\033[22;2m", *bold = "\033[22;1m", *normal = "\033[22m";
111 const char *state = normal;
112 const char *matchstyle = option ? bold : normal;
113 if (!option) option = patt;
114 size_t linelen = strlen(option), patlen = strlen(patt);
116 int *cache = calloc((linelen+1)*(patlen+1), sizeof(int));
117 int backtrack = 0, to_start = 0;
118 for (int i = 0; i < (int)linelen; i++) {
119 if (EQ(patt[p], option[i]) &&
120 run + lcs(option,patt,(int)linelen,(int)patlen,i,p,cache)
121 >= lcs(option,patt,(int)linelen,(int)patlen,i+1,p,cache)) {
122 if (state != matchstyle) {
123 fputs(matchstyle, out);
126 int len = fputc_escaped(option[i], out);
127 if (p >= cursor) backtrack += len;
128 else to_start += len;
131 } else if (patt[p] == ' ') {
141 int len = fputc_escaped(option[i], out);
142 if (p >= cursor) backtrack += len;
143 else to_start += len;
146 if (backtrack) fprintf(out, "\033[0m\033[%dD", backtrack);
151 static void get_history(char **buf, size_t *cap, int index)
153 if (nhistory == 0) return;
154 if (index == nhistory) {
158 FILE *f = fopen(histpath, "r");
160 for (int i = 0; i < histindex; i++) {
161 if (fgets(histline, sizeof(histline), f) == NULL)
163 if (histline[strlen(histline)-1] != '\n')
167 *buf = memcheck(realloc(*buf, (*cap += 100)));
168 if (fgets(*buf, *cap, f) == NULL)
170 } while ((*buf)[strlen(*buf)-1] != '\n');
171 (*buf)[strlen(*buf)-1] = '\0';
175 * A basic fuzzy matcher and line inputter
177 static char *get_input(FILE *in, FILE *out, const char *prompt, const char *initial, int nopts, char **opts)
179 fprintf(out, "\033[K\033[0;1m%s\033[0m", prompt);
180 size_t cap = initial ? strlen(initial) + 100 : 100;
181 char *buf = memcheck(calloc(cap, 1));
185 strcpy(buf, initial);
186 len = (int)strlen(initial);
190 int start = 0, backtrack = 0;
193 for (const char *p = buf; *p; ++p)
194 case_sensitive |= ('A' <= *p && *p <= 'Z');
199 for (int i = 0; i < nopts; i++) {
200 int j = (start + i) % nopts;
201 if (matches(opts[j], buf)) {
211 if (quickpick && ncandidates == 1 && picked)
214 if (backtrack) fprintf(out, "\033[%dD", backtrack);
215 fputs("\033[K", out);
217 if (picked) fputs("\033[0;32m", out);
218 else if (nopts > 0) fputs("\033[0;31m", out);
219 else fputs("\033[0;2m", out);
220 fputc((PASSWORD)[strlen(buf) % strlen(PASSWORD)], out);
221 fputs("\033[0m", out);
224 backtrack = draw_line(out, picked, buf, b);
229 key = bgetkey(in, NULL, NULL, -1);
231 case -1: case -2: case -3: goto skip_redraw;
234 case KEY_CTRL_C: case KEY_ESC:
240 case KEY_CTRL_A: case KEY_HOME:
243 case KEY_CTRL_E: case KEY_END:
249 memmove(buf, buf+b, (size_t)(len-b));
261 for (int i = 1; i < nopts; i++) {
262 int j = (start + i) % nopts;
263 if (matches(opts[j], buf)) {
272 for (int i = nopts-1; i > 0; i--) {
273 int j = (start + i) % nopts;
274 if (matches(opts[j], buf)) {
281 case KEY_BACKSPACE: case KEY_BACKSPACE2:
284 memmove(buf+b, buf+b+1, (size_t)(len-b));
289 if (len == 0) goto cleanup;
293 memmove(buf+b, buf+b+1, (size_t)(len-b));
297 case KEY_ARROW_LEFT: case KEY_CTRL_B:
300 case KEY_ARROW_RIGHT: case KEY_CTRL_F:
306 if (histindex < 0) histindex = 0;
307 get_history(&buf, &cap, histindex);
315 if (histindex > nhistory) histindex = nhistory;
316 get_history(&buf, &cap, histindex);
321 case KEY_CTRL_Q: case KEY_CTRL_V: {
323 while ((nextkey = bgetkey(in, NULL, NULL, -1)) < 0)
325 if (len + 1 >= (int)cap)
326 buf = memcheck(realloc(buf, (cap += 100)));
328 memmove(buf+b+1, buf+b, (size_t)(len-b+1));
329 buf[b++] = (char)nextkey;
334 if (len < 2 || b == 0) break;
343 if ((' ' <= key && key <= '~') || key == '\t') {
344 if (len + 1 >= (int)cap)
345 buf = memcheck(realloc(buf, (cap += 100)));
347 memmove(buf+b+1, buf+b, (size_t)(len-b+1));
348 buf[b++] = (char)key;
355 if (picked) picked = memcheck(strdup(picked));
357 if (backtrack || prompt[0])
358 fprintf(out, "\033[%dD", backtrack + (int)strlen(prompt));
359 fputs("\033[0m\033[K", out);
360 if (picked != buf && buf) free(buf);
364 static int cmp_len(const void *v1, const void *v2)
366 char *s1 = *(char**)v1, *s2 = *(char**)v2;
367 size_t len1 = strlen(s1), len2 = strlen(s2);
368 if (len1 != len2) return (int)(len1 - len2);
369 return strcmp(s1, s2);
372 int main(int argc, char *argv[])
375 char *prompt = NULL, *query = NULL, *histname = NULL;
378 size_t linescap = 0, linecap = 0;
380 for (a = 1; a < argc; a++) {
381 if (strcmp(argv[a], "-H") == 0) {
382 histname = argv[++a];
384 } else if (argv[a][0] == '-' && argv[a][1] != '-') {
385 for (char *p = &argv[a][1]; *p; p++) {
387 case 'P': password = 1; break;
388 case 'Q': quickpick = 1; break;
389 case 'h': goto help_flag;
390 case 'v': goto version_flag;
391 case '0': delim = '\0'; break;
392 case 'y': yes = 1; quickpick = 1; break;
393 case 'n': no = 1; quickpick = 1; break;
395 if (a + 1 >= argc) goto help_flag;
399 if (a + 1 >= argc) goto help_flag;
402 default: goto help_flag;
405 } else if (strncmp(argv[a], "--read0", strlen("--read0")) == 0) {
407 } else if (strncmp(argv[a], "--prompt=", strlen("--prompt=")) == 0) {
408 prompt = &argv[a][strlen("--prompt=")];
409 } else if (strncmp(argv[a], "--query=", strlen("--query=")) == 0) {
410 query = &argv[a][strlen("--query=")];
411 } else if (strcmp(argv[a], "--password") == 0) {
413 } else if (strncmp(argv[a], "--history=", strlen("--history=")) == 0) {
414 histname = &argv[a][strlen("--history=")];
415 } else if (strcmp(argv[a], "--quickpick") == 0) {
417 } else if (strcmp(argv[a], "--yes") == 0) {
420 } else if (strcmp(argv[a], "--no") == 0) {
423 } else if (strcmp(argv[a], "--help") == 0) {
425 printf("ask - A simple command line input tool.\n"
426 "Usage: ask [-Q|--quickpick] [-P|--password] [-v|--version] [-h|--help] "
427 "[-y|--yes] [-n|--no] [-q |--query=query] [[-p |--prompt=]prompt [options...]]\n");
429 } else if (strcmp(argv[a], "--version") == 0) {
431 printf("ask %s\n", ASK_VERSION);
435 if (!prompt && a < argc) prompt = argv[a++];
439 struct pollfd pfd = {STDIN_FILENO, POLLIN, 0};
440 if (poll(&pfd, 1, 50) > 0) {
441 while ((getdelim(&line, &linecap, delim, stdin)) >= 0) {
442 if ((size_t)nopts >= linescap)
443 opts = memcheck(realloc(opts, (linescap += 100)*sizeof(char*)));
444 if (!line[0]) continue;
445 if (line[strlen(line)-1] == '\n')
446 line[strlen(line)-1] = '\0';
447 opts[nopts++] = memcheck(strdup(line));
452 if ((size_t)nopts >= linescap)
453 opts = memcheck(realloc(opts, (linescap += 100)*sizeof(char*)));
454 opts[nopts++] = argv[a++];
458 if ((size_t)nopts + 4 >= linescap)
459 opts = memcheck(realloc(opts, (linescap += 4)*sizeof(char*)));
466 char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char)));
467 sprintf(p2, "%s [Y/n]", prompt);
474 char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char)));
475 sprintf(p2, "%s [y/N]", prompt);
482 if (!prompt) prompt = "> ";
484 FILE *tty_in = fopen("/dev/tty", "r");
485 FILE *tty_out = fopen("/dev/tty", "w");
486 struct termios orig_termios, bb_termios;
487 tcgetattr(fileno(tty_out), &orig_termios);
488 cfmakeraw(&bb_termios);
489 if (tcsetattr(fileno(tty_out), TCSAFLUSH, &bb_termios) == -1)
493 char *xdg_data = getenv("XDG_DATA_HOME");
494 if (xdg_data == NULL) {
495 strcpy(histpath, getenv("HOME"));
496 strcat(histpath, "/.local/share/ask/");
498 strcpy(histpath, xdg_data);
499 strcat(histpath, "/ask/");
501 strcat(histpath, histname);
503 for (char* p = strchr(histpath + 1, '/'); p; p = strchr(p + 1, '/')) {
505 if (mkdir(histpath, 0777) == -1) {
506 if (errno != EEXIST) {
508 printf("Error: could not create history directory at %s", histpath);
516 FILE *f = fopen(histpath, "r");
518 while (fgets(buf, sizeof(buf), f) != NULL) {
519 size_t len = strlen(buf);
520 if (len > 1 && buf[len-1] == '\n')
523 histindex = nhistory;
527 // Prefer shorter matches, but otherwise use alphabetic order
528 qsort(opts, (size_t)nopts, sizeof(char*), cmp_len);
529 char *output = get_input(tty_in, tty_out, prompt, query, nopts, opts);
532 tcsetattr(fileno(tty_out), TCSAFLUSH, &orig_termios);
538 // This doesn't free memory, but it doesn't need to because
539 // the program is exiting
540 if (!output) return 1;
543 return strcmp(output, "N") == 0;
545 return strcmp(output, "Y") != 0;
547 fputs(output, stdout);
549 if (histpath[0] && strlen(output) > 0) {
550 FILE *f = fopen(histpath, "a");
551 fprintf(f, "%s\n", output);
553 if (++nhistory > MAX_HISTORY) {
555 char *args[] = {"sed", "-i", "1d", histpath, NULL};
562 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1