aboutsummaryrefslogtreecommitdiff
path: root/ask.c
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2019-06-04 19:27:54 -0700
committerBruce Hill <bruce@bruce-hill.com>2019-06-04 19:27:54 -0700
commit3f4bcca9693975dce840d5af657b7251605a45be (patch)
treeaf8dc68317e92cefdbd101762778a6b24097a3b3 /ask.c
Initial commit
Diffstat (limited to 'ask.c')
-rw-r--r--ask.c393
1 files changed, 393 insertions, 0 deletions
diff --git a/ask.c b/ask.c
new file mode 100644
index 0000000..42b7b3b
--- /dev/null
+++ b/ask.c
@@ -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