mirror of
https://github.com/tstack/lnav.git
synced 2024-09-11 13:05:51 +03:00
[textview] some more support for handling hyperlinks
This commit is contained in:
parent
0697009b16
commit
9a1f383ce1
5
NEWS.md
5
NEWS.md
@ -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"
|
||||
|
99
src/external_opener.cc
Normal file
99
src/external_opener.cc
Normal file
@ -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
|
49
src/external_opener.cfg.hh
Normal file
49
src/external_opener.cfg.hh
Normal file
@ -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
|
43
src/external_opener.hh
Normal file
43
src/external_opener.hh
Normal file
@ -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
|
||||
|
56
src/text_link_handler.cc
Normal file
56
src/text_link_handler.cc
Normal file
@ -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();
|
||||
}
|
||||
}
|
43
src/text_link_handler.hh
Normal file
43
src/text_link_handler.hh
Normal file
@ -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) {
|
||||
if (tc->tc_text_selection_active || !tc->tc_selected_text) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
const auto& sti = tc->tc_selected_text.value();
|
||||
|
||||
if (sti.sti_line == row) {
|
||||
auto title = " Filter Other "_status_title;
|
||||
auto title = " Actions "_status_title;
|
||||
auto left = std::max(0, sti.sti_x - 2);
|
||||
auto dim = lv.get_dimensions();
|
||||
|
||||
if (left + title.first.length() >= dim.second) {
|
||||
left = dim.second - title.first.length() - 2;
|
||||
if (left + MENU_WIDTH >= dim.second) {
|
||||
left = dim.second - MENU_WIDTH;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
al.append(" ").append("\u2714 IN"_ok).append(" ");
|
||||
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(
|
||||
1_vl,
|
||||
menu_line,
|
||||
line_range{start, start + (int) al.length()},
|
||||
[](const std::string& value) {
|
||||
auto cmd = fmt::format(FMT_STRING(":filter-in {}"),
|
||||
[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(cmd);
|
||||
->execute_with(cmd,
|
||||
std::make_pair("href", sti.sti_href));
|
||||
});
|
||||
start += al.length();
|
||||
al.append(":mag_right:"_emoji)
|
||||
.append(" Search ")
|
||||
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
|
||||
|
||||
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,29 +122,38 @@ 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;
|
||||
|
||||
al.append(" ").append("\u2718 OUT"_error).append(" ");
|
||||
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(
|
||||
2_vl,
|
||||
menu_line,
|
||||
line_range{start, start + (int) al.length()},
|
||||
[](const std::string& value) {
|
||||
auto cmd = fmt::format(FMT_STRING(":filter-out {}"),
|
||||
[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(cmd);
|
||||
->execute_with(cmd,
|
||||
std::make_pair("href", sti.sti_href));
|
||||
});
|
||||
start += al.length();
|
||||
al.append(":clipboard:"_emoji)
|
||||
.append(" Copy ")
|
||||
.append(is_link ? " Copy link " : " Copy ")
|
||||
.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
|
||||
this->tom_menu_items.emplace_back(
|
||||
2_vl,
|
||||
menu_line,
|
||||
line_range{start, start + (int) al.length()},
|
||||
[](const std::string& value) {
|
||||
lnav_data.ld_exec_context.execute("|lnav-copy-text");
|
||||
@ -117,7 +161,6 @@ text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row)
|
||||
retval.emplace_back(attr_line_t().pad_to(left).append(al));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
@ -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()) {
|
||||
const auto iter
|
||||
= std::find(this->tss_files.begin(), this->tss_files.end(), lf);
|
||||
if (iter == this->tss_files.end()) {
|
||||
return;
|
||||
}
|
||||
this->tss_files.front().save_from(*this->tss_view);
|
||||
auto fvs = *iter;
|
||||
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);
|
||||
}
|
||||
}
|
||||
this->tss_files.push_front(lf);
|
||||
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,8 +685,7 @@ textfile_sub_source::text_crumbs_for_line(
|
||||
}
|
||||
|
||||
textfile_sub_source::rescan_result_t
|
||||
textfile_sub_source::rescan_files(
|
||||
textfile_sub_source::scan_callback& callback,
|
||||
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);
|
||||
@ -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>();
|
||||
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>();
|
||||
|
||||
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); };
|
||||
}
|
||||
}
|
||||
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:
|
||||
|
||||
|
||||
|
||||
[4m:[0m[1m[4mxopen[0m[4m [0m[4mpath[0m[4m1[0m[4m [[0m[4m...[0m[4m [0m[4mpath[0m[4mN[0m[4m][0m
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
Use an external command to open the given file(s)
|
||||
[4mParameter[0m
|
||||
[4mpath[0m The path to the file to open
|
||||
|
||||
[4mExample[0m
|
||||
#1 To open the file '/path/to/file':
|
||||
[37m[40m:[0m[1m[36m[40mxopen[0m[37m[40m /path/to/file [0m
|
||||
|
||||
|
||||
|
||||
[4m:[0m[1m[4mzoom-to[0m[4m [0m[4mzoom-level[0m
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
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\[4m🖼 Screenshot[0m]8;;\]8;;docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m🖼 Screenshot[0m]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
|
||||
[34m▌[0m[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
[34m▌[0m[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
@ -185,7 +185,7 @@ The following alternatives are also available:
|
||||
|
||||
[33m•[0m ]8;;https://lnav.org\[4mMain Site[0m]8;;\¹
|
||||
[33m•[0m ]8;;https://docs.lnav.org\[1m[4mDocumentation[0m]8;;\² on Read the Docs
|
||||
[33m•[0m ]8;;ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
[33m•[0m ]8;;file://{top_srcdir}/ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
|
||||
[34m▌[0m[1] - https://lnav.org
|
||||
[34m▌[0m[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\[4m🖼 Screenshot[0m]8;;\]8;;docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m🖼 Screenshot[0m]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
|
||||
[34m▌[0m[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
[34m▌[0m[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
@ -128,7 +128,7 @@ The following alternatives are also available:
|
||||
|
||||
[33m•[0m ]8;;https://lnav.org\[4mMain Site[0m]8;;\¹
|
||||
[33m•[0m ]8;;https://docs.lnav.org\[1m[4mDocumentation[0m]8;;\² on Read the Docs
|
||||
[33m•[0m ]8;;ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
[33m•[0m ]8;;file://{top_srcdir}/ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
|
||||
[34m▌[0m[1] - https://lnav.org
|
||||
[34m▌[0m[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\[4m🖼 Screenshot[0m]8;;\]8;;docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m🖼 Screenshot[0m]8;;\]8;;file://{top_srcdir}/docs/assets/images/lnav-front-page.png\[4m¹[0m]8;;\˒²
|
||||
|
||||
[34m▌[0m[1] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
[34m▌[0m[2] - file://{top_srcdir}/docs/assets/images/lnav-front-page.png
|
||||
@ -128,7 +128,7 @@ The following alternatives are also available:
|
||||
|
||||
[33m•[0m ]8;;https://lnav.org\[4mMain Site[0m]8;;\¹
|
||||
[33m•[0m ]8;;https://docs.lnav.org\[1m[4mDocumentation[0m]8;;\² on Read the Docs
|
||||
[33m•[0m ]8;;ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
[33m•[0m ]8;;file://{top_srcdir}/ARCHITECTURE.md\[4mInternal Architecture[0m]8;;\³
|
||||
|
||||
[34m▌[0m[1] - https://lnav.org
|
||||
[34m▌[0m[2] - https://docs.lnav.org
|
||||
|
Loading…
Reference in New Issue
Block a user