[textview] some more support for handling hyperlinks

pull/1265/head
Tim Stack 1 month ago
parent 0697009b16
commit 9a1f383ce1

@ -9,6 +9,11 @@ Features:
don't have one. Setting an opid allows messages to show don't have one. Setting an opid allows messages to show
up in the Gantt chart view. up in the Gantt chart view.
* Add support for GitHub Markdown Alerts. * Add support for GitHub Markdown Alerts.
* Added the `:xopen` command that will open the given paths
using an external opener like `open` or `xdg-open`.
* Clicking on a link in a markdown file will open the Actions
with options for opening the link target in lnav, opening the
target with `:xopen`, or copying the link to a clipboard.
Interface Changes: Interface Changes:
* In the Gantt chart view, pressing `ENTER` will focus on * In the Gantt chart view, pressing `ENTER` will focus on

@ -184,7 +184,7 @@
"properties": { "properties": {
"test": { "test": {
"title": "/tuning/clipboard/impls/<clipboard_impl_name>/test", "title": "/tuning/clipboard/impls/<clipboard_impl_name>/test",
"description": "The command that checks", "description": "The command that checks if a clipboard command is available",
"type": "string", "type": "string",
"examples": [ "examples": [
"command -v pbcopy" "command -v pbcopy"
@ -209,6 +209,46 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"external-opener": {
"description": "Settings related to opening external files/URLs",
"title": "/tuning/external-opener",
"type": "object",
"properties": {
"impls": {
"description": "External opener implementations",
"title": "/tuning/external-opener/impls",
"type": "object",
"patternProperties": {
"^([\\w\\-]+)$": {
"description": "External opener implementation",
"title": "/tuning/external-opener/impls/<opener_impl_name>",
"type": "object",
"properties": {
"test": {
"title": "/tuning/external-opener/impls/<opener_impl_name>/test",
"description": "The command that checks if an external opener is available",
"type": "string",
"examples": [
"command -v open"
]
},
"command": {
"title": "/tuning/external-opener/impls/<opener_impl_name>/command",
"description": "The command used to open a file or URL",
"type": "string",
"examples": [
"open"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"url-scheme": { "url-scheme": {
"description": "Settings related to custom URL handling", "description": "Settings related to custom URL handling",
"title": "/tuning/url-scheme", "title": "/tuning/url-scheme",

@ -417,6 +417,7 @@ add_library(
elem_to_json.cc elem_to_json.cc
environ_vtab.cc environ_vtab.cc
extension-functions.cc extension-functions.cc
external_opener.cc
field_overlay_source.cc field_overlay_source.cc
file_collection.cc file_collection.cc
file_converter_manager.cc file_converter_manager.cc
@ -492,6 +493,7 @@ add_library(
styling.cc styling.cc
text_anonymizer.cc text_anonymizer.cc
text_format.cc text_format.cc
text_link_handler.cc
text_overlay_menu.cc text_overlay_menu.cc
textfile_highlighters.cc textfile_highlighters.cc
textfile_sub_source.cc textfile_sub_source.cc
@ -533,6 +535,8 @@ add_library(
doc_status_source.hh doc_status_source.hh
dump_internals.hh dump_internals.hh
elem_to_json.hh elem_to_json.hh
external_opener.hh
external_opener.cfg.hh
field_overlay_source.hh field_overlay_source.hh
file_collection.hh file_collection.hh
file_converter_manager.hh file_converter_manager.hh
@ -618,6 +622,7 @@ add_library(
termios_guard.hh termios_guard.hh
text_anonymizer.hh text_anonymizer.hh
text_format.hh text_format.hh
text_link_handler.hh
text_overlay_menu.hh text_overlay_menu.hh
textfile_highlighters.hh textfile_highlighters.hh
textfile_sub_source.hh textfile_sub_source.hh

@ -237,6 +237,8 @@ noinst_HEADERS = \
dump_internals.hh \ dump_internals.hh \
elem_to_json.hh \ elem_to_json.hh \
environ_vtab.hh \ environ_vtab.hh \
external_opener.hh \
external_opener.cfg.hh \
field_overlay_source.hh \ field_overlay_source.hh \
file_collection.hh \ file_collection.hh \
file_converter_manager.hh \ file_converter_manager.hh \
@ -348,6 +350,7 @@ noinst_HEADERS = \
term_extra.hh \ term_extra.hh \
text_anonymizer.hh \ text_anonymizer.hh \
text_format.hh \ text_format.hh \
text_link_handler.hh \
text_overlay_menu.hh \ text_overlay_menu.hh \
textfile_highlighters.hh \ textfile_highlighters.hh \
textfile_sub_source.hh \ textfile_sub_source.hh \
@ -436,6 +439,7 @@ libdiag_a_SOURCES = \
elem_to_json.cc \ elem_to_json.cc \
environ_vtab.cc \ environ_vtab.cc \
extension-functions.cc \ extension-functions.cc \
external_opener.cc \
field_overlay_source.cc \ field_overlay_source.cc \
file_collection.cc \ file_collection.cc \
file_converter_manager.cc \ file_converter_manager.cc \
@ -506,6 +510,7 @@ libdiag_a_SOURCES = \
styling.cc \ styling.cc \
text_anonymizer.cc \ text_anonymizer.cc \
text_format.cc \ text_format.cc \
text_link_handler.cc \
text_overlay_menu.cc \ text_overlay_menu.cc \
textfile_sub_source.cc \ textfile_sub_source.cc \
timer.cc \ timer.cc \

@ -703,9 +703,8 @@ find_string_attr(const string_attrs_t& sa,
const string_attr_type_base* type, const string_attr_type_base* type,
int start) int start)
{ {
string_attrs_t::const_iterator iter; auto iter = sa.begin();
for (; iter != sa.end(); ++iter) {
for (iter = sa.begin(); iter != sa.end(); ++iter) {
if (iter->sa_type == type && iter->sa_range.lr_start >= start) { if (iter->sa_type == type && iter->sa_range.lr_start >= start) {
break; break;
} }

@ -79,12 +79,18 @@ auto_fd::openpt(int flags)
return Ok(auto_fd{rc}); return Ok(auto_fd{rc});
} }
auto_fd::auto_fd(int fd) : af_fd(fd) auto_fd::
auto_fd(int fd)
: af_fd(fd)
{ {
require(fd >= -1); require(fd >= -1);
} }
auto_fd::auto_fd(auto_fd&& af) noexcept : af_fd(af.release()) {} auto_fd::
auto_fd(auto_fd&& af) noexcept
: af_fd(af.release())
{
}
auto_fd auto_fd
auto_fd::dup() const auto_fd::dup() const
@ -98,11 +104,18 @@ auto_fd::dup() const
return auto_fd{new_fd}; return auto_fd{new_fd};
} }
auto_fd::~auto_fd() auto_fd::~
auto_fd()
{ {
this->reset(); this->reset();
} }
void
auto_fd::copy_to(int fd) const
{
dup2(this->get(), fd);
}
void void
auto_fd::reset(int fd) auto_fd::reset(int fd)
{ {
@ -184,7 +197,8 @@ auto_pipe::for_child_fd(int child_fd)
return Ok(std::move(retval)); return Ok(std::move(retval));
} }
auto_pipe::auto_pipe(int child_fd, int child_flags) auto_pipe::
auto_pipe(int child_fd, int child_flags)
: ap_child_flags(child_flags), ap_child_fd(child_fd) : ap_child_flags(child_flags), ap_child_fd(child_fd)
{ {
switch (child_fd) { switch (child_fd) {

@ -146,6 +146,8 @@ public:
return retval; return retval;
} }
void copy_to(int fd) const;
/** /**
* @return The file descriptor. * @return The file descriptor.
*/ */

@ -27,6 +27,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
#include <filesystem>
#include <iostream> #include <iostream>
#include "base/fs_util.hh" #include "base/fs_util.hh"

@ -0,0 +1,99 @@
/**
* 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 "external_opener.hh"
#include <fcntl.h>
#include <unistd.h>
#include "base/auto_pid.hh"
#include "base/fs_util.hh"
#include "base/injector.hh"
#include "external_opener.cfg.hh"
#include "fmt/format.h"
namespace lnav::external_opener {
static std::optional<impl>
get_impl()
{
const auto& cfg = injector::get<const config&>();
for (const auto& pair : cfg.c_impls) {
const auto full_cmd = fmt::format(FMT_STRING("{} > /dev/null 2>&1"),
pair.second.i_test_command);
log_debug("testing opener impl %s using: %s",
pair.first.c_str(),
full_cmd.c_str());
if (system(full_cmd.c_str()) == 0) {
log_info("detected opener: %s", pair.first.c_str());
return pair.second;
}
}
return std::nullopt;
}
Result<void, std::string>
for_href(const std::string& href)
{
static const auto IMPL = get_impl();
if (!IMPL) {
const static std::string MSG = "no external opener found";
return Err(MSG);
}
auto child_pid_res = lnav::pid::from_fork();
if (child_pid_res.isErr()) {
return Err(child_pid_res.unwrapErr());
}
auto child_pid = child_pid_res.unwrap();
if (child_pid.in_child()) {
auto open_res
= lnav::filesystem::open_file("/dev/null", O_RDONLY | O_CLOEXEC);
open_res.then([](auto_fd&& fd) {
fd.copy_to(STDIN_FILENO);
fd.copy_to(STDOUT_FILENO);
});
execlp(IMPL->i_command.c_str(),
IMPL->i_command.c_str(),
href.c_str(),
nullptr);
_exit(EXIT_FAILURE);
}
return Ok();
}
} // namespace lnav::external_opener

@ -0,0 +1,49 @@
/**
* 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_external_opener_cfg_hh
#define lnav_external_opener_cfg_hh
#include <map>
#include <string>
namespace lnav::external_opener {
struct impl {
std::string i_test_command;
std::string i_command;
};
struct config {
std::map<std::string, impl> c_impls;
};
} // namespace lnav::external_opener
#endif

@ -0,0 +1,43 @@
/**
* 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_external_opener_hh
#define lnav_external_opener_hh
#include <string>
#include "base/result.h"
namespace lnav::external_opener {
Result<void, std::string> for_href(const std::string& href);
}
#endif

@ -135,7 +135,8 @@ file_collection::close_files(const std::vector<std::shared_ptr<logfile>>& files)
} else { } else {
this->fc_file_names.erase(lf->get_filename()); this->fc_file_names.erase(lf->get_filename());
} }
auto file_iter = find(this->fc_files.begin(), this->fc_files.end(), lf); auto file_iter
= std::find(this->fc_files.begin(), this->fc_files.end(), lf);
if (file_iter != this->fc_files.end()) { if (file_iter != this->fc_files.end()) {
this->fc_files.erase(file_iter); this->fc_files.erase(file_iter);
} }
@ -220,6 +221,9 @@ file_collection::merge(file_collection& other)
errs->insert(new_errors.begin(), new_errors.end()); errs->insert(new_errors.begin(), new_errors.end());
} }
if (!other.fc_file_names.empty()) {
this->fc_files_generation += 1;
}
for (const auto& fn_pair : other.fc_file_names) { for (const auto& fn_pair : other.fc_file_names) {
this->fc_file_names[fn_pair.first] = fn_pair.second; this->fc_file_names[fn_pair.first] = fn_pair.second;
} }

@ -1728,6 +1728,27 @@
---- ----
.. _xopen:
:xopen *path*
^^^^^^^^^^^^^
Use an external command to open the given file(s)
**Parameters**
* **path** --- The path to the file to open
**Examples**
To open the file '/path/to/file':
.. code-block:: lnav
:xopen /path/to/file
----
.. _zoom_to: .. _zoom_to:
:zoom-to *zoom-level* :zoom-to *zoom-level*

@ -58,6 +58,7 @@
#include "curl_looper.hh" #include "curl_looper.hh"
#include "date/tz.h" #include "date/tz.h"
#include "db_sub_source.hh" #include "db_sub_source.hh"
#include "external_opener.hh"
#include "field_overlay_source.hh" #include "field_overlay_source.hh"
#include "fmt/printf.h" #include "fmt/printf.h"
#include "hasher.hh" #include "hasher.hh"
@ -3071,6 +3072,42 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
struct stat st; struct stat st;
size_t url_index; size_t url_index;
#ifdef HAVE_LIBCURL
if (startswith(fn, "file:")) {
auto* cu = curl_url();
auto set_rc = curl_url_set(cu, CURLUPART_URL, fn.c_str(), 0);
if (set_rc != CURLUE_OK) {
return Err(lnav::console::user_message::error(
attr_line_t("invalid URL: ")
.append(lnav::roles::file(fn)))
.with_reason(curl_url_strerror(set_rc)));
}
char* path_part;
auto get_rc = curl_url_get(cu, CURLUPART_PATH, &path_part, 0);
if (get_rc != CURLUE_OK) {
return Err(lnav::console::user_message::error(
attr_line_t("cannot get path from URL: ")
.append(lnav::roles::file(fn)))
.with_reason(curl_url_strerror(get_rc)));
}
char* frag_part = nullptr;
get_rc = curl_url_get(cu, CURLUPART_FRAGMENT, &frag_part, 0);
if (get_rc != CURLUE_OK && get_rc != CURLUE_NO_FRAGMENT) {
return Err(lnav::console::user_message::error(
attr_line_t("cannot get fragment from URL: ")
.append(lnav::roles::file(fn)))
.with_reason(curl_url_strerror(get_rc)));
}
if (frag_part != nullptr && frag_part[0]) {
fn = fmt::format(FMT_STRING("{}#{}"), path_part, frag_part);
} else {
fn = path_part;
}
}
#endif
if (is_url(fn.c_str())) { if (is_url(fn.c_str())) {
#ifndef HAVE_LIBCURL #ifndef HAVE_LIBCURL
retval = "error: lnav was not compiled with libcurl"; retval = "error: lnav was not compiled with libcurl";
@ -3439,6 +3476,60 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
return Ok(retval); return Ok(retval);
} }
static Result<std::string, lnav::console::user_message>
com_xopen(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
{
static const intern_string_t SRC = intern_string::lookup("path");
std::string retval;
if (args.empty()) {
args.emplace_back("filename");
return Ok(std::string());
}
if (lnav_data.ld_flags & LNF_SECURE_MODE) {
return ec.make_error("{} -- unavailable in secure mode", args[0]);
}
if (args.size() < 2) {
return ec.make_error("expecting file name to open");
}
std::vector<std::string> word_exp;
std::string pat;
file_collection fc;
pat = trim(remaining_args(cmdline, args));
shlex lexer(pat);
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 names")
.with_reason(split_err.te_msg)
.with_snippet(lnav::console::snippet::from(
SRC, lexer.to_attr_line(split_err)));
return Err(um);
}
auto split_args = split_args_res.unwrap()
| lnav::itertools::map([](const auto& elem) { return elem.se_value; });
for (auto fn : split_args) {
auto open_res = lnav::external_opener::for_href(fn);
if (open_res.isErr()) {
auto um = lnav::console::user_message::error(
attr_line_t("Unable to open file: ")
.append(lnav::roles::file(fn)))
.with_reason(open_res.unwrapErr());
return Err(um);
}
}
return Ok(retval);
}
static Result<std::string, lnav::console::user_message> static Result<std::string, lnav::console::user_message>
com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args) com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
{ {
@ -6612,6 +6703,14 @@ readline_context::command_t STD_COMMANDS[] = {
.with_example({"To open the file '/path/to/file'", "/path/to/file"}) .with_example({"To open the file '/path/to/file'", "/path/to/file"})
.with_example({"To open the remote file '/var/log/syslog.log'", .with_example({"To open the remote file '/var/log/syslog.log'",
"dean@host1.example.com:/var/log/syslog.log"})}, "dean@host1.example.com:/var/log/syslog.log"})},
{"xopen",
com_xopen,
help_text(":xopen")
.with_summary("Use an external command to open the given file(s)")
.with_parameter(
help_text{"path", "The path to the file to open"}.one_or_more())
.with_example({"To open the file '/path/to/file'", "/path/to/file"})},
{"hide-file", {"hide-file",
com_hide_file, com_hide_file,

@ -101,6 +101,9 @@ static auto tc = injector::bind<tailer::config>::to_instance(
static auto scc = injector::bind<sysclip::config>::to_instance( static auto scc = injector::bind<sysclip::config>::to_instance(
+[]() { return &lnav_config.lc_sysclip; }); +[]() { return &lnav_config.lc_sysclip; });
static auto oc = injector::bind<lnav::external_opener::config>::to_instance(
+[]() { return &lnav_config.lc_opener; });
static auto uh = injector::bind<lnav::url_handler::config>::to_instance( static auto uh = injector::bind<lnav::url_handler::config>::to_instance(
+[]() { return &lnav_config.lc_url_handlers; }); +[]() { return &lnav_config.lc_url_handlers; });
@ -1347,7 +1350,8 @@ static const struct json_path_container sysclip_impl_cmd_handlers = json_path_co
static const struct json_path_container sysclip_impl_handlers = { static const struct json_path_container sysclip_impl_handlers = {
yajlpp::property_handler("test") yajlpp::property_handler("test")
.with_synopsis("<command>") .with_synopsis("<command>")
.with_description("The command that checks") .with_description(
"The command that checks if a clipboard command is available")
.with_example("command -v pbcopy") .with_example("command -v pbcopy")
.for_field(&sysclip::clipboard::c_test_command), .for_field(&sysclip::clipboard::c_test_command),
yajlpp::property_handler("general") yajlpp::property_handler("general")
@ -1386,6 +1390,44 @@ static const struct json_path_container sysclip_handlers = {
.with_children(sysclip_impls_handlers), .with_children(sysclip_impls_handlers),
}; };
static const json_path_container opener_impl_handlers = {
yajlpp::property_handler("test")
.with_synopsis("<command>")
.with_description(
"The command that checks if an external opener is available")
.with_example("command -v open")
.for_field(&lnav::external_opener::impl::i_test_command),
yajlpp::property_handler("command")
.with_description("The command used to open a file or URL")
.with_example("open")
.for_field(&lnav::external_opener::impl::i_command),
};
static const json_path_container opener_impls_handlers = {
yajlpp::pattern_property_handler("(?<opener_impl_name>[\\w\\-]+)")
.with_synopsis("<name>")
.with_description("External opener implementation")
.with_obj_provider<lnav::external_opener::impl, _lnav_config>(
[](const yajlpp_provider_context& ypc, _lnav_config* root) {
auto& retval = root->lc_opener
.c_impls[ypc.get_substr("opener_impl_name")];
return &retval;
})
.with_path_provider<_lnav_config>(
[](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
for (const auto& iter : cfg->lc_opener.c_impls) {
paths_out.emplace_back(iter.first);
}
})
.with_children(opener_impl_handlers),
};
static const struct json_path_container opener_handlers = {
yajlpp::property_handler("impls")
.with_description("External opener implementations")
.with_children(opener_impls_handlers),
};
static const struct json_path_container log_source_watch_expr_handlers = { static const struct json_path_container log_source_watch_expr_handlers = {
yajlpp::property_handler("expr") yajlpp::property_handler("expr")
.with_synopsis("<SQL-expression>") .with_synopsis("<SQL-expression>")
@ -1531,6 +1573,9 @@ static const struct json_path_container tuning_handlers = {
yajlpp::property_handler("clipboard") yajlpp::property_handler("clipboard")
.with_description("Settings related to the clipboard") .with_description("Settings related to the clipboard")
.with_children(sysclip_handlers), .with_children(sysclip_handlers),
yajlpp::property_handler("external-opener")
.with_description("Settings related to opening external files/URLs")
.with_children(opener_handlers),
yajlpp::property_handler("url-scheme") yajlpp::property_handler("url-scheme")
.with_description("Settings related to custom URL handling") .with_description("Settings related to custom URL handling")
.with_children(url_handlers), .with_children(url_handlers),

@ -43,6 +43,7 @@
#include "base/file_range.hh" #include "base/file_range.hh"
#include "base/lnav.console.hh" #include "base/lnav.console.hh"
#include "base/result.h" #include "base/result.h"
#include "external_opener.cfg.hh"
#include "file_vtab.cfg.hh" #include "file_vtab.cfg.hh"
#include "ghc/filesystem.hpp" #include "ghc/filesystem.hpp"
#include "lnav_config_fwd.hh" #include "lnav_config_fwd.hh"
@ -128,6 +129,7 @@ struct _lnav_config {
lnav::url_handler::config lc_url_handlers; lnav::url_handler::config lc_url_handlers;
logfile_sub_source_ns::config lc_log_source; logfile_sub_source_ns::config lc_log_source;
lnav::log::annotate::config lc_log_annotations; lnav::log::annotate::config lc_log_annotations;
lnav::external_opener::config lc_opener;
}; };
extern struct _lnav_config lnav_config; extern struct _lnav_config lnav_config;

@ -579,11 +579,11 @@ md2attr_line::leave_span(const md4cpp::event_handler::span& sp)
static_cast<int>(this->ml_span_starts.back()), static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()), static_cast<int>(last_block.length()),
}; };
auto abs_href = this->append_url_footnote(href_str);
last_block.with_attr({ last_block.with_attr({
lr, lr,
VC_HYPERLINK.value(href_str), VC_HYPERLINK.value(abs_href),
}); });
this->append_url_footnote(href_str);
} else if (sp.is<MD_SPAN_IMG_DETAIL*>()) { } else if (sp.is<MD_SPAN_IMG_DETAIL*>()) {
const auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>(); const auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>();
const auto src_str const auto src_str
@ -1052,7 +1052,7 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
return Ok(); return Ok();
} }
void std::string
md2attr_line::append_url_footnote(std::string href_str) md2attr_line::append_url_footnote(std::string href_str)
{ {
auto is_internal = startswith(href_str, "#"); auto is_internal = startswith(href_str, "#");
@ -1065,7 +1065,7 @@ md2attr_line::append_url_footnote(std::string href_str)
VC_STYLE.value(text_attrs{A_UNDERLINE}), VC_STYLE.value(text_attrs{A_UNDERLINE}),
}); });
if (is_internal) { if (is_internal) {
return; return href_str;
} }
if (this->ml_last_superscript_index == last_block.length()) { if (this->ml_last_superscript_index == last_block.length()) {
@ -1085,4 +1085,6 @@ md2attr_line::append_url_footnote(std::string href_str)
href.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOOTNOTE_TEXT)); href.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOOTNOTE_TEXT));
href.with_attr_for_all(SA_PREFORMATTED.value()); href.with_attr_for_all(SA_PREFORMATTED.value());
this->ml_footnotes.emplace_back(href); this->ml_footnotes.emplace_back(href);
return href_str;
} }

@ -79,7 +79,7 @@ private:
using list_block_t using list_block_t
= mapbox::util::variant<MD_BLOCK_UL_DETAIL*, MD_BLOCK_OL_DETAIL>; = mapbox::util::variant<MD_BLOCK_UL_DETAIL*, MD_BLOCK_OL_DETAIL>;
void append_url_footnote(std::string href); std::string append_url_footnote(std::string href);
void flush_footnotes(); void flush_footnotes();
attr_line_t to_attr_line(const pugi::xml_node& doc); attr_line_t to_attr_line(const pugi::xml_node& doc);

@ -107,6 +107,18 @@
} }
} }
}, },
"external-opener": {
"impls": {
"MacOS": {
"test": "command -v open",
"command": "open"
},
"XDG": {
"test": "command -v xdg-open",
"command": "xdg-open"
}
}
},
"url-scheme": { "url-scheme": {
"docker": { "docker": {
"handler": "docker-url-handler" "handler": "docker-url-handler"

@ -3,12 +3,17 @@
# @description: Copy text from the top view # @description: Copy text from the top view
# #
;SELECT jget(selected_text, '/value') AS content FROM lnav_top_view ;SELECT
jget(selected_text, '/value') AS sel_value,
jget(selected_text, '/href') AS sel_href
FROM lnav_top_view
;SELECT CASE ;SELECT CASE
WHEN $content IS NULL THEN WHEN $sel_href IS NOT NULL AND $sel_href != '' THEN
':write-to -' ':echo -n ${sel_href}'
WHEN $sel_value IS NOT NULL AND $sel_value != '' THEN
':echo -n ${sel_value}'
ELSE ELSE
':echo -n ${content}' ':write-to -'
END AS cmd END AS cmd
:redirect-to /dev/clipboard :redirect-to /dev/clipboard

@ -0,0 +1,56 @@
/**
* 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_link_handler.hh"
#include <curl/curl.h>
#include "base/injector.hh"
#include "command_executor.hh"
void
text_link_handler::text_open_href(const std::string& href)
{
static auto& ec = injector::get<exec_context&>();
log_info("open link: %s", href.c_str());
if (startswith(href, "#")) {
auto* ta = dynamic_cast<text_anchors*>(this);
if (ta != nullptr) {
ta->row_for_anchor(href) |
[this](auto row) { this->tss_view->set_selection(row); };
}
} else if (!is_url(href) || startswith(href, "file:")) {
ec.execute_with(":open $href", std::make_pair("href", href));
} else {
this->tlh_hrefs.clear();
this->tlh_hrefs.emplace(href);
this->tlh_href_line = this->tss_view->get_selection();
}
}

@ -0,0 +1,43 @@
/**
* 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_link_handler_hh
#define lnav_text_link_handler_hh
#include "textview_curses.hh"
class text_link_handler : public text_sub_source {
public:
void text_open_href(const std::string& href) override;
std::optional<vis_line_t> tlh_href_line;
std::set<std::string> tlh_hrefs;
};
#endif

@ -41,44 +41,79 @@ using namespace lnav::roles::literals;
std::vector<attr_line_t> std::vector<attr_line_t>
text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row) text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row)
{ {
static const auto MENU_WIDTH = 25;
const auto* tc = dynamic_cast<const textview_curses*>(&lv); const auto* tc = dynamic_cast<const textview_curses*>(&lv);
std::vector<attr_line_t> retval; std::vector<attr_line_t> retval;
if (!tc->tc_text_selection_active && tc->tc_selected_text) { if (tc->tc_text_selection_active || !tc->tc_selected_text) {
const auto& sti = tc->tc_selected_text.value(); return retval;
}
if (sti.sti_line == row) { const auto& sti = tc->tc_selected_text.value();
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) { if (sti.sti_line == row) {
left = dim.second - title.first.length() - 2; auto title = " Actions "_status_title;
} auto left = std::max(0, sti.sti_x - 2);
auto dim = lv.get_dimensions();
this->tom_menu_items.clear(); if (left + MENU_WIDTH >= dim.second) {
retval.emplace_back(attr_line_t().pad_to(left).append(title)); left = dim.second - MENU_WIDTH;
{ }
attr_line_t al;
al.append(" ").append("\u2714 IN"_ok).append(" "); this->tom_menu_items.clear();
int start = left;
this->tom_menu_items.emplace_back( auto is_link = !sti.sti_href.empty();
1_vl, auto menu_line = vis_line_t{1};
line_range{start, start + (int) al.length()}, if (is_link) {
[](const std::string& value) { auto ta = text_attrs{};
auto cmd = fmt::format(FMT_STRING(":filter-in {}"),
lnav::pcre2pp::quote(value)); ta.ta_attrs |= A_UNDERLINE;
lnav_data.ld_exec_context auto href_al
.with_provenance(exec_context::mouse_input{}) = attr_line_t(" Link: ")
->execute(cmd); .append(lnav::roles::table_header(sti.sti_href))
}); .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO))
start += al.length(); .with_attr_for_all(VC_STYLE.value(ta));
al.append(":mag_right:"_emoji) retval.emplace_back(href_al);
.append(" Search ") menu_line += 1_vl;
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)); }
retval.emplace_back(attr_line_t().pad_to(left).append(title));
{
attr_line_t al;
int start = left;
if (is_link) {
al.append(":floppy_disk:"_emoji)
.append(" Open in lnav")
.append(" ");
} else {
al.append(" ").append("\u2714 Filter-in"_ok).append(" ");
}
this->tom_menu_items.emplace_back(
menu_line,
line_range{start, start + (int) al.length()},
[is_link, sti](const std::string& value) {
auto cmd = is_link
? ":open $href"
: fmt::format(FMT_STRING(":filter-in {}"),
lnav::pcre2pp::quote(value));
lnav_data.ld_exec_context
.with_provenance(exec_context::mouse_input{})
->execute_with(cmd,
std::make_pair("href", sti.sti_href));
});
start += al.length();
if (is_link) {
al.append(" ");
} else {
al.append(":mag_right:"_emoji).append(" Search ");
}
al.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
if (!is_link) {
this->tom_menu_items.emplace_back( this->tom_menu_items.emplace_back(
1_vl, menu_line,
line_range{start, start + (int) al.length()}, line_range{start, start + (int) al.length()},
[](const std::string& value) { [](const std::string& value) {
auto cmd = fmt::format(FMT_STRING("/{}"), auto cmd = fmt::format(FMT_STRING("/{}"),
@ -87,35 +122,43 @@ text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row)
.with_provenance(exec_context::mouse_input{}) .with_provenance(exec_context::mouse_input{})
->execute(cmd); ->execute(cmd);
}); });
retval.emplace_back(attr_line_t().pad_to(left).append(al));
} }
{ retval.emplace_back(attr_line_t().pad_to(left).append(al));
attr_line_t al; }
{
attr_line_t al;
al.append(" ").append("\u2718 OUT"_error).append(" "); if (is_link) {
int start = left; al.append(":globe_with_meridians:"_emoji).append(" Open ");
this->tom_menu_items.emplace_back( } else {
2_vl, al.append(" ").append("\u2718 Filter-out"_error).append(" ");
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));
} }
menu_line += 1_vl;
int start = left;
this->tom_menu_items.emplace_back(
menu_line,
line_range{start, start + (int) al.length()},
[is_link, sti](const std::string& value) {
auto cmd = is_link
? ":xopen $href"
: fmt::format(FMT_STRING(":filter-out {}"),
lnav::pcre2pp::quote(value));
lnav_data.ld_exec_context
.with_provenance(exec_context::mouse_input{})
->execute_with(cmd,
std::make_pair("href", sti.sti_href));
});
start += al.length();
al.append(":clipboard:"_emoji)
.append(is_link ? " Copy link " : " Copy ")
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
this->tom_menu_items.emplace_back(
menu_line,
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));
} }
} }

@ -316,19 +316,17 @@ textfile_sub_source::text_size_for_line(textview_curses& tc,
void void
textfile_sub_source::to_front(const std::shared_ptr<logfile>& lf) textfile_sub_source::to_front(const std::shared_ptr<logfile>& lf)
{ {
auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf); const auto iter
if (iter != this->tss_files.end()) { = std::find(this->tss_files.begin(), this->tss_files.end(), lf);
this->tss_files.erase(iter); if (iter == this->tss_files.end()) {
} else { return;
iter = std::find(
this->tss_hidden_files.begin(), this->tss_hidden_files.end(), lf);
if (iter != this->tss_hidden_files.end()) {
this->tss_hidden_files.erase(iter);
}
} }
this->tss_files.push_front(lf); this->tss_files.front().save_from(*this->tss_view);
auto fvs = *iter;
this->tss_files.erase(iter);
this->tss_files.emplace_front(fvs);
this->set_time_offset(false); this->set_time_offset(false);
fvs.load_into(*this->tss_view);
this->tss_view->reload_data(); this->tss_view->reload_data();
} }
@ -338,6 +336,8 @@ textfile_sub_source::rotate_left()
if (this->tss_files.size() > 1) { if (this->tss_files.size() > 1) {
this->tss_files.push_back(this->tss_files.front()); this->tss_files.push_back(this->tss_files.front());
this->tss_files.pop_front(); this->tss_files.pop_front();
this->tss_files.back().save_from(*this->tss_view);
this->tss_files.front().load_into(*this->tss_view);
this->set_time_offset(false); this->set_time_offset(false);
this->tss_view->reload_data(); this->tss_view->reload_data();
this->tss_view->redo_search(); this->tss_view->redo_search();
@ -348,8 +348,10 @@ void
textfile_sub_source::rotate_right() textfile_sub_source::rotate_right()
{ {
if (this->tss_files.size() > 1) { if (this->tss_files.size() > 1) {
this->tss_files.push_front(this->tss_files.back()); this->tss_files.front().save_from(*this->tss_view);
this->tss_files.emplace_front(this->tss_files.back());
this->tss_files.pop_back(); this->tss_files.pop_back();
this->tss_files.front().load_into(*this->tss_view);
this->set_time_offset(false); this->set_time_offset(false);
this->tss_view->reload_data(); this->tss_view->reload_data();
this->tss_view->redo_search(); this->tss_view->redo_search();
@ -362,16 +364,13 @@ textfile_sub_source::remove(const std::shared_ptr<logfile>& lf)
auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf); auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf);
if (iter != this->tss_files.end()) { if (iter != this->tss_files.end()) {
this->tss_files.erase(iter); this->tss_files.erase(iter);
detach_observer(lf); this->detach_observer(lf);
} else {
iter = std::find(
this->tss_hidden_files.begin(), this->tss_hidden_files.end(), lf);
if (iter != this->tss_hidden_files.end()) {
this->tss_hidden_files.erase(iter);
detach_observer(lf);
}
} }
this->set_time_offset(false); this->set_time_offset(false);
if (!this->tss_files.empty()) {
this->tss_files.front().load_into(*this->tss_view);
}
this->tss_view->reload_data();
} }
void void
@ -379,7 +378,7 @@ textfile_sub_source::push_back(const std::shared_ptr<logfile>& lf)
{ {
auto* lfo = new line_filter_observer(this->get_filters(), lf); auto* lfo = new line_filter_observer(this->get_filters(), lf);
lf->set_logline_observer(lfo); lf->set_logline_observer(lfo);
this->tss_files.push_back(lf); this->tss_files.emplace_back(lf);
} }
void void
@ -449,7 +448,7 @@ textfile_sub_source::scroll_invoked(textview_curses* tc)
int int
textfile_sub_source::get_filtered_count() const textfile_sub_source::get_filtered_count() const
{ {
std::shared_ptr<logfile> lf = this->current_file(); auto lf = this->current_file();
int retval = 0; int retval = 0;
if (lf != nullptr) { if (lf != nullptr) {
@ -482,7 +481,7 @@ textfile_sub_source::get_text_format() const
return text_format_t::TF_UNKNOWN; return text_format_t::TF_UNKNOWN;
} }
return this->tss_files.front()->get_text_format(); return this->tss_files.front().fvs_file->get_text_format();
} }
static attr_line_t static attr_line_t
@ -517,8 +516,8 @@ textfile_sub_source::text_crumbs_for_line(
[this]() { [this]() {
return this->tss_files | lnav::itertools::map([](const auto& lf) { return this->tss_files | lnav::itertools::map([](const auto& lf) {
return breadcrumb::possibility{ return breadcrumb::possibility{
lf->get_unique_path(), lf.fvs_file->get_unique_path(),
to_display(lf), to_display(lf.fvs_file),
}; };
}); });
}, },
@ -526,7 +525,7 @@ textfile_sub_source::text_crumbs_for_line(
auto lf_opt = this->tss_files auto lf_opt = this->tss_files
| lnav::itertools::find_if([&key](const auto& elem) { | lnav::itertools::find_if([&key](const auto& elem) {
return key.template get<std::string>() return key.template get<std::string>()
== elem->get_unique_path(); == elem.fvs_file->get_unique_path();
}) })
| lnav::itertools::deref(); | lnav::itertools::deref();
@ -534,7 +533,7 @@ textfile_sub_source::text_crumbs_for_line(
return; return;
} }
this->to_front(lf_opt.value()); this->to_front(lf_opt.value().fvs_file);
this->tss_view->reload_data(); this->tss_view->reload_data();
}); });
if (lf->size() == 0) { if (lf->size() == 0) {
@ -686,9 +685,8 @@ textfile_sub_source::text_crumbs_for_line(
} }
textfile_sub_source::rescan_result_t textfile_sub_source::rescan_result_t
textfile_sub_source::rescan_files( textfile_sub_source::rescan_files(textfile_sub_source::scan_callback& callback,
textfile_sub_source::scan_callback& callback, std::optional<ui_clock::time_point> deadline)
std::optional<ui_clock::time_point> deadline)
{ {
static auto& lnav_db = injector::get<auto_sqlite3&>(); static auto& lnav_db = injector::get<auto_sqlite3&>();
@ -709,7 +707,7 @@ textfile_sub_source::rescan_files(
break; break;
} }
std::shared_ptr<logfile> lf = (*iter); std::shared_ptr<logfile> lf = iter->fvs_file;
if (lf->is_closed()) { if (lf->is_closed()) {
iter = this->tss_files.erase(iter); iter = this->tss_files.erase(iter);
@ -971,6 +969,10 @@ textfile_sub_source::rescan_files(
} }
if (!closed_files.empty()) { if (!closed_files.empty()) {
callback.closed_files(closed_files); callback.closed_files(closed_files);
if (!this->tss_files.empty()) {
this->tss_files.front().load_into(*this->tss_view);
}
this->tss_view->set_needs_update();
} }
if (retval.rr_new_data) { if (retval.rr_new_data) {
@ -1005,7 +1007,7 @@ void
textfile_sub_source::quiesce() textfile_sub_source::quiesce()
{ {
for (auto& lf : this->tss_files) { for (auto& lf : this->tss_files) {
lf->quiesce(); lf.fvs_file->quiesce();
} }
} }
@ -1355,20 +1357,13 @@ textfile_sub_source::to_front(const std::string& filename)
{ {
auto lf_opt = this->tss_files auto lf_opt = this->tss_files
| lnav::itertools::find_if([&filename](const auto& elem) { | lnav::itertools::find_if([&filename](const auto& elem) {
return elem->get_filename() == filename; return elem.fvs_file->get_filename() == filename;
}); });
if (!lf_opt) {
lf_opt = this->tss_hidden_files
| lnav::itertools::find_if([&filename](const auto& elem) {
return elem->get_filename() == filename;
});
}
if (!lf_opt) { if (!lf_opt) {
return false; return false;
} }
this->to_front(*(lf_opt.value())); this->to_front(lf_opt.value()->fvs_file);
return true; return true;
} }
@ -1381,7 +1376,8 @@ textfile_sub_source::text_accel_get_line(vis_line_t vl)
return (lf->begin() + lfo->lfo_filter_state.tfs_index[vl]).base(); return (lf->begin() + lfo->lfo_filter_state.tfs_index[vl]).base();
} }
textfile_header_overlay::textfile_header_overlay(textfile_sub_source* src) textfile_header_overlay::
textfile_header_overlay(textfile_sub_source* src)
: tho_src(src) : tho_src(src)
{ {
} }

@ -36,17 +36,16 @@
#include "filter_observer.hh" #include "filter_observer.hh"
#include "logfile.hh" #include "logfile.hh"
#include "plain_text_source.hh" #include "plain_text_source.hh"
#include "text_link_handler.hh"
#include "text_overlay_menu.hh" #include "text_overlay_menu.hh"
#include "textview_curses.hh" #include "textview_curses.hh"
class textfile_sub_source class textfile_sub_source
: public text_sub_source : public text_link_handler
, public vis_location_history , public vis_location_history
, public text_accel_source , public text_accel_source
, public text_anchors { , public text_anchors {
public: public:
using file_iterator = std::deque<std::shared_ptr<logfile>>::iterator;
textfile_sub_source() { this->tss_supports_filtering = true; } textfile_sub_source() { this->tss_supports_filtering = true; }
bool empty() const { return this->tss_files.empty(); } bool empty() const { return this->tss_files.empty(); }
@ -81,7 +80,7 @@ public:
return nullptr; return nullptr;
} }
return this->tss_files.front(); return this->tss_files.front().fvs_file;
} }
std::string text_source_name(const textview_curses& tv) override std::string text_source_name(const textview_curses& tv) override
@ -90,7 +89,7 @@ public:
return ""; return "";
} }
return this->tss_files.front()->get_filename(); return this->tss_files.front().fvs_file->get_filename();
} }
void to_front(const std::shared_ptr<logfile>& lf); void to_front(const std::shared_ptr<logfile>& lf);
@ -149,7 +148,7 @@ public:
std::optional<std::string> anchor_for_row(vis_line_t vl) override; std::optional<std::string> anchor_for_row(vis_line_t vl) override;
std::optional<vis_line_t> adjacent_anchor(vis_line_t vl, std::optional<vis_line_t> adjacent_anchor(vis_line_t vl,
direction dir) override; direction dir) override;
std::unordered_set<std::string> get_anchors() override; std::unordered_set<std::string> get_anchors() override;
@ -177,6 +176,34 @@ private:
delete lfo; delete lfo;
} }
struct file_view_state {
explicit file_view_state(const std::shared_ptr<logfile>& f)
: fvs_file(f)
{
}
bool operator==(const std::shared_ptr<logfile>& lf) const
{
return this->fvs_file == lf;
}
void save_from(const textview_curses& tc)
{
this->fvs_top = tc.get_top();
this->fvs_selection = tc.get_selection();
}
void load_into(textview_curses& tc) const
{
tc.set_selection(this->fvs_selection);
tc.set_top(this->fvs_top);
}
std::shared_ptr<logfile> fvs_file;
vis_line_t fvs_top{0};
vis_line_t fvs_selection{0};
};
struct rendered_file { struct rendered_file {
time_t rf_mtime; time_t rf_mtime;
file_ssize_t rf_file_size; file_ssize_t rf_file_size;
@ -189,8 +216,9 @@ private:
lnav::document::metadata ms_metadata; lnav::document::metadata ms_metadata;
}; };
std::deque<std::shared_ptr<logfile>> tss_files; using file_iterator = std::deque<file_view_state>::iterator;
std::deque<std::shared_ptr<logfile>> tss_hidden_files;
std::deque<file_view_state> tss_files;
std::unordered_map<std::string, rendered_file> tss_rendered_files; std::unordered_map<std::string, rendered_file> tss_rendered_files;
std::unordered_map<std::string, metadata_state> tss_doc_metadata; std::unordered_map<std::string, metadata_state> tss_doc_metadata;
size_t tss_line_indent_size{0}; size_t tss_line_indent_size{0};

@ -628,11 +628,9 @@ textview_curses::handle_mouse(mouse_event& me)
} else { } else {
if (this->tc_press_line.is<main_content>()) { if (this->tc_press_line.is<main_content>()) {
if (me.me_y < 0) { if (me.me_y < 0) {
this->shift_selection( this->shift_selection(shift_amount_t::up_line);
listview_curses::shift_amount_t::up_line);
} else if (me.me_y >= height) { } else if (me.me_y >= height) {
this->shift_selection( this->shift_selection(shift_amount_t::down_line);
listview_curses::shift_amount_t::down_line);
} else if (mouse_line.is<main_content>()) { } else if (mouse_line.is<main_content>()) {
this->set_selection_without_context( this->set_selection_without_context(
mouse_line.get<main_content>().mc_line); mouse_line.get<main_content>().mc_line);
@ -658,12 +656,12 @@ textview_curses::handle_mouse(mouse_event& me)
} }
case mouse_button_state_t::BUTTON_STATE_RELEASED: { case mouse_button_state_t::BUTTON_STATE_RELEASED: {
auto* ov = this->get_overlay_source(); auto* ov = this->get_overlay_source();
if (ov != nullptr && mouse_line.is<listview_curses::overlay_menu>() if (ov != nullptr && mouse_line.is<overlay_menu>()
&& this->tc_selected_text) && this->tc_selected_text)
{ {
auto* tom = dynamic_cast<text_overlay_menu*>(ov); auto* tom = dynamic_cast<text_overlay_menu*>(ov);
if (tom != nullptr) { if (tom != nullptr) {
auto& om = mouse_line.get<listview_curses::overlay_menu>(); auto& om = mouse_line.get<overlay_menu>();
auto& sti = this->tc_selected_text.value(); auto& sti = this->tc_selected_text.value();
for (const auto& mi : tom->tom_menu_items) { for (const auto& mi : tom->tom_menu_items) {
@ -701,20 +699,21 @@ textview_curses::handle_mouse(mouse_event& me)
attr_line_t al; attr_line_t al;
this->textview_value_for_row(mc_line, al); this->textview_value_for_row(mc_line, al);
auto get_res = get_string_attr(al.get_attrs(), auto line_sf = string_fragment::from_str(al.get_string());
&VC_HYPERLINK, auto cursor_sf = line_sf.sub_cell_range(
this->lv_left + me.me_press_x); this->lv_left + me.me_x, this->lv_left + me.me_x);
if (get_res) { auto attr_iter = find_string_attr_containing(
auto href = get_res.value()->sa_value.get<std::string>(); al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
if (attr_iter != al.get_attrs().end()) {
if (startswith(href, "#")) { auto href = attr_iter->sa_value.get<std::string>();
auto* ta
= dynamic_cast<text_anchors*>(this->tc_sub_source); this->tc_selected_text = selected_text_info{
if (ta != nullptr) { me.me_x,
ta->row_for_anchor(href) | mc_line,
[this](auto row) { this->set_selection(row); }; attr_iter->sa_range,
} al.to_string_fragment(attr_iter).to_string(),
} href,
};
} }
} }
if (this->tc_delegate != nullptr) { if (this->tc_delegate != nullptr) {

@ -509,6 +509,8 @@ public:
virtual void scroll_invoked(textview_curses* tc); virtual void scroll_invoked(textview_curses* tc);
virtual void text_open_href(const std::string& href) {}
bool tss_supports_filtering{false}; bool tss_supports_filtering{false};
bool tss_apply_filters{true}; bool tss_apply_filters{true};
@ -808,6 +810,7 @@ public:
int64_t sti_line; int64_t sti_line;
line_range sti_range; line_range sti_range;
std::string sti_value; std::string sti_value;
std::string sti_href;
}; };
std::optional<selected_text_info> tc_selected_text; std::optional<selected_text_info> tc_selected_text;

@ -125,7 +125,7 @@ looper::open(std::string url)
return Err(lnav::console::user_message::error( return Err(lnav::console::user_message::error(
attr_line_t("cannot get scheme from URL: ") attr_line_t("cannot get scheme from URL: ")
.append(lnav::roles::file(url))) .append(lnav::roles::file(url)))
.with_reason(curl_url_strerror(set_rc))); .with_reason(curl_url_strerror(get_rc)));
} }
auto proto_iter = cfg.c_schemes.find(scheme_part); auto proto_iter = cfg.c_schemes.find(scheme_part);

@ -189,6 +189,8 @@ static const typed_json_path_container<textview_curses::selected_text_info>
.with_children(line_range_handlers), .with_children(line_range_handlers),
yajlpp::property_handler("value").for_field( yajlpp::property_handler("value").for_field(
&textview_curses::selected_text_info::sti_value), &textview_curses::selected_text_info::sti_value),
yajlpp::property_handler("href").for_field(
&textview_curses::selected_text_info::sti_href),
}; };
enum class row_details_t { enum class row_details_t {
@ -411,8 +413,7 @@ CREATE TABLE lnav_views (
| [](const auto wrapper) { | [](const auto wrapper) {
auto lf = wrapper.get(); auto lf = wrapper.get();
return std::make_optional( return std::make_optional(lf->get_filename());
lf->get_filename());
}; };
}); });
for (const auto& crumb : crumbs) { for (const auto& crumb : crumbs) {

@ -101,6 +101,18 @@
} }
} }
}, },
"external-opener": {
"impls": {
"MacOS": {
"test": "command -v open",
"command": "open"
},
"XDG": {
"test": "command -v xdg-open",
"command": "xdg-open"
}
}
},
"url-scheme": { "url-scheme": {
"docker": { "docker": {
"handler": "docker-url-handler" "handler": "docker-url-handler"

@ -44,6 +44,10 @@
/tuning/clipboard/impls/tmux/general/read -> root-config.json:92 /tuning/clipboard/impls/tmux/general/read -> root-config.json:92
/tuning/clipboard/impls/tmux/general/write -> root-config.json:91 /tuning/clipboard/impls/tmux/general/write -> root-config.json:91
/tuning/clipboard/impls/tmux/test -> root-config.json:89 /tuning/clipboard/impls/tmux/test -> root-config.json:89
/tuning/external-opener/impls/MacOS/command -> root-config.json:114
/tuning/external-opener/impls/MacOS/test -> root-config.json:113
/tuning/external-opener/impls/XDG/command -> root-config.json:118
/tuning/external-opener/impls/XDG/test -> root-config.json:117
/tuning/piper/max-size -> root-config.json:57 /tuning/piper/max-size -> root-config.json:57
/tuning/piper/rotations -> root-config.json:58 /tuning/piper/rotations -> root-config.json:58
/tuning/piper/ttl -> root-config.json:59 /tuning/piper/ttl -> root-config.json:59
@ -52,12 +56,12 @@
/tuning/remote/ssh/config/ConnectTimeout -> root-config.json:50 /tuning/remote/ssh/config/ConnectTimeout -> root-config.json:50
/tuning/remote/ssh/start-command -> root-config.json:52 /tuning/remote/ssh/start-command -> root-config.json:52
/tuning/remote/ssh/transfer-command -> root-config.json:53 /tuning/remote/ssh/transfer-command -> root-config.json:53
/tuning/url-scheme/docker-compose/handler -> root-config.json:115 /tuning/url-scheme/docker-compose/handler -> root-config.json:127
/tuning/url-scheme/docker/handler -> root-config.json:112 /tuning/url-scheme/docker/handler -> root-config.json:124
/tuning/url-scheme/hw/handler -> {test_dir}/configs/installed/hw-url-handler.json:6 /tuning/url-scheme/hw/handler -> {test_dir}/configs/installed/hw-url-handler.json:6
/tuning/url-scheme/journald/handler -> root-config.json:118 /tuning/url-scheme/journald/handler -> root-config.json:130
/tuning/url-scheme/piper/handler -> root-config.json:121 /tuning/url-scheme/piper/handler -> root-config.json:133
/tuning/url-scheme/podman/handler -> root-config.json:124 /tuning/url-scheme/podman/handler -> root-config.json:136
/ui/clock-format -> root-config.json:4 /ui/clock-format -> root-config.json:4
/ui/default-colors -> root-config.json:6 /ui/default-colors -> root-config.json:6
/ui/dim-text -> root-config.json:5 /ui/dim-text -> root-config.json:5

@ -1890,6 +1890,18 @@ For support questions, email:
:xopen path1 [... pathN]
══════════════════════════════════════════════════════════════════════
Use an external command to open the given file(s)
Parameter
path The path to the file to open
Example
#1 To open the file '/path/to/file':
:xopen /path/to/file 
:zoom-to zoom-level :zoom-to zoom-level
══════════════════════════════════════════════════════════════════════ ══════════════════════════════════════════════════════════════════════
Zoom the histogram view to the given level Zoom the histogram view to the given level

@ -61,7 +61,7 @@ The following screenshot shows a mix of syslog and web access log
files. Failed requests are shown in red. Identifiers, like IP files. Failed requests are shown in red. Identifiers, like IP
address and PIDs are semantically highlighted. address and PIDs are semantically highlighted.
]8;;docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;docs/assets/images/lnav-front-page.png\¹]8;;\˒² ]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\¹]8;;\˒²
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
@ -185,7 +185,7 @@ The following alternatives are also available:
• ]8;;https://lnav.org\Main Site]8;; • ]8;;https://lnav.org\Main Site]8;;
• ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs • ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;; • ]8;;file://{top_srcdir}/ARCHITECTURE.md\Internal Architecture]8;;
▌[1] - https://lnav.org ▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org ▌[2] - https://docs.lnav.org

@ -4,7 +4,7 @@ The following screenshot shows a mix of syslog and web access log
files. Failed requests are shown in red. Identifiers, like IP files. Failed requests are shown in red. Identifiers, like IP
address and PIDs are semantically highlighted. address and PIDs are semantically highlighted.
]8;;docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;docs/assets/images/lnav-front-page.png\¹]8;;\˒² ]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\¹]8;;\˒²
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
@ -128,7 +128,7 @@ The following alternatives are also available:
• ]8;;https://lnav.org\Main Site]8;; • ]8;;https://lnav.org\Main Site]8;;
• ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs • ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;; • ]8;;file://{top_srcdir}/ARCHITECTURE.md\Internal Architecture]8;;
▌[1] - https://lnav.org ▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org ▌[2] - https://docs.lnav.org

@ -4,7 +4,7 @@ The following screenshot shows a mix of syslog and web access log
files. Failed requests are shown in red. Identifiers, like IP files. Failed requests are shown in red. Identifiers, like IP
address and PIDs are semantically highlighted. address and PIDs are semantically highlighted.
]8;;docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;docs/assets/images/lnav-front-page.png\¹]8;;\˒² ]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\🖼 Screenshot]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\¹]8;;\˒²
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
@ -128,7 +128,7 @@ The following alternatives are also available:
• ]8;;https://lnav.org\Main Site]8;; • ]8;;https://lnav.org\Main Site]8;;
• ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs • ]8;;https://docs.lnav.org\Documentation]8;;\² on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;; • ]8;;file://{top_srcdir}/ARCHITECTURE.md\Internal Architecture]8;;
▌[1] - https://lnav.org ▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org ▌[2] - https://docs.lnav.org

Loading…
Cancel
Save