[view] keep a history of view positions

Fixes #577
This commit is contained in:
Timothy Stack 2018-12-14 06:18:31 -08:00
parent 7fe811da07
commit 814ad03ec9
26 changed files with 678 additions and 188 deletions

4
NEWS
View File

@ -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

View File

@ -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
----

View File

@ -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
------------------------

View File

@ -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

View File

@ -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 \

View File

@ -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
------------------------

View File

@ -101,6 +101,60 @@ public:
vector<logline_value> 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<logline_value> 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<string, string>());
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<logfile_sub_source *>(tc->get_sub_source());
/* process the command keystroke */

View File

@ -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

106
src/input_dispatcher.cc Normal file
View File

@ -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 <sys/time.h>
#if defined HAVE_NCURSESW_CURSES_H
# include <ncursesw/curses.h>
#elif defined HAVE_NCURSESW_H
# include <ncursesw.h>
#elif defined HAVE_NCURSES_CURSES_H
# include <ncurses/curses.h>
#elif defined HAVE_NCURSES_H
# include <ncurses.h>
#elif defined HAVE_CURSES_H
# include <curses.h>
#else
# error "SysV or X/Open-compatible Curses header file required"
#endif
#include "input_dispatcher.hh"
void input_dispatcher::new_input(const struct timeval &current_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 &current_time)
{
if (this->id_escape_index == 1) {
struct timeval diff;
gettimeofday((struct timeval *) &current_time, nullptr);
timersub(&current_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;
}
}
}

73
src/input_dispatcher.hh Normal file
View File

@ -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 <sys/types.h>
#include <functional>
#define KEY_ESCAPE 0x1b
#define KEY_CTRL_RBRACKET 0x1d
class input_dispatcher {
public:
void new_input(const struct timeval &current_time, int ch);
void poll(const struct timeval &current_time);
bool in_escape() const {
return this->id_escape_index > 0;
}
enum class escape_match_t {
NONE,
PARTIAL,
FULL,
};
std::function<escape_match_t(const char *)> id_escape_matcher;
std::function<void(int)> id_key_handler;
std::function<void(const char *)> id_escape_handler;
std::function<void()> id_mouse_handler;
private:
void append_to_escape_buffer(int ch) {
this->id_escape_buffer[this->id_escape_index++] = static_cast<char>(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

View File

@ -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"],

View File

@ -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;
}

View File

@ -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<list_input_delegate *> 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

View File

@ -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<vis_line_t>::*f) (vis_line_t) const,
bookmark_type_t *bt,
@ -716,6 +690,10 @@ bool moveto_cluster(vis_line_t(bookmark_vector<vis_line_t>::*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<logline_value> 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<string, string>());
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(&current_time, nullptr);
timersub(&current_time, &escape_start_time, &diff);
if (diff.tv_sec > 0 || diff.tv_usec > (10000)) {
handle_key(KEY_CTRL_RBRACKET);
escape_index = 0;
}
}
gettimeofday(&current_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

View File

@ -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;

View File

@ -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<string> &args)
struct timeval tv;
struct exttm tm;
float value;
nonstd::optional<vis_line_t> 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<string> &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<string> &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<string> &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<string> &ar
return retval;
}
static string com_goto_location(exec_context &ec, string cmdline, vector<string> &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,

View File

@ -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<vis_line_t> *) &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<vis_line_t> 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<vis_line_t>
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;
}

View File

@ -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<vis_line_t>
loc_history_back(vis_line_t current_top) override;
nonstd::optional<vis_line_t>
loc_history_forward(vis_line_t current_top) override;
private:
nonstd::ring_span<content_line_t> 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<vis_line_t> find_from_content(content_line_t cl) {
content_line_t line = cl;
std::shared_ptr<logfile> 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<std::pair<grep_proc_source<vis_line_t> *, grep_proc_sink<vis_line_t> *>>
get_grepper();
nonstd::optional<location_history *> 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

View File

@ -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<location_history *> get_location_history()
{
return this;
}
private:
size_t compute_longest_line() {
size_t retval = 0;

View File

@ -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)

View File

@ -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<std::shared_ptr<logfile>>::iterator file_iterator;
@ -269,6 +270,11 @@ public:
return this->tss_files.front()->get_text_format();
}
nonstd::optional<location_history *> get_location_history()
{
return this;
}
private:
void detach_observer(std::shared_ptr<logfile> lf) {
line_filter_observer *lfo = (line_filter_observer *) lf->get_logline_observer();

View File

@ -35,6 +35,7 @@
#include <utility>
#include <vector>
#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<vis_line_t>
loc_history_back(vis_line_t current_top) = 0;
virtual nonstd::optional<vis_line_t>
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<location_history *> 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<vis_line_t>
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<vis_line_t>
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<vis_line_t> 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() { };

View File

@ -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;

View File

@ -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;

View File

@ -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
------------------------

View File

@ -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" <<EOF
192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
EOF
run_test ${lnav_test} -n -d /tmp/lnav.err \
-c ":goto 0" \
-c ":next-mark error" \
-c ":prev-location" \
-c ":next-location" \
${test_dir}/logfile_access_log.0
check_output "next-location is not working" <<EOF
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
EOF
run_test ${lnav_test} -n -d /tmp/lnav.err \
-c ":filter-in vmk" \
-c ":rebuild" \