1 // Some help understanding how searching in the scroller works
2 //
3 // - Vterm deals only with what's on the screen
4 //   It represents rows 0 through vterm height-1, which is 2 below
5 // - vterminal introduces a scrollback buffer
6 //   It represents rows -1 through -scrollback height, which is -6 below
7 // - vterminal also introduces a scrollback delta
8 //   Allows iterating from 0:height-1 but displaying the scrolled to text
9 //   The default is 0, which is represented by d0
10 //   Scrolling back all the way to -6 is represented by d-6
11 //   Scrolling back partiall to -2 is represented by d-2
12 // - The scroller has introduced the concept of a search id (sid)
13 //   The purpose is to iterate easily over all the text (vterm+scrollback)
14 //
15 // Example inputs and labels
16 //   Screen Height: 3
17 //   Scrollback (sb) size: 6
18 //   vid: VTerm ID (screen only)
19 //   tid: Terminal ID (screen + scrollback + scrollback delta)
20 //   sid: A search ID (for iterating eaisly over all)
21 //   sb start - scrollback buffer start
22 //   sb end - scrollback buffer end
23 //   vt start - vterm buffer start
24 //   vt end - vterm buffer end
25 //
26 //          sid vid tid d0 d-6 d-2
27 // sb start  0      -6       0      abc     0
28 //           1      -5       1         def  1
29 //           2      -4       2      ghi     2
30 //           3      -3                 def
31 //           4      -2           0  jkl
32 // sb end    5      -1           1     def
33 // vt start  6   0  0    0       2  mno
34 //           7   1  1    1             def
35 // vt end    8   2  2    2          pqr
36 //
37 // Your search will start at the row the scroll cursor is at.
38 //
39 // You can loop from 0 to scrollback size + vterm size.
40 //
41 // You can convert your cursor position to the sid by doing:
42 //   sid = cursor_pos + scrollback size + vterminal_scroll_get_delta
43 // You can convert your sid to a cursor position by doing the following:
44 //   cursor_pos = sid - scrollback size - vterminal_scroll_get_delta
45 //
46 // If your delta is -6, and your cursor is on sid 1, and you find a
47 // match on sid 7, you'll have to move the display by moving the delta.
48 // You can move the display to sid by doing the following:
49 //   delta_offset = sid - scrollback size
50 // Then, if delta_offset > 0, delta_offset = 0.
51 
52 /* Local Includes */
53 #if HAVE_CONFIG_H
54 #include "config.h"
55 #endif /* HAVE_CONFIG_H */
56 
57 /* System Includes */
58 #if HAVE_CTYPE_H
59 #include <ctype.h>
60 #endif
61 
62 #if HAVE_STDLIB_H
63 #include <stdlib.h>
64 #endif /* HAVE_STDLIB_H */
65 
66 #if HAVE_STRING_H
67 #include <string.h>
68 #endif /* HAVE_STRING_H */
69 
70 #include <algorithm>
71 
72 /* Local Includes */
73 #include "sys_util.h"
74 #include "stretchy.h"
75 #include "sys_win.h"
76 #include "cgdb.h"
77 #include "cgdbrc.h"
78 #include "highlight_groups.h"
79 #include "scroller.h"
80 #include "highlight.h"
81 #include "vterminal.h"
82 
83 struct scroller {
84     // The virtual terminal
85     VTerminal *vt;
86 
87     // All text sent to the scroller to date.
88     // Vterm does not yet support reflow, so when the terminal is resized,
89     // or when the cgdb window orientation is changed, vterm can't update
90     // the text that well in the scroller. Currently, to work around that,
91     // CGDB creates a new vterm on resize and feeds it all the text found
92     // to date. When vterm supports reflow, this could go away.
93     std::string text;
94 
95     // The window the scroller will be displayed on
96     //
97     // NULL when the height of the scroller is zero
98     // This occurs when the terminal has a height of 1 or if the user
99     // minimized the height of the scroller manually to zero
100     SWINDOW *win;
101 
102     // True if in scroll mode, otherwise false
103     bool in_scroll_mode;
104     // The position of the cursor when in scroll mode
105     int scroll_cursor_row, scroll_cursor_col;
106 
107     // True if in search mode, otherwise false
108     // Can only search when in_scroll_mode is true
109     bool in_search_mode;
110     // The original delta, cursor row and col. Also the initial search id.
111     int delta_init, search_row_init, search_col_init, search_sid_init;
112     // True when searching forward, otherwise searching backwards
113     bool forward;
114     // True when searching case insensitve, false otherwise
115     bool icase;
116 
117     // The current regex if in_search_mode is true
118     struct hl_regex_info *hlregex;
119     // The current row, col start and end matching position
120     int search_row, search_col_start, search_col_end;
121     // The last string regex to be searched for
122     std::string last_regex;
123 };
124 
125 
126 /* ----------------- */
127 /* Exposed Functions */
128 /* ----------------- */
129 
scr_ring_bell(void * data)130 static void scr_ring_bell(void *data)
131 {
132     struct scroller *scr = (struct scroller *)data;
133 
134     // TODO: Ring the bell
135 }
136 
137 // Create a new VTerminal instance
138 //
139 // Please note that when the height or width of the scroller is zero,
140 // than the window (scr->win) will be NULL, as noted in the fields comment.
141 //
142 // In this scenario, we allow the height/width of the virtual terminal
143 // to remain as 1. The virtual terminal requires this. This provides a
144 // benefit that the user can continue typing into the virtual terminal
145 // even when it's not visible.
146 //
147 // @param scr
148 // The scroller to operate on
149 //
150 // @return
151 // The new virtual terminal instance
scr_new_vterminal(struct scroller * scr)152 static VTerminal *scr_new_vterminal(struct scroller *scr)
153 {
154     int scrollback_buffer_size = cgdbrc_get_int(CGDBRC_SCROLLBACK_BUFFER_SIZE);
155 
156     VTerminalOptions options;
157     options.data = (void*)scr;
158     // See note in function comments about std::max usage here
159     options.width = std::max(swin_getmaxx(scr->win), 1);
160     options.height = std::max(swin_getmaxy(scr->win), 1);
161     options.scrollback_buffer_size = scrollback_buffer_size;
162     options.ring_bell = scr_ring_bell;
163 
164     return vterminal_new(options);
165 }
166 
scr_new(SWINDOW * win)167 struct scroller *scr_new(SWINDOW *win)
168 {
169     struct scroller *rv = new scroller();
170 
171     rv->in_scroll_mode = false;
172     rv->scroll_cursor_row = rv->scroll_cursor_col = 0;
173     rv->win = win;
174 
175     rv->in_search_mode = false;
176     rv->hlregex = NULL;
177     rv->search_row = rv->search_col_start = rv->search_col_end = 0;
178 
179     rv->vt = scr_new_vterminal(rv);
180 
181     return rv;
182 }
183 
scr_free(struct scroller * scr)184 void scr_free(struct scroller *scr)
185 {
186     vterminal_free(scr->vt);
187 
188     hl_regex_free(&scr->hlregex);
189     scr->hlregex = NULL;
190 
191     swin_delwin(scr->win);
192     scr->win = NULL;
193 
194     /* Release the scroller object */
195     delete scr;
196 }
197 
scr_set_scroll_mode(struct scroller * scr,bool mode)198 void scr_set_scroll_mode(struct scroller *scr, bool mode)
199 {
200     // If the request is to enable the scroll mode and it's not already
201     // enabled, then enable it
202     if (mode && !scr->in_scroll_mode) {
203         scr->in_scroll_mode = true;
204         // Start the scroll mode cursor at the same location as the
205         // cursor on the screen
206         vterminal_get_cursor_pos(
207                 scr->vt, scr->scroll_cursor_row, scr->scroll_cursor_col);
208     // If the request is to disable the scroll mode and it's currently
209     // enabled, then disable it
210     } else if (!mode && scr->in_scroll_mode) {
211         scr->in_scroll_mode = false;
212     }
213 }
214 
scr_scroll_mode(struct scroller * scr)215 bool scr_scroll_mode(struct scroller *scr)
216 {
217     return scr->in_scroll_mode;
218 }
219 
scr_up(struct scroller * scr,int nlines)220 void scr_up(struct scroller *scr, int nlines)
221 {
222     // When moving 1 line up
223     //   Move the cursor towards the top of the screen
224     //   If it hits the top, then start scrolling back
225     // Otherwise whem moving many lines up, simply scroll
226     if (scr->scroll_cursor_row > 0 && nlines == 1) {
227         scr->scroll_cursor_row = scr->scroll_cursor_row - 1;
228     } else {
229         vterminal_scroll_delta(scr->vt, nlines);
230     }
231 }
232 
scr_down(struct scroller * scr,int nlines)233 void scr_down(struct scroller *scr, int nlines)
234 {
235     int height;
236     int width;
237     vterminal_get_height_width(scr->vt, height, width);
238 
239     // When moving 1 line down
240     //   Move the cursor towards the botttom of the screen
241     //   If it hits the botttom, then start scrolling forward
242     // Otherwise whem moving many lines down, simply scroll
243     if (scr->scroll_cursor_row < height - 1 && nlines == 1) {
244         scr->scroll_cursor_row = scr->scroll_cursor_row + 1;
245     } else {
246         vterminal_scroll_delta(scr->vt, -nlines);
247     }
248 }
249 
scr_home(struct scroller * scr)250 void scr_home(struct scroller *scr)
251 {
252     int sb_num_rows;
253     vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
254     vterminal_scroll_delta(scr->vt, sb_num_rows);
255 }
256 
scr_end(struct scroller * scr)257 void scr_end(struct scroller *scr)
258 {
259     int sb_num_rows;
260     vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
261     vterminal_scroll_delta(scr->vt, -sb_num_rows);
262 }
263 
scr_left(struct scroller * scr)264 void scr_left(struct scroller *scr)
265 {
266     if (scr->scroll_cursor_col > 0) {
267         scr->scroll_cursor_col--;
268     }
269 }
270 
scr_right(struct scroller * scr)271 void scr_right(struct scroller *scr)
272 {
273     int height;
274     int width;
275     vterminal_get_height_width(scr->vt, height, width);
276 
277     if (scr->scroll_cursor_col < width - 1) {
278         scr->scroll_cursor_col++;
279     }
280 }
281 
scr_beginning_of_row(struct scroller * scr)282 void scr_beginning_of_row(struct scroller *scr)
283 {
284     scr->scroll_cursor_col = 0;
285 }
286 
scr_end_of_row(struct scroller * scr)287 void scr_end_of_row(struct scroller *scr)
288 {
289     int height;
290     int width;
291     vterminal_get_height_width(scr->vt, height, width);
292 
293     scr->scroll_cursor_col = width - 1;
294 }
295 
scr_push_screen_to_scrollback(struct scroller * scr)296 void scr_push_screen_to_scrollback(struct scroller *scr)
297 {
298     vterminal_push_screen_to_scrollback(scr->vt);
299 }
300 
scr_add(struct scroller * scr,const char * buf)301 void scr_add(struct scroller *scr, const char *buf)
302 {
303     // Keep a copy of all text sent to vterm
304     // Vterm doesn't yet support resizing, so we would create a new vterm
305     // instance and feed it the same data
306     scr->text.append(buf);
307 
308     vterminal_write(scr->vt, buf, strlen(buf));
309 }
310 
scr_move(struct scroller * scr,SWINDOW * win)311 void scr_move(struct scroller *scr, SWINDOW *win)
312 {
313     swin_delwin(scr->win);
314     scr->win = win;
315 
316     // recreate the vterm session with the new size
317     vterminal_free(scr->vt);
318 
319     scr->vt = scr_new_vterminal(scr);
320 
321     vterminal_write(scr->vt, scr->text.data(), scr->text.size());
322 }
323 
scr_enable_search(struct scroller * scr,bool forward,bool icase)324 void scr_enable_search(struct scroller *scr, bool forward, bool icase)
325 {
326     if (scr->in_scroll_mode) {
327         int delta;
328         vterminal_scroll_get_delta(scr->vt, delta);
329 
330         int sb_num_rows;
331         vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
332 
333         scr->in_search_mode = true;
334         scr->forward = forward;
335         scr->icase = icase;
336         scr->delta_init = delta;
337         scr->search_sid_init = scr->scroll_cursor_row - delta + sb_num_rows;
338         scr->search_row_init = scr->scroll_cursor_row;
339         scr->search_col_init = scr->scroll_cursor_col;
340     }
341 }
342 
scr_disable_search(struct scroller * scr,bool accept)343 void scr_disable_search(struct scroller *scr, bool accept)
344 {
345     if (scr->in_search_mode) {
346         scr->in_search_mode = false;
347 
348         if (accept) {
349             scr->scroll_cursor_row = scr->search_row;
350             scr->scroll_cursor_col = scr->search_col_start;
351 
352             hl_regex_free(&scr->hlregex);
353             scr->hlregex = 0;
354         } else {
355             scr->scroll_cursor_row = scr->search_row_init;
356             scr->scroll_cursor_col = scr->search_col_init;
357             vterminal_scroll_set_delta(scr->vt, scr->delta_init);
358             scr->last_regex.clear();
359         }
360 
361         scr->search_row = 0;
362         scr->search_col_start = 0;
363         scr->search_col_end = 0;
364     }
365 }
366 
scr_search_mode(struct scroller * scr)367 bool scr_search_mode(struct scroller *scr)
368 {
369     return scr->in_search_mode;
370 }
371 
scr_search_regex_forward(struct scroller * scr,const char * regex)372 static int scr_search_regex_forward(struct scroller *scr, const char *regex)
373 {
374     int sb_num_rows;
375     vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
376 
377     int height;
378     int width;
379     vterminal_get_height_width(scr->vt, height, width);
380 
381     int delta;
382     vterminal_scroll_get_delta(scr->vt, delta);
383 
384     int wrapscan_enabled = cgdbrc_get_int(CGDBRC_WRAPSCAN);
385 
386     int count = sb_num_rows + height;
387     int regex_matched = 0;
388 
389     if (!scr || !regex) {
390         // TODO: LOG ERROR
391         return -1;
392     }
393 
394     scr->last_regex = regex;
395 
396     // The starting search row and column
397     int search_row = scr->search_sid_init;
398     int search_col = scr->search_col_init;
399 
400     // Increment the column by 1 to get the starting row/column
401     if (search_col < width - 1) {
402         search_col++;
403     } else {
404         search_row++;
405         if (search_row >= count) {
406             search_row = 0;
407         }
408         search_col = 0;
409     }
410 
411     for (;;)
412     {
413         int start, end;
414         // convert from sid to cursor position taking into account delta
415         int vfr = search_row - sb_num_rows + delta;
416         std::string utf8buf;
417         vterminal_fetch_row(scr->vt, vfr, search_col, width, utf8buf);
418         regex_matched = hl_regex_search(&scr->hlregex, utf8buf.c_str(),
419                 regex, scr->icase, &start, &end);
420         if (regex_matched > 0) {
421             // Need to scroll the terminal if the search is not in view
422             if (count - delta - height <= search_row &&
423                 search_row < count - delta) {
424             } else {
425                 delta = search_row - sb_num_rows;
426                 if (delta > 0) {
427                     delta = 0;
428                 }
429                 delta = -delta;
430                 vterminal_scroll_set_delta(scr->vt, delta);
431             }
432 
433             // convert from sid to cursor position taking into account delta
434             scr->search_row = search_row - sb_num_rows + delta;
435             scr->search_col_start = start + search_col;
436             scr->search_col_end = end + search_col;
437             break;
438         }
439 
440         // Stop searching when made it back to original position
441         if (wrapscan_enabled &&
442             search_row == scr->search_sid_init && search_col == 0) {
443             break;
444         // Or if wrapscan is disabled and searching hit the end
445         } else if (!wrapscan_enabled && search_row == count - 1) {
446             break;
447         }
448 
449         search_row++;
450         if (search_row >= count) {
451             search_row = 0;
452         }
453         search_col = 0;
454     }
455 
456     return regex_matched;
457 }
458 
scr_search_regex_backwards(struct scroller * scr,const char * regex)459 static int scr_search_regex_backwards(struct scroller *scr, const char *regex)
460 {
461     int sb_num_rows;
462     vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
463 
464     int height;
465     int width;
466     vterminal_get_height_width(scr->vt, height, width);
467 
468     int delta;
469     vterminal_scroll_get_delta(scr->vt, delta);
470 
471     int wrapscan_enabled = cgdbrc_get_int(CGDBRC_WRAPSCAN);
472 
473     int count = sb_num_rows + height;
474     int regex_matched = 0;
475 
476     if (!scr || !regex) {
477         // TODO: LOG ERROR
478         return -1;
479     }
480 
481     scr->last_regex = regex;
482 
483     // The starting search row and column
484     int search_row = scr->search_sid_init;
485     int search_col = scr->search_col_init;
486 
487     // Decrement the column by 1 to get the starting row/column
488     if (search_col > 0) {
489         search_col--;
490     } else {
491         search_row--;
492         if (search_row < 0) {
493             search_row = count - 1;
494         }
495         search_col = width - 1;
496     }
497 
498     for (;;)
499     {
500         int start = 0, end = 0;
501         int vfr = search_row - sb_num_rows + delta;
502 
503         // Searching in reverse is more difficult
504         // The idea is to search right to left, however the regex api
505         // doesn't support that. Need to mimic this by searching left
506         // to right to find all the matches on the line, and then
507         // take the right most match.
508         for (int c = 0;;) {
509             std::string utf8buf;
510             vterminal_fetch_row(scr->vt, vfr, c, width, utf8buf);
511 
512             int _start, _end, result;
513             result = hl_regex_search(&scr->hlregex, utf8buf.c_str(),
514                     regex, scr->icase, &_start, &_end);
515             if ((result == 1) && (c + _start <= search_col)) {
516                 regex_matched = 1;
517                 start = c + _start;
518                 end = c + _end;
519                 c = start + 1;
520             } else {
521                 break;
522             }
523         }
524 
525         if (regex_matched > 0) {
526             // Need to scroll the terminal if the search is not in view
527             if (count - delta - height <= search_row &&
528                 search_row < count - delta) {
529             } else {
530                 delta = search_row - sb_num_rows;
531                 if (delta > 0) {
532                     delta = 0;
533                 }
534                 delta = -delta;
535                 vterminal_scroll_set_delta(scr->vt, delta);
536             }
537 
538             scr->search_row = search_row - sb_num_rows + delta;
539             scr->search_col_start = start;
540             scr->search_col_end = end;
541             break;
542         }
543 
544         // Stop searching when made it back to original position
545         if (wrapscan_enabled &&
546             search_row == scr->search_sid_init &&
547             search_col == width - 1) {
548             break;
549         // Or if wrapscan is disabled and searching hit the top
550         } else if (!wrapscan_enabled && search_row == 0) {
551             break;
552         }
553 
554         search_row--;
555         if (search_row < 0) {
556             search_row = count - 1;
557         }
558         search_col = width - 1;
559     }
560 
561     return regex_matched;
562 }
563 
scr_search_regex(struct scroller * scr,const char * regex)564 int scr_search_regex(struct scroller *scr, const char *regex)
565 {
566     int result;
567 
568     if (scr->forward) {
569         result = scr_search_regex_forward(scr, regex);
570     } else {
571         result = scr_search_regex_backwards(scr, regex);
572     }
573 
574     return result;
575 }
576 
scr_search_next(struct scroller * scr,bool forward,bool icase)577 void scr_search_next(struct scroller *scr, bool forward, bool icase)
578 {
579     if (scr->last_regex.size() > 0) {
580         scr_enable_search(scr, forward, icase);
581         scr_search_regex(scr, scr->last_regex.c_str());
582         scr_disable_search(scr, true);
583     }
584 }
585 
scr_refresh(struct scroller * scr,int focus,enum win_refresh dorefresh)586 void scr_refresh(struct scroller *scr, int focus, enum win_refresh dorefresh)
587 {
588     int height;
589     int width;
590     vterminal_get_height_width(scr->vt, height, width);
591 
592     int vterm_cursor_row, vterm_cursor_col;
593     vterminal_get_cursor_pos(scr->vt, vterm_cursor_row, vterm_cursor_col);
594 
595     int sb_num_rows;
596     vterminal_scrollback_num_rows(scr->vt, sb_num_rows);
597 
598     int delta;
599     vterminal_scroll_get_delta(scr->vt, delta);
600 
601     int highlight_attr, search_attr;
602 
603     int cursor_row, cursor_col;
604 
605     if (scr->in_scroll_mode) {
606         cursor_row = scr->scroll_cursor_row;
607         cursor_col = scr->scroll_cursor_col;
608     } else {
609         cursor_row = vterm_cursor_row;
610         cursor_col = vterm_cursor_col;
611     }
612 
613     /* Steal line highlight attribute for our scroll mode status */
614     highlight_attr = hl_groups_get_attr(hl_groups_instance,
615         HLG_SCROLL_MODE_STATUS);
616 
617     search_attr = hl_groups_get_attr(hl_groups_instance, HLG_INCSEARCH);
618 
619     for (int r = 0; r < height; ++r) {
620         for (int c = 0; c < width; ) {
621             std::string utf8buf;
622             int attr = 0;
623             int cellwidth;
624             int in_search = scr->in_search_mode && scr->search_row == r &&
625                     c >= scr->search_col_start && c < scr->search_col_end;
626 
627             vterminal_fetch_row_col(scr->vt, r, c, utf8buf, attr, cellwidth);
628             swin_wmove(scr->win, r,  c);
629             swin_wattron(scr->win, attr);
630             if (in_search)
631                 swin_wattron(scr->win, search_attr);
632 
633             // print the cell utf8 data or an empty char
634             // If nothing is written at all, then the cell will not be colored
635             if (utf8buf.size()) {
636                 swin_waddnstr(scr->win, utf8buf.data(), utf8buf.size());
637             } else {
638                 swin_waddnstr(scr->win, " ", 1);
639             }
640 
641             if (in_search)
642                 swin_wattroff(scr->win, search_attr);
643             swin_wattroff(scr->win, attr);
644             swin_wclrtoeol(scr->win);
645             c += cellwidth;
646         }
647 
648         // If in scroll mode, overlay the percent the scroller is scrolled
649         // back on the top right of the scroller display.
650         if (scr->in_scroll_mode && r == 0) {
651             char status[ 64 ];
652             size_t status_len;
653 
654             snprintf(status, sizeof(status), "[%d/%d]", delta, sb_num_rows);
655 
656             status_len = strlen(status);
657             if ( status_len < width ) {
658                 swin_wattron(scr->win, highlight_attr);
659                 swin_mvwprintw(scr->win, r, width - status_len, "%s", status);
660                 swin_wattroff(scr->win, highlight_attr);
661             }
662         }
663     }
664 
665     // Show the cursor when the scroller is in focus
666     if (focus) {
667         swin_wmove(scr->win, cursor_row, cursor_col);
668         swin_curs_set(1);
669     } else {
670         /* Hide the cursor */
671         swin_curs_set(0);
672     }
673 
674     switch(dorefresh) {
675         case WIN_NO_REFRESH:
676             swin_wnoutrefresh(scr->win);
677             break;
678         case WIN_REFRESH:
679             swin_wrefresh(scr->win);
680             break;
681     }
682 }
683