api doc and minor changes
[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_dict.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         struct ec_comp_item *item;
315         char **matches = NULL;
316         size_t count = 0;
317
318         EC_COMP_FOREACH(item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) {
319                 char **tmp;
320
321                 tmp = realloc(matches, (count + 1) * sizeof(char *));
322                 if (tmp == NULL)
323                         goto fail;
324                 matches = tmp;
325                 matches[count] = strdup(ec_comp_item_get_display(item));
326                 if (matches[count] == NULL)
327                         goto fail;
328                 count++;
329         }
330
331         *matches_out = matches;
332         return count;
333
334 fail:
335         ec_editline_free_completions(matches, count);
336         *matches_out = NULL;
337         return -1;
338 }
339
340 char *
341 ec_editline_append_chars(const struct ec_comp *cmpl)
342 {
343         struct ec_comp_item *item;
344         const char *append;
345         char *ret = NULL;
346         size_t n;
347
348         EC_COMP_FOREACH(item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) {
349                 append = ec_comp_item_get_completion(item);
350                 if (ret == NULL) {
351                         ret = ec_strdup(append);
352                         if (ret == NULL)
353                                 goto fail;
354                 } else {
355                         n = ec_strcmp_count(ret, append);
356                         ret[n] = '\0';
357                 }
358         }
359
360         return ret;
361
362 fail:
363         ec_free(ret);
364
365         return NULL;
366 }
367
368 /* this function builds the help string */
369 static int get_node_help(const struct ec_comp_item *item,
370                         struct ec_editline_help *help)
371 {
372         const struct ec_comp_group *grp;
373         const struct ec_pnode *pstate;
374         const struct ec_node *node;
375         const char *node_help = NULL;
376         char *node_desc = NULL;
377
378         help->desc = NULL;
379         help->help = NULL;
380
381         grp = ec_comp_item_get_grp(item);
382
383         for (pstate = ec_comp_group_get_pstate(grp); pstate != NULL;
384              pstate = ec_pnode_get_parent(pstate)) {
385                 node = ec_pnode_get_node(pstate);
386                 if (node_help == NULL)
387                         node_help = ec_dict_get(ec_node_attrs(node), "help");
388                 if (node_desc == NULL) {
389                         node_desc = ec_node_desc(node);
390                         if (node_desc == NULL)
391                                 goto fail;
392                 }
393         }
394
395         if (node_desc == NULL)
396                 goto fail;
397         if (node_help == NULL)
398                 node_help = "";
399
400         help->desc = node_desc;
401         help->help = ec_strdup(node_help);
402         if (help->help == NULL)
403                 goto fail;
404
405         return 0;
406
407 fail:
408         ec_free(node_desc);
409         ec_free(help->desc);
410         ec_free(help->help);
411         return -1;
412 }
413
414 ssize_t
415 ec_editline_get_helps(const struct ec_editline *editline, const char *line,
416         const char *full_line, struct ec_editline_help **helps_out)
417 {
418         const struct ec_comp_group *grp, *prev_grp = NULL;
419         struct ec_comp_item *item;
420         struct ec_comp *cmpl = NULL;
421         struct ec_pnode *parse = NULL;
422         unsigned int count = 0;
423         struct ec_editline_help *helps = NULL;
424
425         *helps_out = NULL;
426
427         /* check if the current line matches */
428         parse = ec_parse(editline->node, full_line);
429         if (ec_pnode_matches(parse))
430                 count = 1;
431         ec_pnode_free(parse);
432         parse = NULL;
433
434         /* complete at current cursor position */
435         cmpl = ec_complete(editline->node, line);
436         if (cmpl == NULL) //XXX log error
437                 goto fail;
438
439         helps = ec_calloc(1, sizeof(*helps));
440         if (helps == NULL)
441                 goto fail;
442         if (count == 1) {
443                 helps[0].desc = ec_strdup("<return>");
444                 if (helps[0].desc == NULL)
445                         goto fail;
446                 helps[0].help = ec_strdup("Validate command.");
447                 if (helps[0].help == NULL)
448                         goto fail;
449         }
450
451         /* let's display one contextual help per node */
452         EC_COMP_FOREACH(item, cmpl,
453                         EC_COMP_UNKNOWN | EC_COMP_FULL | EC_COMP_PARTIAL) {
454                 struct ec_editline_help *tmp = NULL;
455
456                 /* keep one help per group, skip other items  */
457                 grp = ec_comp_item_get_grp(item);
458                 if (grp == prev_grp)
459                         continue;
460
461                 prev_grp = grp;
462
463                 tmp = ec_realloc(helps, (count + 1) * sizeof(*helps));
464                 if (tmp == NULL)
465                         goto fail;
466                 helps = tmp;
467                 if (get_node_help(item, &helps[count]) < 0)
468                         goto fail;
469                 count++;
470         }
471
472         ec_comp_free(cmpl);
473         *helps_out = helps;
474
475         return count;
476
477 fail:
478         ec_pnode_free(parse);
479         ec_comp_free(cmpl);
480         if (helps != NULL) {
481                 while (count--) {
482                         ec_free(helps[count].desc);
483                         ec_free(helps[count].help);
484                 }
485                 ec_free(helps);
486         }
487
488         return -1;
489 }
490
491 int
492 ec_editline_complete(EditLine *el, int c)
493 {
494         struct ec_editline *editline;
495         const LineInfo *line_info;
496         int ret = CC_REFRESH;
497         struct ec_comp *cmpl = NULL;
498         char *append = NULL;
499         char *line = NULL;
500         void *clientdata;
501         FILE *out, *err;
502         int len;
503
504         if (el_get(el, EL_GETFP, 1, &out))
505                 return -1;
506         if (el_get(el, EL_GETFP, 1, &err))
507                 return -1;
508
509         (void)c;
510
511         if (el_get(el, EL_CLIENTDATA, &clientdata)) {
512                 fprintf(err, "completion failure: no client data\n");
513                 goto fail;
514         }
515         editline = clientdata;
516         (void)editline;
517
518         line_info = el_line(el);
519         if (line_info == NULL) {
520                 fprintf(err, "completion failure: no line info\n");
521                 goto fail;
522         }
523
524         len = line_info->cursor - line_info->buffer;
525         if (ec_asprintf(&line, "%.*s", len, line_info->buffer) < 0) {
526                 fprintf(err, "completion failure: no memory\n");
527                 goto fail;
528         }
529
530         if (editline->node == NULL) {
531                 fprintf(err, "completion failure: no ec_node\n");
532                 goto fail;
533         }
534
535         cmpl = ec_complete(editline->node, line);
536         if (cmpl == NULL)
537                 goto fail;
538
539         append = ec_editline_append_chars(cmpl);
540
541         if (c == '?') {
542                 struct ec_editline_help *helps = NULL;
543                 ssize_t count = 0;
544
545                 count = ec_editline_get_helps(editline, line, line_info->buffer,
546                                 &helps);
547
548                 fprintf(out, "\n");
549                 if (ec_editline_print_helps(editline, helps, count) < 0) {
550                         fprintf(err, "completion failure: cannot show help\n");
551                         ec_editline_free_helps(helps, count);
552                         goto fail;
553                 }
554
555                 ec_editline_free_helps(helps, count);
556                 ret = CC_REDISPLAY;
557         } else if (append == NULL || strcmp(append, "") == 0) {
558                 char **matches = NULL;
559                 ssize_t count = 0;
560
561                 count = ec_editline_get_completions(cmpl, &matches);
562                 if (count < 0) {
563                         fprintf(err, "completion failure: cannot get completions\n");
564                         goto fail;
565                 }
566
567                 if (ec_editline_print_cols(
568                                 editline,
569                                 EC_CAST(matches, char **,
570                                         char const * const *),
571                                 count) < 0) {
572                         fprintf(err, "completion failure: cannot print\n");
573                         ec_editline_free_completions(matches, count);
574                         goto fail;
575                 }
576
577                 ec_editline_free_completions(matches, count);
578                 ret = CC_REDISPLAY;
579         } else {
580                 if (el_insertstr(el, append) < 0) {
581                         fprintf(err, "completion failure: cannot insert\n");
582                         goto fail;
583                 }
584                 if (ec_comp_count(cmpl, EC_COMP_FULL) +
585                         ec_comp_count(cmpl, EC_COMP_PARTIAL) == 1) {
586                         if (el_insertstr(el, " ") < 0) {
587                                 fprintf(err, "completion failure: cannot insert space\n");
588                                 goto fail;
589                         }
590                 }
591         }
592
593         ec_comp_free(cmpl);
594         ec_free(line);
595         ec_free(append);
596
597         return ret;
598
599 fail:
600         ec_comp_free(cmpl);
601         ec_free(line);
602         ec_free(append);
603
604         return CC_ERROR;
605 }
606
607 char *
608 ec_editline_gets(struct ec_editline *editline)
609 {
610         EditLine *el = editline->el;
611         char *line_copy = NULL;
612         const char *line;
613         int count;
614
615         line = el_gets(el, &count);
616         if (line == NULL)
617                 return NULL;
618
619         line_copy = ec_strdup(line);
620         if (line_copy == NULL)
621                 goto fail;
622
623         line_copy[strlen(line_copy) - 1] = '\0'; //XXX needed because of sh_lex bug?
624
625         if (editline->history != NULL && !ec_str_is_space(line_copy)) {
626                 history(editline->history, &editline->histev,
627                         H_ENTER, line_copy);
628         }
629
630         return line_copy;
631
632 fail:
633         ec_free(line_copy);
634         return NULL;
635 }
636
637 struct ec_pnode *
638 ec_editline_parse(struct ec_editline *editline, const struct ec_node *node)
639 {
640         char *line = NULL;
641         struct ec_pnode *parse = NULL;
642
643         /* XXX add sh_lex automatically? This node is required, parse and
644          * complete are based on it. */
645
646         ec_editline_set_node(editline, node);
647
648         line = ec_editline_gets(editline);
649         if (line == NULL)
650                 goto fail;
651
652         parse = ec_parse(node, line);
653         if (parse == NULL)
654                 goto fail;
655
656         ec_free(line);
657         return parse;
658
659 fail:
660         ec_free(line);
661         ec_pnode_free(parse);
662
663         return NULL;
664 }
665