[view_curses] support for selecting text in mouse mode

master
Tim Stack 1 month ago
parent 53ab7b14a6
commit 96765e3abc

@ -15,8 +15,13 @@ Features:
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;
- shift + clicking/dragging in the main view will highlight
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;
- 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
@ -31,6 +36,10 @@ Features:
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.
* Added a `selected_text` column to the `lnav_views` table that
reports information about text that was selected with a mouse.
This makes it possible to script operations that use the
selected text as an input.
Interface changes:
* The bar charts in the DB view have now been moved to their

@ -305,6 +305,11 @@
"title": "/ui/theme-defs/<theme_name>/styles/text",
"$ref": "#/definitions/style"
},
"selected-text": {
"description": "Styling for text selected in a view",
"title": "/ui/theme-defs/<theme_name>/styles/selected-text",
"$ref": "#/definitions/style"
},
"alt-text": {
"description": "Styling for plain text when alternating",
"title": "/ui/theme-defs/<theme_name>/styles/alt-text",

@ -144,8 +144,13 @@ 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;
* shift + clicking/dragging in the main view will highlight
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;
* 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

@ -387,6 +387,43 @@ string_fragment::codepoint_to_byte_index(ssize_t cp_index) const
return Ok(retval);
}
string_fragment
string_fragment::sub_cell_range(int cell_start, int cell_end) const
{
int byte_index = this->sf_begin;
nonstd::optional<int> byte_start;
nonstd::optional<int> byte_end;
int cell_index = 0;
while (byte_index < this->sf_end) {
if (cell_start == cell_index) {
byte_start = byte_index;
}
if (cell_index == cell_end) {
byte_end = byte_index;
}
auto read_res = ww898::utf::utf8::read(
[this, &byte_index]() { return this->sf_string[byte_index++]; });
if (read_res.isErr()) {
byte_index += 1;
} else {
cell_index += wcwidth(read_res.unwrap());
}
}
if (cell_start == cell_index) {
byte_start = byte_index;
}
if (!byte_end) {
byte_end = byte_index;
}
if (byte_start && byte_end) {
return this->sub_range(byte_start.value(), byte_end.value());
}
return string_fragment{};
}
size_t
string_fragment::column_width() const
{

@ -178,6 +178,8 @@ struct string_fragment {
Result<ssize_t, const char*> codepoint_to_byte_index(
ssize_t cp_index) const;
string_fragment sub_cell_range(int cell_start, int cell_end) const;
const char& operator[](int index) const
{
return this->sf_string[sf_begin + index];
@ -281,6 +283,12 @@ struct string_fragment {
this->sf_string, this->sf_begin + begin, this->sf_begin + end};
}
bool contains(const string_fragment& sf) const
{
return this->sf_string == sf.sf_string && this->sf_begin <= sf.sf_begin
&& sf.sf_end <= this->sf_end;
}
size_t count(char ch) const
{
size_t retval = 0;

@ -136,6 +136,7 @@ enum class role_t : int32_t {
VCR_TYPE,
VCR_SEP_REF_ACC,
VCR_SUGGESTION,
VCR_SELECTED_TEXT,
VCR__MAX
};

@ -434,9 +434,12 @@ filter_sub_source::rl_change(readline_curses* rc)
break;
case filter_lang_t::REGEX: {
if (new_value.empty()) {
if (fs.get_filter(top_view->get_current_search()) == nullptr) {
this->fss_editor->set_suggestion(
top_view->get_current_search());
auto sugg = top_view->get_current_search();
if (top_view->tc_selected_text) {
sugg = top_view->tc_selected_text->sti_value;
}
if (fs.get_filter(sugg) == nullptr) {
this->fss_editor->set_suggestion(sugg);
}
} else {
auto regex_res

@ -114,7 +114,7 @@
"alt-msg": "${keymap_def_alt_hour_boundary}"
},
"x63": {
"command": ":write-to /dev/clipboard",
"command": "|lnav-copy-text",
"alt-msg": "${keymap_def_clear}"
},
"x67": {

@ -2447,6 +2447,10 @@ com_filter_prompt(exec_context& ec, const std::string& cmdline)
return {};
}
if (tc->tc_selected_text) {
return {"", tc->tc_selected_text->sti_value};
}
return {"", tc->get_current_search()};
}
@ -5899,11 +5903,11 @@ com_prompt(exec_context& ec,
auto split_args_res = lexer.split(ec.create_resolver());
if (split_args_res.isErr()) {
auto split_err = split_args_res.unwrapErr();
auto um = lnav::console::user_message::error(
"unable to parse file name")
.with_reason(split_err.te_msg)
.with_snippet(lnav::console::snippet::from(
SRC, lexer.to_attr_line(split_err)));
auto um
= lnav::console::user_message::error("unable to parse prompt")
.with_reason(split_err.te_msg)
.with_snippet(lnav::console::snippet::from(
SRC, lexer.to_attr_line(split_err)));
return Err(um);
}

@ -630,6 +630,10 @@ static const struct json_path_container theme_styles_handlers = {
.with_description("Styling for plain text")
.for_child(&lnav_theme::lt_style_text)
.with_children(style_config_handlers),
yajlpp::property_handler("selected-text")
.with_description("Styling for text selected in a view")
.for_child(&lnav_theme::lt_style_selected_text)
.with_children(style_config_handlers),
yajlpp::property_handler("alt-text")
.with_description("Styling for plain text when alternating")
.for_child(&lnav_theme::lt_style_alt_text)

@ -194,7 +194,7 @@ rl_sql_help(readline_curses* rc)
{
auto al = attr_line_t(rc->get_line_buffer());
const auto& sa = al.get_attrs();
size_t x = rc->get_x();
size_t x = rc->get_cursor_x();
bool has_doc = false;
if (x > 0) {
@ -308,6 +308,12 @@ rl_change(readline_curses* rc)
lnav_data.ld_user_message_source.clear();
switch (lnav_data.ld_mode) {
case ln_mode_t::SEARCH: {
if (rc->get_line_buffer().empty() && tc->tc_selected_text) {
rc->set_suggestion(tc->tc_selected_text->sti_value);
}
break;
}
case ln_mode_t::SQL: {
static const auto* sql_cmd_map
= injector::get<readline_context::command_map_t*,
@ -563,7 +569,7 @@ rl_search_internal(readline_curses* rc, ln_mode_t mode, bool complete = false)
auto orig_prql_stmt = attr_line_t(term_val);
orig_prql_stmt.rtrim("| \r\n\t");
annotate_sql_statement(orig_prql_stmt);
auto cursor_x = rc->get_x();
auto cursor_x = rc->get_cursor_x();
if (cursor_x > orig_prql_stmt.get_string().length()) {
cursor_x = orig_prql_stmt.length() - 1;
}

@ -933,6 +933,8 @@ readline_curses::start()
maxfd = std::max(STDIN_FILENO, this->rc_command_pipe[RCF_SLAVE].get());
static uint64_t last_h1, last_h2;
while (looping) {
fd_set ready_rfds;
int rc;
@ -951,8 +953,6 @@ readline_curses::start()
}
} else {
if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
static uint64_t last_h1, last_h2;
struct itimerval itv;
itv.it_value.tv_sec = 0;
@ -960,7 +960,6 @@ readline_curses::start()
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &itv, nullptr);
rl_callback_read_char();
if (RL_ISSTATE(RL_STATE_DONE) && !got_line) {
got_line = 1;
@ -969,6 +968,7 @@ readline_curses::start()
} else {
uint64_t h1 = 1, h2 = 2;
rc_local_suggestion.clear();
if (rl_last_func == readline_context::command_complete) {
rl_last_func = rl_menu_complete;
}
@ -1304,7 +1304,6 @@ readline_curses::check_poll_set(const std::vector<struct pollfd>& pollfds)
if (rc > 0) {
int old_x = this->vc_cursor_x;
this->rc_suggestion.clear();
this->map_output(buffer, rc);
if (this->vc_cursor_x != old_x) {
this->rc_change(this);
@ -1389,7 +1388,7 @@ readline_curses::check_poll_set(const std::vector<struct pollfd>& pollfds)
this->rc_blur(this);
break;
case 'l':
case 'l': {
this->rc_line_buffer = &msg[2];
if (this->rc_active_context != -1) {
this->rc_suggestion.clear();
@ -1400,6 +1399,7 @@ readline_curses::check_poll_set(const std::vector<struct pollfd>& pollfds)
this->rc_display_match(this);
}
break;
}
case 'c':
this->rc_line_buffer = &msg[2];
@ -1496,6 +1496,7 @@ readline_curses::set_suggestion(const std::string& value)
perror("set_suggestion: write failed");
}
this->rc_suggestion = value;
this->set_needs_update();
}
void
@ -1653,7 +1654,7 @@ readline_curses::do_update()
this->vc_x,
this->rc_value,
lr);
this->set_x(0);
this->set_cursor_x(0);
}
if (this->rc_active_context != -1) {

@ -0,0 +1,15 @@
#
# @synopsis: lnav-copy-text
# @description: Copy text from the top view
#
;SELECT jget(selected_text, '/value') AS content FROM lnav_top_view
;SELECT CASE
WHEN $content IS NULL THEN
':write-to -'
ELSE
':echo -n ${content}'
END AS cmd
:redirect-to /dev/clipboard
:eval ${cmd}

@ -3,6 +3,7 @@ BUILTIN_LNAVSCRIPTS = \
$(srcdir)/scripts/dhclient-summary.lnav \
$(srcdir)/scripts/docker-url-handler.lnav \
$(srcdir)/scripts/journald-url-handler.lnav \
$(srcdir)/scripts/lnav-copy-text.lnav \
$(srcdir)/scripts/lnav-pop-view.lnav \
$(srcdir)/scripts/partition-by-boot.lnav \
$(srcdir)/scripts/piper-url-handler.lnav \

@ -148,6 +148,7 @@ struct lnav_theme {
positioned_property<style_config> lt_style_type;
positioned_property<style_config> lt_style_sep_ref_acc;
positioned_property<style_config> lt_style_suggestion;
positioned_property<style_config> lt_style_selected_text;
positioned_property<style_config> lt_style_re_special;
positioned_property<style_config> lt_style_re_repeat;
positioned_property<style_config> lt_style_diff_delete;

@ -37,6 +37,7 @@
#include "base/injector.hh"
#include "base/time_util.hh"
#include "config.h"
#include "data_scanner.hh"
#include "fmt/format.h"
#include "lnav_config.hh"
#include "log_format_fwd.hh"
@ -284,9 +285,21 @@ textview_curses::reload_config(error_reporter& reporter)
}
}
void
textview_curses::invoke_scroll()
{
this->tc_selected_text = nonstd::nullopt;
if (this->tc_sub_source != nullptr) {
this->tc_sub_source->scroll_invoked(this);
}
listview_curses::invoke_scroll();
}
void
textview_curses::reload_data()
{
this->tc_selected_text = nonstd::nullopt;
if (this->tc_sub_source != nullptr) {
this->tc_sub_source->text_update_marks(this->tc_bookmarks);
}
@ -415,6 +428,13 @@ textview_curses::handle_mouse(mouse_event& me)
auto* sub_delegate = dynamic_cast<text_delegate*>(this->tc_sub_source);
if (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();
}
switch (me.me_state) {
case mouse_button_state_t::BUTTON_STATE_PRESSED: {
if (!this->lv_selectable) {
@ -456,7 +476,36 @@ textview_curses::handle_mouse(mouse_event& me)
[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);
attr_line_t al;
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 ds = data_scanner(line_sf);
auto tf = this->tc_sub_source->get_text_format();
while (true) {
auto tok_res = ds.tokenize2(tf);
if (!tok_res) {
break;
}
auto tok = tok_res.value();
auto tok_sf = tok.to_string_fragment();
if (tok_sf.contains(cursor_sf)) {
this->tc_selected_text = selected_text_info{
mc.mc_line,
line_range{
tok_sf.sf_begin,
tok_sf.sf_end,
},
tok_sf.to_string(),
};
this->set_needs_update();
break;
}
}
}
this->set_selection_without_context(mc.mc_line);
}
@ -478,6 +527,27 @@ textview_curses::handle_mouse(mouse_event& me)
}
case mouse_button_state_t::BUTTON_STATE_DRAGGED: {
if (!this->vc_enabled) {
} else if (me.me_y == me.me_press_y) {
if (mouse_line.is<main_content>()) {
auto& mc = mouse_line.get<main_content>();
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);
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 = {
mc.mc_line,
line_range{
cursor_sf.sf_begin,
cursor_sf.sf_end,
},
cursor_sf.to_string(),
};
}
}
} else if (me.me_y < 0) {
this->shift_selection(listview_curses::shift_amount_t::up_line);
mouse_line = main_content{this->get_top()};
@ -615,6 +685,14 @@ textview_curses::textview_value_for_row(vis_line_t row, attr_line_t& value_out)
sa.emplace_back(line_range{orig_line.lr_start, -1},
VC_STYLE.value(text_attrs{A_REVERSE}));
}
if (this->tc_selected_text) {
const auto& sti = this->tc_selected_text.value();
if (sti.sti_line == row) {
sa.emplace_back(sti.sti_range,
VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
}
}
}
void

@ -783,14 +783,7 @@ public:
void revert_search() { this->execute_search(this->tc_previous_search); }
void invoke_scroll()
{
if (this->tc_sub_source != nullptr) {
this->tc_sub_source->scroll_invoked(this);
}
listview_curses::invoke_scroll();
}
void invoke_scroll();
textview_curses& set_reload_config_delegate(
std::function<void(textview_curses&)> func)
@ -807,6 +800,14 @@ public:
nonstd::optional<role_t> tc_cursor_role;
nonstd::optional<role_t> tc_disabled_cursor_role;
struct selected_text_info {
int64_t sti_line;
line_range sti_range;
std::string sti_value;
};
nonstd::optional<selected_text_info> tc_selected_text;
protected:
class grep_highlighter {
public:

@ -11,6 +11,9 @@
"color": "Silver",
"background-color": "Black"
},
"selected-text": {
"background-color": "DarkCyan"
},
"identifier": {
"background-color": "",
"color": "semantic()"

@ -25,6 +25,9 @@
"color": "#f6f6f6",
"background-color": "$black"
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"background-color": "#1c1c1c"
},

@ -24,6 +24,9 @@
"color": "$white",
"background-color": ""
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"bold": true
},

@ -23,6 +23,9 @@
"color": "#f6f6f6",
"background-color": "$black"
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"background-color": "#1c1c1c"
},

@ -23,6 +23,9 @@
"color": "#d6deeb",
"background-color": "#011627"
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"background-color": "#1c1c1c"
},

@ -32,6 +32,9 @@
"color": "$base0",
"background-color": "$base03"
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"background-color": "$base02"
},

@ -31,6 +31,9 @@
"color": "$base00",
"background-color": "$base3"
},
"selected-text": {
"background-color": "$cyan"
},
"alt-text": {
"background-color": "$base2"
},

@ -236,7 +236,7 @@ view_curses::mvwattrline(WINDOW* window,
role_t base_role)
{
auto& sa = al.get_attrs();
auto& line = al.get_string();
const auto& line = al.get_string();
std::vector<utf_to_display_adjustment> utf_adjustments;
std::string full_line;
@ -464,6 +464,12 @@ view_curses::mvwattrline(WINDOW* window,
} else if (iter->sa_type == &VC_ROLE) {
auto role = iter->sa_value.get<role_t>();
attrs = vc.attrs_for_role(role);
if (role == role_t::VCR_SELECTED_TEXT) {
retval.mr_selected_text
= string_fragment::from_str(line).sub_range(
iter->sa_range.lr_start, iter->sa_range.lr_end);
}
} else if (iter->sa_type == &VC_ROLE_FG) {
auto role_attrs
= vc.attrs_for_role(iter->sa_value.get<role_t>());
@ -1138,6 +1144,8 @@ view_colors::init_roles(const lnav_theme& lt,
= this->to_attrs(lt, lt.lt_style_sep_ref_acc, reporter);
this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SUGGESTION)]
= this->to_attrs(lt, lt.lt_style_suggestion, reporter);
this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SELECTED_TEXT)]
= this->to_attrs(lt, lt.lt_style_selected_text, reporter);
this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_RE_SPECIAL)]
= this->to_attrs(lt, lt.lt_style_re_special, reporter);

@ -451,6 +451,7 @@ public:
struct mvwattrline_result {
size_t mr_chars_out{0};
size_t mr_bytes_remaining{0};
string_fragment mr_selected_text;
};
static mvwattrline_result mvwattrline(WINDOW* window,

@ -1529,6 +1529,8 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y)
auto* tc = *(lnav_data.ld_view_stack.top());
if (tc->contains(me.me_x, me.me_y)) {
me.me_press_y = me.me_y - tc->get_y();
me.me_press_x = me.me_x - tc->get_x();
this->lb_last_view = tc;
} else {
for (auto* vc : VIEWS) {

@ -175,6 +175,22 @@ static const typed_json_path_container<top_line_meta> top_line_meta_handlers = {
.with_children(breadcrumb_crumb_handlers),
};
static const typed_json_path_container<line_range> line_range_handlers = {
yajlpp::property_handler("start").for_field(&line_range::lr_start),
yajlpp::property_handler("end").for_field(&line_range::lr_end),
};
static const typed_json_path_container<textview_curses::selected_text_info>
selected_text_handlers = {
yajlpp::property_handler("line").for_field(
&textview_curses::selected_text_info::sti_line),
yajlpp::property_handler("range")
.for_child(&textview_curses::selected_text_info::sti_range)
.with_children(line_range_handlers),
yajlpp::property_handler("value").for_field(
&textview_curses::selected_text_info::sti_value),
};
enum class row_details_t {
hide,
show,
@ -259,7 +275,8 @@ CREATE TABLE lnav_views (
movement TEXT, -- The movement mode, either 'top' or 'cursor'.
top_meta TEXT, -- A JSON object that contains metadata related to the top line in the view.
selection INTEGER, -- The number of the line that is focused for selection.
options TEXT -- A JSON object that contains optional settings for this view.
options TEXT, -- A JSON object that contains optional settings for this view.
selected_text TEXT -- A JSON object that contains information about the text selected by the mouse in the view.
);
)";
@ -456,6 +473,16 @@ CREATE TABLE lnav_views (
}
break;
}
case 14: {
if (tc.tc_selected_text) {
to_sqlite(ctx,
selected_text_handlers.to_json_string(
tc.tc_selected_text.value()));
} else {
sqlite3_result_null(ctx);
}
break;
}
}
return SQLITE_OK;
@ -490,7 +517,8 @@ CREATE TABLE lnav_views (
string_fragment movement,
const char* top_meta,
int64_t selection,
nonstd::optional<string_fragment> options)
nonstd::optional<string_fragment> options,
nonstd::optional<string_fragment> selected_text)
{
auto& tc = lnav_data.ld_views[index];
auto* time_source

@ -146,6 +146,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "DarkCyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "#262626",
@ -742,6 +748,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "#1c1c1c",
@ -1301,6 +1313,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "",
@ -1859,6 +1877,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "",
@ -2418,6 +2442,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "#1c1c1c",
@ -2976,6 +3006,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "#1c1c1c",
@ -3543,6 +3579,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "$base02",
@ -4110,6 +4152,12 @@
"underline": false,
"bold": false
},
"selected-text": {
"color": "",
"background-color": "$cyan",
"underline": false,
"bold": false
},
"alt-text": {
"color": "",
"background-color": "$base2",
@ -4847,7 +4895,7 @@
"alt-msg": ""
},
"x63": {
"command": ":write-to /dev/clipboard",
"command": "|lnav-copy-text",
"alt-msg": "${keymap_def_clear}"
},
"x65": {

Loading…
Cancel
Save