[textview] some more support for handling hyperlinks

pull/1265/head
Tim Stack 4 weeks ago
parent 0697009b16
commit 9a1f383ce1

@ -9,6 +9,11 @@ Features:
don't have one. Setting an opid allows messages to show
up in the Gantt chart view.
* 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:
* In the Gantt chart view, pressing `ENTER` will focus on

@ -184,7 +184,7 @@
"properties": {
"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",
"examples": [
"command -v pbcopy"
@ -209,6 +209,46 @@
},
"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": {
"description": "Settings related to custom URL handling",
"title": "/tuning/url-scheme",

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

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

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

@ -79,12 +79,18 @@ auto_fd::openpt(int flags)
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);
}
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::dup() const
@ -98,11 +104,18 @@ auto_fd::dup() const
return auto_fd{new_fd};
}
auto_fd::~auto_fd()
auto_fd::~
auto_fd()
{
this->reset();
}
void
auto_fd::copy_to(int fd) const
{
dup2(this->get(), fd);
}
void
auto_fd::reset(int fd)
{
@ -184,7 +197,8 @@ auto_pipe::for_child_fd(int child_fd)
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)
{
switch (child_fd) {

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

@ -27,6 +27,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <filesystem>
#include <iostream>
#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 {
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()) {
this->fc_files.erase(file_iter);
}
@ -220,6 +221,9 @@ file_collection::merge(file_collection& other)
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) {
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-level*

@ -58,6 +58,7 @@
#include "curl_looper.hh"
#include "date/tz.h"
#include "db_sub_source.hh"
#include "external_opener.hh"
#include "field_overlay_source.hh"
#include "fmt/printf.h"
#include "hasher.hh"
@ -3071,6 +3072,42 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
struct stat st;
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())) {
#ifndef HAVE_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);
}
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>
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 remote file '/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",
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(
+[]() { 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(
+[]() { 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 = {
yajlpp::property_handler("test")
.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")
.for_field(&sysclip::clipboard::c_test_command),
yajlpp::property_handler("general")
@ -1386,6 +1390,44 @@ static const struct json_path_container sysclip_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 = {
yajlpp::property_handler("expr")
.with_synopsis("<SQL-expression>")
@ -1531,6 +1573,9 @@ static const struct json_path_container tuning_handlers = {
yajlpp::property_handler("clipboard")
.with_description("Settings related to the clipboard")
.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")
.with_description("Settings related to custom URL handling")
.with_children(url_handlers),

@ -43,6 +43,7 @@
#include "base/file_range.hh"
#include "base/lnav.console.hh"
#include "base/result.h"
#include "external_opener.cfg.hh"
#include "file_vtab.cfg.hh"
#include "ghc/filesystem.hpp"
#include "lnav_config_fwd.hh"
@ -128,6 +129,7 @@ struct _lnav_config {
lnav::url_handler::config lc_url_handlers;
logfile_sub_source_ns::config lc_log_source;
lnav::log::annotate::config lc_log_annotations;
lnav::external_opener::config lc_opener;
};
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>(last_block.length()),
};
auto abs_href = this->append_url_footnote(href_str);
last_block.with_attr({
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*>()) {
const auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>();
const auto src_str
@ -1052,7 +1052,7 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
return Ok();
}
void
std::string
md2attr_line::append_url_footnote(std::string 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}),
});
if (is_internal) {
return;
return href_str;
}
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(SA_PREFORMATTED.value());
this->ml_footnotes.emplace_back(href);
return href_str;
}

@ -79,7 +79,7 @@ private:
using list_block_t
= 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();
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": {
"docker": {
"handler": "docker-url-handler"

@ -3,12 +3,17 @@
# @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
WHEN $content IS NULL THEN
':write-to -'
WHEN $sel_href IS NOT NULL AND $sel_href != '' THEN
':echo -n ${sel_href}'
WHEN $sel_value IS NOT NULL AND $sel_value != '' THEN
':echo -n ${sel_value}'
ELSE
':echo -n ${content}'
':write-to -'
END AS cmd
: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>
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);
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 (tc->tc_text_selection_active || !tc->tc_selected_text) {
return retval;
}
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();
const auto& sti = tc->tc_selected_text.value();
if (left + title.first.length() >= dim.second) {
left = dim.second - title.first.length() - 2;
}
if (sti.sti_line == row) {
auto title = " Actions "_status_title;
auto left = std::max(0, sti.sti_x - 2);
auto dim = lv.get_dimensions();
this->tom_menu_items.clear();
retval.emplace_back(attr_line_t().pad_to(left).append(title));
{
attr_line_t al;
if (left + MENU_WIDTH >= dim.second) {
left = dim.second - MENU_WIDTH;
}
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.clear();
auto is_link = !sti.sti_href.empty();
auto menu_line = vis_line_t{1};
if (is_link) {
auto ta = text_attrs{};
ta.ta_attrs |= A_UNDERLINE;
auto href_al
= attr_line_t(" Link: ")
.append(lnav::roles::table_header(sti.sti_href))
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO))
.with_attr_for_all(VC_STYLE.value(ta));
retval.emplace_back(href_al);
menu_line += 1_vl;
}
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(
1_vl,
menu_line,
line_range{start, start + (int) al.length()},
[](const std::string& value) {
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{})
->execute(cmd);
});
retval.emplace_back(attr_line_t().pad_to(left).append(al));
}
{
attr_line_t al;
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));
if (is_link) {
al.append(":globe_with_meridians:"_emoji).append(" Open ");
} else {
al.append(" ").append("\u2718 Filter-out"_error).append(" ");
}
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
textfile_sub_source::to_front(const std::shared_ptr<logfile>& lf)
{
auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf);
if (iter != this->tss_files.end()) {
this->tss_files.erase(iter);
} 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);
}
const auto iter
= std::find(this->tss_files.begin(), this->tss_files.end(), lf);
if (iter == this->tss_files.end()) {
return;
}
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);
fvs.load_into(*this->tss_view);
this->tss_view->reload_data();
}
@ -338,6 +336,8 @@ textfile_sub_source::rotate_left()
if (this->tss_files.size() > 1) {
this->tss_files.push_back(this->tss_files.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->tss_view->reload_data();
this->tss_view->redo_search();
@ -348,8 +348,10 @@ void
textfile_sub_source::rotate_right()
{
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.front().load_into(*this->tss_view);
this->set_time_offset(false);
this->tss_view->reload_data();
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);
if (iter != this->tss_files.end()) {
this->tss_files.erase(iter);
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->detach_observer(lf);
}
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
@ -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);
lf->set_logline_observer(lfo);
this->tss_files.push_back(lf);
this->tss_files.emplace_back(lf);
}
void
@ -449,7 +448,7 @@ textfile_sub_source::scroll_invoked(textview_curses* tc)
int
textfile_sub_source::get_filtered_count() const
{
std::shared_ptr<logfile> lf = this->current_file();
auto lf = this->current_file();
int retval = 0;
if (lf != nullptr) {
@ -482,7 +481,7 @@ textfile_sub_source::get_text_format() const
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
@ -517,8 +516,8 @@ textfile_sub_source::text_crumbs_for_line(
[this]() {
return this->tss_files | lnav::itertools::map([](const auto& lf) {
return breadcrumb::possibility{
lf->get_unique_path(),
to_display(lf),
lf.fvs_file->get_unique_path(),
to_display(lf.fvs_file),
};
});
},
@ -526,7 +525,7 @@ textfile_sub_source::text_crumbs_for_line(
auto lf_opt = this->tss_files
| lnav::itertools::find_if([&key](const auto& elem) {
return key.template get<std::string>()
== elem->get_unique_path();
== elem.fvs_file->get_unique_path();
})
| lnav::itertools::deref();
@ -534,7 +533,7 @@ textfile_sub_source::text_crumbs_for_line(
return;
}
this->to_front(lf_opt.value());
this->to_front(lf_opt.value().fvs_file);
this->tss_view->reload_data();
});
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_files(
textfile_sub_source::scan_callback& callback,
std::optional<ui_clock::time_point> deadline)
textfile_sub_source::rescan_files(textfile_sub_source::scan_callback& callback,
std::optional<ui_clock::time_point> deadline)
{
static auto& lnav_db = injector::get<auto_sqlite3&>();
@ -709,7 +707,7 @@ textfile_sub_source::rescan_files(
break;
}
std::shared_ptr<logfile> lf = (*iter);
std::shared_ptr<logfile> lf = iter->fvs_file;
if (lf->is_closed()) {
iter = this->tss_files.erase(iter);
@ -971,6 +969,10 @@ textfile_sub_source::rescan_files(
}
if (!closed_files.empty()) {
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) {
@ -1005,7 +1007,7 @@ void
textfile_sub_source::quiesce()
{
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
| 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) {
return false;
}
this->to_front(*(lf_opt.value()));
this->to_front(lf_opt.value()->fvs_file);
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();
}
textfile_header_overlay::textfile_header_overlay(textfile_sub_source* src)
textfile_header_overlay::
textfile_header_overlay(textfile_sub_source* src)
: tho_src(src)
{
}

@ -36,17 +36,16 @@
#include "filter_observer.hh"
#include "logfile.hh"
#include "plain_text_source.hh"
#include "text_link_handler.hh"
#include "text_overlay_menu.hh"
#include "textview_curses.hh"
class textfile_sub_source
: public text_sub_source
: public text_link_handler
, public vis_location_history
, public text_accel_source
, public text_anchors {
public:
using file_iterator = std::deque<std::shared_ptr<logfile>>::iterator;
textfile_sub_source() { this->tss_supports_filtering = true; }
bool empty() const { return this->tss_files.empty(); }
@ -81,7 +80,7 @@ public:
return nullptr;
}
return this->tss_files.front();
return this->tss_files.front().fvs_file;
}
std::string text_source_name(const textview_curses& tv) override
@ -90,7 +89,7 @@ public:
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);
@ -149,7 +148,7 @@ public:
std::optional<std::string> anchor_for_row(vis_line_t vl) override;
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;
@ -177,6 +176,34 @@ private:
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 {
time_t rf_mtime;
file_ssize_t rf_file_size;
@ -189,8 +216,9 @@ private:
lnav::document::metadata ms_metadata;
};
std::deque<std::shared_ptr<logfile>> tss_files;
std::deque<std::shared_ptr<logfile>> tss_hidden_files;
using file_iterator = std::deque<file_view_state>::iterator;
std::deque<file_view_state> tss_files;
std::unordered_map<std::string, rendered_file> tss_rendered_files;
std::unordered_map<std::string, metadata_state> tss_doc_metadata;
size_t tss_line_indent_size{0};

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

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

@ -125,7 +125,7 @@ looper::open(std::string url)
return Err(lnav::console::user_message::error(
attr_line_t("cannot get scheme from 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);

@ -189,6 +189,8 @@ static const typed_json_path_container<textview_curses::selected_text_info>
.with_children(line_range_handlers),
yajlpp::property_handler("value").for_field(
&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 {
@ -411,8 +413,7 @@ CREATE TABLE lnav_views (
| [](const auto wrapper) {
auto lf = wrapper.get();
return std::make_optional(
lf->get_filename());
return std::make_optional(lf->get_filename());
};
});
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": {
"docker": {
"handler": "docker-url-handler"

@ -44,6 +44,10 @@
/tuning/clipboard/impls/tmux/general/read -> root-config.json:92
/tuning/clipboard/impls/tmux/general/write -> root-config.json:91
/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/rotations -> root-config.json:58
/tuning/piper/ttl -> root-config.json:59
@ -52,12 +56,12 @@
/tuning/remote/ssh/config/ConnectTimeout -> root-config.json:50
/tuning/remote/ssh/start-command -> root-config.json:52
/tuning/remote/ssh/transfer-command -> root-config.json:53
/tuning/url-scheme/docker-compose/handler -> root-config.json:115
/tuning/url-scheme/docker/handler -> root-config.json:112
/tuning/url-scheme/docker-compose/handler -> root-config.json:127
/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/journald/handler -> root-config.json:118
/tuning/url-scheme/piper/handler -> root-config.json:121
/tuning/url-scheme/podman/handler -> root-config.json:124
/tuning/url-scheme/journald/handler -> root-config.json:130
/tuning/url-scheme/piper/handler -> root-config.json:133
/tuning/url-scheme/podman/handler -> root-config.json:136
/ui/clock-format -> root-config.json:4
/ui/default-colors -> root-config.json:6
/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 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
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
▌[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://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
▌[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
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
▌[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://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
▌[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
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
▌[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://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
▌[2] - https://docs.lnav.org

Loading…
Cancel
Save