diff --git a/NEWS.md b/NEWS.md index 998a764a..24f19b3a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,19 @@ ## lnav v0.12.2 Features: +* Added a `journald://` URL handler that will call `journalctl` + and pass any query parameters as options. For example, the + following command: + + ``` + $ lnav 'journal://?since=yesterday' + ``` + + Will execute the following and capture the output: + + ``` + journalctl --output=json -f --since=yesterday + ``` * Added the "last-word" line-format field shortening algorithm from @flicus. * Added a `stats.hist` PRQL transform that produces a histogram @@ -19,9 +32,9 @@ Features: lines and then toggle their bookmark status on release; - double-clicking will select the underlying token and drag-selecting within a line will select the given text; - - when text is selected: pressing `c` will copy the text to - the clipboard; the text will be used as the suggestion for - searching/filtering; + - when text is selected, a menu will pop up that can be used + to filter based on the current text, search for it, or copy + it to the clipboard; - 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 diff --git a/docs/source/ui.rst b/docs/source/ui.rst index 2666f4cd..e745a551 100644 --- a/docs/source/ui.rst +++ b/docs/source/ui.rst @@ -432,9 +432,9 @@ elements will respond to mouse inputs: lines and then toggle their bookmark status on release; * double-clicking will select the underlying token and drag-selecting within a line will select the given text; -* with selected text, pressing :kbd:`c` will copy the text to - the clipboard and it will be used as the suggestion for - searching/filtering; +* when text is selected, a menu will pop up that can be used + to filter based on the current text, search for it, or copy + it to the clipboard; * 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 @@ -457,7 +457,14 @@ elements will respond to mouse inputs: still likely to be cases where that is insufficient. In those cases, 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. + for switching while a modifier is pressed: + + .. list-table:: + :header-rows: 1 + + * - Key + - Terminal + * - :kbd:`Option` + - iTerm, Hyper + * - :kbd:`Fn` + - Terminal.app diff --git a/src/base/string_attr_type.hh b/src/base/string_attr_type.hh index 9e9696e2..0b8cd853 100644 --- a/src/base/string_attr_type.hh +++ b/src/base/string_attr_type.hh @@ -575,6 +575,13 @@ inline std::pair operator"" _info( VC_ROLE.template value(role_t::VCR_INFO)); } +inline std::pair operator"" _status_title( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_STATUS_TITLE)); +} + inline std::pair operator"" _symbol( const char* str, std::size_t len) { diff --git a/src/data_scanner.hh b/src/data_scanner.hh index 947a43ec..9c07eaaa 100644 --- a/src/data_scanner.hh +++ b/src/data_scanner.hh @@ -130,8 +130,7 @@ public: static const char* token2name(data_token_t token); struct capture_t { - capture_t() - { /* We don't initialize anything since it's a perf hit. */ + capture_t() { /* We don't initialize anything since it's a perf hit. */ } capture_t(int begin, int end) : c_begin(begin), c_end(end) @@ -189,6 +188,14 @@ public: this->tr_capture.c_end); } + string_fragment inner_string_fragment() const + { + return string_fragment::from_byte_range( + this->tr_data, + this->tr_inner_capture.c_begin, + this->tr_inner_capture.c_end); + } + std::string to_string() const { return {&this->tr_data[this->tr_capture.c_begin], diff --git a/src/field_overlay_source.cc b/src/field_overlay_source.cc index 8a47f0f4..b3f309ec 100644 --- a/src/field_overlay_source.cc +++ b/src/field_overlay_source.cc @@ -31,6 +31,7 @@ #include "base/humanize.time.hh" #include "base/snippet_highlighters.hh" +#include "command_executor.hh" #include "config.h" #include "log.annotate.hh" #include "log_format_ext.hh" @@ -513,10 +514,10 @@ field_overlay_source::build_meta_line(const listview_curses& lv, if (!line_meta_opt) { return; } + const auto* tc = dynamic_cast(&lv); auto& vc = view_colors::singleton(); const auto& line_meta = *(line_meta_opt.value()); size_t filename_width = this->fos_lss.get_filename_offset(); - const auto* tc = dynamic_cast(&lv); if (!line_meta.bm_comment.empty()) { const auto* lead = line_meta.bm_tags.empty() ? " \u2514 " : " \u251c "; @@ -666,6 +667,81 @@ field_overlay_source::list_value_for_overlay( this->build_meta_line(lv, value_out, row); } +std::vector +field_overlay_source::list_overlay_menu(const listview_curses& lv, + vis_line_t row) +{ + const auto* tc = dynamic_cast(&lv); + std::vector retval; + + if (!tc->tc_text_selection_active && tc->tc_selected_text) { + const auto& sti = tc->tc_selected_text.value(); + + if (sti.sti_line == row) { + auto left = std::max(0, sti.sti_x - 2); + + this->fos_menu_items.clear(); + retval.emplace_back(attr_line_t().pad_to(left).append( + " Filter Other "_status_title)); + { + attr_line_t al; + + al.append(" ").append("\u2714 IN"_ok).append(" "); + int start = left; + this->fos_menu_items.emplace_back( + 1_vl, + line_range{start, start + (int) al.length()}, + [this](const std::string& value) { + auto cmd = fmt::format(FMT_STRING(":filter-in {}"), + lnav::pcre2pp::quote(value)); + execute_any(*this->fos_lss.get_exec_context(), cmd); + }); + start += al.length(); + al.append(":mag_right:"_emoji) + .append(" Search ") + .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); + this->fos_menu_items.emplace_back( + 1_vl, + line_range{start, start + (int) al.length()}, + [this](const std::string& value) { + auto cmd = fmt::format(FMT_STRING("/{}"), + lnav::pcre2pp::quote(value)); + execute_any(*this->fos_lss.get_exec_context(), cmd); + }); + retval.emplace_back(attr_line_t().pad_to(left).append(al)); + } + { + attr_line_t al; + + al.append(" ").append("\u2718 OUT"_error).append(" "); + int start = left; + this->fos_menu_items.emplace_back( + 2_vl, + line_range{start, start + (int) al.length()}, + [this](const std::string& value) { + auto cmd = fmt::format(FMT_STRING(":filter-out {}"), + lnav::pcre2pp::quote(value)); + execute_any(*this->fos_lss.get_exec_context(), cmd); + }); + start += al.length(); + al.append(":clipboard:"_emoji) + .append(" Copy ") + .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); + this->fos_menu_items.emplace_back( + 2_vl, + line_range{start, start + (int) al.length()}, + [this](const std::string& value) { + execute_any(*this->fos_lss.get_exec_context(), + "|lnav-copy-text"); + }); + retval.emplace_back(attr_line_t().pad_to(left).append(al)); + } + } + } + + return retval; +} + nonstd::optional field_overlay_source::list_header_for_overlay(const listview_curses& lv, vis_line_t vl) diff --git a/src/field_overlay_source.hh b/src/field_overlay_source.hh index 0005ac86..c5383a60 100644 --- a/src/field_overlay_source.hh +++ b/src/field_overlay_source.hh @@ -55,6 +55,9 @@ public: this->fos_meta_lines_row = -1_vl; } + std::vector list_overlay_menu(const listview_curses& lv, + vis_line_t row) override; + nonstd::optional list_header_for_overlay( const listview_curses& lv, vis_line_t vl) override; @@ -103,6 +106,20 @@ public: vis_line_t fos_meta_lines_row{0_vl}; std::vector fos_meta_lines; std::map fos_row_to_field_name; + + struct menu_item { + menu_item(vis_line_t line, + line_range range, + std::function action) + : mi_line(line), mi_range(range), mi_action(std::move(action)) + { + } + + vis_line_t mi_line; + line_range mi_range; + std::function mi_action; + }; + std::vector fos_menu_items; }; #endif // LNAV_FIELD_OVERLAY_SOURCE_H diff --git a/src/files_sub_source.cc b/src/files_sub_source.cc index 941b4283..9815cd26 100644 --- a/src/files_sub_source.cc +++ b/src/files_sub_source.cc @@ -440,7 +440,10 @@ files_overlay_source::list_static_overlay(const listview_curses& lv, } bool -files_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me) +files_sub_source::text_handle_mouse( + textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me) { if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 1, 3)) { this->list_input_handle_key(tc, ' '); diff --git a/src/files_sub_source.hh b/src/files_sub_source.hh index 4b5c70fb..90bcb021 100644 --- a/src/files_sub_source.hh +++ b/src/files_sub_source.hh @@ -61,7 +61,9 @@ public: int line, line_flags_t raw) override; - bool text_handle_mouse(textview_curses& tc, mouse_event& me) override; + bool text_handle_mouse(textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me) override; size_t fss_last_line_len{0}; attr_line_t fss_curr_line; diff --git a/src/filter_sub_source.cc b/src/filter_sub_source.cc index 32e7441e..a3c566aa 100644 --- a/src/filter_sub_source.cc +++ b/src/filter_sub_source.cc @@ -686,7 +686,10 @@ filter_sub_source::list_input_handle_scroll_out(listview_curses& lv) } bool -filter_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me) +filter_sub_source::text_handle_mouse( + textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me) { if (this->fss_editing) { return true; diff --git a/src/filter_sub_source.hh b/src/filter_sub_source.hh index 79dd06c6..6f16bdc6 100644 --- a/src/filter_sub_source.hh +++ b/src/filter_sub_source.hh @@ -72,7 +72,9 @@ public: int line, line_flags_t raw) override; - bool text_handle_mouse(textview_curses& tc, mouse_event& me) override; + bool text_handle_mouse(textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me) override; void rl_change(readline_curses* rc); diff --git a/src/listview_curses.cc b/src/listview_curses.cc index 6a9d8240..d605daf0 100644 --- a/src/listview_curses.cc +++ b/src/listview_curses.cc @@ -475,6 +475,28 @@ listview_curses::do_update() lr.lr_start = this->lv_left; lr.lr_end = this->lv_left + wrap_width; + + auto ov_menu = this->lv_overlay_source->list_overlay_menu( + *this, row); + auto ov_menu_row = 0_vl; + for (auto& ov_menu_line : ov_menu) { + if (y >= bottom) { + break; + } + + this->lv_display_lines.push_back(overlay_menu{ + ov_menu_row, + }); + mvwattrline(this->lv_window, + y, + this->vc_x, + ov_menu_line, + lr, + role_t::VCR_ALT_ROW); + ov_menu_row += 1_vl; + ++y; + } + this->lv_overlay_source->list_value_for_overlay( *this, row, row_overlay_content); auto overlay_height = this->get_overlay_height( diff --git a/src/listview_curses.hh b/src/listview_curses.hh index 0b48765e..607ce2e4 100644 --- a/src/listview_curses.hh +++ b/src/listview_curses.hh @@ -114,6 +114,12 @@ public: return false; } + virtual std::vector list_overlay_menu( + const listview_curses& lv, vis_line_t line) + { + return {}; + } + virtual nonstd::optional list_header_for_overlay( const listview_curses& lv, vis_line_t line) { @@ -528,6 +534,24 @@ public: virtual void invoke_scroll() { this->lv_scroll(this); } + struct main_content { + vis_line_t mc_line; + }; + struct static_overlay_content {}; + struct overlay_menu { + vis_line_t om_line; + }; + struct overlay_content { + vis_line_t oc_line; + }; + struct empty_space {}; + + using display_line_content_t = mapbox::util::variant; + protected: void delegate_scroll_out() { @@ -581,20 +605,6 @@ protected: lv_mode_t lv_mouse_mode{lv_mode_t::NONE}; vis_line_t lv_tail_space{1}; - struct main_content { - vis_line_t mc_line; - }; - struct static_overlay_content {}; - struct overlay_content { - vis_line_t oc_line; - }; - struct empty_space {}; - - using display_line_content_t = mapbox::util::variant; - std::vector lv_display_lines; unsigned int lv_scroll_top{0}; unsigned int lv_scroll_bottom{0}; diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc index d848af9c..160a924c 100644 --- a/src/logfile_sub_source.cc +++ b/src/logfile_sub_source.cc @@ -3022,8 +3022,27 @@ logfile_sub_source::get_anchors() } bool -logfile_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me) +logfile_sub_source::text_handle_mouse( + textview_curses& tc, + const listview_curses::display_line_content_t& mouse_line, + mouse_event& me) { + auto* fos = dynamic_cast(tc.get_overlay_source()); + + if (mouse_line.is() && tc.tc_selected_text) { + auto& om = mouse_line.get(); + auto& sti = tc.tc_selected_text.value(); + + for (const auto& mi : fos->fos_menu_items) { + if (om.om_line == mi.mi_line + && me.is_click_in(mouse_button_t::BUTTON_LEFT, mi.mi_range)) + { + mi.mi_action(sti.sti_value); + break; + } + } + } + if (tc.get_overlay_selection() && me.is_click_in(mouse_button_t::BUTTON_LEFT, 2, 4)) { diff --git a/src/logfile_sub_source.hh b/src/logfile_sub_source.hh index 5ef3dbff..a82db601 100644 --- a/src/logfile_sub_source.hh +++ b/src/logfile_sub_source.hh @@ -689,7 +689,9 @@ public: void text_crumbs_for_line(int line, std::vector& crumbs); - bool text_handle_mouse(textview_curses& tc, mouse_event& me); + bool text_handle_mouse(textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me); Result eval_sql_filter( sqlite3_stmt* stmt, iterator ld, logfile::const_iterator ll); @@ -702,6 +704,8 @@ public: void set_exec_context(exec_context* ec) { this->lss_exec_context = ec; } + exec_context* get_exec_context() const { return this->lss_exec_context; } + static const uint64_t MAX_CONTENT_LINES = (1ULL << 40) - 1; static const uint64_t MAX_LINES_PER_FILE = 256 * 1024 * 1024; static const uint64_t MAX_FILES = (MAX_CONTENT_LINES / MAX_LINES_PER_FILE); diff --git a/src/pcrepp/pcre2pp.cc b/src/pcrepp/pcre2pp.cc index ce549d91..afeaeed8 100644 --- a/src/pcrepp/pcre2pp.cc +++ b/src/pcrepp/pcre2pp.cc @@ -43,12 +43,24 @@ quote(const char* unquoted) for (int lpc = 0; unquoted[lpc]; lpc++) { if (isalnum(unquoted[lpc]) || unquoted[lpc] == '_' + || unquoted[lpc] == '-' || unquoted[lpc] == ' ' + || unquoted[lpc] == ':' || unquoted[lpc] == ';' || unquoted[lpc] & 0x80) { retval.push_back(unquoted[lpc]); } else { retval.push_back('\\'); - retval.push_back(unquoted[lpc]); + switch (unquoted[lpc]) { + case '\t': + retval.push_back('t'); + break; + case '\n': + retval.push_back('n'); + break; + default: + retval.push_back(unquoted[lpc]); + break; + } } } diff --git a/src/textview_curses.cc b/src/textview_curses.cc index f64fa0f4..6e65717d 100644 --- a/src/textview_curses.cc +++ b/src/textview_curses.cc @@ -426,22 +426,24 @@ textview_curses::handle_mouse(mouse_event& me) : this->lv_display_lines[me.me_y]; this->get_dimensions(height, width); - auto* sub_delegate = dynamic_cast(this->tc_sub_source); - - if (me.me_button != mouse_button_t::BUTTON_LEFT - || me.me_state != mouse_button_state_t::BUTTON_STATE_RELEASED) + if (!mouse_line.is() + && (me.me_button != mouse_button_t::BUTTON_LEFT + || me.me_state != mouse_button_state_t::BUTTON_STATE_RELEASED)) { this->tc_selected_text = nonstd::nullopt; this->set_needs_update(); } + auto* sub_delegate = dynamic_cast(this->tc_sub_source); + switch (me.me_state) { case mouse_button_state_t::BUTTON_STATE_PRESSED: { + this->tc_text_selection_active = true; if (!this->lv_selectable) { this->set_selectable(true); } mouse_line.match( - [this, &me, sub_delegate](const main_content& mc) { + [this, &me, sub_delegate, &mouse_line](const main_content& mc) { if (this->vc_enabled) { if (this->tc_supports_marks && me.is_modifier_pressed( @@ -453,12 +455,14 @@ textview_curses::handle_mouse(mouse_event& me) this->tc_press_event = me; } if (this->tc_delegate != nullptr) { - this->tc_delegate->text_handle_mouse(*this, me); + this->tc_delegate->text_handle_mouse( + *this, mouse_line, me); } if (sub_delegate != nullptr) { - sub_delegate->text_handle_mouse(*this, me); + sub_delegate->text_handle_mouse(*this, mouse_line, me); } }, + [](const overlay_menu& om) {}, [](const static_overlay_content& soc) { }, @@ -472,8 +476,9 @@ textview_curses::handle_mouse(mouse_event& me) if (!this->lv_selectable) { this->set_selectable(true); } + this->tc_text_selection_active = false; mouse_line.match( - [this, &me, sub_delegate](const main_content& mc) { + [this, &me, &mouse_line, sub_delegate](const main_content& mc) { if (this->vc_enabled) { if (this->tc_supports_marks) { attr_line_t al; @@ -481,8 +486,9 @@ textview_curses::handle_mouse(mouse_event& me) this->textview_value_for_row(mc.mc_line, al); auto line_sf = string_fragment::from_str(al.get_string()); - auto cursor_sf - = line_sf.sub_cell_range(me.me_x, me.me_x); + auto cursor_sf = line_sf.sub_cell_range( + this->lv_left + me.me_x, + this->lv_left + me.me_x); auto ds = data_scanner(line_sf); auto tf = this->tc_sub_source->get_text_format(); while (true) { @@ -492,9 +498,10 @@ textview_curses::handle_mouse(mouse_event& me) } auto tok = tok_res.value(); - auto tok_sf = tok.to_string_fragment(); + auto tok_sf = tok.inner_string_fragment(); if (tok_sf.contains(cursor_sf)) { this->tc_selected_text = selected_text_info{ + me.me_x, mc.mc_line, line_range{ tok_sf.sf_begin, @@ -510,14 +517,18 @@ textview_curses::handle_mouse(mouse_event& me) this->set_selection_without_context(mc.mc_line); } if (this->tc_delegate != nullptr) { - this->tc_delegate->text_handle_mouse(*this, me); + this->tc_delegate->text_handle_mouse( + *this, mouse_line, me); } if (sub_delegate != nullptr) { - sub_delegate->text_handle_mouse(*this, me); + sub_delegate->text_handle_mouse(*this, mouse_line, me); } }, [](const static_overlay_content& soc) { + }, + [](const overlay_menu& om) { + }, [](const overlay_content& oc) { @@ -526,19 +537,24 @@ textview_curses::handle_mouse(mouse_event& me) break; } case mouse_button_state_t::BUTTON_STATE_DRAGGED: { + this->tc_text_selection_active = true; if (!this->vc_enabled) { } else if (me.me_y == me.me_press_y) { if (mouse_line.is()) { auto& mc = mouse_line.get(); attr_line_t al; - auto low_x = std::min(me.me_x, me.me_press_x); - auto high_x = std::max(me.me_x, me.me_press_x); + auto low_x = std::min(this->lv_left + me.me_x, + this->lv_left + me.me_press_x); + auto high_x = std::max(this->lv_left + me.me_x, + this->lv_left + me.me_press_x); + this->set_selection_without_context(mc.mc_line); this->textview_value_for_row(mc.mc_line, al); auto line_sf = string_fragment::from_str(al.get_string()); auto cursor_sf = line_sf.sub_cell_range(low_x, high_x); if (!cursor_sf.empty()) { this->tc_selected_text = { + me.me_press_x, mc.mc_line, line_range{ cursor_sf.sf_begin, @@ -561,6 +577,7 @@ textview_curses::handle_mouse(mouse_event& me) break; } case mouse_button_state_t::BUTTON_STATE_RELEASED: { + this->tc_text_selection_active = false; if (this->vc_enabled) { if (this->tc_selection_start) { this->toggle_user_mark(&BM_USER, @@ -571,10 +588,14 @@ textview_curses::handle_mouse(mouse_event& me) this->tc_selection_start = nonstd::nullopt; } if (this->tc_delegate != nullptr) { - this->tc_delegate->text_handle_mouse(*this, me); + this->tc_delegate->text_handle_mouse(*this, mouse_line, me); } if (sub_delegate != nullptr) { - sub_delegate->text_handle_mouse(*this, me); + sub_delegate->text_handle_mouse(*this, mouse_line, me); + } + if (mouse_line.is()) { + this->tc_selected_text = nonstd::nullopt; + this->set_needs_update(); } break; } diff --git a/src/textview_curses.hh b/src/textview_curses.hh index dc4abb9d..cb8afa32 100644 --- a/src/textview_curses.hh +++ b/src/textview_curses.hh @@ -551,7 +551,10 @@ class text_delegate { public: virtual ~text_delegate() = default; - virtual bool text_handle_mouse(textview_curses& tc, mouse_event& me) + virtual bool text_handle_mouse( + textview_curses& tc, + const listview_curses::display_line_content_t&, + mouse_event& me) { return false; } @@ -801,12 +804,14 @@ public: nonstd::optional tc_disabled_cursor_role; struct selected_text_info { + int sti_x; int64_t sti_line; line_range sti_range; std::string sti_value; }; nonstd::optional tc_selected_text; + bool tc_text_selection_active{false}; protected: class grep_highlighter {