From 53ab7b14a6c412e2c57aad579a8ec4f33097316c Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Fri, 12 Apr 2024 20:54:23 -0700 Subject: [PATCH] [mouse] flesh out things more --- NEWS.md | 23 +++ docs/schemas/config-v1.schema.json | 21 ++ docs/source/intro.rst | 60 +++++- example-scripts/report-demo.lnav | 4 +- src/breadcrumb_curses.cc | 99 +++++++++- src/breadcrumb_curses.hh | 22 +++ src/files_sub_source.cc | 185 +++++++++--------- src/files_sub_source.hh | 6 +- src/filter_status_source.cc | 4 + src/filter_sub_source.cc | 134 +++++++------ src/filter_sub_source.hh | 8 +- src/help.md | 2 +- src/listview_curses.cc | 84 +++++--- src/listview_curses.hh | 2 + src/lnav.cc | 89 +++++++-- src/lnav_commands.cc | 20 +- src/lnav_config.cc | 28 ++- src/lnav_config.hh | 6 + src/logfile_sub_source.cc | 5 - src/plain_text_source.cc | 8 + src/readline_curses.cc | 29 ++- src/readline_curses.hh | 2 + src/root-config.json | 3 + src/shared_buffer.cc | 2 +- src/shlex.cc | 9 + src/shlex.hh | 2 + src/statusview_curses.cc | 41 +++- src/statusview_curses.hh | 20 ++ src/textfile_sub_source.cc | 4 + src/textview_curses.cc | 96 ++++++--- src/textview_curses.hh | 9 +- src/view_curses.cc | 93 ++++++++- src/view_curses.hh | 20 +- src/view_helpers.cc | 120 ++++++++++-- src/view_helpers.hh | 2 + src/xterm_mouse.cc | 12 +- test/drive_listview.cc | 4 + ...3639753916f71254e8c9cce4ebb8bfd9978d3e.out | 3 + ...06341dd560f927512e92c7c0985ed8b25827ae.out | 79 ++++---- ...a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out | 2 +- test/listview_output_cursor.5 | 20 +- test/listview_output_cursor.6 | 20 +- 42 files changed, 1061 insertions(+), 341 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7f54e190..8f51377c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 `); + - 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 diff --git a/docs/schemas/config-v1.schema.json b/docs/schemas/config-v1.schema.json index d4897131..ce8e90f1 100644 --- a/docs/schemas/config-v1.schema.json +++ b/docs/schemas/config-v1.schema.json @@ -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", diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 2d1ac14c..ee0e0289 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -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 `); +* 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 _` + where pressing :kbd:`Option` will allow you to select + text and copy. Log Formats ^^^^^^^^^^^ diff --git a/example-scripts/report-demo.lnav b/example-scripts/report-demo.lnav index aeb00401..aa80d4ca 100644 --- a/example-scripts/report-demo.lnav +++ b/example-scripts/report-demo.lnav @@ -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 diff --git a/src/breadcrumb_curses.cc b/src/breadcrumb_curses.cc index 62778d0c..62322213 100644 --- a/src/breadcrumb_curses.cc +++ b/src/breadcrumb_curses.cc @@ -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; +} diff --git a/src/breadcrumb_curses.hh b/src/breadcrumb_curses.hh index 9db822e3..93733d6f 100644 --- a/src/breadcrumb_curses.hh +++ b/src/breadcrumb_curses.hh @@ -40,6 +40,8 @@ class breadcrumb_curses : public view_curses { public: + using action = std::function; + 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 bc_displayed_crumbs; + bool bc_initial_mouse_event{true}; }; #endif diff --git a/src/files_sub_source.cc b/src/files_sub_source.cc index 636d24ae..941b4283 100644 --- a/src/files_sub_source.cc +++ b/src/files_sub_source.cc @@ -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 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 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 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; +} diff --git a/src/files_sub_source.hh b/src/files_sub_source.hh index 71008d2c..4b5c70fb 100644 --- a/src/files_sub_source.hh +++ b/src/files_sub_source.hh @@ -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 { diff --git a/src/filter_status_source.cc b/src/filter_status_source.cc index 0f8b4f63..d284d58f 100644 --- a/src/filter_status_source.cc +++ b/src/filter_status_source.cc @@ -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( diff --git a/src/filter_sub_source.cc b/src/filter_sub_source.cc index 128dbf2b..c50ddb82 100644 --- a/src/filter_sub_source.cc +++ b/src/filter_sub_source.cc @@ -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 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; +} diff --git a/src/filter_sub_source.hh b/src/filter_sub_source.hh index 11587dab..79dd06c6 100644 --- a/src/filter_sub_source.hh +++ b/src/filter_sub_source.hh @@ -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 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 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}; diff --git a/src/help.md b/src/help.md index 67815163..9dafca21 100644 --- a/src/help.md +++ b/src/help.md @@ -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 diff --git a/src/listview_curses.cc b/src/listview_curses.cc index 50c13ea5..6a9d8240 100644 --- a/src/listview_curses.cc +++ b/src/listview_curses.cc @@ -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 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(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() { diff --git a/src/listview_curses.hh b/src/listview_curses.hh index 58fd4e84..0b48765e 100644 --- a/src/listview_curses.hh +++ b/src/listview_curses.hh @@ -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, diff --git a/src/lnav.cc b/src/lnav.cc index 581cb6e4..141dc5aa 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -228,6 +228,8 @@ static auto bound_xterm_mouse = injector::bind::to_singleton(); static auto bound_scripts = injector::bind::to_singleton(); +static auto bound_crumbs = injector::bind::to_singleton(); + static auto bound_curl = injector::bind_multiple() .add_singleton(); @@ -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(); + 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(); + 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(); + 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(); static auto* filter_source = injector::get(); + static auto* breadcrumb_view = injector::get(); try { auto* sql_cmd_map = injector::get(); + auto& mouse_i = injector::get(); 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>(); + 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( diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index ab44c638..c5428f0e 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -698,6 +698,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector& args) std::string all_args = remaining_args(cmdline, args); auto* tc = *lnav_data.ld_view_stack.top(); nonstd::optional dst_vl; + auto is_location = false; if (startswith(all_args, "#")) { auto* ta = dynamic_cast(tc->get_sub_source()); @@ -710,6 +711,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector& args) if (!dst_vl) { return ec.make_error("unable to find anchor: {}", all_args); } + is_location = true; } auto* ttt = dynamic_cast(tc->get_sub_source()); @@ -878,7 +880,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector& 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& 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); diff --git a/src/lnav_config.cc b/src/lnav_config.cc index 35e47164..22dc58ba 100644 --- a/src/lnav_config.cc +++ b/src/lnav_config.cc @@ -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("(?\\w+)") .with_synopsis("") @@ -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), diff --git a/src/lnav_config.hh b/src/lnav_config.hh index 63dc425b..abaa33f4 100644 --- a/src/lnav_config.hh +++ b/src/lnav_config.hh @@ -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 lc_ui_keymaps; std::map lc_ui_key_overrides; std::map lc_global_vars; diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc index aa4bd72d..b0a60611 100644 --- a/src/logfile_sub_source.cc +++ b/src/logfile_sub_source.cc @@ -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); } diff --git a/src/plain_text_source.cc b/src/plain_text_source.cc index 88b8e15d..608c6231 100644 --- a/src/plain_text_source.cc +++ b/src/plain_text_source.cc @@ -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); + } }; }); }); diff --git a/src/readline_curses.cc b/src/readline_curses.cc index dc6bab35..9b56f7cc 100644 --- a/src/readline_curses.cc +++ b/src/readline_curses.cc @@ -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 { diff --git a/src/readline_curses.hh b/src/readline_curses.hh index a05236e2..3c1cba7d 100644 --- a/src/readline_curses.hh +++ b/src/readline_curses.hh @@ -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); diff --git a/src/root-config.json b/src/root-config.json index effdbed8..cc7e8c51 100644 --- a/src/root-config.json +++ b/src/root-config.json @@ -6,6 +6,9 @@ "default-colors": true, "keymap": "default", "theme": "default", + "mouse": { + "mode": "disabled" + }, "movement": { "mode": "cursor" } diff --git a/src/shared_buffer.cc b/src/shared_buffer.cc index 01be2010..ff51750a 100644 --- a/src/shared_buffer.cc +++ b/src/shared_buffer.cc @@ -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; } diff --git a/src/shlex.cc b/src/shlex.cc index 6264152f..d33924e2 100644 --- a/src/shlex.cc +++ b/src/shlex.cc @@ -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 { diff --git a/src/shlex.hh b/src/shlex.hh index 42b53bcc..b51d86f7 100644 --- a/src/shlex.hh +++ b/src/shlex.hh @@ -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) diff --git a/src/statusview_curses.cc b/src/statusview_curses.cc index 146f741b..c069f6dd 100644 --- a/src/statusview_curses.cc +++ b/src/statusview_curses.cc @@ -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(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(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; +} diff --git a/src/statusview_curses.hh b/src/statusview_curses.hh index 46f00ab9..ff2bc93a 100644 --- a/src/statusview_curses.hh +++ b/src/statusview_curses.hh @@ -42,6 +42,8 @@ */ class status_field { public: + using action = std::function; + /** * @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 sc_displayed_fields; }; #endif diff --git a/src/textfile_sub_source.cc b/src/textfile_sub_source.cc index 92a5df8d..267f9c67 100644 --- a/src/textfile_sub_source.cc +++ b/src/textfile_sub_source.cc @@ -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); + } } }; } diff --git a/src/textview_curses.cc b/src/textview_curses.cc index 2781d817..ce2a36fb 100644 --- a/src/textview_curses.cc +++ b/src/textview_curses.cc @@ -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(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()) { - this->set_selection(mouse_line.get().mc_line); + this->set_selection_without_context( + mouse_line.get().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; } } diff --git a/src/textview_curses.hh b/src/textview_curses.hh index bc1ef6a4..a3267992 100644 --- a/src/textview_curses.hh +++ b/src/textview_curses.hh @@ -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 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; diff --git a/src/view_curses.cc b/src/view_curses.cc index d7bbde13..afa58eab 100644 --- a/src/view_curses.cc +++ b/src/view_curses.cc @@ -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 @@ -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(); + 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); } } diff --git a/src/view_curses.hh b/src/view_curses.hh index cba68abb..22f96c2e 100644 --- a/src/view_curses.hh +++ b/src/view_curses.hh @@ -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 vc_children; role_t vc_default_role{role_t::VCR_TEXT}; + view_curses* vc_last_drag_child{nullptr}; }; template diff --git a/src/view_helpers.cc b/src/view_helpers.cc index 34c15a44..7b837e30 100644 --- a/src/view_helpers.cc +++ b/src/view_helpers.cc @@ -626,6 +626,7 @@ handle_winch() void layout_views() { + static auto* breadcrumb_view = injector::get(); 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::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(); + + switch (lnav_data.ld_mode) { + case ln_mode_t::BREADCRUMBS: { + breadcrumb_view->blur(); + break; + } + default: + break; + } + lnav_data.ld_mode = mode; +} + +static std::vector +all_views() +{ + static auto* breadcrumb_view = injector::get(); + + std::vector 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(); + static const std::vector 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) { diff --git a/src/view_helpers.hh b/src/view_helpers.hh index 92c7e54a..de3d43ab 100644 --- a/src/view_helpers.hh +++ b/src/view_helpers.hh @@ -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 next_cluster( nonstd::optional (bookmark_vector::*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 diff --git a/src/xterm_mouse.cc b/src/xterm_mouse.cc index 217e306d..0091f79e 100644 --- a/src/xterm_mouse.cc +++ b/src/xterm_mouse.cc @@ -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"); } diff --git a/test/drive_listview.cc b/test/drive_listview.cc index f9d6080b..96fe4597 100644 --- a/test/drive_listview.cc +++ b/test/drive_listview.cc @@ -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); diff --git a/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out b/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out index 9f16b891..24fc3863 100644 --- a/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out +++ b/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out @@ -4644,6 +4644,9 @@ } } }, + "mouse": { + "mode": "disabled" + }, "movement": { "mode": "cursor" }, diff --git a/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out b/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out index e0d7d68c..6e3fec54 100644 --- a/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out +++ b/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out @@ -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 diff --git a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out index 87329d38..46c8890f 100644 --- a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out +++ b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out @@ -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 diff --git a/test/listview_output_cursor.5 b/test/listview_output_cursor.5 index c7d432e0..f25061e7 100644 --- a/test/listview_output_cursor.5 +++ b/test/listview_output_cursor.5 @@ -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 diff --git a/test/listview_output_cursor.6 b/test/listview_output_cursor.6 index be86ee17..ec80b892 100644 --- a/test/listview_output_cursor.6 +++ b/test/listview_output_cursor.6 @@ -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