Blob


1 /*
2 * Copyright (c) 2021, 2024 Omar Polo <op@omarpolo.com>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include "compat.h"
19 #include <sys/time.h>
21 #include <ctype.h>
22 #include <errno.h>
23 #include <limits.h>
24 #include <stdarg.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
29 #include <grapheme.h>
31 #include "certs.h"
32 #include "cmd.h"
33 #include "defaults.h"
34 #include "ev.h"
35 #include "fs.h"
36 #include "hist.h"
37 #include "iri.h"
38 #include "keymap.h"
39 #include "minibuffer.h"
40 #include "session.h"
41 #include "ui.h"
42 #include "utils.h"
43 #include "xwrapper.h"
45 #define nitems(x) (sizeof(x)/sizeof(x[0]))
47 static void *minibuffer_metadata(void);
48 static const char *minibuffer_compl_text(void);
49 static void minibuffer_hist_save_entry(void);
50 static void yornp_self_insert(void);
51 static void yornp_abort(void);
52 static void read_self_insert(void);
53 static void read_abort(void);
54 static void read_select(const char *);
55 static void handle_clear_echoarea(int, int, void *);
57 static unsigned long clechotimer;
58 static struct timeval clechotv = { 5, 0 };
60 static void (*yornp_cb)(int, void *);
61 static void *yornp_data;
63 static void (*read_cb)(const char*, struct tab *);
64 static struct tab *read_data;
66 struct hist *eecmd_history;
67 struct hist *ir_history;
68 struct hist *lu_history;
69 struct hist *read_history;
71 struct ministate ministate;
73 struct buffer minibufferwin;
75 int in_minibuffer;
77 static int
78 codepoint_isgraph(uint32_t cp)
79 {
80 if (cp < INT8_MAX)
81 return isgraph((unsigned char)cp);
82 return 1;
83 }
85 static inline int
86 matches(char **words, size_t len, struct line *l)
87 {
88 size_t i;
89 int lm, am;
91 for (i = 0; i < len; ++i) {
92 lm = am = 0;
94 if (strcasestr(l->line, words[i]) != NULL)
95 lm = 1;
96 if (l->alt != NULL &&
97 strcasestr(l->alt, words[i]) != NULL)
98 am = 1;
100 if (!lm && !am)
101 return 0;
104 return 1;
107 /*
108 * Recompute the visible completions. If add is 1, don't consider the
109 * ones already hidden.
110 */
111 void
112 recompute_completions(int add)
114 static char buf[GEMINI_URL_LEN];
115 const char *text;
116 char *input, **ap, *words[10];
117 size_t len = 0;
118 struct line *l;
119 struct vline *vl;
120 struct buffer *b;
122 if (in_minibuffer != MB_COMPREAD)
123 return;
125 if (!ministate.editing)
126 text = hist_cur(ministate.hist);
127 else
128 text = ministate.buf;
130 strlcpy(buf, text, sizeof(buf));
131 input = buf;
133 /* tokenize the input */
134 for (ap = words; ap < words + nitems(words) &&
135 (*ap = strsep(&input, " ")) != NULL;) {
136 if (**ap != '\0')
137 ap++, len++;
140 b = &ministate.compl.buffer;
141 TAILQ_FOREACH(l, &b->head, lines) {
142 l->type = LINE_COMPL;
143 if (add && l->flags & L_HIDDEN)
144 continue;
145 if (matches(words, len, l)) {
146 if (l->flags & L_HIDDEN)
147 b->line_max++;
148 l->flags &= ~L_HIDDEN;
149 } else {
150 if (!(l->flags & L_HIDDEN))
151 b->line_max--;
152 l->flags |= L_HIDDEN;
156 if (b->current_line == NULL)
157 b->current_line = TAILQ_FIRST(&b->vhead);
158 b->current_line = adjust_line(b->current_line, b);
159 vl = b->current_line;
160 if (ministate.compl.must_select && vl != NULL)
161 vl->parent->type = LINE_COMPL_CURRENT;
164 int
165 minibuffer_insert_current_candidate(void)
167 struct vline *vl;
169 vl = ministate.compl.buffer.current_line;
170 if (vl == NULL || vl->parent->flags & L_HIDDEN)
171 return -1;
173 minibuffer_taint_hist();
174 strlcpy(ministate.buf, vl->parent->line, sizeof(ministate.buf));
175 ministate.buffer.point_offset = strlen(ministate.buf);
176 ministate.vline.len = strlen(ministate.buf);
178 return 0;
181 static void *
182 minibuffer_metadata(void)
184 struct vline *vl;
186 vl = ministate.compl.buffer.current_line;
188 if (vl == NULL || vl->parent->flags & L_HIDDEN)
189 return NULL;
191 return vl->parent->data;
194 static const char *
195 minibuffer_compl_text(void)
197 struct vline *vl;
199 if (!ministate.editing)
200 return hist_cur(ministate.hist);
202 vl = ministate.compl.buffer.current_line;
203 if (vl == NULL || vl->parent->flags & L_HIDDEN ||
204 vl->parent->type == LINE_COMPL || vl->parent->line == NULL)
205 return ministate.buf;
206 return vl->parent->line;
209 static void
210 minibuffer_hist_save_entry(void)
212 if (ministate.hist == NULL)
213 return;
215 hist_append(ministate.hist, minibuffer_compl_text());
218 /*
219 * taint the minibuffer cache: if we're currently showing a history
220 * element, copy that to the current buf and reset the "history
221 * navigation" thing.
222 */
223 void
224 minibuffer_taint_hist(void)
226 if (ministate.editing)
227 return;
229 ministate.editing = 1;
230 strlcpy(ministate.buf, hist_cur(ministate.hist),
231 sizeof(ministate.buf));
232 ministate.buffer.point_offset = 0;
233 ministate.buffer.current_line->parent->line = ministate.buf;
236 void
237 minibuffer_confirm(void)
239 if (!in_minibuffer || ministate.donefn == NULL)
240 return;
242 minibuffer_taint_hist();
243 ministate.donefn(minibuffer_compl_text());
246 void
247 minibuffer_self_insert(void)
249 char *c, tmp[5] = {0};
250 size_t len;
252 minibuffer_taint_hist();
254 if (thiskey.cp == 0)
255 return;
257 len = grapheme_encode_utf8(thiskey.cp, tmp, sizeof(tmp));
259 c = ministate.buffer.current_line->parent->line
260 + ministate.buffer.current_line->from
261 + ministate.buffer.point_offset;
262 if (c + len > ministate.buf + sizeof(ministate.buf) - 1)
263 return;
265 memmove(c + len, c, strlen(c)+1);
266 memcpy(c, tmp, len);
267 ministate.buffer.point_offset += len;
268 ministate.vline.len += len;
270 recompute_completions(1);
273 void
274 sensible_self_insert(void)
276 if (thiskey.meta ||
277 (!codepoint_isgraph(thiskey.key) && thiskey.key != ' ')) {
278 global_key_unbound();
279 return;
282 minibuffer_self_insert();
285 void
286 eecmd_select(const char *t)
288 struct cmd *cmd;
290 for (cmd = cmds; cmd->cmd != NULL; ++cmd) {
291 if (!strcmp(cmd->cmd, t)) {
292 minibuffer_hist_save_entry();
293 exit_minibuffer();
294 cmd->fn(current_buffer());
295 return;
299 message("No match");
302 void
303 ir_select_gemini(const char *text)
305 static struct iri iri;
306 char buf[1025];
307 struct tab *tab = current_tab;
309 minibuffer_hist_save_entry();
311 if (iri_parse(NULL, hist_cur(tab->hist), &iri) == -1)
312 goto err;
313 if (iri_setquery(&iri, text) == -1)
314 goto err;
315 if (iri_unparse(&iri, buf, sizeof(buf)) == -1)
316 goto err;
318 exit_minibuffer();
319 load_url_in_tab(tab, buf, NULL, LU_MODE_NOCACHE);
320 return;
322 err:
323 message("Failed to select URL.");
326 void
327 ir_select_reply(const char *text)
329 static struct iri iri;
330 char buf[1025] = {0};
331 struct tab *tab = current_tab;
333 minibuffer_hist_save_entry();
335 /* a bit ugly but... */
336 iri_parse(NULL, tab->last_input_url, &iri);
337 iri_setquery(&iri, text);
338 iri_unparse(&iri, buf, sizeof(buf));
340 exit_minibuffer();
341 load_url_in_tab(tab, buf, NULL, LU_MODE_NOCACHE);
344 void
345 ir_select_gopher(const char *text)
347 minibuffer_hist_save_entry();
348 gopher_send_search_req(current_tab, text);
349 exit_minibuffer();
352 void
353 lu_select(const char *text)
355 char url[GEMINI_URL_LEN+1];
357 minibuffer_hist_save_entry();
358 humanify_url(text, hist_cur(current_tab->hist), url, sizeof(url));
360 exit_minibuffer();
361 load_url_in_tab(current_tab, url, NULL, LU_MODE_NOCACHE);
364 void
365 bp_select(const char *url)
367 if (*url != '\0') {
368 if (bookmark_page(url) == -1)
369 message("failed to bookmark page: %s",
370 strerror(errno));
371 else
372 message("Bookmarked");
373 } else
374 message("Abort.");
375 exit_minibuffer();
378 void
379 ts_select(const char *text)
381 struct tab *tab;
383 if ((tab = minibuffer_metadata()) == NULL) {
384 message("No tab selected");
385 return;
388 exit_minibuffer();
389 switch_to_tab(tab);
392 void
393 ls_select(const char *text)
395 struct line *l;
397 if ((l = minibuffer_metadata()) == NULL) {
398 message("No link selected");
399 return;
402 exit_minibuffer();
403 load_url_in_tab(current_tab, l->alt, NULL, LU_MODE_NOCACHE);
406 static inline void
407 jump_to_line(struct line *l)
409 struct vline *vl;
410 struct buffer *buffer;
412 buffer = current_buffer();
414 TAILQ_FOREACH(vl, &buffer->vhead, vlines) {
415 if (vl->parent == l)
416 break;
419 if (vl == NULL)
420 message("Ops, %s error! Please report to %s",
421 __func__, PACKAGE_BUGREPORT);
422 else {
423 buffer->top_line = vl;
424 buffer->current_line = vl;
428 void
429 swiper_select(const char *text)
431 struct line *l;
433 if ((l = minibuffer_metadata()) == NULL) {
434 message("No line selected");
435 return;
438 exit_minibuffer();
439 jump_to_line(l);
442 void
443 toc_select(const char *text)
445 struct line *l;
447 if ((l = minibuffer_metadata()) == NULL) {
448 message("No line selected");
449 return;
452 exit_minibuffer();
453 jump_to_line(l);
456 static void
457 save_cert_for_site_cb(int r, void *data)
459 struct tab *tab = data;
461 cert_save_for(tab->client_cert, &tab->iri, r);
464 void
465 uc_select(const char *name)
467 if ((current_tab->client_cert = ccert(name)) == NULL) {
468 message("Certificate %s not found", name);
469 return;
472 exit_minibuffer();
474 yornp("Remember for future sessions too?", save_cert_for_site_cb,
475 current_tab);
478 void
479 search_select(const char *text)
481 static struct iri iri;
482 static char buf[1025];
484 /* a bit ugly but... */
485 if (iri_parse(NULL, default_search_engine, &iri) == -1) {
486 message("default-search-engine is a malformed IRI.");
487 exit_minibuffer();
488 return;
490 iri_setquery(&iri, text);
491 iri_unparse(&iri, buf, sizeof(buf));
493 exit_minibuffer();
494 load_url_in_tab(current_tab, buf, NULL, LU_MODE_NOCACHE);
497 static void
498 yornp_self_insert(void)
500 if (thiskey.key != 'y' && thiskey.key != 'n') {
501 message("Please answer y or n");
502 return;
505 exit_minibuffer();
506 yornp_cb(thiskey.key == 'y', yornp_data);
509 static void
510 yornp_abort(void)
512 exit_minibuffer();
513 yornp_cb(0, yornp_data);
516 static void
517 read_self_insert(void)
519 if (thiskey.meta || !codepoint_isgraph(thiskey.cp)) {
520 global_key_unbound();
521 return;
524 minibuffer_self_insert();
527 static void
528 read_abort(void)
530 exit_minibuffer();
531 read_cb(NULL, read_data);
534 static void
535 read_select(const char *text)
537 exit_minibuffer();
538 minibuffer_hist_save_entry();
539 read_cb(text, read_data);
542 /*
543 * TODO: we should collect this asynchronously...
544 */
545 static inline void
546 populate_compl_buffer(complfn *fn, void *data)
548 const char *s, *descr;
549 struct line *l;
550 struct buffer *b;
551 void *linedata;
553 b = &ministate.compl.buffer;
555 linedata = NULL;
556 descr = NULL;
557 while ((s = fn(&data, &linedata, &descr)) != NULL) {
558 l = xcalloc(1, sizeof(*l));
560 l->type = LINE_COMPL;
561 l->data = linedata;
562 l->alt = (char*)descr;
563 l->line = xstrdup(s);
565 TAILQ_INSERT_TAIL(&b->head, l, lines);
567 linedata = NULL;
568 descr = NULL;
571 if ((l = TAILQ_FIRST(&b->head)) != NULL &&
572 ministate.compl.must_select)
573 l->type = LINE_COMPL_CURRENT;
576 void
577 enter_minibuffer(struct minibuffer *minibuffer, const char *fmt, ...)
579 va_list ap;
581 va_start(ap, fmt);
582 vsnprintf(ministate.prompt, sizeof(ministate.prompt), fmt, ap);
583 va_end(ap);
585 ministate.compl.must_select = minibuffer->must_select;
586 ministate.compl.fn = minibuffer->complfn;
587 ministate.compl.data = minibuffer->compldata;
589 in_minibuffer = minibuffer->complfn == NULL ? MB_READ : MB_COMPREAD;
590 if (in_minibuffer == MB_COMPREAD) {
591 populate_compl_buffer(minibuffer->complfn,
592 minibuffer->compldata);
593 ui_schedule_redraw();
596 base_map = &minibuffer_map;
597 current_map = &minibuffer_map;
599 base_map->unhandled_input = minibuffer->self_insert;
601 ministate.donefn = minibuffer->done;
602 ministate.abortfn = minibuffer->abort;
603 if (ministate.abortfn == NULL)
604 ministate.abortfn = exit_minibuffer;
606 if (minibuffer->input) {
607 strlcpy(ministate.buf, minibuffer->input,
608 sizeof(ministate.buf));
609 ministate.buffer.point_offset = strlen(ministate.buf);
610 ministate.vline.len = strlen(ministate.buf);
611 } else {
612 ministate.buf[0] = '\0';
613 ministate.buffer.point_offset = 0;
614 ministate.vline.len = 0;
617 ministate.buffer.current_line = &ministate.vline;
618 ministate.buffer.current_line->parent->line = ministate.buf;
620 ministate.editing = 1;
621 ministate.hist = minibuffer->history;
622 if (ministate.hist)
623 hist_seek_start(ministate.hist);
626 void
627 exit_minibuffer(void)
629 if (in_minibuffer == MB_COMPREAD) {
630 erase_buffer(&ministate.compl.buffer);
631 ui_schedule_redraw();
634 in_minibuffer = 0;
635 base_map = &global_map;
636 current_map = &global_map;
639 void
640 yornp(const char *prompt, void (*fn)(int, void*), void *data)
642 struct minibuffer m = {
643 .self_insert = yornp_self_insert,
644 .abort = yornp_abort,
645 };
647 if (in_minibuffer) {
648 fn(0, data);
649 return;
652 yornp_cb = fn;
653 yornp_data = data;
654 enter_minibuffer(&m, "%s (y or n)", prompt);
657 void
658 minibuffer_read(const char *prompt, void (*fn)(const char *, struct tab *),
659 struct tab *data, const char *input)
661 struct minibuffer m = {
662 .self_insert = read_self_insert,
663 .done = read_select,
664 .abort = read_abort,
665 .history = read_history,
666 .input = input,
667 };
669 if (in_minibuffer)
670 return;
672 read_cb = fn;
673 read_data = data;
674 enter_minibuffer(&m, "%s: ", prompt);
677 static void
678 handle_clear_echoarea(int fd, int ev, void *d)
680 free(ministate.curmesg);
681 ministate.curmesg = NULL;
683 ui_after_message_hook();
686 void
687 vmessage(const char *fmt, va_list ap)
689 ev_timer_cancel(clechotimer);
690 clechotimer = 0;
692 free(ministate.curmesg);
693 ministate.curmesg = NULL;
695 if (fmt != NULL) {
696 clechotimer = ev_timer(&clechotv, handle_clear_echoarea,
697 NULL);
699 /* TODO: what to do if the allocation fails here? */
700 if (vasprintf(&ministate.curmesg, fmt, ap) == -1)
701 ministate.curmesg = NULL;
704 ui_after_message_hook();
707 void
708 message(const char *fmt, ...)
710 va_list ap;
712 va_start(ap, fmt);
713 vmessage(fmt, ap);
714 va_end(ap);
717 void
718 minibuffer_init(void)
720 if ((eecmd_history = hist_new(HIST_WRAP)) == NULL ||
721 (ir_history = hist_new(HIST_WRAP)) == NULL ||
722 (lu_history = hist_new(HIST_WRAP)) == NULL ||
723 (read_history = hist_new(HIST_WRAP)) == NULL)
724 err(1, "hist_new");
726 TAILQ_INIT(&ministate.compl.buffer.head);
727 TAILQ_INIT(&ministate.compl.buffer.vhead);
729 ministate.line.type = LINE_TEXT;
730 ministate.vline.parent = &ministate.line;
731 ministate.buffer.mode = "*minibuffer*";
732 ministate.buffer.current_line = &ministate.vline;