add meson support
[protos/libecoli.git] / src / editline.c
diff --git a/src/editline.c b/src/editline.c
new file mode 100644 (file)
index 0000000..449aebb
--- /dev/null
@@ -0,0 +1,544 @@
+/*
+ * Copyright 2018 6WIND S.A.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <histedit.h>
+
+#include "string_utils.h"
+#include "editline.h"
+
+#define NC_CLI_HISTORY_SIZE 128
+
+struct nc_cli_editline {
+       EditLine *editline;
+       History *history;
+       HistEvent histev;
+       bool break_received;
+       nc_cli_editline_complete_t complete;
+       FILE *null_out;
+       bool interactive;
+       bool incomplete_line;
+       char *full_line;
+       char *(*prompt_cb)(EditLine *);
+};
+
+struct nc_cli_editline *nc_cli_el;
+
+static int check_quotes(const char *str)
+{
+       char quote = 0;
+       size_t i = 0;
+
+       while (str[i] != '\0') {
+               if (quote == 0) {
+                       if (str[i] == '"' || str[i] == '\'') {
+                               quote = str[i];
+                       }
+                       i++;
+                       continue;
+               } else {
+                       if (str[i] == quote) {
+                               i++;
+                               quote = 0;
+                       } else if (str[i] == '\\' && str[i+1] == quote) {
+                               i += 2;
+                       } else {
+                               i++;
+                       }
+                       continue;
+               }
+       }
+
+       return quote;
+}
+
+static int
+editline_break(EditLine *editline, int c)
+{
+       struct nc_cli_editline *el;
+       void *ptr;
+
+       (void)c;
+
+       if (el_get(editline, EL_CLIENTDATA, &ptr))
+               return CC_ERROR;
+
+       el = ptr;
+       el->break_received = true;
+       nc_cli_printf(el, "\n");
+
+       return CC_EOF;
+}
+
+static int
+editline_suspend(EditLine *editline, int c)
+{
+       (void)editline;
+       (void)c;
+
+       kill(getpid(), SIGSTOP);
+
+       return CC_NORM;
+}
+
+int
+editline_get_screen_size(struct nc_cli_editline *el, size_t *rows, size_t *cols)
+{
+       int w, h;
+
+       if (rows != NULL) {
+               if (el_get(el->editline, EL_GETTC, "li", &h, (void *)0))
+                       return -1;
+               *rows = h;
+       }
+       if (cols != NULL) {
+               if (el_get(el->editline, EL_GETTC, "co", &w, (void *)0))
+                       return -1;
+               *cols = w;
+       }
+       return 0;
+}
+
+FILE *
+nc_cli_editline_get_file(struct nc_cli_editline *el, int num)
+{
+       FILE *f;
+
+       if (el == NULL)
+               return NULL;
+       if (num > 2)
+               return NULL;
+       if (el_get(el->editline, EL_GETFP, num, &f))
+               return NULL;
+
+       return f;
+}
+
+/* match the prototype expected by qsort() */
+static int
+strcasecmp_cb(const void *p1, const void *p2)
+{
+       return strcasecmp(*(char * const *)p1, *(char * const *)p2);
+}
+
+/* Show the matches as a multi-columns list */
+int
+nc_cli_editline_print_cols(struct nc_cli_editline *el,
+                       char const * const *matches, size_t n)
+{
+       size_t max_strlen = 0, len, i, j, ncols;
+       size_t width, height;
+       const char *space;
+       char **matches_copy = NULL;
+
+       nc_cli_printf(nc_cli_el, "\n");
+       if (n == 0)
+               return 0;
+
+       if (editline_get_screen_size(el, &height, &width) < 0)
+               width = 80;
+
+       /* duplicate the matches table, and sort it */
+       matches_copy = calloc(n, sizeof(const char *));
+       if (matches_copy == NULL)
+               return -1;
+       memcpy(matches_copy, matches, sizeof(const char *) * n);
+       qsort(matches_copy, n, sizeof(char *), strcasecmp_cb);
+
+       /* get max string length */
+       for (i = 0; i < n; i++) {
+               len = strlen(matches_copy[i]);
+               if (len > max_strlen)
+                       max_strlen = len;
+       }
+
+       /* write the columns */
+       ncols = width / (max_strlen + 4);
+       if (ncols == 0)
+               ncols = 1;
+       for (i = 0; i < n; i+= ncols) {
+               for (j = 0; j < ncols; j++) {
+                       if (i + j >= n)
+                               break;
+                       if (j == 0)
+                               space = "";
+                       else
+                               space = "    ";
+                       nc_cli_printf(nc_cli_el, "%s%-*s", space,
+                               (int)max_strlen, matches[i+j]);
+               }
+               nc_cli_printf(nc_cli_el, "\n");
+       }
+
+       free(matches_copy);
+       return 0;
+}
+
+static int
+editline_complete(EditLine *editline, int c)
+{
+       enum nc_cli_editline_complete_status ret;
+       const LineInfo *line_info;
+       struct nc_cli_editline *el;
+       int len;
+       char *line;
+       void *ptr;
+
+       if (el_get(editline, EL_CLIENTDATA, &ptr))
+               return CC_ERROR;
+
+       el = ptr;
+
+       if (el->complete == NULL)
+               return CC_NORM;
+
+       line_info = el_line(editline);
+       if (line_info == NULL)
+               return CC_ERROR;
+
+       len = line_info->cursor - line_info->buffer;
+       if (asprintf(&line, "%s%.*s", el->full_line ? : "", len,
+                       line_info->buffer) < 0)
+               return CC_ERROR;
+
+       if (c == '?' && check_quotes(line) != 0) {
+               free(line);
+               el_insertstr(editline, "?");
+               return CC_REFRESH;
+       }
+
+       ret = el->complete(c, line);
+       free(line);
+
+       if (ret == ERROR)
+               return CC_ERROR;
+       else if (ret == REDISPLAY)
+               return CC_REDISPLAY;
+       else
+               return CC_REFRESH;
+}
+
+static bool is_blank_string(const char *s)
+{
+       while (*s) {
+               if (!isspace(*s))
+                       return false;
+               s++;
+       }
+       return true;
+}
+
+static char *
+multiline_prompt_cb(EditLine *e)
+{
+       (void)e;
+       return strdup("... ");
+}
+
+const char *
+nc_cli_editline_edit(struct nc_cli_editline *el)
+{
+       const char *line;
+       int count;
+
+       assert(el->editline != NULL);
+
+       if (el->incomplete_line == false) {
+               free(el->full_line);
+               el->full_line = NULL;
+       }
+
+       el->break_received = false;
+
+       if (el->incomplete_line)
+               el_set(el->editline, EL_PROMPT, multiline_prompt_cb);
+       else
+               el_set(el->editline, EL_PROMPT, el->prompt_cb);
+
+       line = el_gets(el->editline, &count);
+
+       if (line == NULL && el->break_received) {
+               free(el->full_line);
+               el->full_line = NULL;
+               el->incomplete_line = false;
+               return ""; /* abort current line */
+       }
+
+       if (line == NULL || astrcat(&el->full_line, line) < 0) {
+               free(el->full_line);
+               el->full_line = NULL;
+               el->incomplete_line = false;
+               return NULL; /* error / eof */
+       }
+
+       if (check_quotes(el->full_line) != 0) {
+               el->incomplete_line = true;
+               return "";
+       }
+
+       el->incomplete_line = false;
+       if (el->history != NULL && !is_blank_string(el->full_line))
+               history(el->history, &el->histev,
+                       H_ENTER, el->full_line);
+
+       return el->full_line;
+}
+
+int
+nc_cli_editline_register_complete(struct nc_cli_editline *el,
+                               nc_cli_editline_complete_t complete)
+{
+       const char *name;
+
+       if (el_set(el->editline, EL_ADDFN, "ed-complete",
+                       "Complete buffer",
+                       editline_complete))
+               return -1;
+
+       if (complete != NULL)
+               name = "ed-complete";
+       else
+               name = "ed-unassigned";
+       if (el_set(el->editline, EL_BIND, "^I", name, NULL))
+               return -1;
+
+       if (complete != NULL)
+               name = "ed-complete";
+       else
+               name = "ed-insert";
+       if (el_set(el->editline, EL_BIND, "?", name, NULL))
+               return -1;
+
+       el->complete = complete;
+
+       return 0;
+}
+
+int
+nc_cli_editline_insert_str(struct nc_cli_editline *el, const char *str)
+{
+       return el_insertstr(el->editline, str);
+}
+
+int nc_cli_printf(struct nc_cli_editline *el, const char *format, ...)
+{
+       FILE *out = nc_cli_editline_get_file(el, 1);
+       va_list ap;
+       int ret;
+
+       if (out == NULL)
+               out = stdout;
+
+       va_start(ap, format);
+       ret = vfprintf(out, format, ap);
+       va_end(ap);
+
+       return ret;
+}
+
+int nc_cli_eprintf(struct nc_cli_editline *el, const char *format, ...)
+{
+       FILE *out = nc_cli_editline_get_file(el, 2);
+       va_list ap;
+       int ret;
+
+       if (out == NULL)
+               out = stderr;
+
+       va_start(ap, format);
+       ret = vfprintf(out, format, ap);
+       va_end(ap);
+
+       return ret;
+}
+
+bool
+nc_cli_editline_is_interactive(struct nc_cli_editline *el)
+{
+       return el->interactive;
+}
+
+bool
+nc_cli_editline_is_running(struct nc_cli_editline *el)
+{
+       return el == nc_cli_el;
+}
+
+void
+nc_cli_editline_start(struct nc_cli_editline *el)
+{
+       nc_cli_el = el;
+}
+
+void
+nc_cli_editline_stop(struct nc_cli_editline *el)
+{
+       if (el == nc_cli_el)
+               nc_cli_el = NULL;
+}
+
+int
+nc_cli_editline_getc(struct nc_cli_editline *el)
+{
+       char c;
+
+       if (el->interactive == false)
+               return -1;
+
+       if (el_getc(el->editline, &c) != 1)
+               return -1;
+
+       return c;
+}
+
+int
+nc_cli_editline_resize(struct nc_cli_editline *el)
+{
+       el_resize(el->editline);
+       return 0;
+}
+
+int nc_cli_editline_set_prompt_cb(struct nc_cli_editline *el,
+                               char *(*prompt_cb)(EditLine *el))
+{
+       el->prompt_cb = prompt_cb;
+       return 0;
+}
+
+int
+nc_cli_editline_mask_interrupts(bool do_mask)
+{
+       const char *setty = do_mask ? "-isig" : "+isig";
+
+       if (nc_cli_el == NULL)
+               return -1;
+
+       if (nc_cli_el->interactive == false)
+               return 0;
+
+       if (el_set(nc_cli_el->editline, EL_SETTY, "-d", setty, NULL))
+               return -1;
+
+       return 0;
+}
+
+struct nc_cli_editline *
+nc_cli_editline_init(FILE *f_in, FILE *f_out, FILE *f_err, bool interactive,
+               char *(*prompt_cb)(EditLine *el))
+{
+       struct nc_cli_editline *el = NULL;
+
+       el = calloc(1, sizeof(*el));
+       if (el == NULL)
+               goto fail;
+       if (f_in == NULL) {
+               errno = EINVAL;
+               goto fail;
+       }
+       if (f_out == NULL || f_err == NULL) {
+               el->null_out = fopen("/dev/null", "w");
+               if (el->null_out == NULL)
+                       goto fail;
+       }
+       if (f_out == NULL)
+               f_out = el->null_out;
+       if (f_err == NULL)
+               f_err = el->null_out;
+
+       el->interactive = interactive;
+       el->prompt_cb = prompt_cb;
+
+       el->editline = el_init("nc-cli", f_in, f_out, f_err);
+       if (el->editline == NULL)
+               goto fail;
+
+       if (el_set(el->editline, EL_SIGNAL, 1))
+               goto fail;
+
+       if (el_set(el->editline, EL_CLIENTDATA, el))
+               goto fail;
+
+       if (el_set(el->editline, EL_PROMPT, prompt_cb))
+               goto fail;
+
+       if (interactive == false)
+               goto end;
+
+       if (el_set(el->editline, EL_PREP_TERM, 0))
+               goto fail;
+
+       if (el_set(el->editline, EL_EDITOR, "emacs"))
+               goto fail;
+
+       if (el_set(el->editline, EL_SETTY, "-d", "-isig", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_ADDFN, "ed-break",
+                       "Break and flush the buffer",
+                       editline_break))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^C", "ed-break", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_ADDFN, "ed-suspend",
+                       "Suspend the terminal",
+                       editline_suspend))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^Z", "ed-suspend", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^W", "ed-delete-prev-word", NULL))
+               goto fail;
+
+       el->history = history_init();
+       if (!el->history)
+               goto fail;
+       if (history(el->history, &el->histev, H_SETSIZE,
+                       NC_CLI_HISTORY_SIZE) < 0)
+               goto fail;
+       if (history(el->history, &el->histev,
+                       H_SETUNIQUE, 1))
+               goto fail;
+       if (el_set(el->editline, EL_HIST, history,
+                       el->history))
+               goto fail;
+
+end:
+       return el;
+
+fail:
+       if (el != NULL) {
+               if (el->null_out != NULL)
+                       fclose(el->null_out);
+               if (el->history != NULL)
+                       history_end(el->history);
+               if (el->editline != NULL)
+                       el_end(el->editline);
+               free(el);
+       }
+       return NULL;
+}
+
+void
+nc_cli_editline_free(struct nc_cli_editline *el)
+{
+       if (el == NULL)
+               return;
+       nc_cli_editline_stop(el);
+       if (el->null_out != NULL)
+               fclose(el->null_out);
+       if (el->history != NULL)
+               history_end(el->history);
+       el_end(el->editline);
+       free(el->full_line);
+       free(el);
+}