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