/** * Copyright (c) 2015, Timothy Stack * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Timothy Stack nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "hotkeys.hh" #include "base/ansi_scrubber.hh" #include "base/injector.hh" #include "base/itertools.hh" #include "base/math_util.hh" #include "base/opt_util.hh" #include "bookmarks.hh" #include "bound_tags.hh" #include "command_executor.hh" #include "config.h" #include "field_overlay_source.hh" #include "lnav.hh" #include "lnav_config.hh" #include "shlex.hh" #include "sql_util.hh" #include "sqlitepp.client.hh" #include "xterm_mouse.hh" using namespace lnav::roles::literals; class logline_helper { public: explicit logline_helper(logfile_sub_source& lss) : lh_sub_source(lss) {} logline& move_to_msg_start() { content_line_t cl = this->lh_sub_source.at(this->lh_current_line); std::shared_ptr lf = this->lh_sub_source.find(cl); auto ll = lf->begin() + cl; while (!ll->is_message()) { --ll; --this->lh_current_line; } return (*lf)[cl]; } logline& current_line() const { content_line_t cl = this->lh_sub_source.at(this->lh_current_line); std::shared_ptr lf = this->lh_sub_source.find(cl); return (*lf)[cl]; } void annotate() { this->lh_string_attrs.clear(); this->lh_line_values.clear(); content_line_t cl = this->lh_sub_source.at(this->lh_current_line); auto lf = this->lh_sub_source.find(cl); auto ll = lf->begin() + cl; auto format = lf->get_format(); lf->read_full_message(ll, this->lh_line_values.lvv_sbr); this->lh_line_values.lvv_sbr.erase_ansi(); format->annotate( cl, this->lh_string_attrs, this->lh_line_values, false); } std::string to_string(const struct line_range& lr) const { const char* start = this->lh_line_values.lvv_sbr.get_data(); return {&start[lr.lr_start], (size_t) lr.length()}; } logfile_sub_source& lh_sub_source; vis_line_t lh_current_line; string_attrs_t lh_string_attrs; logline_value_vector lh_line_values; }; static int key_sql_callback(exec_context& ec, sqlite3_stmt* stmt) { if (!sqlite3_stmt_busy(stmt)) { return 0; } int ncols = sqlite3_column_count(stmt); auto& vars = ec.ec_local_vars.top(); for (int lpc = 0; lpc < ncols; lpc++) { const char* column_name = sqlite3_column_name(stmt, lpc); if (sql_ident_needs_quote(column_name)) { continue; } auto* raw_value = sqlite3_column_value(stmt, lpc); auto value_type = sqlite3_column_type(stmt, lpc); scoped_value_t value; switch (value_type) { case SQLITE_INTEGER: value = (int64_t) sqlite3_value_int64(raw_value); break; case SQLITE_FLOAT: value = sqlite3_value_double(raw_value); break; case SQLITE_NULL: value = null_value_t{}; break; default: value = std::string((const char*) sqlite3_value_text(raw_value), sqlite3_value_bytes(raw_value)); break; } vars[column_name] = value; } return 0; } bool handle_keyseq(const char* keyseq) { const auto& km = lnav_config.lc_active_keymap; const auto& iter = km.km_seq_to_cmd.find(keyseq); if (iter == km.km_seq_to_cmd.end()) { return false; } logline_value_vector values; exec_context ec(&values, key_sql_callback, pipe_callback); auto& var_stack = ec.ec_local_vars; ec.ec_label_source_stack.push_back(&lnav_data.ld_db_row_source); ec.ec_global_vars = lnav_data.ld_exec_context.ec_global_vars; ec.ec_error_callback_stack = lnav_data.ld_exec_context.ec_error_callback_stack; var_stack.push(std::map()); // XXX push another so it doesn't look like interactive use var_stack.push(std::map()); auto& vars = var_stack.top(); vars["keyseq"] = keyseq; const auto& kc = iter->second; log_debug( "executing key sequence %s: %s", keyseq, kc.kc_cmd.pp_value.c_str()); auto sg = ec.enter_source(kc.kc_cmd.pp_location.sl_source, kc.kc_cmd.pp_location.sl_line_number, kc.kc_cmd.pp_value); auto result = execute_any(ec, kc.kc_cmd.pp_value); if (result.isOk()) { lnav_data.ld_rl_view->set_value(result.unwrap()); } else { auto um = result.unwrapErr(); ec.ec_error_callback_stack.back()(um); } if (!kc.kc_alt_msg.empty()) { shlex lexer(kc.kc_alt_msg); std::string expanded_msg; if (lexer.eval(expanded_msg, { &vars, &ec.ec_global_vars, })) { lnav_data.ld_rl_view->set_alt_value(expanded_msg); } } return true; } bool handle_paging_key(int ch, const char* keyseq) { if (lnav_data.ld_view_stack.empty()) { return false; } textview_curses* tc = *lnav_data.ld_view_stack.top(); exec_context& ec = lnav_data.ld_exec_context; text_sub_source* tc_tss = tc->get_sub_source(); bookmarks::type& bm = tc->get_bookmarks(); if (tc->get_overlay_selection()) { if (tc->handle_key(ch)) { return true; } } if (handle_keyseq(keyseq)) { return true; } if (tc->handle_key(ch)) { return true; } auto lss = dynamic_cast(tc->get_sub_source()); auto text_accel_p = dynamic_cast(tc->get_sub_source()); /* process the command keystroke */ switch (ch) { case 0x7f: case KEY_BACKSPACE: break; case 'a': if (lnav_data.ld_last_view == nullptr) { alerter::singleton().chime("no last view available"); } else { auto* tc = lnav_data.ld_last_view; lnav_data.ld_last_view = nullptr; ensure_view(tc); } break; case 'A': if (lnav_data.ld_last_view == nullptr) { alerter::singleton().chime("no last view available"); } else { auto* tc = lnav_data.ld_last_view; auto* top_tc = *lnav_data.ld_view_stack.top(); auto* dst_view = dynamic_cast(tc->get_sub_source()); auto* src_view = dynamic_cast( top_tc->get_sub_source()); lnav_data.ld_last_view = nullptr; if (src_view != nullptr && dst_view != nullptr) { src_view->time_for_row(top_tc->get_selection()) | [dst_view, tc](auto top_ri) { dst_view->row_for_time(top_ri.ri_time) | [tc](auto row) { tc->set_selection(row); }; }; } ensure_view(tc); } break; case KEY_F(2): if (xterm_mouse::is_available()) { auto& mouse_i = injector::get(); mouse_i.set_enabled(!mouse_i.is_enabled()); auto al = attr_line_t("mouse mode -- ") .append(mouse_i.is_enabled() ? "enabled"_symbol : "disabled"_symbol); if (mouse_i.is_enabled() && lnav_config.lc_mouse_mode == lnav_mouse_mode::disabled) { al.append(" -- enable permanently with ") .append(":config /ui/mouse/mode enabled"_quoted_code); auto clear_note = prepare_stmt(lnav_data.ld_db, R"( DELETE FROM lnav_user_notifications WHERE id = 'org.lnav.mouse-support' )"); clear_note.unwrap().execute(); } auto um = lnav::console::user_message::ok(al); lnav_data.ld_rl_view->set_attr_value(um.to_attr_line()); } else { lnav_data.ld_rl_view->set_value( "error: mouse support is not available, make sure your " "TERM is set to xterm or xterm-256color"); } break; case 'C': if (lss) { lss->text_clear_marks(&textview_curses::BM_USER); } lnav_data.ld_select_start.erase(tc); lnav_data.ld_last_user_mark.erase(tc); tc->get_bookmarks()[&textview_curses::BM_USER].clear(); tc->reload_data(); lnav_data.ld_rl_view->set_attr_value( lnav::console::user_message::ok("Cleared bookmarks") .to_attr_line()); break; case '>': { auto range_opt = tc->horiz_shift( tc->get_top(), tc->get_bottom(), tc->get_left()); if (range_opt && range_opt.value().second != INT_MAX) { tc->set_left(range_opt.value().second); lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(m, "to bookmark a line")); } else { alerter::singleton().chime("no more search hits to the right"); } break; } case '<': if (tc->get_left() == 0) { alerter::singleton().chime("no more search hits to the left"); } else { auto range_opt = tc->horiz_shift( tc->get_top(), tc->get_bottom(), tc->get_left()); if (range_opt && range_opt.value().first != -1) { tc->set_left(range_opt.value().first); } else { tc->set_left(0); } lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(m, "to bookmark a line")); } break; case 'f': if (tc == &lnav_data.ld_views[LNV_LOG]) { bm[&logfile_sub_source::BM_FILES].next(tc->get_selection()) | [&tc](auto vl) { tc->set_selection(vl); }; } else if (tc == &lnav_data.ld_views[LNV_TEXT]) { textfile_sub_source& tss = lnav_data.ld_text_source; if (!tss.empty()) { tss.rotate_left(); } tc->reload_data(); } break; case 'F': if (tc == &lnav_data.ld_views[LNV_LOG]) { bm[&logfile_sub_source::BM_FILES].prev(tc->get_selection()) | [&tc](auto vl) { // setting the selection for movement to previous file // marker instead of the top will move the cursor, too, // if needed. tc->set_selection(vl); }; } else if (tc == &lnav_data.ld_views[LNV_TEXT]) { textfile_sub_source& tss = lnav_data.ld_text_source; if (!tss.empty()) { tss.rotate_right(); } tc->reload_data(); } break; case 'z': if ((lnav_data.ld_zoom_level - 1) < 0) { alerter::singleton().chime("maximum zoom-in level reached"); } else { execute_command( ec, "zoom-to " + lnav_zoom_strings[lnav_data.ld_zoom_level - 1]); } break; case 'Z': if ((lnav_data.ld_zoom_level + 1) >= ZOOM_COUNT) { alerter::singleton().chime("maximum zoom-out level reached"); } else { execute_command( ec, "zoom-to " + lnav_zoom_strings[lnav_data.ld_zoom_level + 1]); } break; case 'J': if (tc->is_selectable()) { if (tc->get_selection() >= 0_vl) { tc->toggle_user_mark(&textview_curses::BM_USER, tc->get_selection()); lnav_data.ld_select_start[tc] = tc->get_selection(); lnav_data.ld_last_user_mark[tc] = tc->get_selection(); if (tc->get_selection() + 1_vl < tc->get_inner_height()) { tc->set_selection(tc->get_selection() + 1_vl); } } } else { if (lnav_data.ld_last_user_mark.find(tc) == lnav_data.ld_last_user_mark.end() || !tc->is_line_visible( vis_line_t(lnav_data.ld_last_user_mark[tc]))) { lnav_data.ld_select_start[tc] = tc->get_selection(); lnav_data.ld_last_user_mark[tc] = tc->get_selection(); } else { vis_line_t height; unsigned long width; tc->get_dimensions(height, width); if (lnav_data.ld_last_user_mark[tc] > (tc->get_bottom() - 2) && tc->get_selection() + height < tc->get_inner_height()) { tc->shift_top(1_vl); } if (lnav_data.ld_last_user_mark[tc] + 1 >= tc->get_inner_height()) { break; } lnav_data.ld_last_user_mark[tc] += 1; } tc->toggle_user_mark( &textview_curses::BM_USER, vis_line_t(lnav_data.ld_last_user_mark[tc])); } tc->reload_data(); lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(c, "to copy marked lines to the clipboard")); break; case 'K': { int new_mark; if (lnav_data.ld_last_user_mark.find(tc) == lnav_data.ld_last_user_mark.end() || !tc->is_line_visible( vis_line_t(lnav_data.ld_last_user_mark[tc]))) { new_mark = tc->get_selection(); } else { new_mark = lnav_data.ld_last_user_mark[tc]; } tc->toggle_user_mark(&textview_curses::BM_USER, vis_line_t(new_mark)); if (new_mark == tc->get_selection() && tc->get_top() > 0_vl) { tc->shift_top(-1_vl); } if (new_mark > 0) { lnav_data.ld_last_user_mark[tc] = new_mark - 1; } else { lnav_data.ld_last_user_mark[tc] = new_mark; alerter::singleton().chime("no more lines to mark"); } lnav_data.ld_select_start[tc] = tc->get_selection(); if (tc->is_selectable() && tc->get_selection() > 0_vl) { tc->set_selection(tc->get_selection() - 1_vl); } tc->reload_data(); lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(c, "to copy marked lines to the clipboard")); break; } case 'M': if (lnav_data.ld_last_user_mark.find(tc) == lnav_data.ld_last_user_mark.end()) { alerter::singleton().chime("no lines have been marked"); } else { int start_line = std::min((int) tc->get_selection(), lnav_data.ld_last_user_mark[tc] + 1); int end_line = std::max((int) tc->get_selection(), lnav_data.ld_last_user_mark[tc] - 1); tc->toggle_user_mark(&textview_curses::BM_USER, vis_line_t(start_line), vis_line_t(end_line)); tc->reload_data(); } break; #if 0 case 'S': { bookmark_vector::iterator iter; for (iter = bm[&textview_curses::BM_SEARCH].begin(); iter != bm[&textview_curses::BM_SEARCH].end(); ++iter) { tc->toggle_user_mark(&textview_curses::BM_USER, *iter); } lnav_data.ld_last_user_mark[tc] = -1; tc->reload_data(); } break; #endif case 's': if (text_accel_p && text_accel_p->is_time_offset_supported()) { auto next_top = tc->get_selection() + 1_vl; if (!tc->is_selectable()) { next_top += 1_vl; } if (!text_accel_p->is_time_offset_enabled()) { lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(T, "to disable elapsed-time mode")); } text_accel_p->set_time_offset(true); while (next_top < tc->get_inner_height()) { if (!text_accel_p->text_accel_get_line(next_top) ->is_message()) { } else if (text_accel_p->get_line_accel_direction(next_top) == log_accel::A_DECEL) { if (!tc->is_selectable()) { --next_top; } tc->set_selection(next_top); break; } ++next_top; } } break; case 'S': if (text_accel_p && text_accel_p->is_time_offset_supported()) { auto next_top = tc->get_selection(); if (tc->is_selectable() && next_top > 0_vl) { next_top -= 1_vl; } if (!text_accel_p->is_time_offset_enabled()) { lnav_data.ld_rl_view->set_alt_value( HELP_MSG_1(T, "to disable elapsed-time mode")); } text_accel_p->set_time_offset(true); while (0 <= next_top && next_top < tc->get_inner_height()) { if (!text_accel_p->text_accel_get_line(next_top) ->is_message()) { } else if (text_accel_p->get_line_accel_direction(next_top) == log_accel::A_DECEL) { if (!tc->is_selectable()) { --next_top; } tc->set_selection(next_top); break; } --next_top; } } break; case '9': if (lss) { double tenth = ((double) tc->get_inner_height()) / 10.0; tc->shift_top(vis_line_t(tenth)); } break; case '(': if (lss) { double tenth = ((double) tc->get_inner_height()) / 10.0; tc->shift_top(vis_line_t(-tenth)); } break; case '0': if (lss) { const int step = 24 * 60 * 60; lss->time_for_row(tc->get_selection()) | [lss, tc](auto first_ri) { lss->find_from_time( roundup_size(first_ri.ri_time.tv_sec, step)) | [tc](auto line) { tc->set_selection(line); }; }; } break; case ')': if (lss) { lss->time_for_row(tc->get_selection()) | [lss, tc](auto first_ri) { time_t day = rounddown(first_ri.ri_time.tv_sec, 24 * 60 * 60); lss->find_from_time(day) | [tc](auto line) { if (line != 0_vl) { --line; } tc->set_selection(line); }; }; } break; case 'D': if (tc->get_selection() == 0) { alerter::singleton().chime( "the top of the log has been reached"); } else if (lss) { lss->time_for_row(tc->get_selection()) | [lss, ch, tc](auto first_ri) { int step = ch == 'D' ? (24 * 60 * 60) : (60 * 60); time_t top_time = first_ri.ri_time.tv_sec; lss->find_from_time(top_time - step) | [tc](auto line) { if (line != 0_vl) { --line; } tc->set_selection(line); }; }; lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search")); } break; case 'd': if (lss) { lss->time_for_row(tc->get_selection()) | [ch, lss, tc](auto first_ri) { int step = ch == 'd' ? (24 * 60 * 60) : (60 * 60); lss->find_from_time(first_ri.ri_time.tv_sec + step) | [tc](auto line) { tc->set_selection(line); }; }; lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search")); } break; case 'o': case 'O': if (lss != nullptr) { logline_helper start_helper(*lss); start_helper.lh_current_line = tc->get_selection(); auto& start_line = start_helper.move_to_msg_start(); start_helper.annotate(); const auto& opid_opt = start_helper.lh_line_values.lvv_opid_value; if (!opid_opt) { alerter::singleton().chime( "Log message does not contain an opid"); lnav_data.ld_rl_view->set_attr_value( lnav::console::user_message::error( "Log message does not contain an opid") .to_attr_line()); } else { unsigned int opid_hash = start_line.get_opid(); logline_helper next_helper(*lss); bool found = false; next_helper.lh_current_line = start_helper.lh_current_line; while (true) { if (ch == 'o') { if (++next_helper.lh_current_line >= tc->get_inner_height()) { break; } } else { if (--next_helper.lh_current_line <= 0) { break; } } logline& next_line = next_helper.current_line(); if (!next_line.is_message()) { continue; } if (next_line.get_opid() != opid_hash) { continue; } next_helper.annotate(); const auto& next_opid_opt = next_helper.lh_line_values.lvv_opid_value; if (next_opid_opt && opid_opt.value() != next_opid_opt.value()) { continue; } found = true; break; } if (found) { lnav_data.ld_rl_view->set_value(""); tc->set_selection(next_helper.lh_current_line); } else { lnav_data.ld_rl_view->set_attr_value( lnav::console::user_message::error( attr_line_t( "No more messages found with opid: ") .append( lnav::roles::symbol(opid_opt.value()))) .to_attr_line()); alerter::singleton().chime( "no more messages found with opid"); } } } break; case 't': if (lnav_data.ld_text_source.current_file() == nullptr) { alerter::singleton().chime("No text files loaded"); lnav_data.ld_rl_view->set_attr_value( lnav::console::user_message::error("No text files loaded") .to_attr_line()); } else if (toggle_view(&lnav_data.ld_views[LNV_TEXT])) { lnav_data.ld_rl_view->set_alt_value( HELP_MSG_2(f, F, "to switch to the next/previous file")); } break; case 'I': { auto& hist_tc = lnav_data.ld_views[LNV_HISTOGRAM]; if (toggle_view(&hist_tc)) { auto* src_view = dynamic_cast(tc->get_sub_source()); if (src_view != nullptr) { src_view->time_for_row(tc->get_selection()) | [](auto log_top_ri) { lnav_data.ld_hist_source2.row_for_time( log_top_ri.ri_time) | [](auto row) { lnav_data.ld_views[LNV_HISTOGRAM] .set_selection(row); }; }; } } else { lnav_data.ld_view_stack.top() | [&](auto top_tc) { auto* dst_view = dynamic_cast( top_tc->get_sub_source()); if (dst_view != nullptr) { auto& hs = lnav_data.ld_hist_source2; auto hist_top_time_opt = hs.time_for_row(hist_tc.get_selection()); auto curr_top_time_opt = dst_view->time_for_row(top_tc->get_selection()); if (hist_top_time_opt && curr_top_time_opt && hs.row_for_time(hist_top_time_opt->ri_time) != hs.row_for_time(curr_top_time_opt->ri_time)) { dst_view->row_for_time(hist_top_time_opt->ri_time) | [top_tc](auto new_top) { top_tc->set_selection(new_top); top_tc->set_needs_update(); }; } } }; } break; } case 'V': { textview_curses* db_tc = &lnav_data.ld_views[LNV_DB]; db_label_source& dls = lnav_data.ld_db_row_source; if (toggle_view(db_tc)) { auto log_line_index = dls.column_name_to_index("log_line"); if (!log_line_index) { log_line_index = dls.column_name_to_index("min(log_line)"); } if (log_line_index) { fmt::memory_buffer linestr; int line_number = (int) tc->get_selection(); unsigned int row; fmt::format_to(std::back_inserter(linestr), FMT_STRING("{}"), line_number); linestr.push_back('\0'); for (row = 0; row < dls.dls_rows.size(); row++) { if (strcmp(dls.dls_rows[row][log_line_index.value()], linestr.data()) == 0) { vis_line_t db_line(row); db_tc->set_selection(db_line); db_tc->set_needs_update(); break; } } } } else if (db_tc->get_inner_height() > 0) { int db_row = db_tc->get_selection(); tc = &lnav_data.ld_views[LNV_LOG]; auto log_line_index = dls.column_name_to_index("log_line"); if (!log_line_index) { log_line_index = dls.column_name_to_index("min(log_line)"); } if (log_line_index) { unsigned int line_number; if (sscanf(dls.dls_rows[db_row][log_line_index.value()], "%d", &line_number) && line_number < tc->listview_rows(*tc)) { tc->set_selection(vis_line_t(line_number)); tc->set_needs_update(); } } else { for (size_t lpc = 0; lpc < dls.dls_headers.size(); lpc++) { date_time_scanner dts; struct timeval tv; struct exttm tm; const char* col_value = dls.dls_rows[db_row][lpc]; size_t col_len = strlen(col_value); if (dts.scan(col_value, col_len, nullptr, &tm, tv) != nullptr) { lnav_data.ld_log_source.find_from_time(tv) | [tc](auto vl) { tc->set_selection(vl); tc->set_needs_update(); }; break; } } } } } break; case '\t': case KEY_BTAB: if (tc == &lnav_data.ld_views[LNV_DB]) { } else if (tc == &lnav_data.ld_views[LNV_SPECTRO]) { lnav_data.ld_mode = ln_mode_t::SPECTRO_DETAILS; } else if (tc_tss != nullptr && tc_tss->tss_supports_filtering) { lnav_data.ld_mode = lnav_data.ld_last_config_mode; lnav_data.ld_filter_view.reload_data(); lnav_data.ld_files_view.reload_data(); if (tc->get_inner_height() > 0_vl) { std::vector rows(1); tc->get_data_source()->listview_value_for_rows( *tc, tc->get_top(), rows); string_attrs_t& sa = rows[0].get_attrs(); auto line_attr_opt = get_string_attr(sa, logline::L_FILE); if (line_attr_opt) { const auto& fc = lnav_data.ld_active_files; auto lf = line_attr_opt.value().get(); auto index_opt = fc.fc_files | lnav::itertools::find(lf); if (index_opt) { auto index_vl = vis_line_t(index_opt.value()); lnav_data.ld_files_view.set_top(index_vl); lnav_data.ld_files_view.set_selection(index_vl); } } } } else { alerter::singleton().chime( "no configuration panels in this view"); } break; case 'r': case 'R': if (lss != nullptr) { const auto& last_time = injector::get(); if (last_time.empty()) { lnav_data.ld_rl_view->set_value( "Use the 'goto' command to set the relative time to " "move by"); } else { vis_line_t vl = tc->get_selection(), new_vl; relative_time rt = last_time; content_line_t cl; struct exttm tm; bool done = false; if (ch == 'r') { if (rt.is_negative()) { rt.negate(); } } else if (ch == 'R') { if (!rt.is_negative()) { rt.negate(); } } cl = lnav_data.ld_log_source.at(vl); logline* ll = lnav_data.ld_log_source.find_line(cl); ll->to_exttm(tm); do { tm = rt.adjust(tm); auto new_vl_opt = lnav_data.ld_log_source.find_from_time(tm); if (!new_vl_opt) { break; } new_vl = new_vl_opt.value(); if (new_vl == 0_vl || new_vl != vl || !rt.is_relative()) { vl = new_vl; done = true; } } while (!done); tc->set_selection(vl); lnav_data.ld_rl_view->set_value(" " + rt.to_string()); } } break; case KEY_CTRL('p'): lnav_data.ld_preview_hidden = !lnav_data.ld_preview_hidden; break; default: log_debug("key sequence %x", ch); return false; } return true; }