1 #include "search.h"
2 
3 #include <string.h>
4 #include <wchar.h>
5 #include <wctype.h>
6 
7 #include <wayland-client.h>
8 #include <xkbcommon/xkbcommon-compose.h>
9 
10 #define LOG_MODULE "search"
11 #define LOG_ENABLE_DBG 0
12 #include "log.h"
13 #include "config.h"
14 #include "extract.h"
15 #include "grid.h"
16 #include "input.h"
17 #include "misc.h"
18 #include "render.h"
19 #include "selection.h"
20 #include "shm.h"
21 #include "util.h"
22 #include "xmalloc.h"
23 
24 /*
25  * Ensures a "new" viewport doesn't contain any unallocated rows.
26  *
27  * This is done by first checking if the *first* row is NULL. If so,
28  * we move the viewport *forward*, until the first row is non-NULL. At
29  * this point, the entire viewport should be allocated rows only.
30  *
31  * If the first row already was non-NULL, we instead check the *last*
32  * row, and if it is NULL, we move the viewport *backward* until the
33  * last row is non-NULL.
34  */
35 static int
ensure_view_is_allocated(struct terminal * term,int new_view)36 ensure_view_is_allocated(struct terminal *term, int new_view)
37 {
38     int view_end = (new_view + term->rows - 1) & (term->grid->num_rows - 1);
39 
40     if (term->grid->rows[new_view] == NULL) {
41         while (term->grid->rows[new_view] == NULL)
42             new_view = (new_view + 1) & (term->grid->num_rows - 1);
43     }
44 
45     else if (term->grid->rows[view_end] == NULL) {
46         while (term->grid->rows[view_end] == NULL) {
47             new_view--;
48             if (new_view < 0)
49                 new_view += term->grid->num_rows;
50             view_end = (new_view + term->rows - 1) & (term->grid->num_rows - 1);
51         }
52     }
53 
54 #if defined(_DEBUG)
55     for (size_t r = 0; r < term->rows; r++)
56         xassert(term->grid->rows[(new_view + r) & (term->grid->num_rows - 1)] != NULL);
57 #endif
58 
59     return new_view;
60 }
61 
62 static bool
search_ensure_size(struct terminal * term,size_t wanted_size)63 search_ensure_size(struct terminal *term, size_t wanted_size)
64 {
65     while (wanted_size >= term->search.sz) {
66         size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2;
67         wchar_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0]));
68 
69         if (new_buf == NULL) {
70             LOG_ERRNO("failed to resize search buffer");
71             return false;
72         }
73 
74         term->search.buf = new_buf;
75         term->search.sz = new_sz;
76     }
77 
78     return true;
79 }
80 
81 static bool
has_wrapped_around(const struct terminal * term,int abs_row_no)82 has_wrapped_around(const struct terminal *term, int abs_row_no)
83 {
84     int scrollback_start = term->grid->offset + term->rows;
85     int rebased_row = abs_row_no - scrollback_start + term->grid->num_rows;
86     rebased_row &= term->grid->num_rows - 1;
87 
88     return rebased_row == 0;
89 }
90 
91 static void
search_cancel_keep_selection(struct terminal * term)92 search_cancel_keep_selection(struct terminal *term)
93 {
94     struct wl_window *win = term->window;
95     wayl_win_subsurface_destroy(&win->search);
96 
97     free(term->search.buf);
98     term->search.buf = NULL;
99     term->search.len = 0;
100     term->search.sz = 0;
101     term->search.cursor = 0;
102     term->search.match = (struct coord){-1, -1};
103     term->search.match_len = 0;
104     term->is_searching = false;
105     term->render.search_glyph_offset = 0;
106 
107     /* Reset IME state */
108     if (term_ime_is_enabled(term)) {
109         term_ime_disable(term);
110         term_ime_enable(term);
111     }
112 
113     term_xcursor_update(term);
114     render_refresh(term);
115 }
116 
117 void
search_begin(struct terminal * term)118 search_begin(struct terminal *term)
119 {
120     LOG_DBG("search: begin");
121 
122     search_cancel_keep_selection(term);
123     selection_cancel(term);
124 
125     /* Reset IME state */
126     if (term_ime_is_enabled(term)) {
127         term_ime_disable(term);
128         term_ime_enable(term);
129     }
130 
131     /* On-demand instantiate wayland surface */
132     bool ret = wayl_win_subsurface_new(term->window, &term->window->search);
133     xassert(ret);
134 
135     term->search.original_view = term->grid->view;
136     term->search.view_followed_offset = term->grid->view == term->grid->offset;
137     term->is_searching = true;
138 
139     term->search.len = 0;
140     term->search.sz = 64;
141     term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
142     term->search.buf[0] = L'\0';
143 
144     term_xcursor_update(term);
145     render_refresh_search(term);
146 }
147 
148 void
search_cancel(struct terminal * term)149 search_cancel(struct terminal *term)
150 {
151     if (!term->is_searching)
152         return;
153 
154     search_cancel_keep_selection(term);
155     selection_cancel(term);
156 }
157 
158 void
search_selection_cancelled(struct terminal * term)159 search_selection_cancelled(struct terminal *term)
160 {
161     term->search.match = (struct coord){-1, -1};
162     term->search.match_len = 0;
163     render_refresh_search(term);
164 }
165 
166 static void
search_update_selection(struct terminal * term,int start_row,int start_col,int end_row,int end_col)167 search_update_selection(struct terminal *term,
168                         int start_row, int start_col,
169                         int end_row, int end_col)
170 {
171     bool move_viewport = true;
172 
173     int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1);
174     if (view_end >= term->grid->view) {
175         /* Viewport does *not* wrap around */
176         if (start_row >= term->grid->view && end_row <= view_end)
177             move_viewport = false;
178     } else {
179         /* Viewport wraps */
180         if (start_row >= term->grid->view || end_row <= view_end)
181             move_viewport = false;
182     }
183 
184     if (move_viewport) {
185         int old_view = term->grid->view;
186         int new_view = start_row - term->rows / 2;
187 
188         while (new_view < 0)
189             new_view += term->grid->num_rows;
190 
191         new_view = ensure_view_is_allocated(term, new_view);
192 
193         /* Don't scroll past scrollback history */
194         int end = (term->grid->offset + term->rows - 1) & (term->grid->num_rows - 1);
195         if (end >= term->grid->offset) {
196             /* Not wrapped */
197             if (new_view >= term->grid->offset && new_view <= end)
198                 new_view = term->grid->offset;
199         } else {
200             if (new_view >= term->grid->offset || new_view <= end)
201                 new_view = term->grid->offset;
202         }
203 
204 #if defined(_DEBUG)
205         /* Verify all to-be-visible rows have been allocated */
206         for (int r = 0; r < term->rows; r++)
207             xassert(term->grid->rows[(new_view + r) & (term->grid->num_rows - 1)] != NULL);
208 #endif
209 
210         /* Update view */
211         term->grid->view = new_view;
212         if (new_view != old_view)
213             term_damage_view(term);
214     }
215 
216     /* Selection endpoint is inclusive */
217     if (--end_col < 0) {
218         end_col = term->cols - 1;
219         end_row--;
220     }
221 
222     /* Begin a new selection if the start coords changed */
223     if (start_row != term->search.match.row ||
224         start_col != term->search.match.col)
225     {
226         int selection_row = start_row - term->grid->view;
227         while (selection_row < 0)
228             selection_row += term->grid->num_rows;
229 
230         xassert(selection_row >= 0 &&
231                selection_row < term->grid->num_rows);
232         selection_start(
233             term, start_col, selection_row, SELECTION_CHAR_WISE, false);
234     }
235 
236     /* Update selection endpoint */
237     {
238         int selection_row = end_row - term->grid->view;
239         while (selection_row < 0)
240             selection_row += term->grid->num_rows;
241 
242         xassert(selection_row >= 0 &&
243                selection_row < term->grid->num_rows);
244         selection_update(term, end_col, selection_row);
245     }
246 }
247 
248 static ssize_t
matches_cell(const struct terminal * term,const struct cell * cell,size_t search_ofs)249 matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs)
250 {
251     assert(search_ofs < term->search.len);
252 
253     wchar_t base = cell->wc;
254     const struct composed *composed = NULL;
255 
256     if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
257     {
258         composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
259         base = composed->chars[0];
260     }
261 
262     if (composed == NULL && base == 0 && term->search.buf[search_ofs] == L' ')
263         return 1;
264 
265     if (wcsncasecmp(&base, &term->search.buf[search_ofs], 1) != 0)
266         return -1;
267 
268     if (composed != NULL) {
269         if (search_ofs + 1 + composed->count > term->search.len)
270             return -1;
271 
272         for (size_t j = 1; j < composed->count; j++) {
273             if (composed->chars[j] != term->search.buf[search_ofs + 1 + j])
274                 return -1;
275         }
276     }
277 
278     return composed != NULL ? 1 + composed->count : 1;
279 }
280 
281 static void
search_find_next(struct terminal * term)282 search_find_next(struct terminal *term)
283 {
284     bool backward = term->search.direction == SEARCH_BACKWARD;
285     term->search.direction = SEARCH_BACKWARD;
286 
287     if (term->search.len == 0) {
288         term->search.match = (struct coord){-1, -1};
289         term->search.match_len = 0;
290         selection_cancel(term);
291         return;
292     }
293 
294     int start_row = term->search.match.row;
295     int start_col = term->search.match.col;
296     size_t len = term->search.match_len;
297 
298     xassert((len == 0 && start_row == -1 && start_col == -1) ||
299            (len > 0 && start_row >= 0 && start_col >= 0));
300 
301     if (len == 0) {
302         if (backward) {
303             start_row = grid_row_absolute_in_view(term->grid, term->rows - 1);
304             start_col = term->cols - 1;
305         } else {
306             start_row = grid_row_absolute_in_view(term->grid, 0);
307             start_col = 0;
308         }
309     }
310 
311     LOG_DBG("search: update: %s: starting at row=%d col=%d (offset = %d, view = %d)",
312             backward ? "backward" : "forward", start_row, start_col,
313             term->grid->offset, term->grid->view);
314 
315 #define ROW_DEC(_r) ((_r) = ((_r) - 1 + term->grid->num_rows) & (term->grid->num_rows - 1))
316 #define ROW_INC(_r) ((_r) = ((_r) + 1) & (term->grid->num_rows - 1))
317 
318     /* Scan backward from current end-of-output */
319     /* TODO: don't search "scrollback" in alt screen? */
320     for (size_t r = 0;
321          r < term->grid->num_rows;
322          backward ? ROW_DEC(start_row) : ROW_INC(start_row), r++)
323     {
324         for (;
325              backward ? start_col >= 0 : start_col < term->cols;
326              backward ? start_col-- : start_col++)
327         {
328             const struct row *row = term->grid->rows[start_row];
329             if (row == NULL)
330                 continue;
331 
332             if (matches_cell(term, &row->cells[start_col], 0) < 0)
333                 continue;
334 
335             /*
336              * Got a match on the first letter. Now we'll see if the
337              * rest of the search buffer matches.
338              */
339 
340             LOG_DBG("search: initial match at row=%d, col=%d", start_row, start_col);
341 
342             int end_row = start_row;
343             int end_col = start_col;
344             size_t match_len = 0;
345 
346             for (size_t i = 0; i < term->search.len;) {
347                 if (end_col >= term->cols) {
348                     end_row = (end_row + 1) & (term->grid->num_rows - 1);
349                     end_col = 0;
350 
351                     if (has_wrapped_around(term, end_row))
352                         break;
353 
354                     row = term->grid->rows[end_row];
355                 }
356 
357                 if (row->cells[end_col].wc >= CELL_SPACER) {
358                     end_col++;
359                     continue;
360                 }
361 
362                 ssize_t additional_chars = matches_cell(term, &row->cells[end_col], i);
363                 if (additional_chars < 0)
364                     break;
365 
366                 i += additional_chars;
367                 match_len += additional_chars;
368                 end_col++;
369             }
370 
371             if (match_len != term->search.len) {
372                 /* Didn't match (completely) */
373                 continue;
374             }
375 
376             /*
377              * We matched the entire buffer. Move view to ensure the
378              * match is visible, create a selection and return.
379              */
380             search_update_selection(term, start_row, start_col, end_row, end_col);
381 
382             /* Update match state */
383             term->search.match.row = start_row;
384             term->search.match.col = start_col;
385             term->search.match_len = match_len;
386 
387             return;
388         }
389 
390         start_col = backward ? term->cols - 1 : 0;
391     }
392 
393     /* No match */
394     LOG_DBG("no match");
395     term->search.match = (struct coord){-1, -1};
396     term->search.match_len = 0;
397     selection_cancel(term);
398 #undef ROW_DEC
399 }
400 
401 void
search_add_chars(struct terminal * term,const char * src,size_t count)402 search_add_chars(struct terminal *term, const char *src, size_t count)
403 {
404     const char *_src = src;
405     mbstate_t ps = {0};
406     size_t wchars = mbsnrtowcs(NULL, &_src, count, 0, &ps);
407 
408     if (wchars == -1) {
409         LOG_ERRNO("failed to convert %.*s to wchars", (int)count, src);
410         return;
411     }
412 
413     _src = src;
414     ps = (mbstate_t){0};
415     wchar_t wcs[wchars + 1];
416     mbsnrtowcs(wcs, &_src, count, wchars, &ps);
417 
418     /* Strip non-printable characters */
419     for (size_t i = 0, j = 0, orig_wchars = wchars; i < orig_wchars; i++) {
420         if (iswprint(wcs[i]))
421             wcs[j++] = wcs[i];
422         else
423             wchars--;
424     }
425 
426     if (!search_ensure_size(term, term->search.len + wchars))
427         return;
428 
429     xassert(term->search.len + wchars < term->search.sz);
430 
431     memmove(&term->search.buf[term->search.cursor + wchars],
432             &term->search.buf[term->search.cursor],
433             (term->search.len - term->search.cursor) * sizeof(wchar_t));
434 
435     memcpy(&term->search.buf[term->search.cursor], wcs, wchars * sizeof(wchar_t));
436 
437     term->search.len += wchars;
438     term->search.cursor += wchars;
439     term->search.buf[term->search.len] = L'\0';
440 }
441 
442 static void
search_match_to_end_of_word(struct terminal * term,bool spaces_only)443 search_match_to_end_of_word(struct terminal *term, bool spaces_only)
444 {
445     if (term->search.match_len == 0)
446         return;
447 
448     xassert(term->selection.end.row != -1);
449 
450     const bool move_cursor = term->search.cursor == term->search.len;
451 
452     const struct coord old_end = term->selection.end;
453     struct coord new_end = old_end;
454     struct row *row = NULL;
455 
456 #define newline(coord) __extension__                                    \
457         ({                                                              \
458             bool wrapped_around = false;                                \
459             if (++(coord).col >= term->cols) {                           \
460                 (coord).row = ((coord).row + 1) & (term->grid->num_rows - 1); \
461                 (coord).col = 0;                                        \
462                 row = term->grid->rows[(coord).row];                    \
463                 if (has_wrapped_around(term, (coord.row)))              \
464                     wrapped_around = true;                              \
465             }                                                           \
466             wrapped_around;                                             \
467         })
468 
469     /* First character to consider is the *next* character */
470     if (newline(new_end))
471         return;
472 
473     xassert(term->grid->rows[new_end.row] != NULL);
474 
475     /* Find next word boundary */
476     new_end.row -= term->grid->view;
477     selection_find_word_boundary_right(term, &new_end, spaces_only);
478     new_end.row += term->grid->view;
479 
480     struct coord pos = old_end;
481     row = term->grid->rows[pos.row];
482 
483     struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
484     if (ctx == NULL)
485         return;
486 
487     do {
488         if (newline(pos))
489             break;
490         if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx))
491             break;
492     } while (pos.col != new_end.col || pos.row != new_end.row);
493 
494     wchar_t *new_text;
495     size_t new_len;
496 
497     if (!extract_finish_wide(ctx, &new_text, &new_len))
498         return;
499 
500     if (!search_ensure_size(term, term->search.len + new_len))
501         return;
502 
503     for (size_t i = 0; i < new_len; i++) {
504         if (new_text[i] == L'\n') {
505             /* extract() adds newlines, which we never match against */
506             continue;
507         }
508 
509         term->search.buf[term->search.len++] = new_text[i];
510     }
511 
512     term->search.buf[term->search.len] = L'\0';
513     free(new_text);
514 
515     if (move_cursor)
516         term->search.cursor = term->search.len;
517 
518     /* search_update_selection() expected end coordinate to be *exclusive* */
519     newline(new_end);
520     search_update_selection(
521         term, term->search.match.row, term->search.match.col,
522         new_end.row, new_end.col);
523 
524     term->search.match_len = term->search.len;
525 
526 #undef newline
527 }
528 
529 static size_t
distance_next_word(const struct terminal * term)530 distance_next_word(const struct terminal *term)
531 {
532     size_t cursor = term->search.cursor;
533 
534     /* First eat non-whitespace. This is the word we're skipping past */
535     while (cursor < term->search.len) {
536         if (iswspace(term->search.buf[cursor++]))
537             break;
538     }
539 
540     xassert(cursor == term->search.len || iswspace(term->search.buf[cursor - 1]));
541 
542     /* Now skip past whitespace, so that we end up at the beginning of
543      * the next word */
544     while (cursor < term->search.len) {
545         if (!iswspace(term->search.buf[cursor++]))
546             break;
547     }
548 
549     xassert(cursor == term->search.len || !iswspace(term->search.buf[cursor - 1]));
550 
551     if (cursor < term->search.len && !iswspace(term->search.buf[cursor]))
552         cursor--;
553 
554     return cursor - term->search.cursor;
555 }
556 
557 static size_t
distance_prev_word(const struct terminal * term)558 distance_prev_word(const struct terminal *term)
559 {
560     int cursor = term->search.cursor;
561 
562     /* First, eat whitespace prefix */
563     while (cursor > 0) {
564         if (!iswspace(term->search.buf[--cursor]))
565             break;
566     }
567 
568     xassert(cursor == 0 || !iswspace(term->search.buf[cursor]));
569 
570     /* Now eat non-whitespace. This is the word we're skipping past */
571     while (cursor > 0) {
572         if (iswspace(term->search.buf[--cursor]))
573             break;
574     }
575 
576     xassert(cursor == 0 || iswspace(term->search.buf[cursor]));
577     if (cursor > 0 && iswspace(term->search.buf[cursor]))
578         cursor++;
579 
580     return term->search.cursor - cursor;
581 }
582 
583 static void
from_clipboard_cb(char * text,size_t size,void * user)584 from_clipboard_cb(char *text, size_t size, void *user)
585 {
586     struct terminal *term = user;
587     search_add_chars(term, text, size);
588 }
589 
590 static void
from_clipboard_done(void * user)591 from_clipboard_done(void *user)
592 {
593     struct terminal *term = user;
594 
595     LOG_DBG("search: buffer: %ls", term->search.buf);
596     search_find_next(term);
597     render_refresh_search(term);
598 }
599 
600 static bool
execute_binding(struct seat * seat,struct terminal * term,enum bind_action_search action,uint32_t serial,bool * update_search_result,bool * redraw)601 execute_binding(struct seat *seat, struct terminal *term,
602                 enum bind_action_search action, uint32_t serial,
603                 bool *update_search_result, bool *redraw)
604 {
605     *update_search_result = *redraw = false;
606 
607     switch (action) {
608     case BIND_ACTION_SEARCH_NONE:
609         return false;
610 
611     case BIND_ACTION_SEARCH_CANCEL:
612         if (term->search.view_followed_offset)
613             term->grid->view = term->grid->offset;
614         else {
615             term->grid->view = ensure_view_is_allocated(
616                 term, term->search.original_view);
617         }
618         term_damage_view(term);
619         search_cancel(term);
620         return true;
621 
622     case BIND_ACTION_SEARCH_COMMIT:
623         selection_finalize(seat, term, serial);
624         search_cancel_keep_selection(term);
625         return true;
626 
627     case BIND_ACTION_SEARCH_FIND_PREV:
628         if (term->search.match_len > 0) {
629             int new_col = term->search.match.col - 1;
630             int new_row = term->search.match.row;
631 
632             if (new_col < 0) {
633                 new_col = term->cols - 1;
634                 new_row--;
635             }
636 
637             if (new_row >= 0) {
638                 term->search.match.col = new_col;
639                 term->search.match.row = new_row;
640             }
641         }
642         *update_search_result = *redraw = true;
643         return true;
644 
645     case BIND_ACTION_SEARCH_FIND_NEXT:
646         if (term->search.match_len > 0) {
647             int new_col = term->search.match.col + 1;
648             int new_row = term->search.match.row;
649 
650             if (new_col >= term->cols) {
651                 new_col = 0;
652                 new_row++;
653             }
654 
655             if (new_row < term->grid->num_rows) {
656                 term->search.match.col = new_col;
657                 term->search.match.row = new_row;
658                 term->search.direction = SEARCH_FORWARD;
659              }
660         }
661         *update_search_result = *redraw = true;
662         return true;
663 
664     case BIND_ACTION_SEARCH_EDIT_LEFT:
665         if (term->search.cursor > 0) {
666             term->search.cursor--;
667             *redraw = true;
668         }
669         return true;
670 
671     case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: {
672         size_t diff = distance_prev_word(term);
673         term->search.cursor -= diff;
674         xassert(term->search.cursor <= term->search.len);
675 
676         if (diff > 0)
677             *redraw = true;
678         return true;
679     }
680 
681     case BIND_ACTION_SEARCH_EDIT_RIGHT:
682         if (term->search.cursor < term->search.len) {
683             term->search.cursor++;
684             *redraw = true;
685         }
686         return true;
687 
688     case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: {
689         size_t diff = distance_next_word(term);
690         term->search.cursor += diff;
691         xassert(term->search.cursor <= term->search.len);
692 
693         if (diff > 0)
694             *redraw = true;
695         return true;
696     }
697 
698     case BIND_ACTION_SEARCH_EDIT_HOME:
699         if (term->search.cursor != 0) {
700             term->search.cursor = 0;
701             *redraw = true;
702         }
703         return true;
704 
705     case BIND_ACTION_SEARCH_EDIT_END:
706         if (term->search.cursor != term->search.len) {
707             term->search.cursor = term->search.len;
708             *redraw = true;
709         }
710         return true;
711 
712     case BIND_ACTION_SEARCH_DELETE_PREV:
713         if (term->search.cursor > 0) {
714             memmove(
715                 &term->search.buf[term->search.cursor - 1],
716                 &term->search.buf[term->search.cursor],
717                 (term->search.len - term->search.cursor) * sizeof(wchar_t));
718             term->search.cursor--;
719             term->search.buf[--term->search.len] = L'\0';
720             *update_search_result = *redraw = true;
721         }
722         return true;
723 
724     case BIND_ACTION_SEARCH_DELETE_PREV_WORD: {
725         size_t diff = distance_prev_word(term);
726         size_t old_cursor = term->search.cursor;
727         size_t new_cursor = old_cursor - diff;
728 
729         if (diff > 0) {
730             memmove(&term->search.buf[new_cursor],
731                     &term->search.buf[old_cursor],
732                     (term->search.len - old_cursor) * sizeof(wchar_t));
733 
734             term->search.len -= diff;
735             term->search.cursor = new_cursor;
736             *update_search_result = *redraw = true;
737         }
738         return true;
739     }
740 
741     case BIND_ACTION_SEARCH_DELETE_NEXT:
742         if (term->search.cursor < term->search.len) {
743             memmove(
744                 &term->search.buf[term->search.cursor],
745                 &term->search.buf[term->search.cursor + 1],
746                 (term->search.len - term->search.cursor - 1) * sizeof(wchar_t));
747             term->search.buf[--term->search.len] = L'\0';
748             *update_search_result = *redraw = true;
749         }
750         return true;
751 
752     case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: {
753         size_t diff = distance_next_word(term);
754         size_t cursor = term->search.cursor;
755 
756         if (diff > 0) {
757             memmove(&term->search.buf[cursor],
758                     &term->search.buf[cursor + diff],
759                     (term->search.len - (cursor + diff)) * sizeof(wchar_t));
760 
761             term->search.len -= diff;
762             *update_search_result = *redraw = true;
763         }
764         return true;
765     }
766 
767     case BIND_ACTION_SEARCH_EXTEND_WORD:
768         search_match_to_end_of_word(term, false);
769         *update_search_result = false;
770         *redraw = true;
771         return true;
772 
773     case BIND_ACTION_SEARCH_EXTEND_WORD_WS:
774         search_match_to_end_of_word(term, true);
775         *update_search_result = false;
776         *redraw = true;
777         return true;
778 
779     case BIND_ACTION_SEARCH_CLIPBOARD_PASTE:
780         text_from_clipboard(
781             seat, term, &from_clipboard_cb, &from_clipboard_done, term);
782         *update_search_result = *redraw = true;
783         return true;
784 
785     case BIND_ACTION_SEARCH_PRIMARY_PASTE:
786         text_from_primary(
787             seat, term, &from_clipboard_cb, &from_clipboard_done, term);
788         *update_search_result = *redraw = true;
789         return true;
790 
791     case BIND_ACTION_SEARCH_COUNT:
792         BUG("Invalid action type");
793         return true;
794     }
795 
796     BUG("Unhandled action type");
797     return false;
798 }
799 
800 void
search_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)801 search_input(struct seat *seat, struct terminal *term, uint32_t key,
802              xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed,
803              const xkb_keysym_t *raw_syms, size_t raw_count,
804              uint32_t serial)
805 {
806     LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x", sym, sym, mods);
807 
808     enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL
809       ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state)
810       : XKB_COMPOSE_NOTHING;
811 
812     bool update_search_result = false;
813     bool redraw = false;
814 
815     /* Key bindings */
816     tll_foreach(seat->kbd.bindings.search, it) {
817         const struct key_binding *bind = &it->item;
818 
819         /* Match translated symbol */
820         if (bind->sym == sym &&
821             bind->mods == (mods & ~consumed)) {
822 
823             if (execute_binding(seat, term, bind->action, serial,
824                                 &update_search_result, &redraw))
825             {
826                 goto update_search;
827             }
828             return;
829         }
830 
831         if (bind->mods != mods)
832             continue;
833 
834         /* Match untranslated symbols */
835         for (size_t i = 0; i < raw_count; i++) {
836             if (bind->sym == raw_syms[i]) {
837                 if (execute_binding(seat, term, bind->action, serial,
838                                     &update_search_result, &redraw))
839                 {
840                     goto update_search;
841                 }
842                 return;
843             }
844         }
845 
846         /* Match raw key code */
847         tll_foreach(bind->key_codes, code) {
848             if (code->item == key) {
849                 if (execute_binding(seat, term, bind->action, serial,
850                                     &update_search_result, &redraw))
851                 {
852                     goto update_search;
853                 }
854                 return;
855             }
856         }
857     }
858 
859     uint8_t buf[64] = {0};
860     int count = 0;
861 
862     if (compose_status == XKB_COMPOSE_COMPOSED) {
863         count = xkb_compose_state_get_utf8(
864             seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf));
865         xkb_compose_state_reset(seat->kbd.xkb_compose_state);
866     } else if (compose_status == XKB_COMPOSE_CANCELLED) {
867         count = 0;
868     } else {
869         count = xkb_state_key_get_utf8(
870             seat->kbd.xkb_state, key, (char *)buf, sizeof(buf));
871     }
872 
873     update_search_result = redraw = count > 0;
874 
875     if (count == 0)
876         return;
877 
878     search_add_chars(term, (const char *)buf, count);
879 
880 update_search:
881     LOG_DBG("search: buffer: %ls", term->search.buf);
882     if (update_search_result)
883         search_find_next(term);
884     if (redraw)
885         render_refresh_search(term);
886 }
887