diff --git a/NEWS b/NEWS index 7617f675..0a05c4e3 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,10 @@ lnav v0.8.5: with hotkeys. * Added an 'lnav_view_filters' SQL table that can be used to programmatically manipulate filters. + * A history of locations in a view is now kept so that you can jump back + to where you were previously using the '{' and '}' keys. The location + history can also be accessed through the ":prev-location" and + ":next-location" commands. * The ":write-*" commands will now accept "/dev/clipboard" as a file name that writes to the system clipboard. * The ":write-to" and ":write-raw-to" commands will now print out comments diff --git a/docs/source/commands.rst b/docs/source/commands.rst index f67bd51c..901c5eef 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -59,6 +59,8 @@ Navigation bookmark of the given type in the current view. * prev-mark error|warning|search|user|file|partition - Move to the previous bookmark of the given type in the current view. +* prev-location - The previous location in the history. +* next-location - The next location in the history. Time ---- diff --git a/docs/source/hotkeys.rst b/docs/source/hotkeys.rst index 98bef734..24714979 100644 --- a/docs/source/hotkeys.rst +++ b/docs/source/hotkeys.rst @@ -129,6 +129,10 @@ Spatial Navigation - |ks| Shift |ke| + |ks| s |ke| - - Next/prevous slow down in the log message rate + * - |ks| { |ke| + - |ks| } |ke| + - + - Previous/next location in history Chronological Navigation ------------------------ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cac3ebe0..4756e830 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ set(diag_STAT_SRCS help_text_formatter.cc hist_source.cc hotkeys.cc + input_dispatcher.cc intern_string.cc is_utf8.cc json-extension-functions.cc @@ -123,6 +124,7 @@ set(diag_STAT_SRCS highlighter.hh hotkeys.hh init-sql.hh + input_dispatcher.hh intern_string.hh is_utf8.hh k_merge_tree.h @@ -145,6 +147,7 @@ set(diag_STAT_SRCS readline_possibilities.hh regexp_vtab.hh relative_time.hh + ring_span.hh sequence_sink.hh shlex.hh simdutf8check.h diff --git a/src/Makefile.am b/src/Makefile.am index 0f83316f..412a8b7f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -174,6 +174,7 @@ noinst_HEADERS = \ hotkeys.hh \ init.sql \ init-sql.hh \ + input_dispatcher.hh \ intern_string.hh \ is_utf8.hh \ json_op.hh \ @@ -293,6 +294,7 @@ libdiag_a_SOURCES = \ help_text_formatter.cc \ hist_source.cc \ hotkeys.cc \ + input_dispatcher.cc \ intern_string.cc \ is_utf8.cc \ json-extension-functions.cc \ diff --git a/src/help.txt b/src/help.txt index 850da3fd..be674cf3 100644 --- a/src/help.txt +++ b/src/help.txt @@ -241,6 +241,12 @@ Spatial Navigation five seconds later, the last message will be highlighted as a slow down. + {/} Move to the previous/next location in history. Whenever + you jump to a new location in the view, the location will + be added to the history. The history is not updated when + using only the arrow keys. + + Chronological Navigation ------------------------ diff --git a/src/hotkeys.cc b/src/hotkeys.cc index 6485bdfa..cb0fd85d 100644 --- a/src/hotkeys.cc +++ b/src/hotkeys.cc @@ -101,6 +101,60 @@ public: 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; + } + if (sqlite3_column_type(stmt, lpc) == SQLITE_NULL) { + continue; + } + + vars[column_name] = string((const char *) sqlite3_column_text(stmt, lpc)); + } + + return 0; +} + +bool handle_keyseq(const char *keyseq) +{ + key_map &km = lnav_config.lc_ui_keymaps[lnav_config.lc_ui_keymap]; + + const auto &iter = km.km_seq_to_cmd.find(keyseq); + if (iter != km.km_seq_to_cmd.end()) { + vector values; + exec_context ec(&values, key_sql_callback, pipe_callback); + auto &var_stack = ec.ec_local_vars; + string result; + + ec.ec_global_vars = lnav_data.ld_exec_context.ec_global_vars; + var_stack.push(map()); + auto &vars = var_stack.top(); + vars["keyseq"] = keyseq; + for (const string &cmd : iter->second) { + log_debug("executing key sequence x%02x: %s", + keyseq, cmd.c_str()); + result = execute_any(ec, cmd); + } + + lnav_data.ld_rl_view->set_value(result); + return true; + } + + return false; +} + void handle_paging_key(int ch) { if (lnav_data.ld_view_stack.vs_views.empty()) { @@ -117,6 +171,18 @@ void handle_paging_key(int ch) return; } + char keyseq[16]; + + snprintf(keyseq, sizeof(keyseq), "x%02x", ch); + + if (handle_keyseq(keyseq)) { + return; + } + + if (tc->handle_key(ch)) { + return; + } + lss = dynamic_cast(tc->get_sub_source()); /* process the command keystroke */ diff --git a/src/hotkeys.hh b/src/hotkeys.hh index f26f3107..a5918390 100644 --- a/src/hotkeys.hh +++ b/src/hotkeys.hh @@ -30,6 +30,7 @@ #ifndef LNAV_HOTKEYS_H #define LNAV_HOTKEYS_H +bool handle_keyseq(const char *keyseq); void handle_paging_key(int ch); #endif //LNAV_HOTKEYS_H diff --git a/src/input_dispatcher.cc b/src/input_dispatcher.cc new file mode 100644 index 00000000..31c7d0c5 --- /dev/null +++ b/src/input_dispatcher.cc @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2018, 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. + * + * @file input_dispatcher.cc + */ + +#include "config.h" + +#include + +#if defined HAVE_NCURSESW_CURSES_H +# include +#elif defined HAVE_NCURSESW_H +# include +#elif defined HAVE_NCURSES_CURSES_H +# include +#elif defined HAVE_NCURSES_H +# include +#elif defined HAVE_CURSES_H +# include +#else +# error "SysV or X/Open-compatible Curses header file required" +#endif + +#include "input_dispatcher.hh" + +void input_dispatcher::new_input(const struct timeval ¤t_time, int ch) +{ + switch (ch) { + case KEY_ESCAPE: + this->id_escape_index = 0; + this->append_to_escape_buffer(ch); + this->id_escape_start_time = current_time; + break; + case KEY_MOUSE: + this->id_mouse_handler(); + break; + default: + if (this->id_escape_index > 0) { + if (strcmp("\x1b[", this->id_escape_buffer) == 0) { + this->id_mouse_handler(); + this->id_escape_index = 0; + } else { + this->append_to_escape_buffer(ch); + + switch (this->id_escape_matcher(this->id_escape_buffer)) { + case escape_match_t::NONE: + for (int lpc = 0; this->id_escape_buffer[lpc]; lpc++) { + this->id_key_handler(this->id_escape_buffer[lpc]); + } + this->id_escape_index = 0; + break; + case escape_match_t::PARTIAL: + break; + case escape_match_t::FULL: + this->id_escape_handler(this->id_escape_buffer); + this->id_escape_index = 0; + break; + } + } + } else { + this->id_key_handler(ch); + } + break; + } +} + +void input_dispatcher::poll(const struct timeval ¤t_time) +{ + if (this->id_escape_index == 1) { + struct timeval diff; + + gettimeofday((struct timeval *) ¤t_time, nullptr); + + timersub(¤t_time, &this->id_escape_start_time, &diff); + if (diff.tv_sec > 0 || diff.tv_usec > (10000)) { + this->id_key_handler(KEY_CTRL_RBRACKET); + this->id_escape_index = 0; + } + } +} diff --git a/src/input_dispatcher.hh b/src/input_dispatcher.hh new file mode 100644 index 00000000..af997a2a --- /dev/null +++ b/src/input_dispatcher.hh @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018, 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. + * + * @file input_dispatcher.hh + */ + +#ifndef INPUT_DISPATCHER_HH +#define INPUT_DISPATCHER_HH + +#include + +#include + +#define KEY_ESCAPE 0x1b +#define KEY_CTRL_RBRACKET 0x1d + +class input_dispatcher { +public: + void new_input(const struct timeval ¤t_time, int ch); + + void poll(const struct timeval ¤t_time); + + bool in_escape() const { + return this->id_escape_index > 0; + } + + enum class escape_match_t { + NONE, + PARTIAL, + FULL, + }; + + std::function id_escape_matcher; + std::function id_key_handler; + std::function id_escape_handler; + std::function id_mouse_handler; +private: + void append_to_escape_buffer(int ch) { + this->id_escape_buffer[this->id_escape_index++] = static_cast(ch); + this->id_escape_buffer[this->id_escape_index] = '\0'; + } + + char id_escape_buffer[32]; + size_t id_escape_index{0}; + struct timeval id_escape_start_time{0, 0}; +}; + +#endif diff --git a/src/keymap-default.json b/src/keymap-default.json index d83b4757..8cb78c2f 100644 --- a/src/keymap-default.json +++ b/src/keymap-default.json @@ -9,7 +9,9 @@ "keymap_def_text_view": "Press ${ansi_bold}t${ansi_norm} to switch to the text view", "keymap_def_pop_view": "Press ${ansi_bold}q${ansi_norm} to return to the previous view", "keymap_def_zoom": "Press ${ansi_bold}z${ansi_norm}/${ansi_bold}z${ansi_norm} to zoom in/out", - "keymap_def_clear": "Press ${ansi_bold}C${ansi_norm} to clear marked messages" + "keymap_def_clear": "Press ${ansi_bold}C${ansi_norm} to clear marked messages", + "keymap_def_prev_location": "Press ${ansi_bold}{${ansi_norm} to move to the previous location in history", + "keymap_def_next_location": "Press ${ansi_bold}}${ansi_norm} to move to the next location in history" }, "keymap_def": { "default": { @@ -70,6 +72,15 @@ ":eval :alt-msg ${keymap_def_scroll_horiz}" ], + "x7d": [ + ":next-location", + ":eval :alt-msg ${keymap_def_prev_location}" + ], + "x7b": [ + ":prev-location", + ":eval :alt-msg ${keymap_def_next_location}" + ], + "x31": [":goto next 10 minutes after the hour"], "x32": [":goto next 20 minutes after the hour"], "x33": [":goto next 30 minutes after the hour"], diff --git a/src/listview_curses.cc b/src/listview_curses.cc index 97c3eb98..224bf799 100644 --- a/src/listview_curses.cc +++ b/src/listview_curses.cc @@ -44,32 +44,14 @@ using namespace std; list_gutter_source listview_curses::DEFAULT_GUTTER_SOURCE; listview_curses::listview_curses() - : lv_source(NULL), - lv_overlay_source(NULL), - lv_window(NULL), - lv_x(0), - lv_y(0), - lv_top(0), - lv_left(0), - lv_height(0), - lv_overlay_needs_update(true), - lv_show_scrollbar(true), - lv_show_bottom_border(false), - lv_gutter_source(&DEFAULT_GUTTER_SOURCE), - lv_word_wrap(false), - lv_scroll_accel(0), - lv_scroll_velo(0), - lv_mouse_y(-1), - lv_mouse_mode(LV_MODE_NONE), - lv_tail_space(1) -{ } += default; listview_curses::~listview_curses() -{ } += default; -void listview_curses::reload_data(void) +void listview_curses::reload_data() { - if (this->lv_source == NULL) { + if (this->lv_source == nullptr) { this->lv_top = 0_vl; this->lv_left = 0; } diff --git a/src/listview_curses.hh b/src/listview_curses.hh index 79e2332f..59b1f72e 100644 --- a/src/listview_curses.hh +++ b/src/listview_curses.hh @@ -569,29 +569,30 @@ protected: static list_gutter_source DEFAULT_GUTTER_SOURCE; std::string lv_title; - list_data_source *lv_source; /*< The data source delegate. */ + list_data_source *lv_source{nullptr}; /*< The data source delegate. */ std::list lv_input_delegates; - list_overlay_source *lv_overlay_source; + list_overlay_source *lv_overlay_source{nullptr}; action lv_scroll; /*< The scroll action. */ - WINDOW * lv_window; /*< The window that contains this view. */ - unsigned int lv_x; - unsigned int lv_y; /*< The y offset of this view. */ - vis_line_t lv_top; /*< The line at the top of the view. */ - unsigned int lv_left; /*< The column at the left of the view. */ - vis_line_t lv_height; /*< The abs/rel height of the view. */ - bool lv_overlay_needs_update; - bool lv_show_scrollbar; /*< Draw the scrollbar in the view. */ - bool lv_show_bottom_border; - list_gutter_source *lv_gutter_source; - bool lv_word_wrap; + WINDOW * lv_window{nullptr}; /*< The window that contains this view. */ + unsigned int lv_x{0}; + unsigned int lv_y{0}; /*< The y offset of this view. */ + vis_line_t lv_top{0}; /*< The line at the top of the view. */ + unsigned int lv_left{0}; /*< The column at the left of the view. */ + vis_line_t lv_height{0}; /*< The abs/rel height of the view. */ + int lv_history_position{0}; + bool lv_overlay_needs_update{true}; + bool lv_show_scrollbar{true}; /*< Draw the scrollbar in the view. */ + bool lv_show_bottom_border{false}; + list_gutter_source *lv_gutter_source{&DEFAULT_GUTTER_SOURCE}; + bool lv_word_wrap{false}; bool lv_selectable{false}; vis_line_t lv_selection{0}; struct timeval lv_mouse_time; int lv_scroll_accel; int lv_scroll_velo; - int lv_mouse_y; - lv_mode_t lv_mouse_mode; - vis_line_t lv_tail_space; + int lv_mouse_y{-1}; + lv_mode_t lv_mouse_mode{LV_MODE_NONE}; + vis_line_t lv_tail_space{1}; }; #endif diff --git a/src/lnav.cc b/src/lnav.cc index 515610a2..bd9d64ff 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -632,32 +632,6 @@ static void sigchld(int sig) lnav_data.ld_child_terminated = true; } -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; - } - if (sqlite3_column_type(stmt, lpc) == SQLITE_NULL) { - continue; - } - - vars[column_name] = string((const char *) sqlite3_column_text(stmt, lpc)); - } - - return 0; -} - vis_line_t next_cluster( vis_line_t(bookmark_vector::*f) (vis_line_t) const, bookmark_type_t *bt, @@ -716,6 +690,10 @@ bool moveto_cluster(vis_line_t(bookmark_vector::*f) (vis_line_t) con new_top = next_cluster(f, bt, tc->get_top()); } if (new_top != -1) { + tc->get_sub_source()->get_location_history() | [new_top] (auto lh) { + lh->loc_history_append(new_top); + }; + tc->set_top(new_top); return true; } @@ -744,6 +722,10 @@ void previous_cluster(bookmark_type_t *bt, textview_curses *tc) } if (new_top != -1) { + tc->get_sub_source()->get_location_history() | [new_top] (auto lh) { + lh->loc_history_append(new_top); + }; + tc->set_top(new_top); } else { @@ -1100,7 +1082,7 @@ public: me.me_state = BUTTON_STATE_PRESSED; } - gettimeofday(&me.me_time, NULL); + gettimeofday(&me.me_time, nullptr); me.me_x = x - 1; me.me_y = y - tc->get_y() - 1; @@ -1116,36 +1098,6 @@ static void handle_key(int ch) { if (lnav_data.ld_mode == LNM_PAGING) { auto top_tc = lnav_data.ld_view_stack.top(); - if (top_tc && (*top_tc)->handle_key(ch)) { - return; - } - - char keyseq[16]; - - snprintf(keyseq, sizeof(keyseq), "x%02x", ch); - - key_map &km = lnav_config.lc_ui_keymaps[lnav_config.lc_ui_keymap]; - - const auto &iter = km.km_seq_to_cmd.find(keyseq); - if (iter != km.km_seq_to_cmd.end()) { - vector values; - exec_context ec(&values, key_sql_callback, pipe_callback); - auto &var_stack = ec.ec_local_vars; - string result; - - ec.ec_global_vars = lnav_data.ld_exec_context.ec_global_vars; - var_stack.push(map()); - auto &vars = var_stack.top(); - vars["keyseq"] = keyseq; - for (const string &cmd : iter->second) { - log_debug("executing key sequence x%02x: %s", - keyseq, cmd.c_str()); - result = execute_any(ec, cmd); - } - - lnav_data.ld_rl_view->set_value(result); - return; - } } switch (ch) { @@ -1180,6 +1132,57 @@ static void handle_key(int ch) { } } +static input_dispatcher::escape_match_t match_escape_seq(const char *escape_buffer) +{ + if (lnav_data.ld_mode != LNM_PAGING) { + return input_dispatcher::escape_match_t::NONE; + } + + char keyseq[32] = ""; + + for (size_t lpc = 0; escape_buffer[lpc]; lpc++) { + snprintf(keyseq + strlen(keyseq), sizeof(keyseq) - strlen(keyseq), + "x%02x", + escape_buffer[lpc]); + } + + key_map &km = lnav_config.lc_ui_keymaps[lnav_config.lc_ui_keymap]; + auto iter = km.km_seq_to_cmd.find(keyseq); + if (iter != km.km_seq_to_cmd.end()) { + return input_dispatcher::escape_match_t::FULL; + } + + auto lb = km.km_seq_to_cmd.lower_bound(keyseq); + + if (lb == km.km_seq_to_cmd.end()) { + return input_dispatcher::escape_match_t::NONE; + } + + auto ub = km.km_seq_to_cmd.upper_bound(keyseq); + auto longest = max_element(lb, ub, [] (auto l, auto r) { + return l.first.size() < r.first.size(); + }); + + if (strlen(escape_buffer) < longest->first.size()) { + return input_dispatcher::escape_match_t::PARTIAL; + } + + return input_dispatcher::escape_match_t::NONE; +} + +static void handle_escape_seq(const char *escape_buffer) +{ + char keyseq[32] = ""; + + for (size_t lpc = 0; escape_buffer[lpc]; lpc++) { + snprintf(keyseq + strlen(keyseq), sizeof(keyseq) - strlen(keyseq), + "x%02x", + escape_buffer[lpc]); + } + + handle_keyseq(keyseq); +} + void update_hits(void *dummy, textview_curses *tc) { auto top_tc = lnav_data.ld_view_stack.top(); @@ -1462,12 +1465,18 @@ static void looper() sb.invoke(*lnav_data.ld_view_stack.top()); vsb.invoke(*lnav_data.ld_view_stack.top()); + { + input_dispatcher &id = lnav_data.ld_input_dispatcher; + + id.id_escape_matcher = match_escape_seq; + id.id_escape_handler = handle_escape_seq; + id.id_key_handler = handle_key; + id.id_mouse_handler = bind(&xterm_mouse::handle_mouse, &lnav_data.ld_mouse); + } + bool session_loaded = false; ui_periodic_timer &timer = ui_periodic_timer::singleton(); struct timeval current_time; - size_t escape_index = 0; - char escape_buffer[32]; - struct timeval escape_start_time; static sig_atomic_t index_counter; @@ -1518,22 +1527,13 @@ static void looper() tc.update_poll_set(pollfds); } - if (escape_index > 0) { + if (lnav_data.ld_input_dispatcher.in_escape()) { to.tv_usec = 15000; } rc = poll(&pollfds[0], pollfds.size(), to.tv_usec / 1000); - if (escape_index == 1) { - struct timeval diff; - - gettimeofday(¤t_time, nullptr); - - timersub(¤t_time, &escape_start_time, &diff); - if (diff.tv_sec > 0 || diff.tv_usec > (10000)) { - handle_key(KEY_CTRL_RBRACKET); - escape_index = 0; - } - } + gettimeofday(¤t_time, nullptr); + lnav_data.ld_input_dispatcher.poll(current_time); if (rc < 0) { switch (errno) { @@ -1554,53 +1554,12 @@ static void looper() while ((ch = getch()) != ERR) { alerter::singleton().new_input(ch); - /* Check to make sure there is enough space for a - * character and a string terminator. - */ - if (escape_index >= sizeof(escape_buffer) - 2) { - escape_index = 0; - } - else if (escape_index > 0) { - escape_buffer[escape_index++] = ch; - escape_buffer[escape_index] = '\0'; - - if (strcmp("\x1b[", escape_buffer) == 0) { - lnav_data.ld_mouse.handle_mouse(ch); - } - else { - for (size_t lpc = 0; lpc < escape_index; lpc++) { - handle_key(escape_buffer[lpc]); - } - } - escape_index = 0; - continue; - } + lnav_data.ld_input_dispatcher.new_input(current_time, ch); lnav_data.ld_view_stack.top() | [ch] (auto tc) { lnav_data.ld_key_repeat_history.update(ch, tc->get_top()); }; - switch (ch) { - case CTRL('d'): - case KEY_RESIZE: - break; - - case KEY_ESCAPE: - escape_index = 0; - escape_buffer[escape_index++] = ch; - escape_buffer[escape_index] = '\0'; - escape_start_time = current_time; - break; - - case KEY_MOUSE: - lnav_data.ld_mouse.handle_mouse(ch); - break; - - default: - handle_key(ch); - break; - } - if (!lnav_data.ld_looping) { // No reason to keep processing input after the // user has quit. The view stack will also be diff --git a/src/lnav.hh b/src/lnav.hh index 58814e3f..1f8726d1 100644 --- a/src/lnav.hh +++ b/src/lnav.hh @@ -64,6 +64,7 @@ #include "spectro_source.hh" #include "command_executor.hh" #include "plain_text_source.hh" +#include "input_dispatcher.hh" #include "filter_sub_source.hh" #include "filter_status_source.hh" #include "preview_status_source.hh" @@ -297,6 +298,7 @@ struct _lnav_data { term_extra ld_term_extra; input_state_tracker ld_input_state; + input_dispatcher ld_input_dispatcher; curl_looper ld_curl_looper; diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 75bc4b68..fd720e81 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -89,7 +89,7 @@ static string refresh_pt_search() } #ifdef HAVE_LIBCURL - for (auto lf : lnav_data.ld_files) { + for (const auto &lf : lnav_data.ld_files) { if (startswith(lf->get_filename(), "pt:")) { lf->close(); } @@ -270,6 +270,7 @@ static string com_goto(exec_context &ec, string cmdline, vector &args) struct timeval tv; struct exttm tm; float value; + nonstd::optional dst_vl; if (rt.parse(all_args, pe)) { if (ttt != nullptr) { @@ -293,16 +294,12 @@ static string com_goto(exec_context &ec, string cmdline, vector &args) } } while (!done); - if (ec.ec_dry_run) { - retval = "info: will move to line " + to_string((int) vl); - } else { - tc->set_top(vl); - retval = ""; - if (!rt.is_absolute() && lnav_data.ld_rl_view != nullptr) { - lnav_data.ld_rl_view->set_alt_value( - HELP_MSG_2(r, R, - "to move forward/backward the same amount of time")); - } + dst_vl = vl; + + if (!ec.ec_dry_run && !rt.is_absolute() && lnav_data.ld_rl_view != nullptr) { + lnav_data.ld_rl_view->set_alt_value( + HELP_MSG_2(r, R, + "to move forward/backward the same amount of time")); } } else { retval = "error: relative time values only work in a time-indexed view"; @@ -311,15 +308,7 @@ static string com_goto(exec_context &ec, string cmdline, vector &args) else if (dts.scan(args[1].c_str(), args[1].size(), nullptr, &tm, tv) != nullptr) { if (ttt != nullptr) { - vis_line_t vl; - - vl = vis_line_t(ttt->row_for_time(tv)); - if (ec.ec_dry_run) { - retval = "info: will move to line " + to_string((int) vl); - } else { - tc->set_top(vl); - retval = ""; - } + dst_vl = vis_line_t(ttt->row_for_time(tv)); } else { retval = "error: time values only work in a time-indexed view"; @@ -337,14 +326,22 @@ static string com_goto(exec_context &ec, string cmdline, vector &args) line_number = tc->get_inner_height() + line_number; } } + + dst_vl = vis_line_t(line_number); + } + + dst_vl | [&ec, tc, &retval] (auto new_top) { if (ec.ec_dry_run) { - retval = "info: will move to line " + to_string(line_number); + retval = "info: will move to line " + to_string((int) new_top); } else { - tc->set_top(vis_line_t(line_number)); + tc->get_sub_source()->get_location_history() | [new_top] (auto lh) { + lh->loc_history_append(new_top); + }; + tc->set_top(new_top); retval = ""; } - } + }; } return retval; @@ -435,6 +432,27 @@ static string com_goto_mark(exec_context &ec, string cmdline, vector &ar return retval; } +static string com_goto_location(exec_context &ec, string cmdline, vector &args) +{ + string retval; + + if (args.empty()) { + } + else if (!ec.ec_dry_run) { + lnav_data.ld_view_stack.top() | [&args] (auto tc) { + tc->get_sub_source()->get_location_history() | [tc, &args] (auto lh) { + return args[0] == "prev-location" ? + lh->loc_history_back(tc->get_top()) : + lh->loc_history_forward(tc->get_top()); + } | [tc] (auto new_top) { + tc->set_top(new_top); + }; + }; + } + + return retval; +} + static bool csv_needs_quoting(const string &str) { return (str.find_first_of(",\"") != string::npos); @@ -3720,6 +3738,22 @@ readline_context::command_t STD_COMMANDS[] = { .with_example({"error"}) .with_tags({"bookmarks", "navigation"}) }, + { + "next-location", + com_goto_location, + + help_text(":next-location") + .with_summary("Move to the next position in the location history") + .with_tags({"navigation"}) + }, + { + "prev-location", + com_goto_location, + + help_text(":prev-location") + .with_summary("Move to the previous position in the location history") + .with_tags({"navigation"}) + }, { "help", com_help, diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc index 4d8b8058..5b063e61 100644 --- a/src/logfile_sub_source.cc +++ b/src/logfile_sub_source.cc @@ -104,7 +104,8 @@ logfile_sub_source::logfile_sub_source() lss_marked_only(false), lss_index_delegate(NULL), lss_longest_line(0), - lss_meta_grepper(*this) + lss_meta_grepper(*this), + lss_location_history(*this) { this->tss_supports_filtering = true; this->clear_line_size_cache(); @@ -785,7 +786,7 @@ void logfile_sub_source::text_update_marks(vis_bookmarks &bm) bm[lss_user_mark.first].insert_once(vl); if (lss_user_mark.first == &textview_curses::BM_USER) { - logfile::iterator ll = lf->begin() + cl; + auto ll = lf->begin() + cl; ll->set_mark(true); } @@ -920,3 +921,63 @@ logfile_sub_source::get_grepper() (grep_proc_sink *) &this->lss_meta_grepper ); } + +void log_location_history::loc_history_append(vis_line_t top) +{ + content_line_t cl = this->llh_log_source.at(top); + + auto iter = this->llh_history.begin(); + iter += this->llh_history.size() - this->lh_history_position; + this->llh_history.erase_from(iter); + this->lh_history_position = 0; + this->llh_history.push_back(cl); +} + +nonstd::optional log_location_history::loc_history_back(vis_line_t current_top) +{ + while (this->lh_history_position < this->llh_history.size()) { + auto iter = this->llh_history.rbegin(); + + auto vis_for_pos = this->llh_log_source.find_from_content(*iter); + + if (this->lh_history_position == 0 && vis_for_pos != current_top) { + return vis_for_pos; + } + + if ((this->lh_history_position + 1) >= this->llh_history.size()) { + break; + } + + this->lh_history_position += 1; + + iter += this->lh_history_position; + + vis_for_pos = this->llh_log_source.find_from_content(*iter); + + if (vis_for_pos) { + return vis_for_pos; + } + } + + return nonstd::nullopt; +} + +nonstd::optional +log_location_history::loc_history_forward(vis_line_t current_top) +{ + while (this->lh_history_position > 0) { + this->lh_history_position -= 1; + + auto iter = this->llh_history.rbegin(); + + iter += this->lh_history_position; + + auto vis_for_pos = this->llh_log_source.find_from_content(*iter); + + if (vis_for_pos) { + return vis_for_pos; + } + } + + return nonstd::nullopt; +} diff --git a/src/logfile_sub_source.hh b/src/logfile_sub_source.hh index 5fd3a060..8ec5381f 100644 --- a/src/logfile_sub_source.hh +++ b/src/logfile_sub_source.hh @@ -97,6 +97,30 @@ protected: pcrepp pf_pcre; }; +class log_location_history : public location_history { +public: + log_location_history(logfile_sub_source &lss) + : llh_history(std::begin(this->llh_backing), + std::end(this->llh_backing)), + llh_log_source(lss) { + } + + ~log_location_history() override = default; + + void loc_history_append(vis_line_t top) override; + + nonstd::optional + loc_history_back(vis_line_t current_top) override; + + nonstd::optional + loc_history_forward(vis_line_t current_top) override; + +private: + nonstd::ring_span llh_history; + logfile_sub_source &llh_log_source; + content_line_t llh_backing[MAX_SIZE]; +}; + /** * Delegate class that merges the contents of multiple log files into a single * source of data for a text view. @@ -478,6 +502,35 @@ public: return this->find_from_time(tv); }; + nonstd::optional find_from_content(content_line_t cl) { + content_line_t line = cl; + std::shared_ptr lf = this->find(line); + + if (lf != nullptr) { + auto ll_iter = lf->begin() + line; + auto &ll = *ll_iter; + vis_line_t vis_start = this->find_from_time(ll.get_timeval()); + + while (vis_start < this->text_line_count()) { + content_line_t guess_cl = this->at(vis_start); + + if (cl == guess_cl) { + return vis_start; + } + + auto guess_line = this->find_line(guess_cl); + + if (!guess_line || ll < *guess_line) { + return nonstd::nullopt; + } + + ++vis_start; + } + } + + return nonstd::nullopt; + } + struct timeval time_for_row(int row) { return this->find_line(this->at(vis_line_t(row)))->get_timeval(); }; @@ -674,6 +727,10 @@ public: nonstd::optional *, grep_proc_sink *>> get_grepper(); + nonstd::optional get_location_history() { + return &this->lss_location_history; + }; + static const uint64_t MAX_CONTENT_LINES = (1ULL << 40) - 1; static const uint64_t MAX_LINES_PER_FILE = 256 * 1024 * 1024; static const uint64_t MAX_FILES = ( @@ -847,6 +904,7 @@ private: index_delegate *lss_index_delegate; size_t lss_longest_line; meta_grepper lss_meta_grepper; + log_location_history lss_location_history; }; #endif diff --git a/src/plain_text_source.hh b/src/plain_text_source.hh index abe47eec..848014c5 100644 --- a/src/plain_text_source.hh +++ b/src/plain_text_source.hh @@ -37,7 +37,7 @@ #include "textview_curses.hh" class plain_text_source - : public text_sub_source { + : public text_sub_source, public vis_location_history { public: plain_text_source() : tds_text_format(TF_UNKNOWN), tds_longest_line(0) { @@ -133,6 +133,11 @@ public: return *this; }; + nonstd::optional get_location_history() + { + return this; + } + private: size_t compute_longest_line() { size_t retval = 0; diff --git a/src/readline_curses.cc b/src/readline_curses.cc index d229804e..98640a7a 100644 --- a/src/readline_curses.cc +++ b/src/readline_curses.cc @@ -900,9 +900,6 @@ void readline_curses::handle_key(int ch) if (write(this->rc_pty[RCF_MASTER], bch, len) == -1) { perror("handle_key: write failed"); } - if (ch == '\t' || ch == '\r') { - // this->vc_past_lines.clear(); - } } void readline_curses::focus(int context, const char *prompt, const char *initial) diff --git a/src/textfile_sub_source.hh b/src/textfile_sub_source.hh index abbc93a9..5840e0a4 100644 --- a/src/textfile_sub_source.hh +++ b/src/textfile_sub_source.hh @@ -36,7 +36,8 @@ #include "textview_curses.hh" #include "filter_observer.hh" -class textfile_sub_source : public text_sub_source { +class textfile_sub_source + : public text_sub_source, public vis_location_history { public: typedef std::list>::iterator file_iterator; @@ -269,6 +270,11 @@ public: return this->tss_files.front()->get_text_format(); } + nonstd::optional get_location_history() + { + return this; + } + private: void detach_observer(std::shared_ptr lf) { line_filter_observer *lfo = (line_filter_observer *) lf->get_logline_observer(); diff --git a/src/textview_curses.hh b/src/textview_curses.hh index 24a8b63a..0d74e80d 100644 --- a/src/textview_curses.hh +++ b/src/textview_curses.hh @@ -35,6 +35,7 @@ #include #include +#include "ring_span.hh" #include "grep_proc.hh" #include "bookmarks.hh" #include "listview_curses.hh" @@ -145,7 +146,7 @@ public: } } - void add_line(logfile_filter_state &lfs, const logfile::const_iterator ll, shared_buffer_ref &line); + void add_line(logfile_filter_state &lfs, logfile::const_iterator ll, shared_buffer_ref &line); void end_of_message(logfile_filter_state &lfs) { uint32_t mask = 0; @@ -339,6 +340,23 @@ protected: struct timeval ttt_top_time{0, 0}; }; +class location_history { +public: + virtual ~location_history() = default; + + virtual void loc_history_append(vis_line_t top) = 0; + + virtual nonstd::optional + loc_history_back(vis_line_t current_top) = 0; + + virtual nonstd::optional + loc_history_forward(vis_line_t current_top) = 0; + + const static int MAX_SIZE = 100; +protected: + int lh_history_position{0}; +}; + /** * Source for the text to be shown in a textview_curses view. */ @@ -455,12 +473,73 @@ public: return nonstd::nullopt; } + virtual nonstd::optional get_location_history() { + return nonstd::nullopt; + } + bool tss_supports_filtering{false}; protected: - textview_curses *tss_view; + textview_curses *tss_view{nullptr}; filter_stack tss_filters; }; +class vis_location_history : public location_history { +public: + vis_location_history() + : vlh_history(std::begin(this->vlh_backing), std::end(this->vlh_backing)) + { + } + + void loc_history_append(vis_line_t top) override { + auto iter = this->vlh_history.begin(); + iter += this->vlh_history.size() - this->lh_history_position; + this->vlh_history.erase_from(iter); + this->lh_history_position = 0; + this->vlh_history.push_back(top); + } + + nonstd::optional + loc_history_back(vis_line_t current_top) override { + if (this->lh_history_position == 0) { + vis_line_t history_top = this->current_position(); + if (history_top != current_top) { + return history_top; + } + } + + if (this->lh_history_position + 1 >= this->vlh_history.size()) { + return nonstd::nullopt; + } + + this->lh_history_position += 1; + + return this->current_position(); + } + + nonstd::optional + loc_history_forward(vis_line_t current_top) override { + if (this->lh_history_position == 0) { + return nonstd::nullopt; + } + + this->lh_history_position -= 1; + + return this->current_position(); + } + + nonstd::ring_span vlh_history; +private: + vis_line_t current_position() { + auto iter = this->vlh_history.rbegin(); + + iter += this->lh_history_position; + + return *iter; + } + + vis_line_t vlh_backing[MAX_SIZE]; +}; + class text_delegate { public: virtual ~text_delegate() { }; diff --git a/src/view_curses.hh b/src/view_curses.hh index 39eb50c7..61f3660d 100644 --- a/src/view_curses.hh +++ b/src/view_curses.hh @@ -70,8 +70,6 @@ #define KEY_CTRL_P 16 #define KEY_CTRL_R 18 #define KEY_CTRL_W 23 -#define KEY_ESCAPE 0x1b -#define KEY_CTRL_RBRACKET 0x1d class view_curses; diff --git a/src/xterm_mouse.hh b/src/xterm_mouse.hh index f1655c1f..bc4de72f 100644 --- a/src/xterm_mouse.hh +++ b/src/xterm_mouse.hh @@ -158,7 +158,7 @@ public: * Handle a KEY_MOUSE character from ncurses. * @param ch unused */ - void handle_mouse(int ch_unused) + void handle_mouse() { bool release = false; int ch; diff --git a/test/expected_help.txt b/test/expected_help.txt index 22c25c45..5fd10dc3 100644 --- a/test/expected_help.txt +++ b/test/expected_help.txt @@ -241,6 +241,12 @@ Spatial Navigation five seconds later, the last message will be highlighted as a slow down. + {/} Move to the previous/next location in history. Whenever + you jump to a new location in the view, the location will + be added to the history. The history is not updated when + using only the arrow keys. + + Chronological Navigation ------------------------ diff --git a/test/test_cmds.sh b/test/test_cmds.sh index 99a8219f..7105455b 100644 --- a/test/test_cmds.sh +++ b/test/test_cmds.sh @@ -1,5 +1,29 @@ #! /bin/bash +run_test ${lnav_test} -n -d /tmp/lnav.err \ + -c ":goto 0" \ + -c ":next-mark error" \ + -c ":prev-location" \ + ${test_dir}/logfile_access_log.0 + +check_output "prev-location is not working" <