code / ask

Lines831 C761 make38 Markdown32
(562 lines)
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
16 * up/down arrow keys
17 */
18 #include <errno.h>
19 #include <limits.h>
20 #include <poll.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <sys/stat.h>
25 #include <sys/types.h>
26 #include <sys/uio.h>
27 #include <termios.h>
28 #include <unistd.h>
30 #include "bterm.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)
43 if (!p) {
44 fprintf(stderr, "Memory allocation failure\n");
45 exit(1);
47 return p;
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.
53 */
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;
58 if (patt[p] == ' ') {
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);
64 // Run starting here
65 int run = 1;
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) {
68 ++run;
70 if (run == 0) run = -1;
71 cache[s*plen + p] = run;
72 return run;
75 static int matches(const char *str, const char *patt)
77 while (*patt) {
78 if (*patt == ' ') {
79 ++patt;
80 continue;
82 while (!EQ(*str, *patt)) {
83 if (!*str) return 0;
84 ++str;
86 ++str;
87 ++patt;
89 return 1;
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]);
97 return 2;
98 } else if (c >= 0 && !(' ' <= c && c <= '~')) { // "\x02", etc.
99 fprintf(out, "\033[35m\\x%02X\033[37m", c);
100 return 4;
101 } else {
102 fputc(c, out);
103 return 1;
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";
110 fputs(normal, out);
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);
115 int p = 0, run = 0;
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);
124 state = matchstyle;
126 int len = fputc_escaped(option[i], out);
127 if (p >= cursor) backtrack += len;
128 else to_start += len;
129 ++run;
130 ++p;
131 } else if (patt[p] == ' ') {
132 run = 0;
133 ++p;
134 --i;
135 } else {
136 run = 0;
137 if (state != dim) {
138 fputs(dim, out);
139 state = dim;
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);
147 free(cache);
148 return to_start;
151 static void get_history(char **buf, size_t *cap, int index)
153 if (nhistory == 0) return;
154 if (index == nhistory) {
155 (*buf)[0] = '\0';
156 return;
158 FILE *f = fopen(histpath, "r");
159 char histline[256];
160 for (int i = 0; i < histindex; i++) {
161 if (fgets(histline, sizeof(histline), f) == NULL)
162 return;
163 if (histline[strlen(histline)-1] != '\n')
164 --i;
166 do {
167 *buf = memcheck(realloc(*buf, (*cap += 100)));
168 if (fgets(*buf, *cap, f) == NULL)
169 return;
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));
182 char *picked = NULL;
183 int b = 0, len = 0;
184 if (initial) {
185 strcpy(buf, initial);
186 len = (int)strlen(initial);
187 b = len;
190 int start = 0, backtrack = 0;
191 for (;;) {
192 case_sensitive = 0;
193 for (const char *p = buf; *p; ++p)
194 case_sensitive |= ('A' <= *p && *p <= 'Z');
196 int ncandidates = 0;
197 picked = NULL;
198 if (buf[0]) {
199 for (int i = 0; i < nopts; i++) {
200 int j = (start + i) % nopts;
201 if (matches(opts[j], buf)) {
202 ++ncandidates;
203 if (!picked) {
204 picked = opts[j];
205 start = j;
211 if (quickpick && ncandidates == 1 && picked)
212 goto finished;
214 if (backtrack) fprintf(out, "\033[%dD", backtrack);
215 fputs("\033[K", out);
216 if (password) {
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);
222 backtrack = 1;
223 } else {
224 backtrack = draw_line(out, picked, buf, b);
226 fflush(out);
227 int key;
228 skip_redraw:
229 key = bgetkey(in, NULL, NULL, -1);
230 switch (key) {
231 case -1: case -2: case -3: goto skip_redraw;
232 case '\r':
233 goto finished;
234 case KEY_CTRL_C: case KEY_ESC:
235 cleanup:
236 free(buf);
237 buf = NULL;
238 picked = NULL;
239 goto finished;
240 case KEY_CTRL_A: case KEY_HOME:
241 b = 0;
242 break;
243 case KEY_CTRL_E: case KEY_END:
244 b = len;
245 break;
246 case KEY_CTRL_U: {
247 int to_clear = b;
248 if (to_clear) {
249 memmove(buf, buf+b, (size_t)(len-b));
250 buf[len -= b] = 0;
251 b = 0;
253 break;
255 case KEY_CTRL_K:
256 if (b < len)
257 buf[len = b] = 0;
258 break;
259 case KEY_CTRL_N:
260 if (picked) {
261 for (int i = 1; i < nopts; i++) {
262 int j = (start + i) % nopts;
263 if (matches(opts[j], buf)) {
264 start = j;
265 break;
269 break;
270 case KEY_CTRL_P:
271 if (picked) {
272 for (int i = nopts-1; i > 0; i--) {
273 int j = (start + i) % nopts;
274 if (matches(opts[j], buf)) {
275 start = j;
276 break;
280 break;
281 case KEY_BACKSPACE: case KEY_BACKSPACE2:
282 if (b > 0) {
283 --b;
284 memmove(buf+b, buf+b+1, (size_t)(len-b));
285 buf[--len] = 0;
287 break;
288 case KEY_CTRL_D:
289 if (len == 0) goto cleanup;
290 // fallthrough
291 case KEY_DELETE:
292 if (b < len) {
293 memmove(buf+b, buf+b+1, (size_t)(len-b));
294 buf[--len] = 0;
296 break;
297 case KEY_ARROW_LEFT: case KEY_CTRL_B:
298 if (b > 0) --b;
299 break;
300 case KEY_ARROW_RIGHT: case KEY_CTRL_F:
301 if (b < len) ++b;
302 break;
303 case KEY_ARROW_UP:
304 if (nhistory > 0) {
305 --histindex;
306 if (histindex < 0) histindex = 0;
307 get_history(&buf, &cap, histindex);
308 len = strlen(buf);
309 b = len;
311 break;
312 case KEY_ARROW_DOWN:
313 if (nhistory > 0) {
314 ++histindex;
315 if (histindex > nhistory) histindex = nhistory;
316 get_history(&buf, &cap, histindex);
317 len = strlen(buf);
318 b = len;
320 break;
321 case KEY_CTRL_Q: case KEY_CTRL_V: {
322 int nextkey;
323 while ((nextkey = bgetkey(in, NULL, NULL, -1)) < 0)
325 if (len + 1 >= (int)cap)
326 buf = memcheck(realloc(buf, (cap += 100)));
327 if (b < len)
328 memmove(buf+b+1, buf+b, (size_t)(len-b+1));
329 buf[b++] = (char)nextkey;
330 buf[++len] = 0;
331 break;
333 case KEY_CTRL_T: {
334 if (len < 2 || b == 0) break;
335 if (b < len)
336 b++;
337 char tmp = buf[b-1];
338 buf[b-1] = buf[b-2];
339 buf[b-2] = tmp;
340 break;
342 default:
343 if ((' ' <= key && key <= '~') || key == '\t') {
344 if (len + 1 >= (int)cap)
345 buf = memcheck(realloc(buf, (cap += 100)));
346 if (b < len)
347 memmove(buf+b+1, buf+b, (size_t)(len-b+1));
348 buf[b++] = (char)key;
349 buf[++len] = 0;
351 break;
354 finished:
355 if (picked) picked = memcheck(strdup(picked));
356 else picked = buf;
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);
361 return picked;
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[])
374 int yes = 0, no = 0;
375 char *prompt = NULL, *query = NULL, *histname = NULL;
376 char **opts = NULL;
377 char delim = '\n';
378 size_t linescap = 0, linecap = 0;
379 int a;
380 for (a = 1; a < argc; a++) {
381 if (strcmp(argv[a], "-H") == 0) {
382 histname = argv[++a];
383 continue;
384 } else if (argv[a][0] == '-' && argv[a][1] != '-') {
385 for (char *p = &argv[a][1]; *p; p++) {
386 switch (*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;
394 case 'p':
395 if (a + 1 >= argc) goto help_flag;
396 prompt = argv[++a];
397 break;
398 case 'q':
399 if (a + 1 >= argc) goto help_flag;
400 query = argv[++a];
401 break;
402 default: goto help_flag;
405 } else if (strncmp(argv[a], "--read0", strlen("--read0")) == 0) {
406 delim = '\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) {
412 password = 1;
413 } else if (strncmp(argv[a], "--history=", strlen("--history=")) == 0) {
414 histname = &argv[a][strlen("--history=")];
415 } else if (strcmp(argv[a], "--quickpick") == 0) {
416 quickpick = 1;
417 } else if (strcmp(argv[a], "--yes") == 0) {
418 yes = 1;
419 quickpick = 1;
420 } else if (strcmp(argv[a], "--no") == 0) {
421 no = 1;
422 quickpick = 1;
423 } else if (strcmp(argv[a], "--help") == 0) {
424 help_flag:
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");
428 return 0;
429 } else if (strcmp(argv[a], "--version") == 0) {
430 version_flag:
431 printf("ask %s\n", ASK_VERSION);
432 return 0;
433 } else break;
435 if (!prompt && a < argc) prompt = argv[a++];
437 int nopts = 0;
438 char *line = NULL;
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));
451 while (a < argc) {
452 if ((size_t)nopts >= linescap)
453 opts = memcheck(realloc(opts, (linescap += 100)*sizeof(char*)));
454 opts[nopts++] = argv[a++];
457 if (yes || no) {
458 if ((size_t)nopts + 4 >= linescap)
459 opts = memcheck(realloc(opts, (linescap += 4)*sizeof(char*)));
460 opts[nopts++] = "Y";
461 opts[nopts++] = "N";
464 if (yes) {
465 if (prompt) {
466 char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char)));
467 sprintf(p2, "%s [Y/n]", prompt);
468 prompt = p2;
469 } else {
470 prompt = "[Y/n]";
472 } else if (no) {
473 if (prompt) {
474 char *p2 = memcheck(calloc(strlen(prompt)+5+1, sizeof(char)));
475 sprintf(p2, "%s [y/N]", prompt);
476 prompt = p2;
477 } else {
478 prompt = "[y/N]";
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)
490 return 1;
492 if (histname) {
493 char *xdg_data = getenv("XDG_DATA_HOME");
494 if (xdg_data == NULL) {
495 strcpy(histpath, getenv("HOME"));
496 strcat(histpath, "/.local/share/ask/");
497 } else {
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, '/')) {
504 *p = '\0';
505 if (mkdir(histpath, 0777) == -1) {
506 if (errno != EEXIST) {
507 *p = '/';
508 printf("Error: could not create history directory at %s", histpath);
509 return 1;
512 *p = '/';
515 char buf[1024];
516 FILE *f = fopen(histpath, "r");
517 if (f) {
518 while (fgets(buf, sizeof(buf), f) != NULL) {
519 size_t len = strlen(buf);
520 if (len > 1 && buf[len-1] == '\n')
521 ++nhistory;
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);
531 fflush(tty_out);
532 tcsetattr(fileno(tty_out), TCSAFLUSH, &orig_termios);
533 fclose(tty_out);
534 tty_out = NULL;
535 fclose(tty_in);
536 tty_in = NULL;
538 // This doesn't free memory, but it doesn't need to because
539 // the program is exiting
540 if (!output) return 1;
542 if (yes)
543 return strcmp(output, "N") == 0;
544 if (no)
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);
552 fclose(f);
553 if (++nhistory > MAX_HISTORY) {
554 if (fork() == 0) {
555 char *args[] = {"sed", "-i", "1d", histpath, NULL};
556 execvp("sed", args);
560 return 0;
562 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1