ask/ask.c
2019-06-04 19:27:54 -07:00

394 lines
12 KiB
C

/* 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