[mouse] flesh out things more

master
Tim Stack 3 weeks ago
parent 7341dc1f1a
commit 53ab7b14a6

@ -9,6 +9,28 @@ Features:
of archive contents.
* Added `humanize_id` SQL function that colorizes a string using
ANSI escape codes.
* Added mouse support that can be toggled with `F2` or enabled
by default with: `:config /ui/mouse/mode enabled`. With
mouse support enabled, many of the UI elements will respond to
mouse inputs:
- clicking on the main view will move the cursor to the given
row and dragging will scroll the view as needed;
- shift + dragging in the main view will highlight lines and
then toggle their bookmark status on release;
- clicking in the scroll area will move the view by a page and
dragging the scrollbar will move the view to the given spot;
- clicking on the breadcrumb bar will select a crumb and
selecting a possibility from the popup will move to that
location in the view;
- clicking on portions of the bottom status bar will trigger
a relevant action (e.g. clicking the line number will open
the command prompt with `:goto <current-line>`);
- clicking on the configuration panel tabs (i.e. Files/Filters)
will open the selected panel and clicking parts of the
display in there will perform the relevant action (e.g.
clicking the diamond will enable/disable the file/filter);
- clicking in a prompt will move the cursor to the location.
This is new work, so there are likely to be some glitches.
Interface changes:
* The bar charts in the DB view have now been moved to their
@ -33,6 +55,7 @@ Bug Fixes:
* A crash during initialization on Apple Silicon and MacOS 12
has been fixed.
* A crash when previewing non-text files.
* Various fixes to make lnav usable as a `PAGER`.
## lnav v0.12.1

@ -732,6 +732,27 @@
},
"additionalProperties": false
},
"mouse": {
"description": "Mouse-related settings",
"title": "/ui/mouse",
"type": "object",
"properties": {
"mode": {
"title": "/ui/mouse/mode",
"description": "Overall control for mouse support",
"type": "string",
"enum": [
"disabled",
"enabled"
],
"examples": [
"enabled",
"disabled"
]
}
},
"additionalProperties": false
},
"movement": {
"description": "Log file cursor movement mode settings",
"title": "/ui/movement",

@ -113,16 +113,62 @@ To create or customize a theme, consult the :ref:`themes` section.
Cursor Mode (v0.11.2+)
^^^^^^^^^^^^^^^^^^^^^^
The default mode for scrolling in **lnav** is to move the contents of the
main view when the arrow keys are pressed. Any interactions, such as
jumping to a search hit, are then focused on the top line in the view.
Alternatively, you can enable "cursor" mode where there is a cursor line
in the view that is moved by the arrow keys and other interactions. You
can enable cursor mode with the following command:
The default mode for scrolling in **lnav** is "cursor" mode where
there is a cursor line in the view that is moved by the arrow keys
and other interactions. Any interactions, such as jumping to a
search hit, are then focused on that line.
Alternatively, you can enable "top" mode where the contents of the
main view are moved when the arrow keys are pressed. Any
interactions, such as jumping to a search hit, are then focused
on the top line in the view. You can change to "top" mode with
the following command:
.. code-block:: lnav
:config /ui/movement/mode top
Mouse Support (v0.12.2+)
^^^^^^^^^^^^^^^^^^^^^^^^
Mouse support can be enabled temporarily by pressing :kbd:`F2`
and can be set as the default by executing the following command:
.. code-block:: lnav
:config /ui/movement/mode cursor
:config /ui/mouse/mode enabled
With mouse support enabled, many of the UI elements will respond to
mouse inputs:
* clicking on the main view will move the cursor to the given
row and dragging will scroll the view as needed;
* shift + dragging in the main view will highlight lines and
then toggle their bookmark status on release;
* clicking in the scroll area will move the view by a page and
dragging the scrollbar will move the view to the given spot;
* clicking on the breadcrumb bar will select a crumb and
selecting a possibility from the popup will move to that
location in the view;
* clicking on portions of the bottom status bar will trigger
a relevant action (e.g. clicking the line number will open
the command prompt with `:goto <current-line>`);
* clicking on the configuration panel tabs (i.e. Files/Filters)
will open the selected panel and clicking parts of the
display in there will perform the relevant action (e.g.
clicking the diamond will enable/disable the file/filter);
* clicking in a prompt will move the cursor to the location.
.. note::
A downside of enabling mouse support is that normal text
selection and copy will no longer work. You can press
:kbd:`F2` to quickly switch back-and-forth. Or, some
terminals have support for switching using a modifier
key, like `iTerm <https://iterm2.com/documentation-preferences-profiles-terminal.html>_`
where pressing :kbd:`Option` will allow you to select
text and copy.
Log Formats
^^^^^^^^^^^

@ -17,7 +17,7 @@
;SELECT printf('\n%d total requests', count(1)) AS msg FROM access_log
:echo $msg
;WITH top_paths AS (
;WITH top_paths AS MATERIALIZED (
SELECT
cs_uri_stem,
count(1) AS total_hits,
@ -28,7 +28,7 @@
GROUP BY cs_uri_stem
ORDER BY total_hits DESC
LIMIT 50),
weekly_hits_with_gaps AS (
weekly_hits_with_gaps AS MATERIALIZED (
SELECT timeslice(log_time_msecs, '1w') AS week,
cs_uri_stem,
count(1) AS weekly_hits

@ -34,6 +34,11 @@
using namespace lnav::roles::literals;
void
breadcrumb_curses::no_op_action(breadcrumb_curses&)
{
}
breadcrumb_curses::breadcrumb_curses()
{
this->bc_match_search_overlay.sos_parent = this;
@ -65,6 +70,7 @@ breadcrumb_curses::do_update()
{
this->bc_last_selected_crumb = crumbs.size() - 1;
}
this->bc_displayed_crumbs.clear();
attr_line_t crumbs_line;
for (const auto& crumb : crumbs) {
auto accum_width = crumbs_line.column_width();
@ -78,17 +84,21 @@ breadcrumb_curses::do_update()
accum_width = 2;
}
line_range crumb_range;
crumb_range.lr_start = (int) crumbs_line.length();
crumbs_line.append(crumb.c_display_value);
crumb_range.lr_end = (int) crumbs_line.length();
if (is_selected) {
sel_crumb_offset = accum_width;
crumbs_line.get_attrs().emplace_back(
line_range{
(int) (crumbs_line.length()
- crumb.c_display_value.length()),
(int) crumbs_line.length(),
},
VC_STYLE.template value(text_attrs{A_REVERSE}));
crumb_range, VC_STYLE.template value(text_attrs{A_REVERSE}));
}
this->bc_displayed_crumbs.emplace_back(
line_range{(int) accum_width,
(int) (accum_width + elem_width),
line_range::unit::codepoint},
crumb_index);
crumb_index += 1;
crumbs_line.append(" \uff1a"_breadcrumb);
}
@ -168,6 +178,9 @@ breadcrumb_curses::reload_data()
this->bc_match_view.set_needs_update();
this->bc_match_view.set_selection(
vis_line_t(selected_value.value_or(-1_vl)));
if (selected_value) {
this->bc_match_view.set_top(vis_line_t(selected_value.value()));
}
this->bc_match_view.reload_data();
this->set_needs_update();
}
@ -454,3 +467,77 @@ breadcrumb_curses::search_overlay_source::list_static_overlay(
return false;
}
bool
breadcrumb_curses::handle_mouse(mouse_event& me)
{
if (me.me_state == mouse_button_state_t::BUTTON_STATE_PRESSED
&& this->bc_focused_crumbs.empty())
{
this->focus();
this->on_focus(*this);
this->do_update();
}
auto find_res = this->bc_displayed_crumbs
| lnav::itertools::find_if([&me](const auto& elem) {
return me.me_button == mouse_button_t::BUTTON_LEFT
&& elem.dc_range.contains(me.me_x);
});
if (!this->bc_focused_crumbs.empty()) {
if (me.me_y > 0 || !find_res
|| find_res.value()->dc_index == this->bc_selected_crumb)
{
if (view_curses::handle_mouse(me)) {
if (me.me_y > 0
&& (me.me_state
== mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK
|| me.me_state
== mouse_button_state_t::BUTTON_STATE_RELEASED))
{
this->perform_selection(perform_behavior_t::if_different);
this->blur();
this->reload_data();
this->on_blur(*this);
this->bc_initial_mouse_event = true;
}
return true;
}
}
if (!this->bc_initial_mouse_event
&& me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
&& me.me_y == 0 && find_res
&& find_res.value()->dc_index == this->bc_selected_crumb.value())
{
this->blur();
this->reload_data();
this->on_blur(*this);
this->bc_initial_mouse_event = true;
return true;
}
}
if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
this->bc_initial_mouse_event = false;
}
if (me.me_y != 0) {
return true;
}
if (find_res) {
auto crumb_index = find_res.value()->dc_index;
if (this->bc_selected_crumb) {
this->blur();
this->focus();
this->reload_data();
this->bc_selected_crumb = crumb_index;
this->bc_current_search.clear();
this->reload_data();
}
}
return true;
}

@ -40,6 +40,8 @@
class breadcrumb_curses : public view_curses {
public:
using action = std::function<void(breadcrumb_curses&)>;
breadcrumb_curses();
void set_window(WINDOW* win)
@ -53,6 +55,8 @@ public:
this->bc_line_source = std::move(ls);
}
bool handle_mouse(mouse_event& me) override;
void focus();
void blur();
@ -62,6 +66,11 @@ public:
void reload_data();
static void no_op_action(breadcrumb_curses&);
action on_focus{no_op_action};
action on_blur{no_op_action};
private:
class search_overlay_source : public list_overlay_source {
public:
@ -92,6 +101,19 @@ private:
plain_text_source bc_match_source;
search_overlay_source bc_match_search_overlay;
textview_curses bc_match_view;
struct displayed_crumb {
displayed_crumb(line_range range, size_t index)
: dc_range(range), dc_index(index)
{
}
line_range dc_range;
size_t dc_index{0};
};
std::vector<displayed_crumb> bc_displayed_crumbs;
bool bc_initial_mouse_event{true};
};
#endif

@ -30,6 +30,7 @@
#include "files_sub_source.hh"
#include "base/ansi_scrubber.hh"
#include "base/attr_line.builder.hh"
#include "base/fs_util.hh"
#include "base/humanize.hh"
#include "base/humanize.network.hh"
@ -40,6 +41,8 @@
#include "mapbox/variant.hpp"
#include "sql_util.hh"
using namespace lnav::roles::literals;
namespace files_model {
files_list_selection
from_selection(vis_line_t sel_vis)
@ -243,26 +246,45 @@ files_sub_source::text_value_for_line(textview_curses& tc,
std::string& value_out,
text_sub_source::line_flags_t flags)
{
bool selected
= lnav_data.ld_mode == ln_mode_t::FILES && line == tc.get_selection();
const auto dim = tc.get_dimensions();
const auto& fc = lnav_data.ld_active_files;
auto filename_width
= std::min(fc.fc_largest_path_length,
std::max((size_t) 40, (size_t) dim.second - 30));
this->fss_curr_line.clear();
auto& al = this->fss_curr_line;
attr_line_builder alb(al);
if (selected) {
al.append(" ", VC_GRAPHIC.value(ACS_RARROW));
} else {
al.append(" ");
}
{
safe::ReadAccess<safe_name_to_errors> errs(*fc.fc_name_to_errors);
if (line < errs->size()) {
auto iter = errs->begin();
std::advance(iter, line);
auto iter = std::next(errs->begin(), line);
auto path = ghc::filesystem::path(iter->first);
auto fn = fmt::to_string(path.filename());
truncate_to(fn, filename_width);
value_out = fmt::format(FMT_STRING(" {:<{}} {}"),
fn,
filename_width,
iter->second.fei_description);
al.append(" ");
{
auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_ERROR));
al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
}
al.append(" ").append(iter->second.fei_description);
if (selected) {
al.with_attr_for_all(
VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
}
value_out = al.get_string();
return;
}
@ -270,23 +292,36 @@ files_sub_source::text_value_for_line(textview_curses& tc,
}
if (line < fc.fc_other_files.size()) {
auto iter = fc.fc_other_files.begin();
std::advance(iter, line);
auto iter = std::next(fc.fc_other_files.begin(), line);
auto path = ghc::filesystem::path(iter->first);
auto fn = fmt::to_string(path);
truncate_to(fn, filename_width);
value_out = fmt::format(FMT_STRING(" {:<{}} {:14} {}"),
fn,
filename_width,
iter->second.ofd_format,
iter->second.ofd_description);
al.append(" ");
{
auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_FILE));
al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
}
al.append(" ")
.appendf(FMT_STRING("{:14}"), iter->second.ofd_format)
.append(" ")
.append(iter->second.ofd_description);
if (selected) {
al.with_attr_for_all(VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
}
if (line == fc.fc_other_files.size() - 1) {
al.with_attr_for_all(VC_STYLE.value(text_attrs{A_UNDERLINE}));
}
value_out = al.get_string();
return;
}
line -= fc.fc_other_files.size();
const auto& lf = fc.fc_files[line];
auto ld_opt = lnav_data.ld_log_source.find_data(lf);
auto fn = fmt::to_string(ghc::filesystem::path(lf->get_unique_path()));
char start_time[64] = "", end_time[64] = "";
std::vector<std::string> file_notes;
@ -299,94 +334,47 @@ files_sub_source::text_value_for_line(textview_curses& tc,
for (const auto& pair : lf->get_notes()) {
file_notes.push_back(pair.second);
}
value_out = fmt::format(FMT_STRING(" {:<{}} {:>8} {} \u2014 {} {}"),
fn,
filename_width,
humanize::file_size(lf->get_index_size(),
humanize::alignment::columnar),
start_time,
end_time,
fmt::join(file_notes, "; "));
this->fss_last_line_len
= filename_width + 23 + strlen(start_time) + strlen(end_time);
}
void
files_sub_source::text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out)
{
bool selected
= lnav_data.ld_mode == ln_mode_t::FILES && line == tc.get_selection();
const auto& fc = lnav_data.ld_active_files;
const auto dim = tc.get_dimensions();
auto filename_width
= std::min(fc.fc_largest_path_length,
std::max((size_t) 40, (size_t) dim.second - 30));
if (selected) {
value_out.emplace_back(line_range{0, 1}, VC_GRAPHIC.value(ACS_RARROW));
}
{
safe::ReadAccess<safe_name_to_errors> errs(*fc.fc_name_to_errors);
if (line < errs->size()) {
if (selected) {
value_out.emplace_back(
line_range{0, -1},
VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
}
value_out.emplace_back(line_range{4 + (int) filename_width, -1},
VC_ROLE_FG.value(role_t::VCR_ERROR));
return;
al.append(" ");
if (ld_opt) {
if (ld_opt.value()->ld_visible) {
al.append("\u25c6"_ok);
} else {
al.append("\u25c7"_comment);
}
line -= errs->size();
} else {
al.append("\u25c6"_comment);
}
al.append(" ");
al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
al.append(" ");
{
auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_NUMBER));
if (line < fc.fc_other_files.size()) {
if (selected) {
value_out.emplace_back(line_range{0, -1},
VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
}
if (line == fc.fc_other_files.size() - 1) {
value_out.emplace_back(line_range{0, -1},
VC_STYLE.value(text_attrs{A_UNDERLINE}));
}
return;
al.appendf(FMT_STRING("{:>8}"),
humanize::file_size(lf->get_index_size(),
humanize::alignment::columnar));
}
line -= fc.fc_other_files.size();
al.append(" ")
.append(start_time)
.append(" \u2014 ")
.append(end_time)
.appendf(FMT_STRING("{}"), fmt::join(file_notes, "; "));
if (selected) {
value_out.emplace_back(line_range{0, -1},
VC_ROLE.value(role_t::VCR_FOCUSED));
al.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOCUSED));
}
auto& lss = lnav_data.ld_log_source;
auto& lf = fc.fc_files[line];
auto ld_opt = lss.find_data(lf);
chtype visible = ACS_DIAMOND;
if (ld_opt && !ld_opt.value()->ld_visible) {
visible = ' ';
}
value_out.emplace_back(line_range{2, 3}, VC_GRAPHIC.value(visible));
if (visible == ACS_DIAMOND) {
value_out.emplace_back(line_range{2, 3},
VC_FOREGROUND.value(COLOR_GREEN));
}
auto lr = line_range{
(int) filename_width + 3 + 4,
(int) filename_width + 3 + 10,
};
value_out.emplace_back(lr, VC_STYLE.value(text_attrs{A_BOLD}));
value_out = al.get_string();
this->fss_last_line_len
= filename_width + 23 + strlen(start_time) + strlen(end_time);
}
lr.lr_start = this->fss_last_line_len;
lr.lr_end = -1;
value_out.emplace_back(lr, VC_FOREGROUND.value(COLOR_YELLOW));
void
files_sub_source::text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out)
{
value_out = this->fss_curr_line.get_attrs();
}
size_t
@ -450,3 +438,16 @@ files_overlay_source::list_static_overlay(const listview_curses& lv,
return false;
}
bool
files_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me)
{
if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 1, 3)) {
this->list_input_handle_key(tc, ' ');
}
if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{4, -1})) {
this->list_input_handle_key(tc, '\r');
}
return false;
}

@ -35,7 +35,8 @@
class files_sub_source
: public text_sub_source
, public list_input_delegate {
, public list_input_delegate
, public text_delegate {
public:
files_sub_source();
@ -60,7 +61,10 @@ public:
int line,
line_flags_t raw) override;
bool text_handle_mouse(textview_curses& tc, mouse_event& me) override;
size_t fss_last_line_len{0};
attr_line_t fss_curr_line;
};
struct files_overlay_source : public list_overlay_source {

@ -54,6 +54,8 @@ filter_status_source::filter_status_source()
this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE);
this->tss_fields[TSF_TITLE].set_value(" " ANSI_ROLE("T") "ext Filters ",
role_t::VCR_STATUS_TITLE_HOTKEY);
this->tss_fields[TSF_TITLE].on_click
= [](status_field&) { set_view_mode(ln_mode_t::FILTER); };
this->tss_fields[TSF_STITCH_TITLE].set_width(2);
this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(
@ -73,6 +75,8 @@ filter_status_source::filter_status_source()
role_t::VCR_STATUS_DISABLED_TITLE);
this->tss_fields[TSF_FILES_TITLE].set_value(" " ANSI_ROLE("F") "iles ",
role_t::VCR_STATUS_HOTKEY);
this->tss_fields[TSF_FILES_TITLE].on_click
= [](status_field&) { set_view_mode(ln_mode_t::FILES); };
this->tss_fields[TSF_FILES_RIGHT_STITCH].set_width(2);
this->tss_fields[TSF_FILES_RIGHT_STITCH].set_stitch_value(

@ -29,6 +29,7 @@
#include "filter_sub_source.hh"
#include "base/attr_line.builder.hh"
#include "base/enum_util.hh"
#include "base/func_util.hh"
#include "base/opt_util.hh"
@ -70,6 +71,14 @@ filter_sub_source::filter_sub_source(std::shared_ptr<readline_curses> editor)
this->fss_match_view.set_default_role(role_t::VCR_POPUP);
}
void
filter_sub_source::register_view(textview_curses* tc)
{
text_sub_source::register_view(tc);
tc->add_child_view(this->fss_editor.get());
tc->add_child_view(&this->fss_match_view);
}
bool
filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
{
@ -169,6 +178,7 @@ filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
lv.reload_data();
this->fss_editing = true;
this->tss_view->vc_enabled = false;
add_view_text_possibilities(this->fss_editor.get(),
filter_lang_t::REGEX,
@ -204,6 +214,7 @@ filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
lv.reload_data();
this->fss_editing = true;
this->tss_view->vc_enabled = false;
add_view_text_possibilities(this->fss_editor.get(),
filter_lang_t::REGEX,
@ -233,6 +244,7 @@ filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
auto tf = *(fs.begin() + lv.get_selection());
this->fss_editing = true;
this->tss_view->vc_enabled = false;
auto tq = tf->get_lang() == filter_lang_t::SQL
? text_quoting::sql
@ -315,17 +327,34 @@ filter_sub_source::text_value_for_line(textview_curses& tc,
auto* tss = top_view->get_sub_source();
auto& fs = tss->get_filters();
auto tf = *(fs.begin() + line);
bool selected
= lnav_data.ld_mode == ln_mode_t::FILTER && line == tc.get_selection();
value_out = " ";
this->fss_curr_line.clear();
auto& al = this->fss_curr_line;
attr_line_builder alb(al);
if (selected) {
al.append(" ", VC_GRAPHIC.value(ACS_RARROW));
} else {
al.append(" ");
}
al.append(" ");
if (tf->is_enabled()) {
al.append("\u25c6"_ok);
} else {
al.append("\u25c7"_comment);
}
al.append(" ");
switch (tf->get_type()) {
case text_filter::INCLUDE:
value_out.append(" IN ");
al.append(" ").append(lnav::roles::ok("IN")).append(" ");
break;
case text_filter::EXCLUDE:
if (tf->get_lang() == filter_lang_t::REGEX) {
value_out.append("OUT ");
al.append(lnav::roles::error("OUT")).append(" ");
} else {
value_out.append(" ");
al.append(" ");
}
break;
default:
@ -333,60 +362,19 @@ filter_sub_source::text_value_for_line(textview_curses& tc,
break;
}
if (this->fss_editing && line == tc.get_selection()) {
fmt::format_to(
std::back_inserter(value_out), FMT_STRING("{:>9} hits | "), "-");
} else {
fmt::format_to(std::back_inserter(value_out),
FMT_STRING("{:>9L} hits | "),
tss->get_filtered_count_for(tf->get_index()));
}
value_out.append(tf->get_id());
}
void
filter_sub_source::text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out)
{
textview_curses* top_view = *lnav_data.ld_view_stack.top();
text_sub_source* tss = top_view->get_sub_source();
filter_stack& fs = tss->get_filters();
auto tf = *(fs.begin() + line);
bool selected
= lnav_data.ld_mode == ln_mode_t::FILTER && line == tc.get_selection();
if (selected) {
value_out.emplace_back(line_range{0, 1}, VC_GRAPHIC.value(ACS_RARROW));
}
chtype enabled = tf->is_enabled() ? ACS_DIAMOND : ' ';
line_range lr{2, 3};
value_out.emplace_back(lr, VC_GRAPHIC.value(enabled));
if (tf->is_enabled()) {
value_out.emplace_back(lr, VC_FOREGROUND.value(COLOR_GREEN));
}
if (selected) {
value_out.emplace_back(line_range{0, -1},
VC_ROLE.value(role_t::VCR_FOCUSED));
{
auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_NUMBER));
if (this->fss_editing && line == tc.get_selection()) {
alb.appendf(FMT_STRING("{:>9}"), "-");
} else {
alb.appendf(FMT_STRING("{:>9}"),
tss->get_filtered_count_for(tf->get_index()));
}
}
role_t fg_role = tf->get_type() == text_filter::INCLUDE ? role_t::VCR_OK
: role_t::VCR_ERROR;
value_out.emplace_back(line_range{4, 7}, VC_ROLE.value(fg_role));
value_out.emplace_back(line_range{4, 7},
VC_STYLE.value(text_attrs{A_BOLD}));
value_out.emplace_back(line_range{8, 17},
VC_STYLE.value(text_attrs{A_BOLD}));
value_out.emplace_back(line_range{23, 24}, VC_GRAPHIC.value(ACS_VLINE));
al.append(" hits ").append("|", VC_GRAPHIC.value(ACS_VLINE)).append(" ");
attr_line_t content{tf->get_id()};
auto& content_attrs = content.get_attrs();
switch (tf->get_lang()) {
case filter_lang_t::REGEX:
readline_regex_highlighter(content, content.length());
@ -397,10 +385,21 @@ filter_sub_source::text_attrs_for_line(textview_curses& tc,
case filter_lang_t::NONE:
break;
}
al.append(content);
if (selected) {
al.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOCUSED));
}
value_out = al.get_string();
}
shift_string_attrs(content_attrs, 0, 25);
value_out.insert(
value_out.end(), content_attrs.begin(), content_attrs.end());
void
filter_sub_source::text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out)
{
value_out = this->fss_curr_line.get_attrs();
}
size_t
@ -592,6 +591,7 @@ filter_sub_source::rl_perform(readline_curses* rc)
lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
lnav_data.ld_filter_help_status_source.fss_prompt.clear();
this->fss_editing = false;
this->tss_view->vc_enabled = true;
this->fss_editor->set_visible(false);
top_view->reload_data();
this->tss_view->reload_data();
@ -615,6 +615,7 @@ filter_sub_source::rl_abort(readline_curses* rc)
this->tss_view->reload_data();
this->fss_editor->set_visible(false);
this->fss_editing = false;
this->tss_view->vc_enabled = true;
this->tss_view->set_needs_update();
tf->set_enabled(this->fss_filter_state);
tss->text_filters_changed();
@ -680,3 +681,22 @@ filter_sub_source::list_input_handle_scroll_out(listview_curses& lv)
lnav_data.ld_mode = ln_mode_t::PAGING;
lnav_data.ld_filter_view.reload_data();
}
bool
filter_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me)
{
if (this->fss_editing) {
return true;
}
if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 1, 3)) {
this->list_input_handle_key(tc, ' ');
}
if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 4, 7)) {
this->list_input_handle_key(tc, 't');
}
if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{25, -1}))
{
this->list_input_handle_key(tc, '\r');
}
return true;
}

@ -37,7 +37,8 @@
class filter_sub_source
: public text_sub_source
, public list_input_delegate {
, public list_input_delegate
, public text_delegate {
public:
filter_sub_source(std::shared_ptr<readline_curses> editor);
@ -52,6 +53,8 @@ public:
void list_input_handle_scroll_out(listview_curses& lv) override;
void register_view(textview_curses* tc) override;
size_t text_line_count() override;
size_t text_line_width(textview_curses& curses) override;
@ -69,6 +72,8 @@ public:
int line,
line_flags_t raw) override;
bool text_handle_mouse(textview_curses& tc, mouse_event& me) override;
void rl_change(readline_curses* rc);
void rl_perform(readline_curses* rc);
@ -84,6 +89,7 @@ public:
std::shared_ptr<readline_curses> fss_editor;
plain_text_source fss_match_source;
textview_curses fss_match_view;
attr_line_t fss_curr_line;
bool fss_editing{false};
bool fss_filter_state{false};

@ -313,7 +313,7 @@ If you are using Xterm, or a compatible terminal, you can use the mouse to
mark lines of text and move the view by grabbing the scrollbar.
NOTE: You need to manually enable this feature by setting the LNAV_EXP
environment variable to "mouse". F2 toggles mouse support.
environment variable to "mouse". `F2` toggles mouse support.
## Log Analysis

@ -50,6 +50,14 @@ listview_curses::listview_curses() : lv_scroll(noop_func{}) {}
bool
listview_curses::contains(int x, int y) const
{
if (!this->vc_visible) {
return false;
}
if (view_curses::contains(x, y)) {
return true;
}
auto dim = this->get_dimensions();
if (this->vc_x <= x && x < this->vc_x + dim.second && this->vc_y <= y
@ -76,26 +84,20 @@ listview_curses::update_top_from_selection()
this->set_top(0_vl);
} else if (this->lv_sync_selection_and_top) {
this->set_top(this->lv_selection);
} else if (this->lv_selection == this->get_inner_height() - 1_vl) {
this->set_top(this->get_top_for_last_row());
} else if (height <= this->lv_tail_space) {
this->set_top(this->lv_selection);
} else if (this->lv_selection
>= (this->lv_top + height - this->lv_tail_space - 1_vl))
{
auto diff = this->lv_selection
- (this->lv_top + height - this->lv_tail_space - 1_vl);
} else if (this->lv_selection > (this->lv_top + height - 1_vl)) {
auto diff = this->lv_selection - (this->lv_top + height - 1_vl);
if (height < 10 || diff < (height / 8_vl)) {
// for small differences between the bottom and the
// selection, just move a little bit.
this->set_top(
this->lv_selection - height + 1_vl + this->lv_tail_space, true);
this->set_top(this->lv_selection - height + 1_vl, true);
} else {
// for large differences, put the focus in the middle
this->set_top(this->lv_selection - height / 2_vl, true);
}
} else if (this->lv_selection <= this->lv_top) {
} else if (this->lv_selection < this->lv_top) {
auto diff = this->lv_top - this->lv_selection;
if (this->lv_selection > 0 && (height < 10 || diff < (height / 8_vl))) {
@ -126,9 +128,10 @@ listview_curses::reload_data()
}
if (this->lv_selectable) {
if (this->get_inner_height() == 0) {
this->set_selection(-1_vl);
this->set_selection_without_context(-1_vl);
} else if (this->lv_selection >= this->get_inner_height()) {
this->set_selection(this->get_inner_height() - 1_vl);
this->set_selection_without_context(this->get_inner_height()
- 1_vl);
} else {
auto curr_sel = this->get_selection();
@ -136,7 +139,7 @@ listview_curses::reload_data()
curr_sel = 0_vl;
}
this->lv_selection = -1_vl;
this->set_selection(curr_sel);
this->set_selection_without_context(curr_sel);
}
this->update_top_from_selection();
@ -425,7 +428,6 @@ listview_curses::do_update()
}
size_t row_count = this->get_inner_height();
size_t blank_rows = 0;
row = this->lv_top;
bottom = y + height;
std::vector<attr_line_t> rows(
@ -586,14 +588,11 @@ listview_curses::do_update()
this->lv_display_lines.push_back(empty_space{});
mvwhline(this->lv_window, y, this->vc_x, ' ', width);
++y;
blank_rows += 1;
}
}
if (this->lv_selectable && !this->lv_sync_selection_and_top
&& this->lv_selection >= 0 && (row > this->lv_tail_space)
&& (blank_rows < this->lv_tail_space)
&& ((row - this->lv_tail_space) < this->lv_selection))
&& this->lv_selection >= 0 && row < this->lv_selection)
{
this->shift_top(this->lv_selection - row + this->lv_tail_space);
continue;
@ -649,13 +648,15 @@ listview_curses::do_update()
if (this->lv_show_bottom_border) {
cchar_t row_ch[width];
int y = this->vc_y + height - 1;
int bottom_y = this->vc_y + height - 1;
mvwin_wchnstr(this->lv_window, y, this->vc_x, row_ch, width - 1);
mvwin_wchnstr(
this->lv_window, bottom_y, this->vc_x, row_ch, width - 1);
for (unsigned long lpc = 0; lpc < width - 1; lpc++) {
row_ch[lpc].attr |= A_UNDERLINE;
}
mvwadd_wchnstr(this->lv_window, y, this->vc_x, row_ch, width - 1);
mvwadd_wchnstr(
this->lv_window, bottom_y, this->vc_x, row_ch, width - 1);
}
this->vc_needs_update = false;
@ -763,7 +764,7 @@ listview_curses::shift_selection(shift_amount_t sa)
{
this->set_top(top_for_last);
if (this->lv_selection <= top_for_last) {
this->set_selection(top_for_last + 1_vl);
new_selection = top_for_last + 1_vl;
}
} else {
this->shift_top(rows_avail);
@ -772,7 +773,7 @@ listview_curses::shift_selection(shift_amount_t sa)
if (this->lv_selectable && this->lv_top >= top_for_last
&& inner_height > 0_vl)
{
this->set_selection(inner_height - 1_vl);
new_selection = inner_height - 1_vl;
}
}
}
@ -794,6 +795,14 @@ listview_curses::handle_mouse(mouse_event& me)
auto GUTTER_REPEAT_DELAY
= std::chrono::duration_cast<std::chrono::microseconds>(100ms).count();
if (view_curses::handle_mouse(me)) {
return true;
}
if (!this->vc_enabled) {
return false;
}
vis_line_t inner_height, height;
struct timeval diff;
unsigned long width;
@ -911,10 +920,10 @@ listview_curses::set_top(vis_line_t top, bool suppress_flash)
this->lv_focused_overlay_selection = 0_vl;
if (this->lv_selectable) {
if (this->lv_selection < 0_vl) {
this->set_selection(top);
this->set_selection_without_context(top);
} else if (this->lv_selection < top) {
auto sel_diff = this->lv_selection - old_top;
this->set_selection(top + sel_diff);
this->set_selection_without_context(top + sel_diff);
} else {
auto sel_diff = this->lv_selection - old_top;
auto bot = this->get_bottom();
@ -924,14 +933,14 @@ listview_curses::set_top(vis_line_t top, bool suppress_flash)
this->get_dimensions(height, width);
if (bot == -1_vl) {
this->set_selection(this->lv_top);
this->set_selection_without_context(this->lv_top);
} else if (this->lv_selection < this->lv_top
|| bot < this->lv_selection)
{
if (top + sel_diff > bot) {
this->set_selection(bot);
this->set_selection_without_context(bot);
} else {
this->set_selection(top + sel_diff);
this->set_selection_without_context(top + sel_diff);
}
}
}
@ -996,7 +1005,7 @@ listview_curses::rows_available(vis_line_t line,
}
void
listview_curses::set_selection(vis_line_t sel)
listview_curses::set_selection_without_context(vis_line_t sel)
{
if (this->lv_selectable) {
if (this->lv_selection == sel) {
@ -1055,6 +1064,23 @@ listview_curses::set_selection(vis_line_t sel)
}
}
void
listview_curses::set_selection(vis_line_t sel)
{
this->set_selection_without_context(sel);
auto dim = this->get_dimensions();
if (this->lv_selection > 0 && this->lv_selection <= this->lv_top) {
this->set_top(this->lv_selection - 1_vl);
} else if (dim.first > this->lv_tail_space
&& (this->lv_selection
> (this->lv_top + (dim.first - 1_vl) - this->lv_tail_space)))
{
this->set_top(this->lv_selection + this->lv_tail_space
- (dim.first - 1_vl));
}
}
vis_line_t
listview_curses::get_top_for_last_row()
{

@ -225,6 +225,8 @@ public:
void set_selection(vis_line_t sel);
void set_selection_without_context(vis_line_t sel);
enum class shift_amount_t {
up_line,
up_page,

@ -228,6 +228,8 @@ static auto bound_xterm_mouse = injector::bind<xterm_mouse>::to_singleton();
static auto bound_scripts = injector::bind<available_scripts>::to_singleton();
static auto bound_crumbs = injector::bind<breadcrumb_curses>::to_singleton();
static auto bound_curl
= injector::bind_multiple<isc::service_base>()
.add_singleton<curl_looper, services::curl_streamer_t>();
@ -274,8 +276,6 @@ force_linking(services::main_t anno)
}
} // namespace injector
static breadcrumb_curses breadcrumb_view;
struct lnav_data_t lnav_data;
bool
@ -367,6 +367,12 @@ static void
handle_rl_key(int ch)
{
switch (ch) {
case KEY_F(2):
if (xterm_mouse::is_available()) {
auto& mouse_i = injector::get<xterm_mouse&>();
mouse_i.set_enabled(!mouse_i.is_enabled());
}
break;
case KEY_PPAGE:
case KEY_NPAGE:
case KEY_CTRL('p'):
@ -670,6 +676,14 @@ handle_config_ui_key(int ch)
{
bool retval = false;
if (ch == KEY_F(2)) {
if (xterm_mouse::is_available()) {
auto& mouse_i = injector::get<xterm_mouse&>();
mouse_i.set_enabled(!mouse_i.is_enabled());
}
return retval;
}
switch (lnav_data.ld_mode) {
case ln_mode_t::FILES:
retval = lnav_data.ld_files_view.handle_key(ch);
@ -722,6 +736,8 @@ handle_config_ui_key(int ch)
static bool
handle_key(int ch)
{
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
lnav_data.ld_input_state.push_back(ch);
switch (ch) {
@ -731,7 +747,7 @@ handle_key(int ch)
switch (lnav_data.ld_mode) {
case ln_mode_t::PAGING:
if (ch == '`') {
breadcrumb_view.focus();
breadcrumb_view->focus();
lnav_data.ld_mode = ln_mode_t::BREADCRUMBS;
return true;
}
@ -739,7 +755,7 @@ handle_key(int ch)
return handle_paging_key(ch);
case ln_mode_t::BREADCRUMBS:
if (ch == '`' || !breadcrumb_view.handle_key(ch)) {
if (ch == '`' || !breadcrumb_view->handle_key(ch)) {
lnav_data.ld_mode = ln_mode_t::PAGING;
lnav_data.ld_view_stack.set_needs_update();
return true;
@ -986,6 +1002,7 @@ looper()
{
static auto* ps = injector::get<pollable_supervisor*>();
static auto* filter_source = injector::get<filter_sub_source*>();
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
try {
auto* sql_cmd_map = injector::get<readline_context::command_map_t*,
@ -1147,10 +1164,12 @@ looper()
ui_periodic_timer::singleton();
auto mouse_i = injector::get<xterm_mouse&>();
auto& mouse_i = injector::get<xterm_mouse&>();
mouse_i.set_behavior(&lb);
mouse_i.set_enabled(check_experimental("mouse"));
mouse_i.set_enabled(check_experimental("mouse")
|| lnav_config.lc_mouse_mode
== lnav_mouse_mode::enabled);
lnav_data.ld_window = sc.get_window();
keypad(stdscr, TRUE);
@ -1219,7 +1238,6 @@ looper()
execute_examples();
rlc->set_window(lnav_data.ld_window);
rlc->set_y(-1);
rlc->set_focus_action(rl_focus);
rlc->set_change_action(rl_change);
rlc->set_perform_action(rl_callback);
@ -1252,9 +1270,15 @@ looper()
vsb.push_back(sb);
breadcrumb_view.set_y(1);
breadcrumb_view.set_window(lnav_data.ld_window);
breadcrumb_view.set_line_source(lnav_crumb_source);
breadcrumb_view->on_focus
= [](breadcrumb_curses&) { set_view_mode(ln_mode_t::BREADCRUMBS); };
breadcrumb_view->on_blur = [](breadcrumb_curses&) {
set_view_mode(ln_mode_t::PAGING);
lnav_data.ld_view_stack.set_needs_update();
};
breadcrumb_view->set_y(1);
breadcrumb_view->set_window(lnav_data.ld_window);
breadcrumb_view->set_line_source(lnav_crumb_source);
auto event_handler = [](auto&& tc) {
auto top_view = lnav_data.ld_view_stack.top();
@ -1274,6 +1298,13 @@ looper()
= role_t::VCR_DISABLED_CURSOR_LINE;
lnav_data.ld_views[lpc].tc_state_event_handler = event_handler;
}
lnav_data.ld_views[LNV_DB].set_supports_marks(true);
lnav_data.ld_views[LNV_HELP].set_supports_marks(true);
lnav_data.ld_views[LNV_HISTOGRAM].set_supports_marks(true);
lnav_data.ld_views[LNV_LOG].set_supports_marks(true);
lnav_data.ld_views[LNV_TEXT].set_supports_marks(true);
lnav_data.ld_views[LNV_SCHEMA].set_supports_marks(true);
lnav_data.ld_views[LNV_PRETTY].set_supports_marks(true);
lnav_data.ld_doc_view.set_window(lnav_data.ld_window);
lnav_data.ld_doc_view.set_show_scrollbar(false);
@ -1288,10 +1319,12 @@ looper()
lnav_data.ld_preview_view[1].set_window(lnav_data.ld_window);
lnav_data.ld_preview_view[1].set_show_scrollbar(false);
lnav_data.ld_filter_view.set_title("Text Filters");
lnav_data.ld_filter_view.set_selectable(true);
lnav_data.ld_filter_view.set_window(lnav_data.ld_window);
lnav_data.ld_filter_view.set_show_scrollbar(true);
lnav_data.ld_files_view.set_title("Files");
lnav_data.ld_files_view.set_selectable(true);
lnav_data.ld_files_view.set_window(lnav_data.ld_window);
lnav_data.ld_files_view.set_show_scrollbar(true);
@ -1332,6 +1365,32 @@ looper()
auto top_source = injector::get<std::shared_ptr<top_status_source>>();
lnav_data.ld_bottom_source.get_field(bottom_status_source::BSF_HELP)
.on_click
= [](status_field&) { ensure_view(&lnav_data.ld_views[LNV_HELP]); };
lnav_data.ld_bottom_source
.get_field(bottom_status_source::BSF_LINE_NUMBER)
.on_click
= [](status_field&) {
auto cmd = fmt::format(
FMT_STRING("prompt command : 'goto {}'"),
(int) lnav_data.ld_view_stack.top().value()->get_top());
execute_command(lnav_data.ld_exec_context, cmd);
};
lnav_data.ld_bottom_source
.get_field(bottom_status_source::BSF_SEARCH_TERM)
.on_click
= [](status_field&) {
auto term = lnav_data.ld_view_stack.top()
.value()
->get_current_search();
auto cmd
= fmt::format(FMT_STRING("prompt search / '{}'"), term);
execute_command(lnav_data.ld_exec_context, cmd);
};
lnav_data.ld_status[LNS_TOP].set_y(0);
lnav_data.ld_status[LNS_TOP].set_default_role(
role_t::VCR_INACTIVE_STATUS);
@ -1551,12 +1610,12 @@ looper()
}
if (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS
&& breadcrumb_view.get_needs_update())
&& breadcrumb_view->get_needs_update())
{
lnav_data.ld_view_stack.set_needs_update();
}
if (lnav_data.ld_view_stack.do_update()) {
breadcrumb_view.set_needs_update();
breadcrumb_view->set_needs_update();
}
lnav_data.ld_doc_view.do_update();
lnav_data.ld_example_view.do_update();
@ -1577,7 +1636,7 @@ looper()
if (filter_source->fss_editing) {
filter_source->fss_match_view.set_needs_update();
}
breadcrumb_view.do_update();
breadcrumb_view->do_update();
// These updates need to be done last so their readline views can
// put the cursor in the right place.
switch (lnav_data.ld_mode) {
@ -2791,9 +2850,7 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%'
lnav_data.ld_preview_view[0].set_sub_source(
&lnav_data.ld_preview_source[0]);
lnav_data.ld_filter_view.set_sub_source(filter_source)
.add_input_delegate(*filter_source)
.add_child_view(&filter_source->fss_match_view)
.add_child_view(filter_source->fss_editor.get());
.add_input_delegate(*filter_source);
lnav_data.ld_files_view.set_sub_source(&lnav_data.ld_files_source)
.add_input_delegate(lnav_data.ld_files_source);
lnav_data.ld_user_message_view.set_sub_source(

@ -698,6 +698,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
std::string all_args = remaining_args(cmdline, args);
auto* tc = *lnav_data.ld_view_stack.top();
nonstd::optional<vis_line_t> dst_vl;
auto is_location = false;
if (startswith(all_args, "#")) {
auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source());
@ -710,6 +711,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
if (!dst_vl) {
return ec.make_error("unable to find anchor: {}", all_args);
}
is_location = true;
}
auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source());
@ -878,7 +880,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
return Err(um);
}
dst_vl | [&ec, tc, &retval](auto new_top) {
dst_vl | [&ec, tc, &retval, is_location](auto new_top) {
if (ec.ec_dry_run) {
retval = "info: will move to line "
+ std::to_string((int) new_top);
@ -886,6 +888,9 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
tc->get_sub_source()->get_location_history() |
[new_top](auto lh) { lh->loc_history_append(new_top); };
tc->set_selection(new_top);
if (tc->is_selectable() && is_location) {
tc->set_top(new_top - 2_vl, false);
}
retval = "";
}
@ -1210,7 +1215,12 @@ com_goto_location(exec_context& ec,
? lh->loc_history_back(tc->get_selection())
: lh->loc_history_forward(tc->get_selection());
}
| [tc](auto new_top) { tc->set_selection(new_top); };
| [tc](auto new_top) {
tc->set_selection(new_top);
if (tc->is_selectable()) {
tc->set_top(new_top - 2_vl, false);
}
};
};
}
@ -1240,6 +1250,9 @@ com_next_section(exec_context& ec,
}
tc->set_selection(adj_opt.value());
if (tc->is_selectable()) {
tc->set_top(adj_opt.value() - 2_vl, false);
}
}
return Ok(retval);
@ -1268,6 +1281,9 @@ com_prev_section(exec_context& ec,
}
tc->set_selection(adj_opt.value());
if (tc->is_selectable()) {
tc->set_top(adj_opt.value() - 2_vl, false);
}
}
return Ok(retval);

@ -382,13 +382,11 @@ update_installs_from_git()
git_dir.string());
int ret = system(pull_cmd.c_str());
if (ret == -1) {
std::cerr << "Failed to spawn command "
<< "\"" << pull_cmd << "\": " << strerror(errno)
<< std::endl;
std::cerr << "Failed to spawn command " << "\"" << pull_cmd
<< "\": " << strerror(errno) << std::endl;
retval = false;
} else if (ret > 0) {
std::cerr << "Command "
<< "\"" << pull_cmd
std::cerr << "Command " << "\"" << pull_cmd
<< "\" failed: " << strerror(errno) << std::endl;
retval = false;
}
@ -561,6 +559,23 @@ static const struct json_path_container movement_handlers = {
.for_field<>(&_lnav_config::lc_ui_movement, &movement_config::mode),
};
static const json_path_handler_base::enum_value_t _mouse_mode_values[] = {
{"disabled", lnav_mouse_mode::disabled},
{"enabled", lnav_mouse_mode::enabled},
json_path_handler_base::ENUM_TERMINATOR,
};
static const struct json_path_container mouse_handlers = {
yajlpp::property_handler("mode")
.with_synopsis("enabled|disabled")
.with_enum_values(_mouse_mode_values)
.with_example("enabled")
.with_example("disabled")
.with_description("Overall control for mouse support")
.for_field<>(&_lnav_config::lc_mouse_mode),
};
static const struct json_path_container global_var_handlers = {
yajlpp::pattern_property_handler("(?<var_name>\\w+)")
.with_synopsis("<name>")
@ -1130,6 +1145,9 @@ static const struct json_path_container ui_handlers = {
yajlpp::property_handler("theme-defs")
.with_description("Theme definitions.")
.with_children(theme_defs_handlers),
yajlpp::property_handler("mouse")
.with_description("Mouse-related settings")
.with_children(mouse_handlers),
yajlpp::property_handler("movement")
.with_description("Log file cursor movement mode settings")
.with_children(movement_handlers),

@ -97,6 +97,11 @@ struct movement_config {
config_movement_mode mode{config_movement_mode::TOP};
};
enum class lnav_mouse_mode {
disabled,
enabled,
};
struct _lnav_config {
top_status_source_cfg lc_top_status_cfg;
bool lc_ui_dim_text;
@ -104,6 +109,7 @@ struct _lnav_config {
std::string lc_ui_keymap;
std::string lc_ui_theme;
movement_config lc_ui_movement;
lnav_mouse_mode lc_mouse_mode;
std::map<std::string, key_map> lc_ui_keymaps;
std::map<std::string, std::string> lc_ui_key_overrides;
std::map<std::string, std::string> lc_global_vars;

@ -1905,11 +1905,6 @@ logfile_sub_source::text_clear_marks(const bookmark_type_t* bm)
for (iter = this->lss_user_marks[bm].begin();
iter != this->lss_user_marks[bm].end();)
{
auto line_meta_opt = this->find_bookmark_metadata(*iter);
if (line_meta_opt) {
++iter;
continue;
}
this->find_line(*iter)->set_mark(false);
iter = this->lss_user_marks[bm].erase(iter);
}

@ -327,6 +327,10 @@ plain_text_source::text_crumbs_for_line(int line,
this->line_for_offset(sib_iter->second->hn_start) |
[this](const auto new_top) {
this->tss_view->set_selection(new_top);
if (this->tss_view->is_selectable()) {
this->tss_view->set_top(new_top - 2_vl,
false);
}
};
},
[this, parent_node](size_t index) {
@ -337,6 +341,10 @@ plain_text_source::text_crumbs_for_line(int line,
this->line_for_offset(sib->hn_start) |
[this](const auto new_top) {
this->tss_view->set_selection(new_top);
if (this->tss_view->is_selectable()) {
this->tss_view->set_top(new_top - 2_vl,
false);
}
};
});
});

@ -1037,7 +1037,7 @@ readline_curses::start()
{
looping = false;
} else {
int context, prompt_start = 0;
int context, prompt_start = 0, new_point = 0;
char type[1024];
msg[rc] = '\0';
@ -1047,6 +1047,14 @@ readline_curses::start()
log_perror(chdir(cwd));
} else if (startswith(msg, "sugg:")) {
rc_local_suggestion = &msg[5];
} else if (sscanf(msg, "x:%d", &new_point) == 1) {
if (rl_prompt) {
new_point -= strlen(rl_prompt);
}
if (0 <= new_point && new_point <= rl_end) {
rl_point = new_point;
rl_redisplay();
}
} else if (sscanf(msg, "i:%d:%n", &rl_point, &prompt_start)
== 1)
{
@ -1671,6 +1679,25 @@ readline_curses::do_update()
return true;
}
bool
readline_curses::handle_mouse(mouse_event& me)
{
if (this->rc_active_context == -1) {
return false;
}
char buffer[32];
snprintf(buffer, sizeof(buffer), "x:%d", me.me_x);
if (sendstring(
this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
== -1)
{
perror("handle_mouse: write failed");
}
return true;
}
std::string
readline_curses::get_match_string() const
{

@ -188,6 +188,8 @@ public:
bool do_update() override;
bool handle_mouse(mouse_event& me) override;
void window_change();
void line_ready(const char* line);

@ -6,6 +6,9 @@
"default-colors": true,
"keymap": "default",
"theme": "default",
"mouse": {
"mode": "disabled"
},
"movement": {
"mode": "cursor"
}

@ -216,7 +216,7 @@ shared_buffer_ref::widen(narrow_result old_data_length)
void
shared_buffer_ref::erase_ansi()
{
if (!this->sb_metadata.m_has_ansi) {
if (!this->sb_metadata.m_valid_utf || !this->sb_metadata.m_has_ansi) {
return;
}

@ -34,10 +34,19 @@
#endif
#include "config.h"
#include "pcrepp/pcre2pp.hh"
#include "shlex.hh"
using namespace lnav::roles::literals;
std::string
shlex::escape(std::string s)
{
static const auto SH_CHARS = lnav::pcre2pp::code::from_const("'");
return SH_CHARS.replace(s, "\\'");
}
attr_line_t
shlex::to_attr_line(const shlex::tokenize_error_t& te) const
{

@ -59,6 +59,8 @@ enum class shlex_token_t {
class shlex {
public:
static std::string escape(std::string s);
shlex(const char* str, size_t len) : s_str(str), s_len(len) {}
explicit shlex(const string_fragment& sf)

@ -35,8 +35,14 @@
#include "statusview_curses.hh"
#include "base/ansi_scrubber.hh"
#include "base/itertools.hh"
#include "config.h"
void
status_field::no_op_action(status_field&)
{
}
void
status_field::set_value(std::string value)
{
@ -61,7 +67,7 @@ status_field::do_cylon()
: (this->sf_width - (cycle_pos - this->sf_width) - 1);
auto stop = std::min(start + 3, this->sf_width);
struct line_range lr(std::max<long>(start, 0L), stop);
auto& vc = view_colors::singleton();
const auto& vc = view_colors::singleton();
auto attrs = vc.attrs_for_role(role_t::VCR_ACTIVE_STATUS);
attrs.ta_attrs |= A_REVERSE;
@ -87,10 +93,11 @@ status_field::set_stitch_value(role_t left, role_t right)
bool
statusview_curses::do_update()
{
int top, field, field_count, left = 0, right;
int top, left = 0, right;
auto& vc = view_colors::singleton();
unsigned long width, height;
this->sc_displayed_fields.clear();
if (!this->vc_visible || this->sc_window == nullptr) {
return false;
}
@ -110,8 +117,8 @@ statusview_curses::do_update()
whline(this->sc_window, ' ', width);
if (this->sc_source != nullptr) {
field_count = this->sc_source->statusview_fields();
for (field = 0; field < field_count; field++) {
auto field_count = this->sc_source->statusview_fields();
for (size_t field = 0; field < field_count; field++) {
auto& sf = this->sc_source->statusview_value_for_field(field);
struct line_range lr(0, sf.get_width());
int x;
@ -177,7 +184,11 @@ statusview_curses::do_update()
}
}
mvwattrline(this->sc_window, top, x, val, lr, default_role);
auto write_res
= mvwattrline(this->sc_window, top, x, val, lr, default_role);
this->sc_displayed_fields.emplace_back(
line_range{x, static_cast<int>(x + write_res.mr_chars_out)},
field);
}
}
wmove(this->sc_window, top + 1, 0);
@ -240,3 +251,23 @@ statusview_curses::window_change()
sf->set_width(actual_width);
}
}
bool
statusview_curses::handle_mouse(mouse_event& me)
{
auto find_res = this->sc_displayed_fields
| lnav::itertools::find_if([&me](const auto& elem) {
return me.is_click_in(mouse_button_t::BUTTON_LEFT,
elem.df_range.lr_start,
elem.df_range.lr_end);
});
if (find_res) {
auto& sf = this->sc_source->statusview_value_for_field(
find_res.value()->df_field_index);
sf.on_click(sf);
}
return true;
}

@ -42,6 +42,8 @@
*/
class status_field {
public:
using action = std::function<void(status_field&)>;
/**
* @param width The maximum width of the field in characters.
* @param role The color role for this field, defaults to VCR_STATUS.
@ -121,6 +123,10 @@ public:
int get_share() const { return this->sf_share; }
static void no_op_action(status_field&);
action on_click{no_op_action};
protected:
ssize_t sf_width; /*< The maximum display width, in chars. */
ssize_t sf_min_width{0}; /*< The minimum display width, in chars. */
@ -175,11 +181,25 @@ public:
bool do_update() override;
bool handle_mouse(mouse_event& me) override;
private:
status_data_source* sc_source{nullptr};
WINDOW* sc_window{nullptr};
bool sc_enabled{true};
role_t sc_default_role{role_t::VCR_STATUS};
struct displayed_field {
displayed_field(line_range lr, size_t field_index)
: df_range(lr), df_field_index(field_index)
{
}
line_range df_range;
size_t df_field_index;
};
std::vector<displayed_field> sc_displayed_fields;
};
#endif

@ -988,6 +988,10 @@ textfile_sub_source::set_top_from_off(file_off_t off)
if (new_top_opt) {
this->tss_view->set_selection(vis_line_t(new_top_opt.value()));
if (this->tss_view->is_selectable()) {
this->tss_view->set_top(this->tss_view->get_selection() - 2_vl,
false);
}
}
};
}

@ -396,13 +396,11 @@ textview_curses::handle_mouse(mouse_event& me)
unsigned long width;
vis_line_t height;
if (!this->tc_selection_start && listview_curses::handle_mouse(me)) {
return true;
if (!this->vc_visible || this->lv_height == 0) {
return false;
}
if (this->tc_delegate != nullptr
&& this->tc_delegate->text_handle_mouse(*this, me))
{
if (!this->tc_selection_start && listview_curses::handle_mouse(me)) {
return true;
}
@ -410,22 +408,64 @@ textview_curses::handle_mouse(mouse_event& me)
return false;
}
auto mouse_line = this->lv_display_lines[me.me_y];
auto mouse_line = (me.me_y < 0 || me.me_y >= this->lv_display_lines.size())
? empty_space{}
: this->lv_display_lines[me.me_y];
this->get_dimensions(height, width);
auto* sub_delegate = dynamic_cast<text_delegate*>(this->tc_sub_source);
switch (me.me_state) {
case mouse_button_state_t::BUTTON_STATE_PRESSED: {
if (!this->lv_selectable) {
this->set_selectable(true);
}
mouse_line.match(
[this, &me](const main_content& mc) {
if (me.is_modifier_pressed(mouse_event::modifier_t::shift))
{
this->tc_selection_start = mc.mc_line;
[this, &me, sub_delegate](const main_content& mc) {
if (this->vc_enabled) {
if (this->tc_supports_marks
&& me.is_modifier_pressed(
mouse_event::modifier_t::shift))
{
this->tc_selection_start = mc.mc_line;
}
this->set_selection_without_context(mc.mc_line);
this->tc_press_event = me;
}
if (this->tc_delegate != nullptr) {
this->tc_delegate->text_handle_mouse(*this, me);
}
if (sub_delegate != nullptr) {
sub_delegate->text_handle_mouse(*this, me);
}
},
[](const static_overlay_content& soc) {
},
[](const overlay_content& oc) {
},
[](const empty_space& es) {});
break;
}
case mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK: {
if (!this->lv_selectable) {
this->set_selectable(true);
}
mouse_line.match(
[this, &me, sub_delegate](const main_content& mc) {
if (this->vc_enabled) {
if (this->tc_supports_marks) {
this->toggle_user_mark(&BM_USER, mc.mc_line);
}
this->set_selection_without_context(mc.mc_line);
}
if (this->tc_delegate != nullptr) {
this->tc_delegate->text_handle_mouse(*this, me);
}
if (sub_delegate != nullptr) {
sub_delegate->text_handle_mouse(*this, me);
}
this->set_selection(mc.mc_line);
this->tc_press_event = me;
},
[](const static_overlay_content& soc) {
@ -437,29 +477,35 @@ textview_curses::handle_mouse(mouse_event& me)
break;
}
case mouse_button_state_t::BUTTON_STATE_DRAGGED: {
if (me.me_y <= 0) {
if (!this->vc_enabled) {
} else if (me.me_y < 0) {
this->shift_selection(listview_curses::shift_amount_t::up_line);
me.me_y = 0;
mouse_line = main_content{this->get_top()};
} else if (me.me_y >= height
&& this->get_top() < this->get_top_for_last_row())
{
} else if (me.me_y >= height) {
this->shift_selection(
listview_curses::shift_amount_t::down_line);
me.me_y = height;
} else if (mouse_line.is<main_content>()) {
this->set_selection(mouse_line.get<main_content>().mc_line);
this->set_selection_without_context(
mouse_line.get<main_content>().mc_line);
}
break;
}
case mouse_button_state_t::BUTTON_STATE_RELEASED: {
if (this->tc_selection_start) {
this->toggle_user_mark(&BM_USER,
this->tc_selection_start.value(),
this->get_selection());
this->reload_data();
if (this->vc_enabled) {
if (this->tc_selection_start) {
this->toggle_user_mark(&BM_USER,
this->tc_selection_start.value(),
this->get_selection());
this->reload_data();
}
this->tc_selection_start = nonstd::nullopt;
}
if (this->tc_delegate != nullptr) {
this->tc_delegate->text_handle_mouse(*this, me);
}
if (sub_delegate != nullptr) {
sub_delegate->text_handle_mouse(*this, me);
}
this->tc_selection_start = nonstd::nullopt;
break;
}
}

@ -385,7 +385,7 @@ public:
{
}
void register_view(textview_curses* tc) { this->tss_view = tc; }
virtual void register_view(textview_curses* tc) { this->tss_view = tc; }
/**
* @return The total number of lines available from the source.
@ -606,6 +606,12 @@ public:
text_sub_source* get_sub_source() const { return this->tc_sub_source; }
textview_curses& set_supports_marks(bool m)
{
this->tc_supports_marks = m;
return *this;
}
textview_curses& set_delegate(std::shared_ptr<text_delegate> del)
{
this->tc_delegate = del;
@ -851,6 +857,7 @@ protected:
mouse_event tc_press_event;
bool tc_hide_fields{true};
bool tc_paused{false};
bool tc_supports_marks{false};
std::string tc_current_search;
std::string tc_previous_search;

@ -40,12 +40,14 @@
#include "base/ansi_scrubber.hh"
#include "base/attr_line.hh"
#include "base/from_trait.hh"
#include "base/injector.hh"
#include "base/itertools.hh"
#include "base/lnav_log.hh"
#include "config.h"
#include "lnav_config.hh"
#include "shlex.hh"
#include "view_curses.hh"
#include "xterm_mouse.hh"
#if defined HAVE_NCURSESW_CURSES_H
# include <ncursesw/term.h>
@ -129,10 +131,86 @@ struct utf_to_display_adjustment {
}
};
bool
mouse_event::is_click_in(mouse_button_t button, int x_start, int x_end) const
{
return this->me_button == button
&& this->me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
&& (x_start <= this->me_x && this->me_x <= x_end)
&& (x_start <= this->me_press_x && this->me_press_x <= x_end)
&& this->me_y == this->me_press_y;
}
bool
mouse_event::is_press_in(mouse_button_t button, line_range lr) const
{
return this->me_button == button
&& this->me_state == mouse_button_state_t::BUTTON_STATE_PRESSED
&& lr.contains(this->me_x);
}
bool
mouse_event::is_drag_in(mouse_button_t button, line_range lr) const
{
return this->me_button == button
&& this->me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED
&& lr.contains(this->me_x);
}
bool
mouse_event::is_double_click_in(mouse_button_t button, line_range lr) const
{
return this->me_button == button
&& this->me_state == mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK
&& lr.contains(this->me_x) && this->me_y == this->me_press_y;
}
bool
view_curses::handle_mouse(mouse_event& me)
{
if (me.me_state != mouse_button_state_t::BUTTON_STATE_DRAGGED) {
this->vc_last_drag_child = nullptr;
}
for (auto* child : this->vc_children) {
auto x = this->vc_x + me.me_x;
auto y = this->vc_y + me.me_y;
if ((me.me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED
&& child == this->vc_last_drag_child && child->vc_x <= x
&& x < (child->vc_x + child->vc_width))
|| child->contains(x, y))
{
auto sub_me = me;
sub_me.me_x = x - child->vc_x;
sub_me.me_y = y - child->vc_y;
sub_me.me_press_x = this->vc_x + me.me_press_x - child->vc_x;
sub_me.me_press_y = this->vc_y + me.me_press_y - child->vc_y;
if (me.me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED) {
this->vc_last_drag_child = child;
}
return child->handle_mouse(sub_me);
}
}
return false;
}
bool
view_curses::contains(int x, int y) const
{
if (this->vc_x <= x && x < this->vc_x + this->vc_width && this->vc_y == y) {
if (!this->vc_visible) {
return false;
}
for (auto* child : this->vc_children) {
if (child->contains(x, y)) {
return true;
}
}
if (this->vc_x <= x
&& (this->vc_width < 0 || x < this->vc_x + this->vc_width)
&& this->vc_y == y)
{
return true;
}
return false;
@ -562,9 +640,9 @@ static const std::string COLOR_NAMES[] = {
"white",
};
class color_listener : public lnav_config_listener {
class ui_listener : public lnav_config_listener {
public:
color_listener() : lnav_config_listener(__FILE__) {}
ui_listener() : lnav_config_listener(__FILE__) {}
void reload_config(error_reporter& reporter) override
{
@ -597,11 +675,16 @@ public:
if (view_colors::initialized) {
vc.init_roles(iter->second, reporter);
auto& mouse_i = injector::get<xterm_mouse&>();
mouse_i.set_enabled(check_experimental("mouse")
|| lnav_config.lc_mouse_mode
== lnav_mouse_mode::enabled);
}
}
};
static color_listener _COLOR_LISTENER;
static ui_listener _UI_LISTENER;
term_color_palette* view_colors::vc_active_palette;
void
@ -627,7 +710,7 @@ view_colors::init(bool headless)
auto reporter
= [](const void*, const lnav::console::user_message& um) {};
_COLOR_LISTENER.reload_config(reporter);
_UI_LISTENER.reload_config(reporter);
}
}

@ -314,6 +314,7 @@ enum class mouse_button_state_t {
BUTTON_STATE_PRESSED,
BUTTON_STATE_DRAGGED,
BUTTON_STATE_RELEASED,
BUTTON_STATE_DOUBLE_CLICK,
};
struct mouse_event {
@ -339,12 +340,26 @@ struct mouse_event {
return this->me_modifiers & lnav::enums::to_underlying(mod);
}
bool is_click_in(mouse_button_t button, int x_start, int x_end) const;
bool is_click_in(mouse_button_t button, line_range lr) const
{
return this->is_click_in(button, lr.lr_start, lr.lr_end);
}
bool is_press_in(mouse_button_t button, line_range lr) const;
bool is_drag_in(mouse_button_t button, line_range lr) const;
bool is_double_click_in(mouse_button_t button, line_range lr) const;
mouse_button_t me_button;
mouse_button_state_t me_state;
uint8_t me_modifiers;
struct timeval me_time {};
int me_x;
int me_y;
int me_press_x{-1};
int me_press_y{-1};
};
/**
@ -373,7 +388,7 @@ public:
return retval;
}
virtual bool handle_mouse(mouse_event& me) { return false; }
virtual bool handle_mouse(mouse_event& me);
virtual bool contains(int x, int y) const;
@ -445,6 +460,8 @@ public:
const struct line_range& lr,
role_t base_role = role_t::VCR_TEXT);
bool vc_enabled{true};
protected:
bool vc_visible{true};
/** Flag to indicate if a display update is needed. */
@ -454,6 +471,7 @@ protected:
long vc_width{0};
std::vector<view_curses*> vc_children;
role_t vc_default_role{role_t::VCR_TEXT};
view_curses* vc_last_drag_child{nullptr};
};
template<class T>

@ -626,6 +626,7 @@ handle_winch()
void
layout_views()
{
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
int width, height;
getmaxyx(lnav_data.ld_window, height, width);
@ -694,20 +695,27 @@ layout_views()
auto um_height = std::min(um_rows, (height - 4) / 2);
lnav_data.ld_user_message_view.set_height(vis_line_t(um_height));
auto config_panel_open = (lnav_data.ld_mode == ln_mode_t::FILTER
|| lnav_data.ld_mode == ln_mode_t::FILES
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILES);
auto filters_open = (lnav_data.ld_mode == ln_mode_t::FILTER
|| lnav_data.ld_mode == ln_mode_t::FILES
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILES);
int filter_height = filters_open ? 5 : 0;
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS);
auto files_open = (lnav_data.ld_mode == ln_mode_t::FILES
|| lnav_data.ld_mode == ln_mode_t::SEARCH_FILES);
int filter_height = config_panel_open ? 5 : 0;
bool breadcrumb_open = (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS);
auto bottom_min = std::min(2 + 3, height);
auto bottom = clamped<int>::from(height, bottom_min, height);
lnav_data.ld_rl_view->set_y(height - 1);
bottom -= lnav_data.ld_rl_view->get_height();
lnav_data.ld_rl_view->set_width(width);
breadcrumb_view->set_width(width);
bool vis;
vis = bottom.try_consume(lnav_data.ld_match_view.get_height());
lnav_data.ld_match_view.set_y(bottom);
@ -719,7 +727,8 @@ layout_views()
bottom -= 1;
lnav_data.ld_status[LNS_BOTTOM].set_y(bottom);
lnav_data.ld_status[LNS_BOTTOM].set_enabled(!filters_open
lnav_data.ld_status[LNS_BOTTOM].set_width(width);
lnav_data.ld_status[LNS_BOTTOM].set_enabled(!config_panel_open
&& !breadcrumb_open);
vis = preview_open1 && bottom.try_consume(preview_height1 + 1);
@ -728,6 +737,7 @@ layout_views()
lnav_data.ld_preview_view[1].set_visible(vis);
lnav_data.ld_status[LNS_PREVIEW1].set_y(bottom);
lnav_data.ld_status[LNS_PREVIEW1].set_width(width);
lnav_data.ld_status[LNS_PREVIEW1].set_visible(vis);
vis = preview_open0 && bottom.try_consume(preview_height0 + 1);
@ -736,6 +746,7 @@ layout_views()
lnav_data.ld_preview_view[0].set_visible(vis);
lnav_data.ld_status[LNS_PREVIEW0].set_y(bottom);
lnav_data.ld_status[LNS_PREVIEW0].set_width(width);
lnav_data.ld_status[LNS_PREVIEW0].set_visible(vis);
if (doc_side_by_side && doc_height > 0) {
@ -771,6 +782,7 @@ layout_views()
auto has_doc = lnav_data.ld_example_view.get_height() > 0_vl
|| lnav_data.ld_doc_view.get_height() > 0_vl;
lnav_data.ld_status[LNS_DOC].set_y(bottom);
lnav_data.ld_status[LNS_DOC].set_width(width);
lnav_data.ld_status[LNS_DOC].set_visible(has_doc && vis);
if (is_gantt) {
@ -784,9 +796,10 @@ layout_views()
lnav_data.ld_gantt_details_view.set_visible(vis);
lnav_data.ld_status[LNS_GANTT].set_y(bottom);
lnav_data.ld_status[LNS_GANTT].set_width(width);
lnav_data.ld_status[LNS_GANTT].set_visible(vis);
vis = bottom.try_consume(filter_height + (filters_open ? 1 : 0)
vis = bottom.try_consume(filter_height + (config_panel_open ? 1 : 0)
+ (filters_supported ? 1 : 0));
lnav_data.ld_filter_view.set_height(vis_line_t(filter_height));
lnav_data.ld_filter_view.set_y(bottom + 2);
@ -796,14 +809,16 @@ layout_views()
lnav_data.ld_files_view.set_height(vis_line_t(filter_height));
lnav_data.ld_files_view.set_y(bottom + 2);
lnav_data.ld_files_view.set_width(width);
lnav_data.ld_files_view.set_visible(filters_open && vis);
lnav_data.ld_files_view.set_visible(files_open && vis);
lnav_data.ld_status[LNS_FILTER_HELP].set_visible(filters_open && vis);
lnav_data.ld_status[LNS_FILTER_HELP].set_visible(config_panel_open && vis);
lnav_data.ld_status[LNS_FILTER_HELP].set_y(bottom + 1);
lnav_data.ld_status[LNS_FILTER_HELP].set_width(width);
lnav_data.ld_status[LNS_FILTER].set_visible(vis);
lnav_data.ld_status[LNS_FILTER].set_enabled(filters_open);
lnav_data.ld_status[LNS_FILTER].set_enabled(config_panel_open);
lnav_data.ld_status[LNS_FILTER].set_y(bottom);
lnav_data.ld_status[LNS_FILTER].set_width(width);
vis = is_spectro && bottom.try_consume(5 + 1);
lnav_data.ld_spectro_details_view.set_y(bottom + 1);
@ -812,6 +827,7 @@ layout_views()
lnav_data.ld_spectro_details_view.set_visible(vis);
lnav_data.ld_status[LNS_SPECTRO].set_y(bottom);
lnav_data.ld_status[LNS_SPECTRO].set_width(width);
lnav_data.ld_status[LNS_SPECTRO].set_visible(vis);
lnav_data.ld_status[LNS_SPECTRO].set_enabled(lnav_data.ld_mode
== ln_mode_t::SPECTRO_DETAILS);
@ -1402,9 +1418,55 @@ clear_preview()
}
}
void
set_view_mode(ln_mode_t mode)
{
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
switch (lnav_data.ld_mode) {
case ln_mode_t::BREADCRUMBS: {
breadcrumb_view->blur();
break;
}
default:
break;
}
lnav_data.ld_mode = mode;
}
static std::vector<view_curses*>
all_views()
{
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
std::vector<view_curses*> retval;
retval.push_back(breadcrumb_view);
for (auto& sc : lnav_data.ld_status) {
retval.push_back(&sc);
}
retval.push_back(&lnav_data.ld_doc_view);
retval.push_back(&lnav_data.ld_example_view);
retval.push_back(&lnav_data.ld_preview_view[0]);
retval.push_back(&lnav_data.ld_preview_view[1]);
retval.push_back(&lnav_data.ld_files_view);
retval.push_back(&lnav_data.ld_filter_view);
retval.push_back(&lnav_data.ld_user_message_view);
retval.push_back(&lnav_data.ld_spectro_details_view);
retval.push_back(&lnav_data.ld_gantt_details_view);
retval.push_back(lnav_data.ld_rl_view);
return retval;
}
void
lnav_behavior::mouse_event(int button, bool release, int x, int y)
{
static auto* breadcrumb_view = injector::get<breadcrumb_curses*>();
static const std::vector<view_curses*> VIEWS = all_views();
static const auto CLICK_INTERVAL
= std::chrono::milliseconds(mouseinterval(-1) * 2);
struct mouse_event me;
switch (button & xterm_mouse::XT_BUTTON__MASK) {
@ -1425,9 +1487,16 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y)
break;
}
gettimeofday(&me.me_time, nullptr);
me.me_modifiers = button & xterm_mouse::XT_MODIFIER_MASK;
if (button & xterm_mouse::XT_DRAG_FLAG) {
if (release
&& (to_mstime(me.me_time)
- to_mstime(this->lb_last_release_event.me_time))
< CLICK_INTERVAL.count())
{
me.me_state = mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK;
} else if (button & xterm_mouse::XT_DRAG_FLAG) {
me.me_state = mouse_button_state_t::BUTTON_STATE_DRAGGED;
} else if (release) {
me.me_state = mouse_button_state_t::BUTTON_STATE_RELEASED;
@ -1436,8 +1505,9 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y)
}
auto width = getmaxx(lnav_data.ld_window);
gettimeofday(&me.me_time, nullptr);
me.me_press_x = this->lb_last_event.me_press_x;
me.me_press_y = this->lb_last_event.me_press_y;
me.me_x = x - 1;
if (me.me_x >= width) {
me.me_x = width - 1;
@ -1445,15 +1515,38 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y)
me.me_y = y - 1;
switch (me.me_state) {
case mouse_button_state_t::BUTTON_STATE_PRESSED: {
case mouse_button_state_t::BUTTON_STATE_PRESSED:
case mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK: {
if (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS) {
if (breadcrumb_view->contains(me.me_x, me.me_y)) {
this->lb_last_view = breadcrumb_view;
break;
} else {
set_view_mode(ln_mode_t::PAGING);
lnav_data.ld_view_stack.set_needs_update();
}
}
auto* tc = *(lnav_data.ld_view_stack.top());
if (tc->contains(me.me_x, me.me_y)) {
this->lb_last_view = tc;
} else {
for (auto* vc : VIEWS) {
if (vc->contains(me.me_x, me.me_y)) {
this->lb_last_view = vc;
me.me_press_y = me.me_y - vc->get_y();
me.me_press_x = me.me_x - vc->get_x();
break;
}
}
}
break;
}
case mouse_button_state_t::BUTTON_STATE_DRAGGED:
case mouse_button_state_t::BUTTON_STATE_DRAGGED: {
break;
}
case mouse_button_state_t::BUTTON_STATE_RELEASED: {
this->lb_last_release_event = me;
break;
}
}
@ -1465,6 +1558,7 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y)
}
this->lb_last_event = me;
if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
|| me.me_state == mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK
|| me.me_button == mouse_button_t::BUTTON_SCROLL_UP
|| me.me_button == mouse_button_t::BUTTON_SCROLL_DOWN)
{

@ -89,6 +89,7 @@ bool handle_winch();
void layout_views();
void update_hits(textview_curses* tc);
void clear_preview();
void set_view_mode(ln_mode_t mode);
nonstd::optional<vis_line_t> next_cluster(
nonstd::optional<vis_line_t> (bookmark_vector<vis_line_t>::*f)(vis_line_t)
@ -108,6 +109,7 @@ public:
view_curses* lb_last_view{nullptr};
struct mouse_event lb_last_event;
struct mouse_event lb_last_release_event;
};
#endif

@ -80,11 +80,13 @@ void
xterm_mouse::set_enabled(bool enabled)
{
if (is_available()) {
putp(tparm((char*) XT_TERMCAP, enabled ? 1 : 0));
putp(tparm((char*) XT_TERMCAP_TRACKING, enabled ? 1 : 0));
putp(tparm((char*) XT_TERMCAP_SGR, enabled ? 1 : 0));
fflush(stdout);
this->xm_enabled = enabled;
if (this->xm_enabled != enabled) {
putp(tparm((char*) XT_TERMCAP, enabled ? 1 : 0));
putp(tparm((char*) XT_TERMCAP_TRACKING, enabled ? 1 : 0));
putp(tparm((char*) XT_TERMCAP_SGR, enabled ? 1 : 0));
fflush(stdout);
this->xm_enabled = enabled;
}
} else {
log_warning("mouse support is not available");
}

@ -91,6 +91,10 @@ main(int argc, char* argv[])
my_source ms;
WINDOW* win;
setenv("DUMP_CRASH", "1", 1);
log_install_handlers();
lnav_log_crash_dir = "/tmp";
win = initscr();
lv.set_data_source(&ms);
lv.set_window(win);

@ -4644,6 +4644,9 @@
}
}
},
"mouse": {
"mode": "disabled"
},
"movement": {
"mode": "cursor"
},

@ -12,48 +12,48 @@
/global/keymap_def_text_view -> default-keymap.json:10
/global/keymap_def_time_offset -> default-keymap.json:17
/global/keymap_def_zoom -> default-keymap.json:12
/log/annotations/com.vmware.vmacore.backtrace/condition -> root-config.json:20
/log/annotations/com.vmware.vmacore.backtrace/description -> root-config.json:19
/log/annotations/com.vmware.vmacore.backtrace/handler -> root-config.json:21
/log/annotations/com.vmware.vmacore.backtrace/condition -> root-config.json:23
/log/annotations/com.vmware.vmacore.backtrace/description -> root-config.json:22
/log/annotations/com.vmware.vmacore.backtrace/handler -> root-config.json:24
/log/annotations/org.lnav.test/condition -> {test_dir}/configs/installed/anno-test.json:7
/log/annotations/org.lnav.test/description -> {test_dir}/configs/installed/anno-test.json:6
/log/annotations/org.lnav.test/handler -> {test_dir}/configs/installed/anno-test.json:8
/log/date-time/convert-zoned-to-local -> root-config.json:15
/tuning/archive-manager/cache-ttl -> root-config.json:28
/tuning/archive-manager/min-free-space -> root-config.json:27
/tuning/clipboard/impls/MacOS/find/read -> root-config.json:56
/tuning/clipboard/impls/MacOS/find/write -> root-config.json:55
/tuning/clipboard/impls/MacOS/general/read -> root-config.json:52
/tuning/clipboard/impls/MacOS/general/write -> root-config.json:51
/tuning/clipboard/impls/MacOS/test -> root-config.json:49
/tuning/clipboard/impls/NeoVim/general/read -> root-config.json:84
/tuning/clipboard/impls/NeoVim/general/write -> root-config.json:83
/tuning/clipboard/impls/NeoVim/test -> root-config.json:81
/tuning/clipboard/impls/Wayland/general/read -> root-config.json:63
/tuning/clipboard/impls/Wayland/general/write -> root-config.json:62
/tuning/clipboard/impls/Wayland/test -> root-config.json:60
/tuning/clipboard/impls/Windows/general/write -> root-config.json:90
/tuning/clipboard/impls/Windows/test -> root-config.json:88
/tuning/clipboard/impls/X11-xclip/general/read -> root-config.json:70
/tuning/clipboard/impls/X11-xclip/general/write -> root-config.json:69
/tuning/clipboard/impls/X11-xclip/test -> root-config.json:67
/tuning/clipboard/impls/tmux/general/read -> root-config.json:77
/tuning/clipboard/impls/tmux/general/write -> root-config.json:76
/tuning/clipboard/impls/tmux/test -> root-config.json:74
/tuning/piper/max-size -> root-config.json:42
/tuning/piper/rotations -> root-config.json:43
/tuning/piper/ttl -> root-config.json:44
/tuning/remote/ssh/command -> root-config.json:32
/tuning/remote/ssh/config/BatchMode -> root-config.json:34
/tuning/remote/ssh/config/ConnectTimeout -> root-config.json:35
/tuning/remote/ssh/start-command -> root-config.json:37
/tuning/remote/ssh/transfer-command -> root-config.json:38
/tuning/url-scheme/docker-compose/handler -> root-config.json:100
/tuning/url-scheme/docker/handler -> root-config.json:97
/log/date-time/convert-zoned-to-local -> root-config.json:18
/tuning/archive-manager/cache-ttl -> root-config.json:31
/tuning/archive-manager/min-free-space -> root-config.json:30
/tuning/clipboard/impls/MacOS/find/read -> root-config.json:59
/tuning/clipboard/impls/MacOS/find/write -> root-config.json:58
/tuning/clipboard/impls/MacOS/general/read -> root-config.json:55
/tuning/clipboard/impls/MacOS/general/write -> root-config.json:54
/tuning/clipboard/impls/MacOS/test -> root-config.json:52
/tuning/clipboard/impls/NeoVim/general/read -> root-config.json:87
/tuning/clipboard/impls/NeoVim/general/write -> root-config.json:86
/tuning/clipboard/impls/NeoVim/test -> root-config.json:84
/tuning/clipboard/impls/Wayland/general/read -> root-config.json:66
/tuning/clipboard/impls/Wayland/general/write -> root-config.json:65
/tuning/clipboard/impls/Wayland/test -> root-config.json:63
/tuning/clipboard/impls/Windows/general/write -> root-config.json:93
/tuning/clipboard/impls/Windows/test -> root-config.json:91
/tuning/clipboard/impls/X11-xclip/general/read -> root-config.json:73
/tuning/clipboard/impls/X11-xclip/general/write -> root-config.json:72
/tuning/clipboard/impls/X11-xclip/test -> root-config.json:70
/tuning/clipboard/impls/tmux/general/read -> root-config.json:80
/tuning/clipboard/impls/tmux/general/write -> root-config.json:79
/tuning/clipboard/impls/tmux/test -> root-config.json:77
/tuning/piper/max-size -> root-config.json:45
/tuning/piper/rotations -> root-config.json:46
/tuning/piper/ttl -> root-config.json:47
/tuning/remote/ssh/command -> root-config.json:35
/tuning/remote/ssh/config/BatchMode -> root-config.json:37
/tuning/remote/ssh/config/ConnectTimeout -> root-config.json:38
/tuning/remote/ssh/start-command -> root-config.json:40
/tuning/remote/ssh/transfer-command -> root-config.json:41
/tuning/url-scheme/docker-compose/handler -> root-config.json:103
/tuning/url-scheme/docker/handler -> root-config.json:100
/tuning/url-scheme/hw/handler -> {test_dir}/configs/installed/hw-url-handler.json:6
/tuning/url-scheme/journald/handler -> root-config.json:103
/tuning/url-scheme/piper/handler -> root-config.json:106
/tuning/url-scheme/podman/handler -> root-config.json:109
/tuning/url-scheme/journald/handler -> root-config.json:106
/tuning/url-scheme/piper/handler -> root-config.json:109
/tuning/url-scheme/podman/handler -> root-config.json:112
/ui/clock-format -> root-config.json:4
/ui/default-colors -> root-config.json:6
/ui/dim-text -> root-config.json:5
@ -171,7 +171,8 @@
/ui/keymap-defs/us/x38/command -> us-keymap.json:28
/ui/keymap-defs/us/x40/command -> us-keymap.json:34
/ui/keymap-defs/us/x5e/command -> us-keymap.json:46
/ui/movement/mode -> root-config.json:10
/ui/mouse/mode -> root-config.json:10
/ui/movement/mode -> root-config.json:13
/ui/theme -> root-config.json:8
/ui/theme-defs/default/highlights/colors/pattern -> default-theme.json:292
/ui/theme-defs/default/highlights/colors/style/color -> default-theme.json:294

@ -467,7 +467,7 @@ mouse to mark lines of text and move the view by grabbing the
scrollbar.
NOTE: You need to manually enable this feature by setting the LNAV_EXP
environment variable to "mouse". F2 toggles mouse support.
environment variable to "mouse".  F2  toggles mouse support.
Log Analysis

@ -6,25 +6,25 @@ S -1 ┋
A └ normal
CSI Reset Replace mode
CSI Erase all
S 1 ┋17 x┋
S 1 ┋+18 x┋
A └┛ alt
S 2 ┋+18 x┋
S 2 ┋19 x┋
A └┛ alt
S 3 ┋19 x┋
S 3 ┋20 x┋
A └┛ alt
S 4 ┋20 x┋
S 4 ┋21 x┋
A └┛ alt
S 5 ┋21 x┋
S 5 ┋22 x┋
A └┛ alt
S 6 ┋22 x┋
S 6 ┋23 x┋
A └┛ alt
S 7 ┋23 x┋
S 7 ┋24 x┋
A └┛ alt
S 8 ┋24 x┋
S 8 ┋25 x┋
A └┛ alt
S 9 ┋25 x┋
S 9 ┋26 x┋
A └┛ alt
S 10 ┋26 x┋
S 10 ┋27 x┋
A └┛ alt
CSI Erase all
CSI Use normal screen buffer

@ -6,25 +6,25 @@ S -1 ┋
A └ normal
CSI Reset Replace mode
CSI Erase all
S 1 ┋8 x┋
S 1 ┋9 x┋
A └┛ alt
S 2 ┋+9 x┋
S 2 ┋10 x┋
A └┛ alt
S 3 ┋10 x┋
S 3 ┋11 x┋
A └┛ alt
S 4 ┋11 x┋
S 4 ┋12 x┋
A └┛ alt
S 5 ┋12 x┋
S 5 ┋13 x┋
A └┛ alt
S 6 ┋13 x┋
S 6 ┋14 x┋
A └┛ alt
S 7 ┋14 x┋
S 7 ┋15 x┋
A └┛ alt
S 8 ┋15 x┋
S 8 ┋16 x┋
A └┛ alt
S 9 ┋16 x┋
S 9 ┋17 x┋
A └┛ alt
S 10 ┋17 x┋
S 10 ┋+18 x┋
A └┛ alt
CSI Erase all
CSI Use normal screen buffer

Loading…
Cancel
Save