[mouse] refactor overlay menu

pull/1265/head v0.12.2-beta1
Tim Stack 1 month ago
parent 65634ad9b3
commit 45d8e27ae4

@ -482,6 +482,7 @@ add_library(
styling.cc
text_anonymizer.cc
text_format.cc
text_overlay_menu.cc
textfile_highlighters.cc
textfile_sub_source.cc
textview_curses.cc
@ -608,6 +609,7 @@ add_library(
termios_guard.hh
text_anonymizer.hh
text_format.hh
text_overlay_menu.hh
textfile_highlighters.hh
textfile_sub_source.hh
textview_curses.hh

@ -349,6 +349,7 @@ noinst_HEADERS = \
term_extra.hh \
text_anonymizer.hh \
text_format.hh \
text_overlay_menu.hh \
textfile_highlighters.hh \
textfile_sub_source.hh \
textview_curses.hh \
@ -506,6 +507,7 @@ libdiag_a_SOURCES = \
styling.cc \
text_anonymizer.cc \
text_format.cc \
text_overlay_menu.cc \
textfile_sub_source.cc \
timer.cc \
sql_commands.cc \

@ -399,8 +399,9 @@ string_fragment::sub_cell_range(int cell_start, int cell_end) const
if (cell_start == cell_index) {
byte_start = byte_index;
}
if (cell_index == cell_end) {
if (!byte_end && cell_index >= cell_end) {
byte_end = byte_index;
break;
}
auto read_res = ww898::utf::utf8::read(
[this, &byte_index]() { return this->sf_string[byte_index++]; });

@ -76,6 +76,7 @@ is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator)
break;
}
retval.usr_column_width_guess += 1;
if (retval.usr_message != nullptr) {
i += 1;
continue;
@ -83,6 +84,9 @@ is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator)
valid_end = i;
if (ustr[i] <= 0x7F) /* 00..7F */ {
if (ustr[i] == '\t') {
retval.usr_column_width_guess += 7;
}
i += 1;
} else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ {
if (i + 1 < str.length()) /* Expect a 2nd byte */ {

@ -40,6 +40,7 @@ struct utf8_scan_result {
string_fragment usr_valid_frag{string_fragment::invalid()};
nonstd::optional<string_fragment> usr_remaining;
bool usr_has_ansi{false};
size_t usr_column_width_guess{0};
const char* remaining_ptr(const string_fragment& frag) const
{

@ -348,3 +348,77 @@ formatter<lnav::tainted_string>::format(const lnav::tainted_string& ts,
return format_to(ctx.out(), FMT_STRING("{:?}"), ts.ts_str);
}
} // namespace fmt
namespace lnav {
namespace pcre2pp {
static bool
is_meta(char ch)
{
switch (ch) {
case '\\':
case '^':
case '$':
case '.':
case '[':
case ']':
case '(':
case ')':
case '*':
case '+':
case '?':
case '{':
case '}':
return true;
default:
return false;
}
}
static nonstd::optional<const char*>
char_escape_seq(char ch)
{
switch (ch) {
case '\t':
return "\\t";
case '\n':
return "\\n";
}
return nonstd::nullopt;
}
std::string
quote(string_fragment str)
{
std::string retval;
while (true) {
auto cp_pair_opt = str.consume_codepoint();
if (!cp_pair_opt) {
break;
}
auto cp_pair = cp_pair_opt.value();
if ((cp_pair.first & ~0xff) == 0) {
if (is_meta(cp_pair.first)) {
retval.push_back('\\');
} else {
auto esc_seq = char_escape_seq(cp_pair.first);
if (esc_seq) {
retval.append(esc_seq.value());
str = cp_pair_opt->second;
continue;
}
}
}
ww898::utf::utf8::write(cp_pair.first,
[&retval](char ch) { retval.push_back(ch); });
str = cp_pair_opt->second;
}
return retval;
}
} // namespace pcre2pp
} // namespace lnav

@ -282,9 +282,17 @@ private:
namespace fmt {
template<>
struct formatter<lnav::tainted_string> : formatter<string_view> {
auto format(const lnav::tainted_string& ts, format_context& ctx)
-> decltype(ctx.out()) const;
auto format(const lnav::tainted_string& ts,
format_context& ctx) -> decltype(ctx.out()) const;
};
} // namespace fmt
namespace lnav {
namespace pcre2pp {
std::string quote(string_fragment sf);
}
} // namespace lnav
#endif

@ -304,3 +304,102 @@ data_scanner::cleanup_end()
}
}
}
nonstd::optional<data_scanner::tokenize_result>
data_scanner::tokenize2(text_format_t tf)
{
auto retval = this->tokenize_int(tf);
if (this->ds_last_bracket_matched) {
this->ds_matching_brackets.pop_back();
this->ds_last_bracket_matched = false;
}
if (retval) {
auto dt = retval.value().tr_token;
switch (dt) {
case DT_LSQUARE:
case DT_LCURLY:
case DT_LPAREN:
this->ds_matching_brackets.emplace_back(retval.value());
break;
case DT_RSQUARE:
case DT_RCURLY:
case DT_RPAREN:
if (!this->ds_matching_brackets.empty()
&& this->ds_matching_brackets.back().tr_token
== to_opener(dt))
{
this->ds_last_bracket_matched = true;
}
break;
default:
break;
}
}
return retval;
}
nonstd::optional<data_scanner::tokenize_result>
data_scanner::find_matching_bracket(text_format_t tf, tokenize_result tr)
{
switch (tr.tr_token) {
case DT_LSQUARE:
case DT_LCURLY:
case DT_LPAREN: {
auto curr_size = this->ds_matching_brackets.size();
while (true) {
auto tok_res = this->tokenize2(tf);
if (!tok_res) {
break;
}
if (this->ds_matching_brackets.size() == curr_size
&& this->ds_last_bracket_matched)
{
return tokenize_result{
DNT_GROUP,
{
tr.tr_capture.c_begin,
tok_res->tr_capture.c_end,
},
{
tr.tr_capture.c_begin,
tok_res->tr_capture.c_end,
},
tr.tr_data,
};
}
}
break;
}
case DT_RSQUARE:
case DT_RCURLY:
case DT_RPAREN: {
for (auto riter = this->ds_matching_brackets.rbegin();
riter != this->ds_matching_brackets.rend();
++riter)
{
if (riter->tr_token == to_opener(tr.tr_token)) {
return data_scanner::tokenize_result{
DNT_GROUP,
{
riter->tr_capture.c_begin,
tr.tr_capture.c_end,
},
{
riter->tr_capture.c_begin,
tr.tr_capture.c_end,
},
tr.tr_data,
};
}
}
break;
}
default:
break;
}
return nonstd::nullopt;
}

@ -206,6 +206,9 @@ public:
nonstd::optional<tokenize_result> tokenize2(text_format_t tf
= text_format_t::TF_UNKNOWN);
nonstd::optional<tokenize_result> find_matching_bracket(text_format_t tf,
tokenize_result tr);
void reset() { this->ds_next_offset = this->ds_init_offset; }
int get_init_offset() const { return this->ds_init_offset; }
@ -222,6 +225,9 @@ private:
bool is_credit_card(string_fragment frag) const;
nonstd::optional<tokenize_result> tokenize_int(text_format_t tf
= text_format_t::TF_UNKNOWN);
std::string ds_line;
shared_buffer_ref ds_sbr;
string_fragment ds_input;
@ -229,8 +235,27 @@ private:
int ds_next_offset{0};
bool ds_bol{true};
bool ds_units{false};
std::vector<tokenize_result> ds_matching_brackets;
bool ds_last_bracket_matched{false};
};
inline data_token_t
to_opener(data_token_t dt)
{
switch (dt) {
case DT_XML_CLOSE_TAG:
return DT_XML_OPEN_TAG;
case DT_RCURLY:
return DT_LCURLY;
case DT_RSQUARE:
return DT_LSQUARE;
case DT_RPAREN:
return DT_LPAREN;
default:
ensure(0);
}
}
inline data_token_t
to_closer(data_token_t dt)
{

@ -1,4 +1,4 @@
/* Generated by re2c 3.1 on Mon Apr 1 11:22:40 2024 */
/* Generated by re2c 3.1 on Thu Apr 18 13:48:53 2024 */
#line 1 "../../lnav/src/data_scanner_re.re"
/**
* Copyright (c) 2015, Timothy Stack
@ -48,7 +48,7 @@ enum YYCONDTYPE {
#line 38 "../../lnav/src/data_scanner_re.re"
nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize2(text_format_t tf)
nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize_int(text_format_t tf)
{
data_token_t token_out = DT_INVALID;
capture_t cap_all;

@ -37,7 +37,7 @@
/*!conditions:re2c*/
nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize2(text_format_t tf)
nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize_int(text_format_t tf)
{
data_token_t token_out = DT_INVALID;
capture_t cap_all;

@ -672,87 +672,6 @@ field_overlay_source::list_value_for_overlay(
this->build_meta_line(lv, value_out, row);
}
std::vector<attr_line_t>
field_overlay_source::list_overlay_menu(const listview_curses& lv,
vis_line_t row)
{
const auto* tc = dynamic_cast<const textview_curses*>(&lv);
std::vector<attr_line_t> 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));
this->fos_lss.get_exec_context()
->with_provenance(exec_context::mouse_input{})
->execute(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));
this->fos_lss.get_exec_context()
->with_provenance(exec_context::mouse_input{})
->execute(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));
this->fos_lss.get_exec_context()
->with_provenance(exec_context::mouse_input{})
->execute(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) {
this->fos_lss.get_exec_context()->execute(
"|lnav-copy-text");
});
retval.emplace_back(attr_line_t().pad_to(left).append(al));
}
}
}
return retval;
}
nonstd::optional<attr_line_t>
field_overlay_source::list_header_for_overlay(const listview_curses& lv,
vis_line_t vl)

@ -36,9 +36,10 @@
#include "listview_curses.hh"
#include "log_data_helper.hh"
#include "logfile_sub_source.hh"
#include "text_overlay_menu.hh"
#include "textfile_sub_source.hh"
class field_overlay_source : public list_overlay_source {
class field_overlay_source : public text_overlay_menu {
public:
explicit field_overlay_source(logfile_sub_source& lss,
textfile_sub_source& tss)
@ -52,12 +53,8 @@ public:
{
this->fos_lines.clear();
this->fos_meta_lines.clear();
this->fos_meta_lines_row = -1_vl;
}
std::vector<attr_line_t> list_overlay_menu(const listview_curses& lv,
vis_line_t row) override;
nonstd::optional<attr_line_t> list_header_for_overlay(
const listview_curses& lv, vis_line_t vl) override;
@ -103,23 +100,8 @@ public:
int fos_known_key_size{0};
int fos_unknown_key_size{0};
std::vector<attr_line_t> fos_lines;
vis_line_t fos_meta_lines_row{0_vl};
std::vector<attr_line_t> fos_meta_lines;
std::map<size_t, logline_value_meta> fos_row_to_field_meta;
struct menu_item {
menu_item(vis_line_t line,
line_range range,
std::function<void(const std::string&)> action)
: mi_line(line), mi_range(range), mi_action(std::move(action))
{
}
vis_line_t mi_line;
line_range mi_range;
std::function<void(const std::string&)> mi_action;
};
std::vector<menu_item> fos_menu_items;
};
#endif // LNAV_FIELD_OVERLAY_SOURCE_H

@ -34,6 +34,7 @@
#include "gantt_status_source.hh"
#include "logfile_sub_source.hh"
#include "plain_text_source.hh"
#include "text_overlay_menu.hh"
#include "textview_curses.hh"
class gantt_source
@ -164,7 +165,7 @@ public:
exec_context* gs_exec_context;
};
class gantt_header_overlay : public list_overlay_source {
class gantt_header_overlay : public text_overlay_menu {
public:
explicit gantt_header_overlay(std::shared_ptr<gantt_source> src);

@ -43,6 +43,7 @@
#include "lnav_config.hh"
#include "shlex.hh"
#include "sql_util.hh"
#include "sqlitepp.client.hh"
#include "xterm_mouse.hh"
using namespace lnav::roles::literals;
@ -267,10 +268,22 @@ handle_paging_key(int ch)
if (xterm_mouse::is_available()) {
auto& mouse_i = injector::get<xterm_mouse&>();
mouse_i.set_enabled(!mouse_i.is_enabled());
auto um = lnav::console::user_message::ok(
attr_line_t("mouse mode -- ")
.append(mouse_i.is_enabled() ? "enabled"_symbol
: "disabled"_symbol));
auto al = attr_line_t("mouse mode -- ")
.append(mouse_i.is_enabled() ? "enabled"_symbol
: "disabled"_symbol);
if (mouse_i.is_enabled()
&& lnav_config.lc_mouse_mode == lnav_mouse_mode::disabled)
{
al.append(" -- enable permanently with ")
.append(":config /ui/mouse/mode enabled"_quoted_code);
auto clear_note = prepare_stmt(lnav_data.ld_db, R"(
DELETE FROM lnav_user_notifications WHERE id = 'org.lnav.mouse-support'
)");
clear_note.unwrap().execute();
}
auto um = lnav::console::user_message::ok(al);
lnav_data.ld_rl_view->set_attr_value(um.to_attr_line());
} else {
lnav_data.ld_rl_view->set_value(

@ -128,7 +128,7 @@ CREATE TABLE lnav_user_notifications
);
INSERT INTO lnav_user_notifications (id, priority, expiration, message)
VALUES ('org.lnav.breadcrumb.focus', -1, DATETIME('now', '+1 minute'),
VALUES ('org.lnav.breadcrumb.focus', -1, DATETIME('now', '+2 minute'),
'Press <span class="-lnav_status-styles_hotkey">`</span> to focus on the breadcrumb bar');
CREATE TABLE lnav_views_echo AS

@ -843,9 +843,12 @@ listview_curses::handle_mouse(mouse_event& me)
default:
break;
}
if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0) {
if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0
|| (me.me_press_x < (int) (width - 2)))
{
return false;
}
switch (this->lv_mouse_mode) {
case lv_mode_t::NONE: {
if (me.me_x < (int) (width - 2)) {
@ -1140,6 +1143,33 @@ listview_curses::shift_top(vis_line_t offset, bool suppress_flash)
return this->lv_top;
}
void
listview_curses::set_left(int left)
{
if (this->lv_left == left || left < 0) {
return;
}
if (left > this->lv_left) {
unsigned long width;
vis_line_t height;
this->get_dimensions(height, width);
if (this->lv_show_scrollbar) {
width -= 1;
}
if ((this->get_inner_width() - this->lv_left) <= width) {
alerter::singleton().chime(
"the maximum width of the view has been reached");
return;
}
}
this->lv_left = left;
this->invoke_scroll();
this->set_needs_update();
}
size_t
listview_curses::get_overlay_height(size_t total, vis_line_t view_height)
{

@ -364,31 +364,10 @@ public:
*
* @param left The new value for left.
*/
void set_left(unsigned int left)
{
if (this->lv_left == left) {
return;
}
if (left > this->lv_left) {
unsigned long width;
vis_line_t height;
this->get_dimensions(height, width);
if ((this->get_inner_width() - this->lv_left) <= width) {
alerter::singleton().chime(
"the maximum width of the view has been reached");
return;
}
}
this->lv_left = left;
this->invoke_scroll();
this->set_needs_update();
}
void set_left(int left);
/** @return The column number that is displayed at the left. */
unsigned int get_left() const { return this->lv_left; }
int get_left() const { return this->lv_left; }
/**
* Shift the value of left by the given value.
@ -396,7 +375,7 @@ public:
* @param offset The amount to change top by.
* @return The final value of top.
*/
unsigned int shift_left(int offset)
int shift_left(int offset)
{
if (this->lv_word_wrap) {
alerter::singleton().chime(
@ -526,7 +505,7 @@ public:
this->lv_title.c_str(),
this->vc_y,
(int) this->lv_top,
(int) this->lv_left,
this->lv_left,
this->lv_height,
(int) this->lv_selection,
(int) this->get_inner_height());
@ -582,7 +561,7 @@ protected:
action lv_scroll; /*< The scroll action. */
WINDOW* lv_window{nullptr}; /*< The window that contains this view. */
vis_line_t lv_top{0}; /*< The line at the top of the view. */
unsigned int lv_left{0}; /*< The column at the left of the view. */
int lv_left{0}; /*< The column at the left of the view. */
vis_line_t lv_height{0}; /*< The abs/rel height of the view. */
bool lv_overlay_focused{false};
vis_line_t lv_focused_overlay_top{0_vl};

@ -1134,6 +1134,27 @@ looper()
}
auto echo_views_stmt = echo_views_stmt_res.unwrap();
if (xterm_mouse::is_available()
&& lnav_config.lc_mouse_mode == lnav_mouse_mode::disabled)
{
auto mouse_note = prepare_stmt(lnav_data.ld_db, R"(
INSERT INTO lnav_user_notifications (id, priority, expiration, message)
VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'),
'Press <span class="-lnav_status-styles_hotkey">F2</span> to enable mouse support');
)");
if (mouse_note.isErr()) {
lnav::console::print(
stderr,
lnav::console::user_message::error(
"unable to prepare INSERT statement for "
"lnav_user_notifications table")
.with_reason(mouse_note.unwrapErr()));
return;
}
mouse_note.unwrap().execute();
}
(void) signal(SIGINT, sigint);
(void) signal(SIGTERM, sigint);
(void) signal(SIGWINCH, sigwinch);

@ -5799,7 +5799,7 @@ search_files_prompt(std::vector<std::string>& args)
lnav_data.ld_mode = ln_mode_t::SEARCH_FILES;
for (const auto& lf : lnav_data.ld_active_files.fc_files) {
auto path = lnav::pcre2pp::quote(lf->get_unique_path());
auto path = lnav::pcre2pp::quote(lf->get_unique_path().string());
lnav_data.ld_rl_view->add_possibility(
ln_mode_t::SEARCH_FILES, "*", path);
}

@ -846,7 +846,8 @@ logfile::rebuild_index(nonstd::optional<ui_clock::time_point> deadline)
}
this->lf_longest_line
= std::max(this->lf_longest_line, sbr.length());
= std::max(this->lf_longest_line,
li.li_utf8_scan_result.usr_column_width_guess);
this->lf_partial_line = li.li_partial;
sort_needed = this->process_prefix(sbr, li, sbc) || sort_needed;

@ -993,8 +993,8 @@ logfile_sub_source::rebuild_index(
if (lf == nullptr) {
continue;
}
this->lss_longest_line = std::max(this->lss_longest_line,
lf->get_longest_line_length());
this->lss_longest_line = std::max(
this->lss_longest_line, lf->get_longest_line_length() + 1);
this->lss_basename_width
= std::max(this->lss_basename_width,
lf->get_unique_path().native().size());
@ -3066,22 +3066,6 @@ logfile_sub_source::text_handle_mouse(
const listview_curses::display_line_content_t& mouse_line,
mouse_event& me)
{
auto* fos = dynamic_cast<field_overlay_source*>(tc.get_overlay_source());
if (mouse_line.is<listview_curses::overlay_menu>() && tc.tc_selected_text) {
auto& om = mouse_line.get<listview_curses::overlay_menu>();
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()) {
if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 2, 4)) {
this->list_input_handle_key(tc, ' ');

@ -32,41 +32,11 @@
#include "pcre2pp.hh"
#include "config.h"
#include "ww898/cp_utf8.hpp"
namespace lnav {
namespace pcre2pp {
std::string
quote(const char* unquoted)
{
std::string retval;
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('\\');
switch (unquoted[lpc]) {
case '\t':
retval.push_back('t');
break;
case '\n':
retval.push_back('n');
break;
default:
retval.push_back(unquoted[lpc]);
break;
}
}
}
return retval;
}
std::string
match_data::to_string() const
{

@ -46,14 +46,6 @@
namespace lnav {
namespace pcre2pp {
std::string quote(const char* unquoted);
inline std::string
quote(const std::string& unquoted)
{
return quote(unquoted.c_str());
}
class code;
struct capture_builder;
class matcher;

@ -0,0 +1,123 @@
/**
* Copyright (c) 2024, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "text_overlay_menu.hh"
#include "command_executor.hh"
#include "config.h"
#include "lnav.hh"
#include "md4cpp.hh"
#include "textview_curses.hh"
using namespace md4cpp::literals;
using namespace lnav::roles::literals;
std::vector<attr_line_t>
text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row)
{
const auto* tc = dynamic_cast<const textview_curses*>(&lv);
std::vector<attr_line_t> 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 title = " Filter Other "_status_title;
auto left = std::max(0, sti.sti_x - 2);
auto dim = lv.get_dimensions();
if (left + title.first.length() >= dim.second) {
left = dim.second - title.first.length() - 2;
}
this->tom_menu_items.clear();
retval.emplace_back(attr_line_t().pad_to(left).append(title));
{
attr_line_t al;
al.append(" ").append("\u2714 IN"_ok).append(" ");
int start = left;
this->tom_menu_items.emplace_back(
1_vl,
line_range{start, start + (int) al.length()},
[](const std::string& value) {
auto cmd = fmt::format(FMT_STRING(":filter-in {}"),
lnav::pcre2pp::quote(value));
lnav_data.ld_exec_context
.with_provenance(exec_context::mouse_input{})
->execute(cmd);
});
start += al.length();
al.append(":mag_right:"_emoji)
.append(" Search ")
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
this->tom_menu_items.emplace_back(
1_vl,
line_range{start, start + (int) al.length()},
[](const std::string& value) {
auto cmd = fmt::format(FMT_STRING("/{}"),
lnav::pcre2pp::quote(value));
lnav_data.ld_exec_context
.with_provenance(exec_context::mouse_input{})
->execute(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->tom_menu_items.emplace_back(
2_vl,
line_range{start, start + (int) al.length()},
[](const std::string& value) {
auto cmd = fmt::format(FMT_STRING(":filter-out {}"),
lnav::pcre2pp::quote(value));
lnav_data.ld_exec_context
.with_provenance(exec_context::mouse_input{})
->execute(cmd);
});
start += al.length();
al.append(":clipboard:"_emoji)
.append(" Copy ")
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
this->tom_menu_items.emplace_back(
2_vl,
line_range{start, start + (int) al.length()},
[](const std::string& value) {
lnav_data.ld_exec_context.execute("|lnav-copy-text");
});
retval.emplace_back(attr_line_t().pad_to(left).append(al));
}
}
}
return retval;
}

@ -0,0 +1,55 @@
/**
* Copyright (c) 2024, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef lnav_text_overlay_menu_hh
#define lnav_text_overlay_menu_hh
#include "listview_curses.hh"
class text_overlay_menu : public list_overlay_source {
public:
std::vector<attr_line_t> list_overlay_menu(const listview_curses& lv,
vis_line_t line) override;
struct menu_item {
menu_item(vis_line_t line,
line_range range,
std::function<void(const std::string&)> action)
: mi_line(line), mi_range(range), mi_action(std::move(action))
{
}
vis_line_t mi_line;
line_range mi_range;
std::function<void(const std::string&)> mi_action;
};
std::vector<menu_item> tom_menu_items;
};
#endif

@ -36,6 +36,7 @@
#include "filter_observer.hh"
#include "logfile.hh"
#include "plain_text_source.hh"
#include "text_overlay_menu.hh"
#include "textview_curses.hh"
class textfile_sub_source
@ -198,7 +199,7 @@ private:
int64_t tss_content_line{0};
};
class textfile_header_overlay : public list_overlay_source {
class textfile_header_overlay : public text_overlay_menu {
public:
explicit textfile_header_overlay(textfile_sub_source* src);

@ -43,6 +43,7 @@
#include "log_format_fwd.hh"
#include "logfile.hh"
#include "shlex.hh"
#include "text_overlay_menu.hh"
#include "view_curses.hh"
const auto REVERSE_SEARCH_OFFSET = 2000_vl;
@ -461,6 +462,7 @@ textview_curses::handle_mouse(mouse_event& me)
case mouse_button_state_t::BUTTON_STATE_PRESSED: {
this->tc_text_selection_active = true;
this->tc_press_line = mouse_line;
this->tc_press_left = this->lv_left + me.me_press_x;
if (!this->lv_selectable) {
this->set_selectable(true);
}
@ -523,10 +525,27 @@ textview_curses::handle_mouse(mouse_event& me)
}
auto tok = tok_res.value();
auto tok_sf = tok.inner_string_fragment();
auto tok_sf
= (tok.tr_token
== data_token_t::DT_QUOTED_STRING
&& (cursor_sf.sf_begin
== tok.to_string_fragment()
.sf_begin
|| cursor_sf.sf_begin
== tok.to_string_fragment()
.sf_end
- 1))
? tok.to_string_fragment()
: tok.inner_string_fragment();
if (tok_sf.contains(cursor_sf)
&& tok.tr_token != data_token_t::DT_WHITE)
{
auto group_tok
= ds.find_matching_bracket(tf, tok);
if (group_tok) {
tok_sf = group_tok.value()
.to_string_fragment();
}
this->tc_selected_text = selected_text_info{
me.me_x,
mc.mc_line,
@ -570,10 +589,10 @@ textview_curses::handle_mouse(mouse_event& me)
if (mouse_line.is<main_content>()) {
auto& mc = mouse_line.get<main_content>();
attr_line_t al;
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);
auto low_x = std::min(this->tc_press_left,
(int) this->lv_left + me.me_x);
auto high_x = std::max(this->tc_press_left,
(int) this->lv_left + me.me_x);
this->set_selection_without_context(mc.mc_line);
if (me.me_button == mouse_button_t::BUTTON_LEFT) {
@ -581,9 +600,14 @@ textview_curses::handle_mouse(mouse_event& me)
auto line_sf
= string_fragment::from_str(al.get_string());
auto cursor_sf = line_sf.sub_cell_range(low_x, high_x);
if (me.me_x <= 1) {
this->set_left(this->lv_left - 1);
} else if (me.me_x >= width - 1) {
this->set_left(this->lv_left + 1);
}
if (!cursor_sf.empty()) {
this->tc_selected_text = {
me.me_press_x,
me.me_x,
mc.mc_line,
line_range{
cursor_sf.sf_begin,
@ -626,6 +650,26 @@ textview_curses::handle_mouse(mouse_event& me)
break;
}
case mouse_button_state_t::BUTTON_STATE_RELEASED: {
auto* ov = this->get_overlay_source();
if (ov != nullptr && mouse_line.is<listview_curses::overlay_menu>()
&& this->tc_selected_text)
{
auto* tom = dynamic_cast<text_overlay_menu*>(ov);
if (tom != nullptr) {
auto& om = mouse_line.get<listview_curses::overlay_menu>();
auto& sti = this->tc_selected_text.value();
for (const auto& mi : tom->tom_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;
}
}
}
}
this->tc_text_selection_active = false;
if (me.is_click_in(mouse_button_t::BUTTON_RIGHT, 0, INT_MAX)) {
auto* lov = this->get_overlay_source();

@ -813,6 +813,7 @@ public:
nonstd::optional<selected_text_info> tc_selected_text;
bool tc_text_selection_active{false};
display_line_content_t tc_press_line;
int tc_press_left{0};
protected:
class grep_highlighter {

@ -43,7 +43,7 @@ SELECT message FROM lnav_user_notifications
(expiration IS NULL OR expiration > datetime('now')) AND
(views IS NULL OR
json_contains(views, (SELECT name FROM lnav_top_view)))
ORDER BY priority DESC
ORDER BY priority DESC, expiration ASC
LIMIT 1
)";

@ -154,7 +154,7 @@ 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);
&& lr.contains(this->me_press_x) && lr.contains(this->me_x);
}
bool
@ -264,6 +264,13 @@ view_curses::mvwattrline(WINDOW* window,
do {
expanded_line.push_back(' ');
char_index += 1;
if (char_index == lr_chars.lr_start) {
lr_bytes.lr_start = expanded_line.size();
}
if (char_index == lr_chars.lr_end) {
lr_bytes.lr_end = expanded_line.size();
retval.mr_chars_out = char_index;
}
} while (expanded_line.size() % 8);
utf_adjustments.emplace_back(
lpc, expanded_line.size() - exp_start_index - 1);

Loading…
Cancel
Save