aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2019-09-22 15:40:26 -0700
committerBruce Hill <bruce@bruce-hill.com>2019-09-22 15:40:26 -0700
commitf78960c3829a3d9917cbf0f562da3f864ee4474a (patch)
treea485f6acf25f834b82220ff6a1a8668562ce3cb1
parent0ec5a7475ca70222af19a149ad34fe2d482ae744 (diff)
Added history through the --history=<name> flag.
-rw-r--r--README.md15
-rw-r--r--ask.c128
2 files changed, 129 insertions, 14 deletions
diff --git a/README.md b/README.md
index 1d8a036..ece7021 100644
--- a/README.md
+++ b/README.md
@@ -17,5 +17,20 @@ Here's a simple program to move a file from the current directory:
file="`ls | ask "Pick a file: "`"
mv "$file" "`ask "Move $file to: "`"
+`ask` also supports a few other command line options:
+
+* `ask -y` or `ask --yes` and `ask -n` or `ask --no` will append " [Y/n]" or
+ " [y/N]" respectively to the prompt, and provide "Y" and "N" as the only
+ options, and will exit with success or failure accordingly. (e.g. `if ask
+ --yes "Continue?"; then ...`)
+* `ask --quickpick` or `ask -Q` will pick an option automatically without
+ pressing enter if there is only one valid option.
+* `ask --password` or `ask -P` will show a typing indicator without displaying
+ the typed letters on the screen. (e.g. `password="$(ask -P "Enter your
+ password: ")"`)
+* `ask --history=<name>` will load/save previous `ask` responses in
+ `$XDG_DATA/ask/<name>.hist` (`~/.local/share/ask/<name>.hist` by default) for
+ use with up/down arrow keys. Maximum of 1000 entries are stored per log file.
+
## License
`ask` is released under the MIT License. See LICENSE for details.
diff --git a/ask.c b/ask.c
index 62f4052..c0fefed 100644
--- a/ask.c
+++ b/ask.c
@@ -1,20 +1,26 @@
/* ask - a simple command line asker
* Copyright 2019 Bruce Hill
* Released under the MIT License (see LICENSE for details)
- * Usage: ask [-Q|--quickpick] [-y|--yes] [-n|--no] [-p|--password]
- * [-v|--version] [-h|--help] [-q |--query=initial] [[--prompt=]prompt [options...]]
+ * Usage: ask [-Q|--quickpick] [-y|--yes] [-n|--no] [-P|--password] [[-H |--history=]name]
+ * [-v|--version] [-h|--help] [-q |--query=initial] [[-p |--prompt=]prompt [options...]]
* --password: password mode
* --quickpick: quickpick mode (exit when only one option remains)
* --version: print version and exit
* --help: print usage and exit
* --query: initial value to pre-populate user input
* --prompt: use the given prompt (displayed in bold)
- * --yes: append "[Y/n]" to the prompt, use quickpick mode,
+ * --yes: append " [Y/n]" to the prompt, use quickpick mode,
+ * --no: append " [y/N]" to the prompt, use quickpick mode,
+ * --history: store the selected value in a history file, which can be browsed with
+ * up/down arrow keys
*/
+#include <errno.h>
+#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <termios.h>
@@ -26,8 +32,10 @@
#define LOWERCASE(c) ('A' <= (c) && (c) <= 'Z' ? ((c) + 'a' - 'A') : (c))
#define EQ(a, b) (case_sensitive ? (a) == (b) : LOWERCASE(a) == LOWERCASE(b))
#define PASSWORD "-\\|/"
+#define MAX_HISTORY 1000
-static int password = 0, quickpick = 0, case_sensitive = 0;
+static int password = 0, quickpick = 0, case_sensitive = 0, histindex = 0, nhistory = 0;
+static char histpath[PATH_MAX] = {0};
static inline void *memcheck(void *p)
{
@@ -139,6 +147,29 @@ static int draw_line(FILE *out, const char *option, const char *patt, int cursor
return to_start;
}
+static void get_history(char **buf, size_t *cap, int index)
+{
+ if (nhistory == 0) return;
+ if (index == nhistory) {
+ (*buf)[0] = '\0';
+ return;
+ }
+ FILE *f = fopen(histpath, "r");
+ char histline[256];
+ for (int i = 0; i < histindex; i++) {
+ if (fgets(histline, sizeof(histline), f) == NULL)
+ return;
+ if (histline[strlen(histline)-1] != '\n')
+ --i;
+ }
+ do {
+ *buf = memcheck(realloc(*buf, (*cap += 100)));
+ if (fgets(*buf, *cap, f) == NULL)
+ return;
+ } while ((*buf)[strlen(*buf)-1] != '\n');
+ (*buf)[strlen(*buf)-1] = '\0';
+}
+
/*
* A basic fuzzy matcher and line inputter
*/
@@ -264,6 +295,24 @@ static char *get_input(FILE *in, FILE *out, const char *prompt, const char *init
case KEY_ARROW_RIGHT: case KEY_CTRL_F:
if (b < len) ++b;
break;
+ case KEY_ARROW_UP:
+ if (nhistory > 0) {
+ --histindex;
+ if (histindex < 0) histindex = 0;
+ get_history(&buf, &cap, histindex);
+ len = strlen(buf);
+ b = len;
+ }
+ break;
+ case KEY_ARROW_DOWN:
+ if (nhistory > 0) {
+ ++histindex;
+ if (histindex > nhistory) histindex = nhistory;
+ get_history(&buf, &cap, histindex);
+ len = strlen(buf);
+ b = len;
+ }
+ break;
case KEY_CTRL_Q: case KEY_CTRL_V: {
int nextkey;
while ((nextkey = bgetkey(in, NULL, NULL, -1)) < 0)
@@ -318,7 +367,7 @@ static int cmp_len(const void *v1, const void *v2)
int main(int argc, char *argv[])
{
int yes = 0, no = 0;
- char *prompt = NULL, *query = NULL;
+ char *prompt = NULL, *query = NULL, *histname = NULL;
char **opts = NULL;
size_t linescap = 0, linecap = 0;
int nopts = 0;
@@ -334,9 +383,12 @@ int main(int argc, char *argv[])
opts[nopts++] = memcheck(strdup(line));
}
}
- int a = 1;
- while (a < argc) {
- if (argv[a][0] == '-' && argv[a][1] != '-') {
+ int a;
+ for (a = 1; a < argc; a++) {
+ if (strcmp(argv[a], "-H") == 0) {
+ histname = argv[++a];
+ continue;
+ } else if (argv[a][0] == '-' && argv[a][1] != '-') {
for (char *p = &argv[a][1]; *p; p++) {
switch (*p) {
case 'P': password = 1; break;
@@ -357,6 +409,8 @@ int main(int argc, char *argv[])
query = &argv[a][strlen("--query=")];
} else if (strcmp(argv[a], "--password") == 0) {
password = 1;
+ } else if (strncmp(argv[a], "--history=", strlen("--history=")) == 0) {
+ histname = &argv[a][strlen("--history=")];
} else if (strcmp(argv[a], "--quickpick") == 0) {
quickpick = 1;
} else if (strcmp(argv[a], "--yes") == 0) {
@@ -376,7 +430,6 @@ int main(int argc, char *argv[])
printf("ask %s\n", ASK_VERSION);
return 0;
} else break;
- ++a;
}
if (!prompt && a < argc) prompt = argv[a++];
@@ -396,12 +449,12 @@ int main(int argc, char *argv[])
if (yes) {
char *p2 = memcheck(calloc((prompt ? strlen(prompt) : 0)+5+1, sizeof(char)));
if (prompt) strcpy(p2, prompt);
- strcat(p2, "[Y/n]");
+ strcat(p2, " [Y/n]");
prompt = p2;
} else if (no) {
char *p2 = memcheck(calloc((prompt ? strlen(prompt) : 0)+5+1, sizeof(char)));
if (prompt) strcpy(p2, prompt);
- strcat(p2, "[y/N]");
+ strcat(p2, " [y/N]");
prompt = p2;
}
@@ -415,6 +468,41 @@ int main(int argc, char *argv[])
if (tcsetattr(fileno(tty_out), TCSAFLUSH, &bb_termios) == -1)
return 1;
+ if (histname) {
+ char *xdg_data = getenv("XDG_DATA_HOME");
+ if (xdg_data == NULL) {
+ strcpy(histpath, getenv("HOME"));
+ strcat(histpath, "/.local/.share/ask/");
+ } else {
+ strcpy(histpath, xdg_data);
+ strcat(histpath, "/ask/");
+ }
+ strcat(histpath, histname);
+
+ for (char* p = strchr(histpath + 1, '/'); p; p = strchr(p + 1, '/')) {
+ *p = '\0';
+ if (mkdir(histpath, 0777) == -1) {
+ if (errno != EEXIST) {
+ *p = '/';
+ printf("Error: could not create history directory at %s", histpath);
+ return 1;
+ }
+ }
+ *p = '/';
+ }
+
+ char buf[1024];
+ FILE *f = fopen(histpath, "r");
+ if (f) {
+ while (fgets(buf, sizeof(buf), f) != NULL) {
+ size_t len = strlen(buf);
+ if (len > 1 && buf[len-1] == '\n')
+ ++nhistory;
+ }
+ histindex = nhistory;
+ }
+ }
+
// Prefer shorter matches, but otherwise use alphabetic order
qsort(opts, (size_t)nopts, sizeof(char*), cmp_len);
char *output = get_input(tty_in, tty_out, prompt, query, nopts, opts);
@@ -430,12 +518,24 @@ int main(int argc, char *argv[])
// the program is exiting
if (!output) return 1;
- fputs(output, stdout);
if (yes)
- return strcmp(output, "n") == 0 && strcmp(output, "N") == 0;
+ return strcmp(output, "N") == 0;
if (no)
- return strcmp(output, "y") != 0 && strcmp(output, "Y") != 0;
+ return strcmp(output, "Y") != 0;
+
+ fputs(output, stdout);
+ if (histpath[0] && strlen(output) > 0) {
+ FILE *f = fopen(histpath, "a");
+ fprintf(f, "%s\n", output);
+ fclose(f);
+ if (++nhistory > MAX_HISTORY) {
+ if (fork() == 0) {
+ char *args[] = {"sed", "-i", "1d", histpath, NULL};
+ execvp("sed", args);
+ }
+ }
+ }
return 0;
}
// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1