diff options
Diffstat (limited to 'ask.c')
| -rw-r--r-- | ask.c | 393 |
1 files changed, 393 insertions, 0 deletions
@@ -0,0 +1,393 @@ +/* ask - a simple command line asker + * Copyright 2019 Bruce Hill + * Released under the MIT License (see LICENSE for details) + * Usage: ask [-p | --password] [-q | --quickpick] [prompt [initial value]] + * -p: password mode + * -q: quickpick mode (exit when only one option remains) + */ +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/uio.h> +#include <termios.h> +#include <unistd.h> + +#include "bterm.h" + +static int password = 0, quickpick = 0, case_sensitive = 0, to_skip = 0; + +#define LOWERCASE(c) ('A' <= (c) && (c) <= 'Z' ? ((c) + 'a' - 'A') : (c)) +#define EQ(a, b) (case_sensitive ? (a) == (b) : LOWERCASE(a) == LOWERCASE(b)) + +static inline void *memcheck(void *p) +{ + if (!p) { + fprintf(stderr, "Memory allocation failure\n"); + exit(1); + } + return p; +} + +static int score(const char *str, const char *patt) +{ + if (!patt[0]) return 0; + int score = 0; + const char *sp = str, *pp = patt; + while (*sp && !EQ(*sp, *pp)) ++sp; + while (*sp && *pp && EQ(*sp, *pp)) { + ++score; + ++sp; + ++pp; + } + for (; *pp; ++sp, ++pp) { + while (*sp && !EQ(*sp, *pp)) ++sp; + if (!*sp) return 0; + } + if (*pp && !*sp) return 0; + return score; +} + +static void draw_line(FILE *out, const char *line, const char *hl, const char *cur) +{ + int dim = 0, backtrack = 0; + for (const char *pl = line, *phl = hl; *pl; pl++) { + if (phl >= cur) ++backtrack; + if (case_sensitive ? *pl == *phl : LOWERCASE(*pl) == LOWERCASE(*phl)) { + ++phl; + if (dim) { + fputs("\033[22m", out); + dim = 0; + } + } else if (!dim) { + fputs("\033[2m", out); + dim = 1; + } + fputc(*pl, out); + } + if (backtrack) + fprintf(out, "\033[%dD", backtrack); +} + +/* + * A basic fuzzly matcher + */ +static char *fzline(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); + } + + // 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); + } + picked = NULL; + case_sensitive = 0; + for (const char *p = buf; *p; ++p) + case_sensitive |= ('A' <= *p && *p <= 'Z'); + + int ncandidates = 0; + for (int i = 0; i < nopts; i++) + ncandidates += score(opts[i], buf) > 0; + + int bestscore = 0; + char *best = NULL; + int skipped = 0; + for (int i = 0; i < nopts; i++) { + int s = score(opts[i], buf); + if (s && skipped < to_skip) { + ++skipped; + continue; + } + if (s > bestscore) { + best = opts[i]; + bestscore = s; + } + } + if (quickpick && ncandidates == 1 && best) + return best; + + picked = best; + if (best) { + draw_line(out, best, buf, &buf[b]); + } else { + fprintf(out, "\033[0;31m%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); + 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 (ncandidates) + to_skip = (to_skip + 1) % ncandidates; + break; + case KEY_CTRL_P: + if (ncandidates) + to_skip = (to_skip + ncandidates - 1) % ncandidates; + 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 (!buf) goto finished; + } + if (b < len) + memmove(buf+b+1, buf+b, (size_t)(len-b+1)); + buf[b++] = (char)key; + buf[++len] = 0; + } + break; + } + } + finished: + return picked; +} + +/** + * A basic readline implementation. No history, but all the normal editing + * key bindings like Ctrl-U to clear to the beginning of the line, backspace, + * arrow keys, etc. + * + * Takes an input file and output file, and an optional prompt and returns + * a malloc'd string containing the user's input or NULL if an error occurred + * or if the user hit Escape or Ctrl-c. + */ +static char *breadline(FILE *in, FILE *out, const char *prompt, const char *initial) +{ + size_t cap = initial ? strlen(initial) + 100 : 100; + char *buf = calloc(cap, 1); + if (!buf) return NULL; + int i = 0, len = 0; + if (prompt) { + fputs("\033[1G\033[K\033[37;1m", out); + fputs(prompt, out); + fputs("\033[0m", out); + } + if (initial) { + strcpy(buf, initial); + len = (int)strlen(initial); + i = len; + fputs(initial, out); + } + // Show cursor + fputs("\033[?25h", out); + while (1) { + fflush(out); + int key = bgetkey(in, NULL, NULL, -1); + switch (key) { + case -1: case -2: case -3: continue; + case '\r': + // TODO: support backslash-enter + goto finished; + case KEY_CTRL_C: case KEY_ESC: + free(buf); + buf = NULL; + goto finished; + case KEY_CTRL_A: + if (i > 0) { + fprintf(out, "\033[%dD", i); + i = 0; + } + break; + case KEY_CTRL_E: + if (i < len) { + fprintf(out, "\033[%dC", len-i); + i = len; + } + break; + case KEY_CTRL_U: { + int to_clear = i; + if (to_clear) { + memmove(buf, buf+i, (size_t)(len-i)); + buf[len -= i] = 0; + i = 0; + fprintf(out, "\033[%dD\033[K", to_clear); + if (len > 0) + fprintf(out, "%s\033[%dD", buf, len); + } + break; + } + case KEY_CTRL_K: + if (i < len) { + buf[len = i] = 0; + fputs("\033[K", out); + } + break; + case KEY_BACKSPACE: case KEY_BACKSPACE2: + if (i > 0) { + --i; + memmove(buf+i, buf+i+1, (size_t)(len-i)); + buf[--len] = 0; + if (i == len) fputs("\033[D \033[D", out); + else fprintf(out, "\033[D%s\033[K\033[%dD", buf+i, len-i); + } + break; + case KEY_DELETE: case KEY_CTRL_D: + if (i < len) { + memmove(buf+i, buf+i+1, (size_t)(len-i)); + buf[--len] = 0; + if (i == len) fputs(" \033[D", out); + else fprintf(out, "%s\033[K\033[%dD", buf+i, len-i); + } + break; + case KEY_ARROW_LEFT: case KEY_CTRL_B: + if (i > 0) { + --i; + fputs("\033[D", out); + } + break; + case KEY_ARROW_RIGHT: case KEY_CTRL_F: + if (i < len) { + ++i; + fputs("\033[C", out); + } + break; + default: + if (' ' <= key && key <= '~') { + if (len + 1 >= (int)cap) { + cap += 100; + buf = realloc(buf, cap); + if (!buf) goto finished; + } + if (i < len) + memmove(buf+i+1, buf+i, (size_t)(len-i+1)); + buf[i++] = (char)key; + buf[++len] = 0; + if (i == len) + fputc(key, out); + else + fprintf(out, "%c%s\033[%dD", (char)key, buf+i, len-i); + } + break; + } + } + finished: + // Reset cursor to block + fputs("\033[1G\033[1 q\033[2K", out); + return buf; +} + +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 (strcmp(argv[a], "-p") == 0 || strcmp(argv[a], "--password") == 0) { + password = 1; + } else if (strcmp(argv[a], "-q") == 0 || strcmp(argv[a], "--quickpick") == 0) { + quickpick = 1; + } else break; + ++a; + } + if (!prompt && a < argc) prompt = argv[a++]; + if (!initial && a < argc) initial = argv[a++]; + + while (a < argc) { + if ((size_t)nlines >= linescap) + lines = memcheck(realloc(lines, (linescap += 100)*sizeof(char*))); + lines[nlines++] = argv[a++]; + } + + char *output = nlines > 0 + ? fzline(tty_in, tty_out, prompt, initial, nlines, lines) + : breadline(tty_in, tty_out, prompt, initial); + + 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); + return 0; +} +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 |
