1 #include "url-mode.h"
2
3 #include <stdlib.h>
4 #include <string.h>
5 #include <wctype.h>
6 #include <unistd.h>
7
8 #include <sys/stat.h>
9 #include <fcntl.h>
10
11 #define LOG_MODULE "url-mode"
12 #define LOG_ENABLE_DBG 0
13 #include "log.h"
14 #include "grid.h"
15 #include "render.h"
16 #include "selection.h"
17 #include "spawn.h"
18 #include "terminal.h"
19 #include "uri.h"
20 #include "util.h"
21 #include "xmalloc.h"
22
23 static void url_destroy(struct url *url);
24
25 static bool
execute_binding(struct seat * seat,struct terminal * term,enum bind_action_url action,uint32_t serial)26 execute_binding(struct seat *seat, struct terminal *term,
27 enum bind_action_url action, uint32_t serial)
28 {
29 switch (action) {
30 case BIND_ACTION_URL_NONE:
31 return false;
32
33 case BIND_ACTION_URL_CANCEL:
34 urls_reset(term);
35 return true;
36
37 case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL:
38 term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label;
39 render_refresh_urls(term);
40 return true;
41
42 case BIND_ACTION_URL_COUNT:
43 return false;
44
45 }
46 return true;
47 }
48
49 static void
activate_url(struct seat * seat,struct terminal * term,const struct url * url)50 activate_url(struct seat *seat, struct terminal *term, const struct url *url)
51 {
52 char *url_string = NULL;
53
54 char *scheme, *host, *path;
55 if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL,
56 &host, NULL, &path, NULL, NULL))
57 {
58 if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) {
59 /*
60 * This is a file in *this* computer. Pass only the
61 * filename to the URL-launcher.
62 *
63 * I.e. strip the ‘file://user@host/’ prefix.
64 */
65 url_string = path;
66 } else
67 free(path);
68
69 free(scheme);
70 free(host);
71 }
72
73 if (url_string == NULL)
74 url_string = xstrdup(url->url);
75
76 switch (url->action) {
77 case URL_ACTION_COPY:
78 if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) {
79 /* Now owned by our clipboard “manager” */
80 url_string = NULL;
81 }
82 break;
83
84 case URL_ACTION_LAUNCH: {
85 size_t argc;
86 char **argv;
87
88 int dev_null = open("/dev/null", O_RDWR);
89
90 if (dev_null < 0) {
91 LOG_ERRNO("failed to open /dev/null");
92 break;
93 }
94
95 if (spawn_expand_template(
96 &term->conf->url.launch, 1,
97 (const char *[]){"url"},
98 (const char *[]){url_string},
99 &argc, &argv))
100 {
101 spawn(term->reaper, term->cwd, argv, dev_null, dev_null, dev_null);
102
103 for (size_t i = 0; i < argc; i++)
104 free(argv[i]);
105 free(argv);
106 }
107
108 close(dev_null);
109 break;
110 }
111 }
112
113 free(url_string);
114 }
115
116 void
urls_input(struct seat * seat,struct terminal * term,uint32_t key,xkb_keysym_t sym,xkb_mod_mask_t mods,xkb_mod_mask_t consumed,const xkb_keysym_t * raw_syms,size_t raw_count,uint32_t serial)117 urls_input(struct seat *seat, struct terminal *term, uint32_t key,
118 xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed,
119 const xkb_keysym_t *raw_syms, size_t raw_count,
120 uint32_t serial)
121 {
122 /* Key bindings */
123 tll_foreach(seat->kbd.bindings.url, it) {
124 const struct key_binding *bind = &it->item;
125
126 /* Match translated symbol */
127 if (bind->sym == sym &&
128 bind->mods == (mods & ~consumed))
129 {
130 execute_binding(seat, term, bind->action, serial);
131 return;
132 }
133
134 if (bind->mods != mods)
135 continue;
136
137 for (size_t i = 0; i < raw_count; i++) {
138 if (bind->sym == raw_syms[i]) {
139 execute_binding(seat, term, bind->action, serial);
140 return;
141 }
142 }
143
144 /* Match raw key code */
145 tll_foreach(bind->key_codes, code) {
146 if (code->item == key) {
147 execute_binding(seat, term, bind->action, serial);
148 return;
149 }
150 }
151 }
152
153 size_t seq_len = wcslen(term->url_keys);
154
155 if (sym == XKB_KEY_BackSpace) {
156 if (seq_len > 0) {
157 term->url_keys[seq_len - 1] = L'\0';
158 render_refresh_urls(term);
159 }
160
161 return;
162 }
163
164 if (mods & ~consumed)
165 return;
166
167 wchar_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key);
168
169 /*
170 * Determine if this is a “valid” key. I.e. if there is an URL
171 * label with a key combo where this key is the next in
172 * sequence.
173 */
174
175 bool is_valid = false;
176 const struct url *match = NULL;
177
178 tll_foreach(term->urls, it) {
179 if (it->item.key == NULL)
180 continue;
181
182 const struct url *url = &it->item;
183 const size_t key_len = wcslen(it->item.key);
184
185 if (key_len >= seq_len + 1 &&
186 wcsncasecmp(url->key, term->url_keys, seq_len) == 0 &&
187 towlower(url->key[seq_len]) == towlower(wc))
188 {
189 is_valid = true;
190 if (key_len == seq_len + 1) {
191 match = url;
192 break;
193 }
194 }
195 }
196
197 if (match) {
198 activate_url(seat, term, match);
199 urls_reset(term);
200 }
201
202 else if (is_valid) {
203 xassert(seq_len + 1 <= ALEN(term->url_keys));
204 term->url_keys[seq_len] = wc;
205 render_refresh_urls(term);
206 }
207 }
208
209 static int
wccmp(const void * _a,const void * _b)210 wccmp(const void *_a, const void *_b)
211 {
212 const wchar_t *a = _a;
213 const wchar_t *b = _b;
214 return *a - *b;
215 }
216
217 static void
auto_detected(const struct terminal * term,enum url_action action,url_list_t * urls)218 auto_detected(const struct terminal *term, enum url_action action,
219 url_list_t *urls)
220 {
221 const struct config *conf = term->conf;
222
223 const wchar_t *uri_characters = conf->url.uri_characters;
224 if (uri_characters == NULL)
225 return;
226
227 const size_t uri_characters_count = wcslen(uri_characters);
228 if (uri_characters_count == 0)
229 return;
230
231 size_t max_prot_len = conf->url.max_prot_len;
232 wchar_t proto_chars[max_prot_len];
233 struct coord proto_start[max_prot_len];
234 size_t proto_char_count = 0;
235
236 enum {
237 STATE_PROTOCOL,
238 STATE_URL,
239 } state = STATE_PROTOCOL;
240
241 struct coord start = {-1, -1};
242 wchar_t url[term->cols * term->rows + 1];
243 size_t len = 0;
244
245 ssize_t parenthesis = 0;
246 ssize_t brackets = 0;
247 ssize_t ltgts = 0;
248
249 for (int r = 0; r < term->rows; r++) {
250 const struct row *row = grid_row_in_view(term->grid, r);
251
252 for (int c = 0; c < term->cols; c++) {
253 const struct cell *cell = &row->cells[c];
254 wchar_t wc = cell->wc;
255
256 switch (state) {
257 case STATE_PROTOCOL:
258 for (size_t i = 0; i < max_prot_len - 1; i++) {
259 proto_chars[i] = proto_chars[i + 1];
260 proto_start[i] = proto_start[i + 1];
261 }
262
263 if (proto_char_count >= max_prot_len)
264 proto_char_count = max_prot_len - 1;
265
266 proto_chars[max_prot_len - 1] = wc;
267 proto_start[max_prot_len - 1] = (struct coord){c, r};
268 proto_char_count++;
269
270 for (size_t i = 0; i < conf->url.prot_count; i++) {
271 size_t prot_len = wcslen(conf->url.protocols[i]);
272
273 if (proto_char_count < prot_len)
274 continue;
275
276 const wchar_t *proto = &proto_chars[max_prot_len - prot_len];
277
278 if (wcsncasecmp(conf->url.protocols[i], proto, prot_len) == 0) {
279 state = STATE_URL;
280 start = proto_start[max_prot_len - prot_len];
281
282 wcsncpy(url, proto, prot_len);
283 len = prot_len;
284
285 parenthesis = brackets = ltgts = 0;
286 break;
287 }
288 }
289 break;
290
291 case STATE_URL: {
292 const wchar_t *match = bsearch(
293 &wc,
294 uri_characters,
295 uri_characters_count,
296 sizeof(uri_characters[0]),
297 &wccmp);
298
299 bool emit_url = false;
300
301 if (match == NULL) {
302 /*
303 * Character is not a valid URI character. Emit
304 * the URL we’ve collected so far, *without*
305 * including _this_ character.
306 */
307 emit_url = true;
308 } else {
309 xassert(*match == wc);
310
311 switch (wc) {
312 default:
313 url[len++] = wc;
314 break;
315
316 case L'(':
317 parenthesis++;
318 url[len++] = wc;
319 break;
320
321 case L'[':
322 brackets++;
323 url[len++] = wc;
324 break;
325
326 case L'<':
327 ltgts++;
328 url[len++] = wc;
329 break;
330
331 case L')':
332 if (--parenthesis < 0)
333 emit_url = true;
334 else
335 url[len++] = wc;
336 break;
337
338 case L']':
339 if (--brackets < 0)
340 emit_url = true;
341 else
342 url[len++] = wc;
343 break;
344
345 case L'>':
346 if (--ltgts < 0)
347 emit_url = true;
348 else
349 url[len++] = wc;
350 break;
351 }
352 }
353
354 if (c >= term->cols - 1 && row->linebreak) {
355 /*
356 * Endpoint is inclusive, and we’ll be subtracting
357 * 1 from the column when emitting the URL.
358 */
359 c++;
360 emit_url = true;
361 }
362
363 if (emit_url) {
364 struct coord end = {c, r};
365
366 if (--end.col < 0) {
367 end.row--;
368 end.col = term->cols - 1;
369 }
370
371 /* Heuristic to remove trailing characters that
372 * are valid URL characters, but typically not at
373 * the end of the URL */
374 bool done = false;
375 do {
376 switch (url[len - 1]) {
377 case L'.': case L',': case L':': case L';': case L'?':
378 case L'!': case L'"': case L'\'': case L'%':
379 len--;
380 end.col--;
381 if (end.col < 0) {
382 end.row--;
383 end.col = term->cols - 1;
384 }
385 break;
386
387 default:
388 done = true;
389 break;
390 }
391 } while (!done);
392
393 url[len] = L'\0';
394
395 start.row += term->grid->view;
396 end.row += term->grid->view;
397
398 size_t chars = wcstombs(NULL, url, 0);
399 if (chars != (size_t)-1) {
400 char *url_utf8 = xmalloc((chars + 1) * sizeof(wchar_t));
401 wcstombs(url_utf8, url, chars + 1);
402
403 tll_push_back(
404 *urls,
405 ((struct url){
406 .id = (uint64_t)rand() << 32 | rand(),
407 .url = url_utf8,
408 .start = start,
409 .end = end,
410 .action = action,
411 .osc8 = false}));
412 }
413
414 state = STATE_PROTOCOL;
415 len = 0;
416 parenthesis = brackets = ltgts = 0;
417 }
418 break;
419 }
420 }
421 }
422 }
423 }
424
425 static void
osc8_uris(const struct terminal * term,enum url_action action,url_list_t * urls)426 osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls)
427 {
428 bool dont_touch_url_attr = false;
429
430 switch (term->conf->url.osc8_underline) {
431 case OSC8_UNDERLINE_URL_MODE:
432 dont_touch_url_attr = false;
433 break;
434
435 case OSC8_UNDERLINE_ALWAYS:
436 dont_touch_url_attr = true;
437 break;
438 }
439
440 for (int r = 0; r < term->rows; r++) {
441 const struct row *row = grid_row_in_view(term->grid, r);
442 const struct row_data *extra = row->extra;
443
444 if (extra == NULL)
445 continue;
446
447 for (size_t i = 0; i < extra->uri_ranges.count; i++) {
448 const struct row_uri_range *range = &extra->uri_ranges.v[i];
449
450 struct coord start = {
451 .col = range->start,
452 .row = r + term->grid->view,
453 };
454 struct coord end = {
455 .col = range->end,
456 .row = r + term->grid->view,
457 };
458 tll_push_back(
459 *urls,
460 ((struct url){
461 .id = range->id,
462 .url = xstrdup(range->uri),
463 .start = start,
464 .end = end,
465 .action = action,
466 .url_mode_dont_change_url_attr = dont_touch_url_attr,
467 .osc8 = true}));
468 }
469 }
470 }
471
472 static void
remove_overlapping(url_list_t * urls,int cols)473 remove_overlapping(url_list_t *urls, int cols)
474 {
475 tll_foreach(*urls, outer) {
476 tll_foreach(*urls, inner) {
477 if (outer == inner)
478 continue;
479
480 const struct url *out = &outer->item;
481 const struct url *in = &inner->item;
482
483 uint64_t in_start = in->start.row * cols + in->start.col;
484 uint64_t in_end = in->end.row * cols + in->end.col;
485
486 uint64_t out_start = out->start.row * cols + out->start.col;
487 uint64_t out_end = out->end.row * cols + out->end.col;
488
489 if ((in_start <= out_start && in_end >= out_start) ||
490 (in_start <= out_end && in_end >= out_end) ||
491 (in_start >= out_start && in_end <= out_end))
492 {
493 /*
494 * OSC-8 URLs can’t overlap with each
495 * other.
496 *
497 * Similarly, auto-detected URLs cannot overlap with
498 * each other.
499 *
500 * But OSC-8 URLs can overlap with auto-detected ones.
501 */
502 xassert(in->osc8 || out->osc8);
503
504 if (in->osc8)
505 outer->item.duplicate = true;
506 else
507 inner->item.duplicate = true;
508 }
509 }
510 }
511
512 tll_foreach(*urls, it) {
513 if (it->item.duplicate) {
514 url_destroy(&it->item);
515 tll_remove(*urls, it);
516 }
517 }
518 }
519
520 void
urls_collect(const struct terminal * term,enum url_action action,url_list_t * urls)521 urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls)
522 {
523 xassert(tll_length(term->urls) == 0);
524 osc8_uris(term, action, urls);
525 auto_detected(term, action, urls);
526 remove_overlapping(urls, term->grid->num_cols);
527 }
528
529 static int
wcscmp_qsort_wrapper(const void * _a,const void * _b)530 wcscmp_qsort_wrapper(const void *_a, const void *_b)
531 {
532 const wchar_t *a = *(const wchar_t **)_a;
533 const wchar_t *b = *(const wchar_t **)_b;
534 return wcscmp(a, b);
535 }
536
537 static void
generate_key_combos(const struct config * conf,size_t count,wchar_t * combos[static count])538 generate_key_combos(const struct config *conf,
539 size_t count, wchar_t *combos[static count])
540 {
541 const wchar_t *alphabet = conf->url.label_letters;
542 const size_t alphabet_len = wcslen(alphabet);
543
544 size_t hints_count = 1;
545 wchar_t **hints = xmalloc(hints_count * sizeof(hints[0]));
546
547 hints[0] = xwcsdup(L"");
548
549 size_t offset = 0;
550 do {
551 const wchar_t *prefix = hints[offset++];
552 const size_t prefix_len = wcslen(prefix);
553
554 hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0]));
555
556 const wchar_t *wc = &alphabet[0];
557 for (size_t i = 0; i < alphabet_len; i++, wc++) {
558 wchar_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(wchar_t));
559 hints[hints_count + i] = hint;
560
561 /* Will be reversed later */
562 hint[0] = *wc;
563 wcscpy(&hint[1], prefix);
564 }
565 hints_count += alphabet_len;
566 } while (hints_count - offset < count);
567
568 xassert(hints_count - offset >= count);
569
570 /* Copy slice of ‘hints’ array to the caller provided array */
571 for (size_t i = 0; i < hints_count; i++) {
572 if (i >= offset && i < offset + count)
573 combos[i - offset] = hints[i];
574 else
575 free(hints[i]);
576 }
577 free(hints);
578
579 /* Sorting is a kind of shuffle, since we’re sorting on the
580 * *reversed* strings */
581 qsort(combos, count, sizeof(wchar_t *), &wcscmp_qsort_wrapper);
582
583 /* Reverse all strings */
584 for (size_t i = 0; i < count; i++) {
585 const size_t len = wcslen(combos[i]);
586 for (size_t j = 0; j < len / 2; j++) {
587 wchar_t tmp = combos[i][j];
588 combos[i][j] = combos[i][len - j - 1];
589 combos[i][len - j - 1] = tmp;
590 }
591 }
592 }
593
594 void
urls_assign_key_combos(const struct config * conf,url_list_t * urls)595 urls_assign_key_combos(const struct config *conf, url_list_t *urls)
596 {
597 const size_t count = tll_length(*urls);
598 if (count == 0)
599 return;
600
601 uint64_t seen_ids[count];
602 wchar_t *combos[count];
603 generate_key_combos(conf, count, combos);
604
605 size_t combo_idx = 0;
606 size_t id_idx = 0;
607
608 tll_foreach(*urls, it) {
609 bool id_already_seen = false;
610
611 for (size_t i = 0; i < id_idx; i++) {
612 if (it->item.id == seen_ids[i]) {
613 id_already_seen = true;
614 break;
615 }
616 }
617
618 if (id_already_seen)
619 continue;
620 seen_ids[id_idx++] = it->item.id;
621
622 /*
623 * Scan previous URLs, and check if *this* URL matches any of
624 * them; if so, re-use the *same* key combo.
625 */
626 bool url_already_seen = false;
627 tll_foreach(*urls, it2) {
628 if (&it->item == &it2->item)
629 break;
630
631 if (strcmp(it->item.url, it2->item.url) == 0) {
632 it->item.key = xwcsdup(it2->item.key);
633 url_already_seen = true;
634 break;
635 }
636 }
637
638 if (!url_already_seen)
639 it->item.key = combos[combo_idx++];
640 }
641
642 /* Free combos we didn’t use up */
643 for (size_t i = combo_idx; i < count; i++)
644 free(combos[i]);
645
646 #if defined(_DEBUG) && LOG_ENABLE_DBG
647 tll_foreach(*urls, it) {
648 if (it->item.key == NULL)
649 continue;
650
651 char key[32];
652 wcstombs(key, it->item.key, sizeof(key) - 1);
653 LOG_DBG("URL: %s (%s)", it->item.url, key);
654 }
655 #endif
656 }
657
658 static void
tag_cells_for_url(struct terminal * term,const struct url * url,bool value)659 tag_cells_for_url(struct terminal *term, const struct url *url, bool value)
660 {
661 if (url->url_mode_dont_change_url_attr)
662 return;
663
664 const struct coord *start = &url->start;
665 const struct coord *end = &url->end;
666
667 size_t end_r = end->row & (term->grid->num_rows - 1);
668
669 size_t r = start->row & (term->grid->num_rows - 1);
670 size_t c = start->col;
671
672 struct row *row = term->grid->rows[r];
673 row->dirty = true;
674
675 while (true) {
676 struct cell *cell = &row->cells[c];
677 cell->attrs.url = value;
678 cell->attrs.clean = 0;
679
680 if (r == end_r && c == end->col)
681 break;
682
683 if (++c >= term->cols) {
684 r = (r + 1) & (term->grid->num_rows - 1);
685 c = 0;
686
687 row = term->grid->rows[r];
688 if (row == NULL) {
689 /* Un-allocated scrollback. This most likely means a
690 * runaway OSC-8 URL. */
691 break;
692 }
693 row->dirty = true;
694 }
695 }
696 }
697
698 void
urls_render(struct terminal * term)699 urls_render(struct terminal *term)
700 {
701 struct wl_window *win = term->window;
702
703 if (tll_length(win->term->urls) == 0)
704 return;
705
706 xassert(tll_length(win->urls) == 0);
707 tll_foreach(win->term->urls, it) {
708 struct wl_url url = {.url = &it->item};
709 wayl_win_subsurface_new(win, &url.surf);
710
711 tll_push_back(win->urls, url);
712 tag_cells_for_url(term, &it->item, true);
713 }
714
715 /* Dirty the last cursor, to ensure it is erased */
716 {
717 struct row *cursor_row = term->render.last_cursor.row;
718 if (cursor_row != NULL) {
719 struct cell *cell = &cursor_row->cells[term->render.last_cursor.col];
720 cell->attrs.clean = 0;
721 cursor_row->dirty = true;
722 }
723 }
724 term->render.last_cursor.row = NULL;
725
726 /* Clear scroll damage, to ensure we don’t apply it twice (once on
727 * the snapshot:ed grid, and then later again on the real grid) */
728 tll_free(term->grid->scroll_damage);
729
730 /* Damage the entire view, to ensure a full screen redraw, both
731 * now, when entering URL mode, and later, when exiting it. */
732 term_damage_view(term);
733
734 /* Snapshot the current grid */
735 term->url_grid_snapshot = grid_snapshot(term->grid);
736
737 render_refresh_urls(term);
738 render_refresh(term);
739 }
740
741 static void
url_destroy(struct url * url)742 url_destroy(struct url *url)
743 {
744 free(url->url);
745 free(url->key);
746 }
747
748 void
urls_reset(struct terminal * term)749 urls_reset(struct terminal *term)
750 {
751 if (likely(tll_length(term->urls) == 0)) {
752 xassert(term->url_grid_snapshot == NULL);
753 return;
754 }
755
756 grid_free(term->url_grid_snapshot);
757 free(term->url_grid_snapshot);
758 term->url_grid_snapshot = NULL;
759
760 /*
761 * Make sure “last cursor” doesn’t point to a row in the just
762 * free:d snapshot grid.
763 *
764 * Note that it will still be erased properly (if hasn’t already),
765 * since we marked the cell as dirty *before* taking the grid
766 * snapshot.
767 */
768 term->render.last_cursor.row = NULL;
769
770 if (term->window != NULL) {
771 tll_foreach(term->window->urls, it) {
772 wayl_win_subsurface_destroy(&it->item.surf);
773 tll_remove(term->window->urls, it);
774 }
775 }
776
777 tll_foreach(term->urls, it) {
778 tag_cells_for_url(term, &it->item, false);
779 url_destroy(&it->item);
780 tll_remove(term->urls, it);
781 }
782
783 term->urls_show_uri_on_jump_label = false;
784 memset(term->url_keys, 0, sizeof(term->url_keys));
785
786 render_refresh(term);
787 }
788