add meson support
[protos/libecoli.git] / src / editline.c
1 /*
2  * Copyright 2018 6WIND S.A.
3  */
4
5 #define _GNU_SOURCE
6 #include <stdio.h>
7 #include <stdbool.h>
8 #include <stdlib.h>
9 #include <errno.h>
10 #include <string.h>
11 #include <assert.h>
12 #include <ctype.h>
13 #include <stdarg.h>
14 #include <signal.h>
15 #include <unistd.h>
16
17 #include <histedit.h>
18
19 #include "string_utils.h"
20 #include "editline.h"
21
22 #define NC_CLI_HISTORY_SIZE 128
23
24 struct nc_cli_editline {
25         EditLine *editline;
26         History *history;
27         HistEvent histev;
28         bool break_received;
29         nc_cli_editline_complete_t complete;
30         FILE *null_out;
31         bool interactive;
32         bool incomplete_line;
33         char *full_line;
34         char *(*prompt_cb)(EditLine *);
35 };
36
37 struct nc_cli_editline *nc_cli_el;
38
39 static int check_quotes(const char *str)
40 {
41         char quote = 0;
42         size_t i = 0;
43
44         while (str[i] != '\0') {
45                 if (quote == 0) {
46                         if (str[i] == '"' || str[i] == '\'') {
47                                 quote = str[i];
48                         }
49                         i++;
50                         continue;
51                 } else {
52                         if (str[i] == quote) {
53                                 i++;
54                                 quote = 0;
55                         } else if (str[i] == '\\' && str[i+1] == quote) {
56                                 i += 2;
57                         } else {
58                                 i++;
59                         }
60                         continue;
61                 }
62         }
63
64         return quote;
65 }
66
67 static int
68 editline_break(EditLine *editline, int c)
69 {
70         struct nc_cli_editline *el;
71         void *ptr;
72
73         (void)c;
74
75         if (el_get(editline, EL_CLIENTDATA, &ptr))
76                 return CC_ERROR;
77
78         el = ptr;
79         el->break_received = true;
80         nc_cli_printf(el, "\n");
81
82         return CC_EOF;
83 }
84
85 static int
86 editline_suspend(EditLine *editline, int c)
87 {
88         (void)editline;
89         (void)c;
90
91         kill(getpid(), SIGSTOP);
92
93         return CC_NORM;
94 }
95
96 int
97 editline_get_screen_size(struct nc_cli_editline *el, size_t *rows, size_t *cols)
98 {
99         int w, h;
100
101         if (rows != NULL) {
102                 if (el_get(el->editline, EL_GETTC, "li", &h, (void *)0))
103                         return -1;
104                 *rows = h;
105         }
106         if (cols != NULL) {
107                 if (el_get(el->editline, EL_GETTC, "co", &w, (void *)0))
108                         return -1;
109                 *cols = w;
110         }
111         return 0;
112 }
113
114 FILE *
115 nc_cli_editline_get_file(struct nc_cli_editline *el, int num)
116 {
117         FILE *f;
118
119         if (el == NULL)
120                 return NULL;
121         if (num > 2)
122                 return NULL;
123         if (el_get(el->editline, EL_GETFP, num, &f))
124                 return NULL;
125
126         return f;
127 }
128
129 /* match the prototype expected by qsort() */
130 static int
131 strcasecmp_cb(const void *p1, const void *p2)
132 {
133         return strcasecmp(*(char * const *)p1, *(char * const *)p2);
134 }
135
136 /* Show the matches as a multi-columns list */
137 int
138 nc_cli_editline_print_cols(struct nc_cli_editline *el,
139                         char const * const *matches, size_t n)
140 {
141         size_t max_strlen = 0, len, i, j, ncols;
142         size_t width, height;
143         const char *space;
144         char **matches_copy = NULL;
145
146         nc_cli_printf(nc_cli_el, "\n");
147         if (n == 0)
148                 return 0;
149
150         if (editline_get_screen_size(el, &height, &width) < 0)
151                 width = 80;
152
153         /* duplicate the matches table, and sort it */
154         matches_copy = calloc(n, sizeof(const char *));
155         if (matches_copy == NULL)
156                 return -1;
157         memcpy(matches_copy, matches, sizeof(const char *) * n);
158         qsort(matches_copy, n, sizeof(char *), strcasecmp_cb);
159
160         /* get max string length */
161         for (i = 0; i < n; i++) {
162                 len = strlen(matches_copy[i]);
163                 if (len > max_strlen)
164                         max_strlen = len;
165         }
166
167         /* write the columns */
168         ncols = width / (max_strlen + 4);
169         if (ncols == 0)
170                 ncols = 1;
171         for (i = 0; i < n; i+= ncols) {
172                 for (j = 0; j < ncols; j++) {
173                         if (i + j >= n)
174                                 break;
175                         if (j == 0)
176                                 space = "";
177                         else
178                                 space = "    ";
179                         nc_cli_printf(nc_cli_el, "%s%-*s", space,
180                                 (int)max_strlen, matches[i+j]);
181                 }
182                 nc_cli_printf(nc_cli_el, "\n");
183         }
184
185         free(matches_copy);
186         return 0;
187 }
188
189 static int
190 editline_complete(EditLine *editline, int c)
191 {
192         enum nc_cli_editline_complete_status ret;
193         const LineInfo *line_info;
194         struct nc_cli_editline *el;
195         int len;
196         char *line;
197         void *ptr;
198
199         if (el_get(editline, EL_CLIENTDATA, &ptr))
200                 return CC_ERROR;
201
202         el = ptr;
203
204         if (el->complete == NULL)
205                 return CC_NORM;
206
207         line_info = el_line(editline);
208         if (line_info == NULL)
209                 return CC_ERROR;
210
211         len = line_info->cursor - line_info->buffer;
212         if (asprintf(&line, "%s%.*s", el->full_line ? : "", len,
213                         line_info->buffer) < 0)
214                 return CC_ERROR;
215
216         if (c == '?' && check_quotes(line) != 0) {
217                 free(line);
218                 el_insertstr(editline, "?");
219                 return CC_REFRESH;
220         }
221
222         ret = el->complete(c, line);
223         free(line);
224
225         if (ret == ERROR)
226                 return CC_ERROR;
227         else if (ret == REDISPLAY)
228                 return CC_REDISPLAY;
229         else
230                 return CC_REFRESH;
231 }
232
233 static bool is_blank_string(const char *s)
234 {
235         while (*s) {
236                 if (!isspace(*s))
237                         return false;
238                 s++;
239         }
240         return true;
241 }
242
243 static char *
244 multiline_prompt_cb(EditLine *e)
245 {
246         (void)e;
247         return strdup("... ");
248 }
249
250 const char *
251 nc_cli_editline_edit(struct nc_cli_editline *el)
252 {
253         const char *line;
254         int count;
255
256         assert(el->editline != NULL);
257
258         if (el->incomplete_line == false) {
259                 free(el->full_line);
260                 el->full_line = NULL;
261         }
262
263         el->break_received = false;
264
265         if (el->incomplete_line)
266                 el_set(el->editline, EL_PROMPT, multiline_prompt_cb);
267         else
268                 el_set(el->editline, EL_PROMPT, el->prompt_cb);
269
270         line = el_gets(el->editline, &count);
271
272         if (line == NULL && el->break_received) {
273                 free(el->full_line);
274                 el->full_line = NULL;
275                 el->incomplete_line = false;
276                 return ""; /* abort current line */
277         }
278
279         if (line == NULL || astrcat(&el->full_line, line) < 0) {
280                 free(el->full_line);
281                 el->full_line = NULL;
282                 el->incomplete_line = false;
283                 return NULL; /* error / eof */
284         }
285
286         if (check_quotes(el->full_line) != 0) {
287                 el->incomplete_line = true;
288                 return "";
289         }
290
291         el->incomplete_line = false;
292         if (el->history != NULL && !is_blank_string(el->full_line))
293                 history(el->history, &el->histev,
294                         H_ENTER, el->full_line);
295
296         return el->full_line;
297 }
298
299 int
300 nc_cli_editline_register_complete(struct nc_cli_editline *el,
301                                 nc_cli_editline_complete_t complete)
302 {
303         const char *name;
304
305         if (el_set(el->editline, EL_ADDFN, "ed-complete",
306                         "Complete buffer",
307                         editline_complete))
308                 return -1;
309
310         if (complete != NULL)
311                 name = "ed-complete";
312         else
313                 name = "ed-unassigned";
314         if (el_set(el->editline, EL_BIND, "^I", name, NULL))
315                 return -1;
316
317         if (complete != NULL)
318                 name = "ed-complete";
319         else
320                 name = "ed-insert";
321         if (el_set(el->editline, EL_BIND, "?", name, NULL))
322                 return -1;
323
324         el->complete = complete;
325
326         return 0;
327 }
328
329 int
330 nc_cli_editline_insert_str(struct nc_cli_editline *el, const char *str)
331 {
332         return el_insertstr(el->editline, str);
333 }
334
335 int nc_cli_printf(struct nc_cli_editline *el, const char *format, ...)
336 {
337         FILE *out = nc_cli_editline_get_file(el, 1);
338         va_list ap;
339         int ret;
340
341         if (out == NULL)
342                 out = stdout;
343
344         va_start(ap, format);
345         ret = vfprintf(out, format, ap);
346         va_end(ap);
347
348         return ret;
349 }
350
351 int nc_cli_eprintf(struct nc_cli_editline *el, const char *format, ...)
352 {
353         FILE *out = nc_cli_editline_get_file(el, 2);
354         va_list ap;
355         int ret;
356
357         if (out == NULL)
358                 out = stderr;
359
360         va_start(ap, format);
361         ret = vfprintf(out, format, ap);
362         va_end(ap);
363
364         return ret;
365 }
366
367 bool
368 nc_cli_editline_is_interactive(struct nc_cli_editline *el)
369 {
370         return el->interactive;
371 }
372
373 bool
374 nc_cli_editline_is_running(struct nc_cli_editline *el)
375 {
376         return el == nc_cli_el;
377 }
378
379 void
380 nc_cli_editline_start(struct nc_cli_editline *el)
381 {
382         nc_cli_el = el;
383 }
384
385 void
386 nc_cli_editline_stop(struct nc_cli_editline *el)
387 {
388         if (el == nc_cli_el)
389                 nc_cli_el = NULL;
390 }
391
392 int
393 nc_cli_editline_getc(struct nc_cli_editline *el)
394 {
395         char c;
396
397         if (el->interactive == false)
398                 return -1;
399
400         if (el_getc(el->editline, &c) != 1)
401                 return -1;
402
403         return c;
404 }
405
406 int
407 nc_cli_editline_resize(struct nc_cli_editline *el)
408 {
409         el_resize(el->editline);
410         return 0;
411 }
412
413 int nc_cli_editline_set_prompt_cb(struct nc_cli_editline *el,
414                                 char *(*prompt_cb)(EditLine *el))
415 {
416         el->prompt_cb = prompt_cb;
417         return 0;
418 }
419
420 int
421 nc_cli_editline_mask_interrupts(bool do_mask)
422 {
423         const char *setty = do_mask ? "-isig" : "+isig";
424
425         if (nc_cli_el == NULL)
426                 return -1;
427
428         if (nc_cli_el->interactive == false)
429                 return 0;
430
431         if (el_set(nc_cli_el->editline, EL_SETTY, "-d", setty, NULL))
432                 return -1;
433
434         return 0;
435 }
436
437 struct nc_cli_editline *
438 nc_cli_editline_init(FILE *f_in, FILE *f_out, FILE *f_err, bool interactive,
439                 char *(*prompt_cb)(EditLine *el))
440 {
441         struct nc_cli_editline *el = NULL;
442
443         el = calloc(1, sizeof(*el));
444         if (el == NULL)
445                 goto fail;
446         if (f_in == NULL) {
447                 errno = EINVAL;
448                 goto fail;
449         }
450         if (f_out == NULL || f_err == NULL) {
451                 el->null_out = fopen("/dev/null", "w");
452                 if (el->null_out == NULL)
453                         goto fail;
454         }
455         if (f_out == NULL)
456                 f_out = el->null_out;
457         if (f_err == NULL)
458                 f_err = el->null_out;
459
460         el->interactive = interactive;
461         el->prompt_cb = prompt_cb;
462
463         el->editline = el_init("nc-cli", f_in, f_out, f_err);
464         if (el->editline == NULL)
465                 goto fail;
466
467         if (el_set(el->editline, EL_SIGNAL, 1))
468                 goto fail;
469
470         if (el_set(el->editline, EL_CLIENTDATA, el))
471                 goto fail;
472
473         if (el_set(el->editline, EL_PROMPT, prompt_cb))
474                 goto fail;
475
476         if (interactive == false)
477                 goto end;
478
479         if (el_set(el->editline, EL_PREP_TERM, 0))
480                 goto fail;
481
482         if (el_set(el->editline, EL_EDITOR, "emacs"))
483                 goto fail;
484
485         if (el_set(el->editline, EL_SETTY, "-d", "-isig", NULL))
486                 goto fail;
487         if (el_set(el->editline, EL_ADDFN, "ed-break",
488                         "Break and flush the buffer",
489                         editline_break))
490                 goto fail;
491         if (el_set(el->editline, EL_BIND, "^C", "ed-break", NULL))
492                 goto fail;
493         if (el_set(el->editline, EL_ADDFN, "ed-suspend",
494                         "Suspend the terminal",
495                         editline_suspend))
496                 goto fail;
497         if (el_set(el->editline, EL_BIND, "^Z", "ed-suspend", NULL))
498                 goto fail;
499         if (el_set(el->editline, EL_BIND, "^W", "ed-delete-prev-word", NULL))
500                 goto fail;
501
502         el->history = history_init();
503         if (!el->history)
504                 goto fail;
505         if (history(el->history, &el->histev, H_SETSIZE,
506                         NC_CLI_HISTORY_SIZE) < 0)
507                 goto fail;
508         if (history(el->history, &el->histev,
509                         H_SETUNIQUE, 1))
510                 goto fail;
511         if (el_set(el->editline, EL_HIST, history,
512                         el->history))
513                 goto fail;
514
515 end:
516         return el;
517
518 fail:
519         if (el != NULL) {
520                 if (el->null_out != NULL)
521                         fclose(el->null_out);
522                 if (el->history != NULL)
523                         history_end(el->history);
524                 if (el->editline != NULL)
525                         el_end(el->editline);
526                 free(el);
527         }
528         return NULL;
529 }
530
531 void
532 nc_cli_editline_free(struct nc_cli_editline *el)
533 {
534         if (el == NULL)
535                 return;
536         nc_cli_editline_stop(el);
537         if (el->null_out != NULL)
538                 fclose(el->null_out);
539         if (el->history != NULL)
540                 history_end(el->history);
541         el_end(el->editline);
542         free(el->full_line);
543         free(el);
544 }