add meson support
[protos/libecoli.git] / src / ecoli_editline.c
1 /* SPDX-License-Identifier: BSD-3-Clause
2  * Copyright 2018, Olivier MATZ <zer0@droids-corp.org>
3  */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <errno.h>
8
9 #include <histedit.h>
10
11 #include <ecoli_utils.h>
12 #include <ecoli_malloc.h>
13 #include <ecoli_string.h>
14 #include <ecoli_editline.h>
15 #include <ecoli_keyval.h>
16 #include <ecoli_node.h>
17 #include <ecoli_parse.h>
18 #include <ecoli_complete.h>
19
20 struct ec_editline {
21         EditLine *el;
22         History *history;
23         HistEvent histev;
24         const struct ec_node *node;
25         char *prompt;
26 };
27
28 /* used by qsort below */
29 static int
30 strcasecmp_cb(const void *p1, const void *p2)
31 {
32         return strcasecmp(*(char * const *)p1, *(char * const *)p2);
33 }
34
35 /* Show the matches as a multi-columns list */
36 int
37 ec_editline_print_cols(struct ec_editline *editline,
38                 char const * const *matches, size_t n)
39 {
40         size_t max_strlen = 0, len, i, j, ncols;
41         int width, height;
42         const char *space;
43         char **matches_copy = NULL;
44         FILE *f;
45
46         if (el_get(editline->el, EL_GETFP, 1, &f))
47                 return -1;
48
49         fprintf(f, "\n");
50         if (n == 0)
51                 return 0;
52
53         if (el_get(editline->el, EL_GETTC, "li", &height, (void *)0))
54                 return -1;
55         if (el_get(editline->el, EL_GETTC, "co", &width, (void *)0))
56                 return -1;
57
58         /* duplicate the matches table, and sort it */
59         matches_copy = calloc(n, sizeof(const char *));
60         if (matches_copy == NULL)
61                 return -1;
62         memcpy(matches_copy, matches, sizeof(const char *) * n);
63         qsort(matches_copy, n, sizeof(char *), strcasecmp_cb);
64
65         /* get max string length */
66         for (i = 0; i < n; i++) {
67                 len = strlen(matches_copy[i]);
68                 if (len > max_strlen)
69                         max_strlen = len;
70         }
71
72         /* write the columns */
73         ncols = width / (max_strlen + 4);
74         if (ncols == 0)
75                 ncols = 1;
76         for (i = 0; i < n; i+= ncols) {
77                 for (j = 0; j < ncols; j++) {
78                         if (i + j >= n)
79                                 break;
80                         if (j == 0)
81                                 space = "";
82                         else
83                                 space = "    ";
84                         fprintf(f, "%s%-*s", space,
85                                 (int)max_strlen, matches[i+j]);
86                 }
87                 fprintf(f, "\n");
88         }
89
90         free(matches_copy);
91         return 0;
92 }
93
94 /* Show the helps on editline output */
95 int
96 ec_editline_print_helps(struct ec_editline *editline,
97                         const struct ec_editline_help *helps, size_t len)
98 {
99         size_t i;
100         FILE *out;
101
102         if (el_get(editline->el, EL_GETFP, 1, &out))
103                 return -1;
104
105         for (i = 0; i < len; i++) {
106                 if (fprintf(out, "%-20s %s\n",
107                                 helps[i].desc, helps[i].help) < 0)
108                         return -1;
109         }
110
111         return 0;
112 }
113
114 void
115 ec_editline_free_helps(struct ec_editline_help *helps, size_t len)
116 {
117         size_t i;
118
119         if (helps == NULL)
120                 return;
121         for (i = 0; i < len; i++) {
122                 ec_free(helps[i].desc);
123                 ec_free(helps[i].help);
124         }
125         ec_free(helps);
126 }
127
128 int
129 ec_editline_set_prompt(struct ec_editline *editline, const char *prompt)
130 {
131         char *copy = NULL;
132
133         if (prompt != NULL) {
134                 ec_strdup(prompt);
135                 if (copy == NULL)
136                         return -1;
137         }
138
139         ec_free(editline->prompt);
140         editline->prompt = copy;
141
142         return 0;
143 }
144
145 static char *
146 prompt_cb(EditLine *el)
147 {
148         struct ec_editline *editline;
149         void *clientdata;
150
151         if (el_get(el, EL_CLIENTDATA, &clientdata))
152                 return "> ";
153         editline = clientdata;
154
155         if (editline == NULL)
156                 return "> ";
157
158         return editline->prompt;
159 }
160
161 struct ec_editline *
162 ec_editline(const char *name, FILE *f_in, FILE *f_out, FILE *f_err,
163         unsigned int flags)
164 {
165         struct ec_editline *editline = NULL;
166         EditLine *el;
167
168         if (f_in == NULL || f_out == NULL || f_err == NULL) {
169                 errno = EINVAL;
170                 goto fail;
171         }
172
173         editline = ec_calloc(1, sizeof(*editline));
174         if (editline == NULL)
175                 goto fail;
176
177         el = el_init(name, f_in, f_out, f_err);
178         if (el == NULL)
179                 goto fail;
180         editline->el = el;
181
182         /* save editline pointer as user data */
183         if (el_set(el, EL_CLIENTDATA, editline))
184                 goto fail;
185
186         /* install default editline signals */
187         if (el_set(el, EL_SIGNAL, 1))
188                 goto fail;
189
190         if (el_set(el, EL_PREP_TERM, 0))
191                 goto fail;
192
193         /* use emacs bindings */
194         if (el_set(el, EL_EDITOR, "emacs"))
195                 goto fail;
196         if (el_set(el, EL_BIND, "^W", "ed-delete-prev-word", NULL))
197                 goto fail;
198
199         /* ask terminal to not send signals */
200         if (flags & EC_EDITLINE_DISABLE_SIGNALS) {
201                 if (el_set(el, EL_SETTY, "-d", "-isig", NULL))
202                         goto fail;
203         }
204
205         /* set prompt */
206         editline->prompt = ec_strdup("> ");
207         if (editline->prompt == NULL)
208                 goto fail;
209         if (el_set(el, EL_PROMPT, prompt_cb))
210                 goto fail;
211
212         /* set up history */
213         if ((flags & EC_EDITLINE_DISABLE_HISTORY) == 0) {
214                 if (ec_editline_set_history(
215                                 editline, EC_EDITLINE_HISTORY_SIZE) < 0)
216                         goto fail;
217         }
218
219         /* register completion callback */
220         if ((flags & EC_EDITLINE_DISABLE_COMPLETION) == 0) {
221                 if (el_set(el, EL_ADDFN, "ed-complete", "Complete buffer",
222                                 ec_editline_complete))
223                         goto fail;
224                 if (el_set(el, EL_BIND, "^I", "ed-complete", NULL))
225                         goto fail;
226                 if (el_set(el, EL_BIND, "?", "ed-complete", NULL))
227                         goto fail;
228         }
229
230         return editline;
231
232 fail:
233         ec_editline_free(editline);
234         return NULL;
235 }
236
237 void ec_editline_free(struct ec_editline *editline)
238 {
239         if (editline == NULL)
240                 return;
241         if (editline->el != NULL)
242                 el_end(editline->el);
243         if (editline->history != NULL)
244                 history_end(editline->history);
245         ec_free(editline->prompt);
246         ec_free(editline);
247 }
248
249 EditLine *ec_editline_get_el(struct ec_editline *editline)
250 {
251         return editline->el;
252 }
253
254 const struct ec_node *
255 ec_editline_get_node(struct ec_editline *editline)
256 {
257         return editline->node;
258 }
259
260 void
261 ec_editline_set_node(struct ec_editline *editline, const struct ec_node *node)
262 {
263         editline->node = node;
264 }
265
266 int ec_editline_set_history(struct ec_editline *editline,
267         size_t hist_size)
268 {
269         EditLine *el = editline->el;
270
271         if (editline->history != NULL)
272                 history_end(editline->history);
273
274         if (hist_size == 0)
275                 return 0;
276
277         editline->history = history_init();
278         if (editline->history == NULL)
279                 goto fail;
280         if (history(editline->history, &editline->histev, H_SETSIZE,
281                         hist_size) < 0)
282                 goto fail;
283         if (history(editline->history, &editline->histev, H_SETUNIQUE, 1))
284                 goto fail;
285         if (el_set(el, EL_HIST, history, editline->history))
286                 goto fail;
287
288         return 0;
289
290 fail:
291         //XXX errno
292         if (editline->history != NULL) {
293                 history_end(editline->history);
294                 editline->history = NULL;
295         }
296         return -1;
297 }
298
299 void ec_editline_free_completions(char **matches, size_t len)
300 {
301         size_t i;
302
303         // XXX use ec_malloc/ec_free() instead for consistency
304         if (matches == NULL)
305                 return;
306         for (i = 0; i < len; i++)
307                 free(matches[i]);
308         free(matches);
309 }
310
311 ssize_t
312 ec_editline_get_completions(const struct ec_comp *cmpl, char ***matches_out)
313 {
314         const struct ec_comp_item *item;
315         struct ec_comp_iter *iter = NULL;
316         char **matches = NULL;
317         size_t count = 0;
318
319         iter = ec_comp_iter(cmpl, EC_COMP_FULL | EC_COMP_PARTIAL);
320         if (iter == NULL)
321                 goto fail;
322
323         while ((item = ec_comp_iter_next(iter)) != NULL) {
324                 char **tmp;
325
326                 tmp = realloc(matches, (count + 1) * sizeof(char *));
327                 if (tmp == NULL)
328                         goto fail;
329                 matches = tmp;
330                 matches[count] = strdup(ec_comp_item_get_display(item));
331                 if (matches[count] == NULL)
332                         goto fail;
333                 count++;
334         }
335
336         *matches_out = matches;
337         return count;
338
339 fail:
340         ec_editline_free_completions(matches, count);
341         *matches_out = NULL;
342         ec_comp_iter_free(iter);
343         return -1;
344 }
345
346 char *
347 ec_editline_append_chars(const struct ec_comp *cmpl)
348 {
349         const struct ec_comp_item *item;
350         struct ec_comp_iter *iter = NULL;
351         const char *append;
352         char *ret = NULL;
353         size_t n;
354
355         iter = ec_comp_iter(cmpl, EC_COMP_FULL | EC_COMP_PARTIAL);
356         if (iter == NULL)
357                 goto fail;
358
359         while ((item = ec_comp_iter_next(iter)) != NULL) {
360                 append = ec_comp_item_get_completion(item);
361                 if (ret == NULL) {
362                         ret = ec_strdup(append);
363                         if (ret == NULL)
364                                 goto fail;
365                 } else {
366                         n = ec_strcmp_count(ret, append);
367                         ret[n] = '\0';
368                 }
369         }
370         ec_comp_iter_free(iter);
371
372         return ret;
373
374 fail:
375         ec_comp_iter_free(iter);
376         ec_free(ret);
377
378         return NULL;
379 }
380
381 /* this function builds the help string */
382 static int get_node_help(const struct ec_comp_item *item,
383                         struct ec_editline_help *help)
384 {
385         const struct ec_comp_group *grp;
386         const struct ec_parse *state;
387         const struct ec_node *node;
388         const char *node_help = NULL;
389         const char *node_desc = NULL;
390
391         help->desc = NULL;
392         help->help = NULL;
393
394         grp = ec_comp_item_get_grp(item);
395
396         for (state = grp->state; state != NULL;
397              state = ec_parse_get_parent(state)) {
398                 node = ec_parse_get_node(state);
399                 if (node_help == NULL)
400                         node_help = ec_keyval_get(ec_node_attrs(node), "help");
401                 if (node_desc == NULL)
402                         node_desc = ec_node_desc(node);
403         }
404
405         if (node_help == NULL)
406                 node_help = "";
407         if (node_desc == NULL)
408                 goto fail;
409
410         help->desc = ec_strdup(node_desc);
411         if (help->desc == NULL)
412                 goto fail;
413
414         help->help = ec_strdup(node_help);
415         if (help->help == NULL)
416                 goto fail;
417
418         return 0;
419
420 fail:
421         ec_free(help->desc);
422         ec_free(help->help);
423         return -1;
424 }
425
426 ssize_t
427 ec_editline_get_helps(const struct ec_editline *editline, const char *line,
428         const char *full_line, struct ec_editline_help **helps_out)
429 {
430         struct ec_comp_iter *iter = NULL;
431         const struct ec_comp_group *grp, *prev_grp = NULL;
432         const struct ec_comp_item *item;
433         struct ec_comp *cmpl = NULL;
434         struct ec_parse *parse = NULL;
435         unsigned int count = 0;
436         struct ec_editline_help *helps = NULL;
437
438         *helps_out = NULL;
439
440         /* check if the current line matches */
441         parse = ec_node_parse(editline->node, full_line);
442         if (ec_parse_matches(parse))
443                 count = 1;
444         ec_parse_free(parse);
445         parse = NULL;
446
447         /* complete at current cursor position */
448         cmpl = ec_node_complete(editline->node, line);
449         if (cmpl == NULL) //XXX log error
450                 goto fail;
451
452         /* let's display one contextual help per node */
453         iter = ec_comp_iter(cmpl,
454                 EC_COMP_UNKNOWN | EC_COMP_FULL | EC_COMP_PARTIAL);
455         if (iter == NULL)
456                 goto fail;
457
458         helps = ec_calloc(1, sizeof(*helps));
459         if (helps == NULL)
460                 goto fail;
461         if (count == 1) {
462                 helps[0].desc = ec_strdup("<return>");
463                 if (helps[0].desc == NULL)
464                         goto fail;
465                 helps[0].help = ec_strdup("Validate command.");
466                 if (helps[0].help == NULL)
467                         goto fail;
468         }
469
470         while ((item = ec_comp_iter_next(iter)) != NULL) {
471                 struct ec_editline_help *tmp = NULL;
472
473                 /* keep one help per group, skip other items  */
474                 grp = ec_comp_item_get_grp(item);
475                 if (grp == prev_grp)
476                         continue;
477
478                 prev_grp = grp;
479
480                 tmp = ec_realloc(helps, (count + 1) * sizeof(*helps));
481                 if (tmp == NULL)
482                         goto fail;
483                 helps = tmp;
484                 if (get_node_help(item, &helps[count]) < 0)
485                         goto fail;
486                 count++;
487         }
488
489         ec_comp_iter_free(iter);
490         ec_comp_free(cmpl);
491         *helps_out = helps;
492
493         return count;
494
495 fail:
496         ec_comp_iter_free(iter);
497         ec_parse_free(parse);
498         ec_comp_free(cmpl);
499         if (helps != NULL) {
500                 while (count--) {
501                         ec_free(helps[count].desc);
502                         ec_free(helps[count].help);
503                 }
504                 ec_free(helps);
505         }
506
507         return -1;
508 }
509
510 int
511 ec_editline_complete(EditLine *el, int c)
512 {
513         struct ec_editline *editline;
514         const LineInfo *line_info;
515         int ret = CC_REFRESH;
516         struct ec_comp *cmpl = NULL;
517         char *append = NULL;
518         char *line = NULL;
519         void *clientdata;
520         FILE *out, *err;
521         int len;
522
523         if (el_get(el, EL_GETFP, 1, &out))
524                 return -1;
525         if (el_get(el, EL_GETFP, 1, &err))
526                 return -1;
527
528         (void)c;
529
530         if (el_get(el, EL_CLIENTDATA, &clientdata)) {
531                 fprintf(err, "completion failure: no client data\n");
532                 goto fail;
533         }
534         editline = clientdata;
535         (void)editline;
536
537         line_info = el_line(el);
538         if (line_info == NULL) {
539                 fprintf(err, "completion failure: no line info\n");
540                 goto fail;
541         }
542
543         len = line_info->cursor - line_info->buffer;
544         if (ec_asprintf(&line, "%.*s", len, line_info->buffer) < 0) {
545                 fprintf(err, "completion failure: no memory\n");
546                 goto fail;
547         }
548
549         if (editline->node == NULL) {
550                 fprintf(err, "completion failure: no ec_node\n");
551                 goto fail;
552         }
553
554         cmpl = ec_node_complete(editline->node, line);
555         if (cmpl == NULL)
556                 goto fail;
557
558         append = ec_editline_append_chars(cmpl);
559
560         if (c == '?') {
561                 struct ec_editline_help *helps = NULL;
562                 ssize_t count = 0;
563
564                 count = ec_editline_get_helps(editline, line, line_info->buffer,
565                                 &helps);
566
567                 fprintf(out, "\n");
568                 if (ec_editline_print_helps(editline, helps, count) < 0) {
569                         fprintf(err, "completion failure: cannot show help\n");
570                         ec_editline_free_helps(helps, count);
571                         goto fail;
572                 }
573
574                 ec_editline_free_helps(helps, count);
575                 ret = CC_REDISPLAY;
576         } else if (append == NULL || strcmp(append, "") == 0) {
577                 char **matches = NULL;
578                 ssize_t count = 0;
579
580                 count = ec_editline_get_completions(cmpl, &matches);
581                 if (count < 0) {
582                         fprintf(err, "completion failure: cannot get completions\n");
583                         goto fail;
584                 }
585
586                 if (ec_editline_print_cols(
587                                 editline,
588                                 EC_CAST(matches, char **,
589                                         char const * const *),
590                                 count) < 0) {
591                         fprintf(err, "completion failure: cannot print\n");
592                         ec_editline_free_completions(matches, count);
593                         goto fail;
594                 }
595
596                 ec_editline_free_completions(matches, count);
597                 ret = CC_REDISPLAY;
598         } else {
599                 if (el_insertstr(el, append) < 0) {
600                         fprintf(err, "completion failure: cannot insert\n");
601                         goto fail;
602                 }
603                 if (ec_comp_count(cmpl, EC_COMP_FULL) +
604                         ec_comp_count(cmpl, EC_COMP_PARTIAL) == 1) {
605                         if (el_insertstr(el, " ") < 0) {
606                                 fprintf(err, "completion failure: cannot insert space\n");
607                                 goto fail;
608                         }
609                 }
610         }
611
612         ec_comp_free(cmpl);
613         ec_free(line);
614         ec_free(append);
615
616         return ret;
617
618 fail:
619         ec_comp_free(cmpl);
620         ec_free(line);
621         ec_free(append);
622
623         return CC_ERROR;
624 }
625
626 char *
627 ec_editline_gets(struct ec_editline *editline)
628 {
629         EditLine *el = editline->el;
630         char *line_copy = NULL;
631         const char *line;
632         int count;
633
634         line = el_gets(el, &count);
635         if (line == NULL)
636                 return NULL;
637
638         line_copy = ec_strdup(line);
639         if (line_copy == NULL)
640                 goto fail;
641
642         line_copy[strlen(line_copy) - 1] = '\0'; //XXX needed because of sh_lex bug?
643
644         if (editline->history != NULL && !ec_str_is_space(line_copy)) {
645                 history(editline->history, &editline->histev,
646                         H_ENTER, line_copy);
647         }
648
649         return line_copy;
650
651 fail:
652         ec_free(line_copy);
653         return NULL;
654 }
655
656 struct ec_parse *
657 ec_editline_parse(struct ec_editline *editline, const struct ec_node *node)
658 {
659         char *line = NULL;
660         struct ec_parse *parse = NULL;
661
662         /* XXX add sh_lex automatically? This node is required, parse and
663          * complete are based on it. */
664
665         ec_editline_set_node(editline, node);
666
667         line = ec_editline_gets(editline);
668         if (line == NULL)
669                 goto fail;
670
671         parse = ec_node_parse(node, line);
672         if (parse == NULL)
673                 goto fail;
674
675         ec_free(line);
676         return parse;
677
678 fail:
679         ec_free(line);
680         ec_parse_free(parse);
681
682         return NULL;
683 }
684