1 /**
2  * Copyright (c) 2015, Timothy Stack
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * * Redistributions of source code must retain the above copyright notice, this
10  * list of conditions and the following disclaimer.
11  * * Redistributions in binary form must reproduce the above copyright notice,
12  * this list of conditions and the following disclaimer in the documentation
13  * and/or other materials provided with the distribution.
14  * * Neither the name of Timothy Stack nor the names of its contributors
15  * may be used to endorse or promote products derived from this software
16  * without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29 
30 #include "config.h"
31 
32 #include "base/injector.hh"
33 #include "base/math_util.hh"
34 #include "lnav.hh"
35 #include "bookmarks.hh"
36 #include "sql_util.hh"
37 #include "environ_vtab.hh"
38 #include "plain_text_source.hh"
39 #include "pretty_printer.hh"
40 #include "sysclip.hh"
41 #include "log_data_helper.hh"
42 #include "command_executor.hh"
43 #include "termios_guard.hh"
44 #include "readline_highlighters.hh"
45 #include "field_overlay_source.hh"
46 #include "hotkeys.hh"
47 #include "base/opt_util.hh"
48 #include "shlex.hh"
49 #include "lnav_util.hh"
50 #include "lnav_config.hh"
51 #include "bound_tags.hh"
52 #include "xterm_mouse.hh"
53 
54 using namespace std;
55 
56 class logline_helper {
57 
58 public:
logline_helper(logfile_sub_source & lss)59     logline_helper(logfile_sub_source &lss) : lh_sub_source(lss) {
60 
61     };
62 
move_to_msg_start()63     logline &move_to_msg_start() {
64         content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
65         std::shared_ptr<logfile> lf = this->lh_sub_source.find(cl);
66         auto ll = lf->begin() + cl;
67         while (!ll->is_message()) {
68             --ll;
69             --this->lh_current_line;
70         }
71 
72         return (*lf)[cl];
73     };
74 
current_line()75     logline &current_line() {
76         content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
77         std::shared_ptr<logfile> lf = this->lh_sub_source.find(cl);
78 
79         return (*lf)[cl];
80     };
81 
annotate()82     void annotate() {
83         this->lh_string_attrs.clear();
84         this->lh_line_values.clear();
85         content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
86         std::shared_ptr<logfile> lf = this->lh_sub_source.find(cl);
87         auto ll = lf->begin() + cl;
88         auto format = lf->get_format();
89         lf->read_full_message(ll, this->lh_msg_buffer);
90         format->annotate(cl,
91                          this->lh_msg_buffer,
92                          this->lh_string_attrs,
93                          this->lh_line_values,
94                          false);
95     };
96 
to_string(const struct line_range & lr)97     std::string to_string(const struct line_range &lr) {
98         const char *start = this->lh_msg_buffer.get_data();
99 
100         return string(&start[lr.lr_start], lr.length());
101     }
102 
103     logfile_sub_source &lh_sub_source;
104     vis_line_t lh_current_line;
105     shared_buffer_ref lh_msg_buffer;
106     string_attrs_t lh_string_attrs;
107     vector<logline_value> lh_line_values;
108 };
109 
key_sql_callback(exec_context & ec,sqlite3_stmt * stmt)110 static int key_sql_callback(exec_context &ec, sqlite3_stmt *stmt)
111 {
112     if (!sqlite3_stmt_busy(stmt)) {
113         return 0;
114     }
115 
116     int ncols = sqlite3_column_count(stmt);
117 
118     auto &vars = ec.ec_local_vars.top();
119 
120     for (int lpc = 0; lpc < ncols; lpc++) {
121         const char *column_name = sqlite3_column_name(stmt, lpc);
122 
123         if (sql_ident_needs_quote(column_name)) {
124             continue;
125         }
126         if (sqlite3_column_type(stmt, lpc) == SQLITE_NULL) {
127             continue;
128         }
129 
130         vars[column_name] = string((const char *) sqlite3_column_text(stmt, lpc));
131     }
132 
133     return 0;
134 }
135 
handle_keyseq(const char * keyseq)136 bool handle_keyseq(const char *keyseq)
137 {
138     key_map &km = lnav_config.lc_active_keymap;
139 
140     const auto &iter = km.km_seq_to_cmd.find(keyseq);
141     if (iter == km.km_seq_to_cmd.end()) {
142         return false;
143     }
144 
145     vector<logline_value> values;
146     exec_context ec(&values, key_sql_callback, pipe_callback);
147     auto &var_stack = ec.ec_local_vars;
148 
149     ec.ec_global_vars = lnav_data.ld_exec_context.ec_global_vars;
150     var_stack.push(map<string, string>());
151     auto &vars = var_stack.top();
152     vars["keyseq"] = keyseq;
153     const auto &kc = iter->second;
154 
155     log_debug("executing key sequence %s: %s", keyseq, kc.kc_cmd.c_str());
156     auto result = execute_any(ec, kc.kc_cmd);
157     lnav_data.ld_rl_view->set_value(
158         result.map(ok_prefix).orElse(err_to_ok).unwrap());
159 
160     if (!kc.kc_alt_msg.empty()) {
161         shlex lexer(kc.kc_alt_msg);
162         string expanded_msg;
163 
164         if (lexer.eval(expanded_msg, {
165             &vars,
166             &ec.ec_global_vars,
167         })) {
168             lnav_data.ld_rl_view->set_alt_value(expanded_msg);
169         }
170     }
171 
172     return true;
173 }
174 
handle_paging_key(int ch)175 bool handle_paging_key(int ch)
176 {
177     if (lnav_data.ld_view_stack.empty()) {
178         return false;
179     }
180 
181     textview_curses *tc = *lnav_data.ld_view_stack.top();
182     exec_context &ec = lnav_data.ld_exec_context;
183     logfile_sub_source *lss = nullptr;
184     text_sub_source *tc_tss = tc->get_sub_source();
185     bookmarks<vis_line_t>::type &     bm  = tc->get_bookmarks();
186 
187     char keyseq[16];
188 
189     snprintf(keyseq, sizeof(keyseq), "x%02x", ch);
190 
191     if (handle_keyseq(keyseq)) {
192         return true;
193     }
194 
195     if (tc->handle_key(ch)) {
196         return true;
197     }
198 
199     lss = dynamic_cast<logfile_sub_source *>(tc->get_sub_source());
200 
201     /* process the command keystroke */
202     switch (ch) {
203         case 0x7f:
204         case KEY_BACKSPACE:
205             break;
206 
207         case 'a':
208             if (lnav_data.ld_last_view == nullptr) {
209                 alerter::singleton().chime();
210             }
211             else {
212                 textview_curses *tc = lnav_data.ld_last_view;
213 
214                 lnav_data.ld_last_view = nullptr;
215                 ensure_view(tc);
216             }
217             break;
218 
219         case 'A':
220             if (lnav_data.ld_last_view == nullptr) {
221                 alerter::singleton().chime();
222             }
223             else {
224                 textview_curses *tc = lnav_data.ld_last_view;
225                 textview_curses *top_tc = *lnav_data.ld_view_stack.top();
226                 auto *dst_view = dynamic_cast<text_time_translator *>(tc->get_sub_source());
227                 auto *src_view = dynamic_cast<text_time_translator *>(top_tc->get_sub_source());
228 
229                 lnav_data.ld_last_view = nullptr;
230                 if (src_view != nullptr && dst_view != nullptr) {
231                     src_view->time_for_row(top_tc->get_top()) | [dst_view, tc](auto top_time) {
232                         dst_view->row_for_time(top_time) | [tc](auto row) {
233                             tc->set_top(row);
234                         };
235                     };
236                 }
237                 ensure_view(tc);
238             }
239             break;
240 
241         case KEY_F(2):
242             if (xterm_mouse::is_available()) {
243                 auto& mouse_i = injector::get<xterm_mouse&>();
244                 mouse_i.set_enabled(!mouse_i.is_enabled());
245                 lnav_data.ld_rl_view->set_value(
246                     ok_prefix("info: mouse mode -- ") +
247                     (mouse_i.is_enabled() ?
248                      ANSI_BOLD("enabled") : ANSI_BOLD("disabled")));
249             }
250             else {
251                 lnav_data.ld_rl_view->set_value(
252                         "error: mouse support is not available, make sure your TERM is set to "
253                                 "xterm or xterm-256color");
254             }
255             break;
256 
257         case 'C':
258             if (lss) {
259                 lss->text_clear_marks(&textview_curses::BM_USER);
260             }
261 
262             lnav_data.ld_select_start.erase(tc);
263             lnav_data.ld_last_user_mark.erase(tc);
264             tc->get_bookmarks()[&textview_curses::BM_USER].clear();
265             tc->reload_data();
266 
267             lnav_data.ld_rl_view->set_value(ok_prefix("Cleared bookmarks"));
268             break;
269 
270         case '>':
271         {
272             std::pair<int, int> range;
273 
274             tc->horiz_shift(tc->get_top(),
275                             tc->get_bottom(),
276                             tc->get_left(),
277                             range);
278             if (range.second != INT_MAX) {
279                 tc->set_left(range.second);
280                 lnav_data.ld_rl_view->set_alt_value(
281                         HELP_MSG_1(m, "to bookmark a line"));
282             }
283             else{
284                 alerter::singleton().chime();
285             }
286         }
287             break;
288 
289         case '<':
290             if (tc->get_left() == 0) {
291                 alerter::singleton().chime();
292             }
293             else {
294                 std::pair<int, int> range;
295 
296                 tc->horiz_shift(tc->get_top(),
297                                 tc->get_bottom(),
298                                 tc->get_left(),
299                                 range);
300                 if (range.first != -1) {
301                     tc->set_left(range.first);
302                 }
303                 else{
304                     tc->set_left(0);
305                 }
306                 lnav_data.ld_rl_view->set_alt_value(
307                         HELP_MSG_1(m, "to bookmark a line"));
308             }
309             break;
310 
311         case 'f':
312             if (tc == &lnav_data.ld_views[LNV_LOG]) {
313                 tc->set_top(bm[&logfile_sub_source::BM_FILES].next(tc->get_top()));
314             }
315             else if (tc == &lnav_data.ld_views[LNV_TEXT]) {
316                 textfile_sub_source &tss = lnav_data.ld_text_source;
317 
318                 if (!tss.empty()) {
319                     tss.rotate_left();
320                 }
321             }
322             break;
323 
324         case 'F':
325             if (tc == &lnav_data.ld_views[LNV_LOG]) {
326                 tc->set_top(bm[&logfile_sub_source::BM_FILES].prev(tc->get_top()));
327             }
328             else if (tc == &lnav_data.ld_views[LNV_TEXT]) {
329                 textfile_sub_source &tss = lnav_data.ld_text_source;
330 
331                 if (!tss.empty()) {
332                     tss.rotate_right();
333                 }
334             }
335             break;
336 
337         case 'z':
338             if ((lnav_data.ld_zoom_level - 1) < 0) {
339                 alerter::singleton().chime();
340             }
341             else {
342                 execute_command(ec, "zoom-to " + string(lnav_zoom_strings[lnav_data.ld_zoom_level - 1]));
343             }
344             break;
345 
346         case 'Z':
347             if ((lnav_data.ld_zoom_level + 1) >= ZOOM_COUNT) {
348                 alerter::singleton().chime();
349             }
350             else {
351                 execute_command(ec, "zoom-to " + string(lnav_zoom_strings[lnav_data.ld_zoom_level + 1]));
352             }
353             break;
354 
355         case 'J':
356             if (lnav_data.ld_last_user_mark.find(tc) ==
357                 lnav_data.ld_last_user_mark.end() ||
358                 !tc->is_line_visible(vis_line_t(lnav_data.ld_last_user_mark[tc]))) {
359                 lnav_data.ld_select_start[tc] = tc->get_top();
360                 lnav_data.ld_last_user_mark[tc] = tc->get_top();
361             }
362             else {
363                 vis_line_t    height;
364                 unsigned long width;
365 
366                 tc->get_dimensions(height, width);
367                 if (lnav_data.ld_last_user_mark[tc] > (tc->get_bottom() - 2) &&
368                     tc->get_top() + height < tc->get_inner_height()) {
369                     tc->shift_top(1_vl);
370                 }
371                 if (lnav_data.ld_last_user_mark[tc] + 1 >=
372                     tc->get_inner_height()) {
373                     break;
374                 }
375                 lnav_data.ld_last_user_mark[tc] += 1;
376             }
377             tc->toggle_user_mark(&textview_curses::BM_USER,
378                                  vis_line_t(lnav_data.ld_last_user_mark[tc]));
379             tc->reload_data();
380 
381             lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(
382                     c,
383                     "to copy marked lines to the clipboard"));
384             break;
385 
386         case 'K':
387         {
388             int new_mark;
389 
390             if (lnav_data.ld_last_user_mark.find(tc) ==
391                 lnav_data.ld_last_user_mark.end() ||
392                 !tc->is_line_visible(vis_line_t(lnav_data.ld_last_user_mark[tc]))) {
393                 new_mark = tc->get_top();
394             }
395             else {
396                 new_mark = lnav_data.ld_last_user_mark[tc];
397             }
398 
399             tc->toggle_user_mark(&textview_curses::BM_USER,
400                                  vis_line_t(new_mark));
401             if (new_mark == tc->get_top()) {
402                 tc->shift_top(-1_vl);
403             }
404             if (new_mark > 0) {
405                 lnav_data.ld_last_user_mark[tc] = new_mark - 1;
406             }
407             else {
408                 lnav_data.ld_last_user_mark[tc] = new_mark;
409                 alerter::singleton().chime();
410             }
411             lnav_data.ld_select_start[tc] = tc->get_top();
412             tc->reload_data();
413 
414             lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(
415                     c,
416                     "to copy marked lines to the clipboard"));
417         }
418             break;
419 
420         case 'M':
421             if (lnav_data.ld_last_user_mark.find(tc) ==
422                 lnav_data.ld_last_user_mark.end()) {
423                 alerter::singleton().chime();
424             }
425             else {
426                 int start_line = min((int)tc->get_top(),
427                                      lnav_data.ld_last_user_mark[tc] + 1);
428                 int end_line = max((int)tc->get_top(),
429                                    lnav_data.ld_last_user_mark[tc] - 1);
430 
431                 tc->toggle_user_mark(&textview_curses::BM_USER,
432                                      vis_line_t(start_line),
433                                      vis_line_t(end_line));
434                 tc->reload_data();
435             }
436             break;
437 
438 #if 0
439     case 'S':
440         {
441             bookmark_vector<vis_line_t>::iterator iter;
442 
443             for (iter = bm[&textview_curses::BM_SEARCH].begin();
444                  iter != bm[&textview_curses::BM_SEARCH].end();
445                  ++iter) {
446                 tc->toggle_user_mark(&textview_curses::BM_USER, *iter);
447             }
448 
449             lnav_data.ld_last_user_mark[tc] = -1;
450             tc->reload_data();
451         }
452         break;
453 #endif
454 
455         case 's':
456             if (lss) {
457                 vis_line_t next_top = vis_line_t(tc->get_top() + 2);
458 
459                 if (!lss->is_time_offset_enabled()) {
460                     lnav_data.ld_rl_view->set_alt_value(
461                             HELP_MSG_1(T, "to disable elapsed-time mode"));
462                 }
463                 lss->set_time_offset(true);
464                 while (next_top < tc->get_inner_height()) {
465                     if (!lss->find_line(lss->at(next_top))->is_message()) {
466                     }
467                     else if (lss->get_line_accel_direction(next_top) ==
468                              log_accel::A_DECEL) {
469                         --next_top;
470                         tc->set_top(next_top);
471                         break;
472                     }
473 
474                     ++next_top;
475                 }
476             }
477             break;
478 
479         case 'S':
480             if (lss) {
481                 vis_line_t next_top = tc->get_top();
482 
483                 if (!lss->is_time_offset_enabled()) {
484                     lnav_data.ld_rl_view->set_alt_value(
485                             HELP_MSG_1(T, "to disable elapsed-time mode"));
486                 }
487                 lss->set_time_offset(true);
488                 while (0 <= next_top && next_top < tc->get_inner_height()) {
489                     if (!lss->find_line(lss->at(next_top))->is_message()) {
490                     }
491                     else if (lss->get_line_accel_direction(next_top) ==
492                              log_accel::A_DECEL) {
493                         --next_top;
494                         tc->set_top(next_top);
495                         break;
496                     }
497 
498                     --next_top;
499                 }
500             }
501             break;
502 
503         case '9':
504             if (lss) {
505                 double tenth = ((double)tc->get_inner_height()) / 10.0;
506 
507                 tc->shift_top(vis_line_t(tenth));
508             }
509             break;
510 
511         case '(':
512             if (lss) {
513                 double tenth = ((double)tc->get_inner_height()) / 10.0;
514 
515                 tc->shift_top(vis_line_t(-tenth));
516             }
517             break;
518 
519         case '0':
520             if (lss) {
521                 const int step = 24 * 60 * 60;
522                 lss->time_for_row(tc->get_top()) | [lss, tc](auto first_time) {
523                     lss->find_from_time(roundup_size(first_time.tv_sec, step)) | [tc](auto line) {
524                         tc->set_top(line);
525                     };
526                 };
527             }
528             break;
529 
530         case ')':
531             if (lss) {
532                 lss->time_for_row(tc->get_top()) | [lss, tc](auto first_time) {
533                     time_t day = rounddown(first_time.tv_sec, 24 * 60 * 60);
534                     lss->find_from_time(day) | [tc](auto line) {
535                         if (line != 0_vl) {
536                             --line;
537                         }
538                         tc->set_top(line);
539                     };
540                 };
541             }
542             break;
543 
544         case 'D':
545             if (tc->get_top() == 0) {
546                 alerter::singleton().chime();
547             }
548             else if (lss) {
549                 lss->time_for_row(tc->get_top()) | [lss, ch, tc](auto first_time) {
550                     int step = ch == 'D' ? (24 * 60 * 60) : (60 * 60);
551                     time_t     top_time = first_time.tv_sec;
552                     lss->find_from_time(top_time - step) | [tc](auto line) {
553                         if (line != 0_vl) {
554                             --line;
555                         }
556                         tc->set_top(line);
557                     };
558                 };
559 
560                 lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search"));
561             }
562             break;
563 
564         case 'd':
565             if (lss) {
566                 lss->time_for_row(tc->get_top()) | [ch, lss, tc](auto first_time) {
567                     int step = ch == 'd' ? (24 * 60 * 60) : (60 * 60);
568                     lss->find_from_time(first_time.tv_sec + step) | [tc](auto line) {
569                         tc->set_top(line);
570                     };
571                 };
572 
573                 lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search"));
574             }
575             break;
576 
577         case 'o':
578         case 'O':
579             if (lss) {
580                 logline_helper start_helper(*lss);
581 
582                 start_helper.lh_current_line = tc->get_top();
583                 logline &start_line = start_helper.move_to_msg_start();
584                 start_helper.annotate();
585 
586                 struct line_range opid_range = find_string_attr_range(
587                         start_helper.lh_string_attrs, &logline::L_OPID);
588                 if (!opid_range.is_valid()) {
589                     alerter::singleton().chime();
590                     lnav_data.ld_rl_view->set_value(
591                         err_prefix("Log message does not contain an opid"));
592                 } else {
593                     unsigned int opid_hash = start_line.get_opid();
594                     logline_helper next_helper(*lss);
595                     bool found = false;
596 
597                     next_helper.lh_current_line = start_helper.lh_current_line;
598 
599                     while (true) {
600                         if (ch == 'o') {
601                             if (++next_helper.lh_current_line >= tc->get_inner_height()) {
602                                 break;
603                             }
604                         }
605                         else {
606                             if (--next_helper.lh_current_line <= 0) {
607                                 break;
608                             }
609                         }
610                         logline &next_line = next_helper.current_line();
611                         if (!next_line.is_message()) {
612                             continue;
613                         }
614                         if (next_line.get_opid() != opid_hash) {
615                             continue;
616                         }
617                         next_helper.annotate();
618                         struct line_range opid_next_range = find_string_attr_range(
619                                 next_helper.lh_string_attrs, &logline::L_OPID);
620                         const char *start_opid = start_helper.lh_msg_buffer.get_data_at(opid_range.lr_start);
621                         const char *next_opid = next_helper.lh_msg_buffer.get_data_at(opid_next_range.lr_start);
622                         if (opid_range.length() != opid_next_range.length() ||
623                             memcmp(start_opid, next_opid, opid_range.length()) != 0) {
624                             continue;
625                         }
626                         found = true;
627                         break;
628                     }
629                     if (found) {
630                         lnav_data.ld_rl_view->set_value("");
631                         tc->set_top(next_helper.lh_current_line);
632                     }
633                     else {
634                         string opid_str = start_helper.to_string(opid_range);
635 
636                         lnav_data.ld_rl_view->set_value(
637                             err_prefix("No more messages found with opid: " + opid_str));
638                         alerter::singleton().chime();
639                     }
640                 }
641             }
642             break;
643 
644         case 'p':
645             if (tc == &lnav_data.ld_views[LNV_LOG]) {
646                 field_overlay_source *fos;
647 
648                 fos = (field_overlay_source *) tc->get_overlay_source();
649                 fos->fos_contexts.top().c_show = !fos->fos_contexts.top().c_show;
650                 tc->reload_data();
651             }
652             else if (tc == &lnav_data.ld_views[LNV_DB]) {
653                 db_overlay_source *dos = (db_overlay_source *) tc->get_overlay_source();
654 
655                 dos->dos_active = !dos->dos_active;
656                 tc->reload_data();
657             }
658             break;
659 
660         case 't':
661             if (lnav_data.ld_text_source.current_file() == nullptr) {
662                 alerter::singleton().chime();
663                 lnav_data.ld_rl_view->set_value(
664                     err_prefix("No text files loaded"));
665             }
666             else if (toggle_view(&lnav_data.ld_views[LNV_TEXT])) {
667                 lnav_data.ld_rl_view->set_alt_value(HELP_MSG_2(
668                         f, F,
669                         "to switch to the next/previous file"));
670             }
671             break;
672 
673         case 'T':
674             lnav_data.ld_log_source.toggle_time_offset();
675             if (lss && lss->is_time_offset_enabled()) {
676                 lnav_data.ld_rl_view->set_alt_value(
677                         HELP_MSG_2(s, S, "to move forward/backward through slow downs"));
678             }
679             tc->reload_data();
680             break;
681 
682         case 'I':
683         {
684             auto &hist_tc = lnav_data.ld_views[LNV_HISTOGRAM];
685 
686             if (toggle_view(&hist_tc)) {
687                 auto *src_view = dynamic_cast<text_time_translator *>(tc->get_sub_source());
688 
689                 if (src_view != nullptr) {
690                     src_view->time_for_row(tc->get_top()) | [](auto log_top) {
691                         lnav_data.ld_hist_source2.row_for_time(log_top) | [](auto row) {
692                             lnav_data.ld_views[LNV_HISTOGRAM].set_top(row);
693                         };
694                     };
695                 }
696             }
697             else {
698                 lnav_data.ld_view_stack.top() | [&](auto top_tc) {
699                     auto *dst_view = dynamic_cast<text_time_translator *>(top_tc->get_sub_source());
700 
701                     if (dst_view != nullptr) {
702                         auto& hs = lnav_data.ld_hist_source2;
703                         auto hist_top_time_opt = hs.time_for_row(hist_tc.get_top());
704                         auto curr_top_time_opt = dst_view->time_for_row(top_tc->get_top());
705                         if (hist_top_time_opt && curr_top_time_opt &&
706                             hs.row_for_time(hist_top_time_opt.value()) !=
707                             hs.row_for_time(curr_top_time_opt.value())) {
708                             dst_view->row_for_time(hist_top_time_opt.value()) | [top_tc](auto new_top) {
709                                 top_tc->set_top(new_top);
710                                 top_tc->set_needs_update();
711                             };
712                         }
713                     }
714                 };
715             }
716         }
717             break;
718 
719         case 'V':
720         {
721             textview_curses *db_tc = &lnav_data.ld_views[LNV_DB];
722             db_label_source &dls   = lnav_data.ld_db_row_source;
723 
724             if (toggle_view(db_tc)) {
725                 long log_line_index = dls.column_name_to_index("log_line");
726 
727                 if (log_line_index == -1) {
728                     log_line_index = dls.column_name_to_index("min(log_line)");
729                 }
730 
731                 if (log_line_index != -1) {
732                     char         linestr[64];
733                     int          line_number = (int)tc->get_top();
734                     unsigned int row;
735 
736                     snprintf(linestr, sizeof(linestr), "%d", line_number);
737                     for (row = 0; row < dls.dls_rows.size(); row++) {
738                         if (strcmp(dls.dls_rows[row][log_line_index],
739                                    linestr) == 0) {
740                             vis_line_t db_line(row);
741 
742                             db_tc->set_top(db_line);
743                             db_tc->set_needs_update();
744                             break;
745                         }
746                     }
747                 }
748             }
749             else if (db_tc->get_inner_height() > 0) {
750                 int db_row = db_tc->get_top();
751                 tc = &lnav_data.ld_views[LNV_LOG];
752                 long log_line_index = dls.column_name_to_index("log_line");
753 
754                 if (log_line_index == -1) {
755                     log_line_index = dls.column_name_to_index("min(log_line)");
756                 }
757 
758                 if (log_line_index != -1) {
759                     unsigned int line_number;
760 
761                     if (sscanf(dls.dls_rows[db_row][log_line_index],
762                                "%d",
763                                &line_number) &&
764                         line_number < tc->listview_rows(*tc)) {
765                         tc->set_top(vis_line_t(line_number));
766                         tc->set_needs_update();
767                     }
768                 }
769                 else {
770                     for (size_t lpc = 0; lpc < dls.dls_headers.size(); lpc++) {
771                         date_time_scanner dts;
772                         struct timeval tv;
773                         struct exttm tm;
774                         const char *col_value = dls.dls_rows[db_row][lpc];
775                         size_t col_len = strlen(col_value);
776 
777                         if (dts.scan(col_value, col_len, nullptr, &tm, tv) != nullptr) {
778                             lnav_data.ld_log_source.find_from_time(tv) | [tc](auto vl) {
779                                 tc->set_top(vl);
780                                 tc->set_needs_update();
781                             };
782                             break;
783                         }
784                     }
785                 }
786             }
787         }
788             break;
789 
790         case '\t':
791         case KEY_BTAB:
792             if (tc == &lnav_data.ld_views[LNV_DB])
793             {
794                 auto &chart = lnav_data.ld_db_row_source.dls_chart;
795                 const auto &state = chart.show_next_ident(
796                     ch == '\t' ?
797                     stacked_bar_chart_base::direction::forward :
798                     stacked_bar_chart_base::direction::backward);
799 
800                 state.match(
801                     [&] (stacked_bar_chart_base::show_none) {
802                         lnav_data.ld_rl_view->set_value("Graphing no values");
803                     },
804                     [&] (stacked_bar_chart_base::show_all) {
805                         lnav_data.ld_rl_view->set_value("Graphing all values");
806                     },
807                     [&] (stacked_bar_chart_base::show_one) {
808                         string colname;
809 
810                         chart.get_ident_to_show(colname);
811                         lnav_data.ld_rl_view->set_value(
812                             "Graphing column " ANSI_BOLD_START +
813                             colname + ANSI_NORM);
814                     }
815                 );
816 
817                 tc->reload_data();
818             } else if (tc_tss != nullptr && tc_tss->tss_supports_filtering) {
819                 lnav_data.ld_mode = lnav_data.ld_last_config_mode;
820                 lnav_data.ld_filter_view.reload_data();
821                 lnav_data.ld_files_view.reload_data();
822                 if (tc->get_inner_height() > 0_vl) {
823                     string_attrs_t::const_iterator line_attr;
824                     std::vector<attr_line_t> rows(1);
825 
826                     tc->get_data_source()->
827                         listview_value_for_rows(*tc, tc->get_top(), rows);
828                     string_attrs_t &sa = rows[0].get_attrs();
829                     line_attr = find_string_attr(sa, &logline::L_FILE);
830                     if (line_attr != sa.end()) {
831                         const auto &fc = lnav_data.ld_active_files;
832                         auto lf = ((logfile *)line_attr->sa_value.sav_ptr)->
833                             shared_from_this();
834                         auto iter = find(fc.fc_files.begin(),
835                                          fc.fc_files.end(),
836                                          lf);
837                         if (iter != fc.fc_files.end()) {
838                             auto index = distance(fc.fc_files.begin(), iter);
839                             auto index_vl = vis_line_t(index);
840 
841                             lnav_data.ld_files_view.set_top(index_vl);
842                             lnav_data.ld_files_view.set_selection(index_vl);
843                         }
844                     }
845                 }
846             } else {
847                 alerter::singleton().chime();
848             }
849             break;
850 
851         case 'x':
852             if (tc->toggle_hide_fields()) {
853                 lnav_data.ld_rl_view->set_value("Showing hidden fields");
854             } else {
855                 lnav_data.ld_rl_view->set_value("Hiding hidden fields");
856             }
857             tc->set_needs_update();
858             break;
859 
860         case 'r':
861         case 'R':
862             if (lss) {
863                 auto& last_time =
864                     injector::get<const relative_time&, last_relative_time_tag>();
865 
866                 if (last_time.empty()) {
867                     lnav_data.ld_rl_view->set_value(
868                             "Use the 'goto' command to set the relative time to move by");
869                 }
870                 else {
871                     vis_line_t vl = tc->get_top(), new_vl;
872                     relative_time rt = last_time;
873                     content_line_t cl;
874                     struct exttm tm;
875                     bool done = false;
876 
877                     if (ch == 'r') {
878                         if (rt.is_negative()) {
879                             rt.negate();
880                         }
881                     } else if (ch == 'R') {
882                         if (!rt.is_negative()) {
883                             rt.negate();
884                         }
885                     }
886 
887                     cl = lnav_data.ld_log_source.at(vl);
888                     logline *ll = lnav_data.ld_log_source.find_line(cl);
889                     ll->to_exttm(tm);
890                     do {
891                         tm = rt.adjust(tm);
892                         auto new_vl_opt = lnav_data.ld_log_source.find_from_time(tm);
893                         if (!new_vl_opt) {
894                             break;
895                         }
896                         new_vl = new_vl_opt.value();
897 
898                         if (new_vl == 0_vl || new_vl != vl || !rt.is_relative()) {
899                             vl = new_vl;
900                             done = true;
901                         }
902                     } while (!done);
903                     tc->set_top(vl);
904                     lnav_data.ld_rl_view->set_value(" " + rt.to_string());
905                 }
906             }
907             break;
908 
909         case KEY_CTRL_W:
910             execute_command(ec, lnav_data.ld_views[LNV_LOG].get_word_wrap() ?
911                             "disable-word-wrap" : "enable-word-wrap");
912             break;
913 
914         case KEY_CTRL_P:
915             lnav_data.ld_preview_hidden = !lnav_data.ld_preview_hidden;
916             break;
917 
918         default:
919             return false;
920     }
921     return true;
922 }
923