From 9694d93ff3abbb63547a07158a3994b3ab14af31 Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Sat, 1 Jun 2024 09:10:04 -0700 Subject: [PATCH] [files] add a detail view to the files panel --- NEWS.md | 4 + docs/source/ui.rst | 13 +- src/base/attr_line.hh | 2 +- src/base/intern_string.cc | 24 ++ src/base/intern_string.hh | 7 + src/base/piper.file.cc | 81 ++++++- src/base/piper.file.hh | 6 +- src/base/time_util.cc | 10 + src/base/time_util.hh | 15 +- src/command_executor.cc | 12 +- src/file_collection.cc | 41 +++- src/file_collection.hh | 11 +- src/file_format.cc | 12 +- src/file_format.hh | 11 +- src/files_sub_source.cc | 226 ++++++++++++++++-- src/files_sub_source.hh | 8 +- src/filter_status_source.cc | 25 +- src/formats/formats.am | 1 - src/formats/papertrail_log.json | 52 ---- src/line_buffer.cc | 7 + src/listview_curses.cc | 3 +- src/lnav.cc | 54 ++++- src/lnav.hh | 2 + src/lnav_commands.cc | 4 +- src/log_format.cc | 29 ++- src/log_format.hh | 14 +- src/log_format_ext.hh | 4 +- src/logfile.cc | 64 ++++- src/logfile.hh | 13 +- src/logfile_fwd.hh | 1 + src/logfile_sub_source.cc | 10 +- src/piper.looper.cc | 35 ++- src/piper.looper.hh | 33 ++- src/plain_text_source.cc | 1 + src/readline_callbacks.cc | 2 + src/root-config.json | 1 + src/text_overlay_menu.cc | 94 +++++--- src/textview_curses.cc | 41 ++-- src/timeline_source.cc | 2 +- src/timeline_source.hh | 3 +- src/view_helpers.cc | 50 +++- src/view_helpers.hh | 5 +- ...3639753916f71254e8c9cce4ebb8bfd9978d3e.out | 2 +- ...06341dd560f927512e92c7c0985ed8b25827ae.out | 79 +++--- ...8ecb88b4753010c4264b3ac351260b4811612f.out | 4 +- 45 files changed, 832 insertions(+), 286 deletions(-) delete mode 100644 src/formats/papertrail_log.json diff --git a/NEWS.md b/NEWS.md index 0ea427ef..19ad76e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,10 @@ Features: so that you can manually set an opid on log messages that don't have one. Setting an opid allows messages to show up in the timeline view. +* The Files panel now has a details view on the right side + that shows extra information about the selected file. + You can look here for details of why lnav selected a + particular log format. * 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`. diff --git a/docs/source/ui.rst b/docs/source/ui.rst index eef2832e..feef4c0b 100644 --- a/docs/source/ui.rst +++ b/docs/source/ui.rst @@ -104,14 +104,19 @@ To hide the panels again, press :kbd:`q`. Screenshot of the files panel showing the loaded files. The Files panel is open initially to display progress in loading files. -The following information can be displayed for each file: +The following information is displayed for each file: + +* the "unique" portion of the path relative to the other files and +* the amount of data that has been indexed. + +To the right of the file list is a panel that shows details for each +file, such as: -* the "unique" portion of the path relative to the other files; -* the amount of data that has been indexed; * the date range of log messages contained in the file; * the errors that were encountered while trying to index the file; * the notes recorded for files where some automatic action was taken, - like hiding the file if it was seen as a duplicate of another file. + like hiding the file if it was seen as a duplicate of another file; +* the details of the demultiplexing and log format matching process. .. figure:: lnav-filters-panel.png :align: center diff --git a/src/base/attr_line.hh b/src/base/attr_line.hh index 07cbdbae..bc325646 100644 --- a/src/base/attr_line.hh +++ b/src/base/attr_line.hh @@ -301,7 +301,7 @@ public: size_t start_len = this->al_string.length(); - this->al_string.append(std::move(value.first)); + this->append(std::move(value.first)); line_range lr{(int) start_len, (int) this->al_string.length()}; diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc index 782a6828..d976e27a 100644 --- a/src/base/intern_string.cc +++ b/src/base/intern_string.cc @@ -180,6 +180,30 @@ string_fragment::trim() const return this->trim(" \t\r\n"); } +string_fragment +string_fragment::rtrim(const char* tokens) const +{ + string_fragment retval = *this; + + while (retval.sf_begin < retval.sf_end) { + bool found = false; + + for (int lpc = 0; tokens[lpc] != '\0'; lpc++) { + if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) { + found = true; + break; + } + } + if (!found) { + break; + } + + retval.sf_end -= 1; + } + + return retval; +} + std::optional string_fragment::consume_n(int amount) const { diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh index df60ee9b..6a7a5537 100644 --- a/src/base/intern_string.hh +++ b/src/base/intern_string.hh @@ -281,6 +281,12 @@ struct string_fragment { string_fragment sub_range(int begin, int end) const { + if (this->sf_begin + begin > this->sf_end) { + begin = this->sf_end - this->sf_begin; + } + if (this->sf_begin + end > this->sf_end) { + end = this->sf_end - this->sf_begin; + } return string_fragment{ this->sf_string, this->sf_begin + begin, this->sf_begin + end}; } @@ -575,6 +581,7 @@ struct string_fragment { } string_fragment trim(const char* tokens) const; + string_fragment rtrim(const char* tokens) const; string_fragment trim() const; string_fragment prepend(const char* str, int amount) const diff --git a/src/base/piper.file.cc b/src/base/piper.file.cc index 5a948fbe..7d8a7dd0 100644 --- a/src/base/piper.file.cc +++ b/src/base/piper.file.cc @@ -35,7 +35,11 @@ #include "base/injector.hh" #include "base/lnav_log.hh" #include "base/paths.hh" +#include "base/snippet_highlighters.hh" #include "piper.looper.cfg.hh" +#include "readline_highlighters.hh" + +using namespace lnav::roles::literals; namespace lnav { namespace piper { @@ -88,7 +92,7 @@ multiplex_matcher::match(const string_fragment& line) for (const auto& demux_pair : cfg.c_demux_definitions) { const auto& df = demux_pair.second; - if (!df.dd_valid || !df.dd_enabled) { + if (!df.dd_valid) { continue; } @@ -109,24 +113,86 @@ multiplex_matcher::match(const string_fragment& line) log_info(" demuxer pattern matched"); if (!md[df.dd_muxid_capture_index].has_value()) { log_info(" however, mux_id was not captured"); + + auto match_um = lnav::console::user_message::warning( + attr_line_t("demuxer ") + .append_quoted(demux_pair.first) + .append(" matched, however the ") + .append("mux_id"_symbol) + .append(" was not captured")); + this->mm_details.emplace_back(match_um); continue; } if (!md[df.dd_body_capture_index].has_value()) { log_info(" however, body was not captured"); + auto match_um = lnav::console::user_message::warning( + attr_line_t("demuxer ") + .append_quoted(demux_pair.first) + .append(" matched, however the ") + .append("body"_symbol) + .append(" was not captured")); + this->mm_details.emplace_back(match_um); continue; } log_info(" and required captures were found, using demuxer"); - return found{demux_pair.first}; + + if (df.dd_enabled) { + auto match_um = lnav::console::user_message::ok( + attr_line_t("demuxer ") + .append_quoted(demux_pair.first) + .append(" matched line ") + .append(lnav::roles::number( + fmt::to_string(this->mm_line_count)))); + this->mm_details.emplace_back(match_um); + return found{demux_pair.first}; + } + auto config_al = attr_line_t().append( + fmt::format(FMT_STRING(":config /log/demux/{}/enabled " + "true"), + demux_pair.first)); + readline_lnav_highlighter(config_al, -1); + auto match_um + = lnav::console::user_message::info( + attr_line_t("demuxer ") + .append_quoted(demux_pair.first) + .append(" matched line ") + .append(lnav::roles::number( + fmt::to_string(this->mm_line_count))) + .append(", however, it is disabled")) + .with_help( + attr_line_t("Use ") + .append_quoted( + lnav::roles::quoted_code(config_al)) + .append(" to enable this demuxer")); + this->mm_details.emplace_back(match_um); } auto partial_size = df.dd_pattern.pp_value->match_partial( line.sub_range(0, 1024)); - if (partial_size > 0) { - log_info(" only partial match by pattern: %s", - df.dd_pattern.pp_value->get_pattern().c_str()); - log_info(" %s", - line.sub_range(0, partial_size).to_string().c_str()); + auto regex_al = attr_line_t(df.dd_pattern.pp_value->get_pattern()); + lnav::snippets::regex_highlighter( + regex_al, -1, line_range{0, (int) regex_al.length()}); + auto in_line = line.sub_range(0, 1024).rtrim("\n").to_string(); + auto esc_res + = fmt::v10::detail::find_escape(&in_line[0], &(*in_line.end())); + if (esc_res.end != nullptr) { + in_line = fmt::format(FMT_STRING("{:?}"), in_line); } + auto note = attr_line_t("pattern: ") + .append(regex_al) + .append("\n ") + .append(lnav::roles::quoted_code(in_line)) + .append("\n") + .append(partial_size + 2, ' ') + .append("^ matched up to here"); + auto match_um = lnav::console::user_message::info( + attr_line_t("demuxer ") + .append_quoted(demux_pair.first) + .append(" did not match line ") + .append(lnav::roles::number( + fmt::to_string(this->mm_line_count)))) + .with_note(note); + this->mm_details.emplace_back(match_um); } if (df.dd_control_pattern.pp_value) { md = df.dd_control_pattern.pp_value->create_match_data(); @@ -141,6 +207,7 @@ multiplex_matcher::match(const string_fragment& line) } } + this->mm_line_count += 1; if (this->mm_partial_match_ids.empty()) { return not_found{}; } diff --git a/src/base/piper.file.hh b/src/base/piper.file.hh index 89607181..9863e51c 100644 --- a/src/base/piper.file.hh +++ b/src/base/piper.file.hh @@ -30,6 +30,7 @@ #ifndef lnav_piper_file_hh #define lnav_piper_file_hh +#include #include #include #include @@ -39,7 +40,7 @@ #include "auto_mem.hh" #include "base/intern_string.hh" -#include +#include "lnav.console.hh" #include "mapbox/variant_io.hpp" #include "time_util.hh" @@ -96,8 +97,11 @@ public: match_result match(const string_fragment& line); + std::vector mm_details; + private: std::set mm_partial_match_ids; + size_t mm_line_count{0}; }; } // namespace piper diff --git a/src/base/time_util.cc b/src/base/time_util.cc index 940cacc4..95fddd43 100644 --- a/src/base/time_util.cc +++ b/src/base/time_util.cc @@ -79,6 +79,16 @@ strftime_rfc3339( return index; } +std::string +to_rfc3339_string(time64_t tim, int millis, char sep) +{ + char buf[64]; + + strftime_rfc3339(buf, sizeof(buf), tim, millis, sep); + + return buf; +} + static std::optional get_posix_zone(const char* name) { diff --git a/src/base/time_util.hh b/src/base/time_util.hh index 1b82be37..ba6f20b3 100644 --- a/src/base/time_util.hh +++ b/src/base/time_util.hh @@ -46,11 +46,16 @@ namespace lnav { using time64_t = uint64_t; -ssize_t strftime_rfc3339(char* buffer, - size_t buffer_size, - lnav::time64_t tim, - int millis, - char sep = ' '); +ssize_t strftime_rfc3339( + char* buffer, size_t buffer_size, time64_t tim, int millis, char sep = ' '); + +std::string to_rfc3339_string(time64_t tim, int millis, char sep = ' '); + +inline std::string +to_rfc3339_string(struct timeval tv, char sep = ' ') +{ + return to_rfc3339_string(tv.tv_sec, tv.tv_usec / 1000, sep); +} date::sys_info sys_time_to_info(date::sys_seconds secs); diff --git a/src/command_executor.cc b/src/command_executor.cc index d392a654..1a178e01 100644 --- a/src/command_executor.cc +++ b/src/command_executor.cc @@ -245,13 +245,11 @@ bind_sql_parameters(exec_context& ec, sqlite3_stmt* stmt) static void execute_search(const std::string& search_cmd) { - lnav_data.ld_view_stack.top() | [&search_cmd](auto tc) { - auto search_term - = string_fragment(search_cmd) - .find_right_boundary(0, string_fragment::tag1{'\n'}) - .to_string(); - tc->execute_search(search_term); - }; + textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode); + auto search_term = string_fragment(search_cmd) + .find_right_boundary(0, string_fragment::tag1{'\n'}) + .to_string(); + tc->execute_search(search_term); } Result diff --git a/src/file_collection.cc b/src/file_collection.cc index 29a4744d..2805419f 100644 --- a/src/file_collection.cc +++ b/src/file_collection.cc @@ -406,12 +406,15 @@ file_collection::watch_logfile(const std::string& filename, } } - auto ff = detect_file_format(filename); + auto ff_res = detect_file_format(filename); - loo.loo_file_format = ff; - switch (ff) { + loo.loo_file_format = ff_res.dffr_file_format; + switch (ff_res.dffr_file_format) { case file_format_t::SQLITE_DB: - retval.fc_other_files[filename].ofd_format = ff; + retval.fc_other_files[filename].ofd_format + = ff_res.dffr_file_format; + retval.fc_other_files[filename].ofd_details + = ff_res.dffr_details; break; case file_format_t::MULTIPLEXED: { @@ -430,7 +433,10 @@ file_collection::watch_logfile(const std::string& filename, looper_options); if (create_res.isOk()) { - retval.fc_other_files[filename] = ff; + auto& ofd = retval.fc_other_files[filename]; + + ofd.ofd_format = ff_res.dffr_file_format; + ofd.ofd_details = ff_res.dffr_details; retval.fc_file_names[filename] = loo; retval.fc_file_names[filename].with_piper( create_res.unwrap()); @@ -498,7 +504,9 @@ file_collection::watch_logfile(const std::string& filename, res.unwrapErr(), }); } else { - retval.fc_other_files[filename] = ff; + auto& ofd = retval.fc_other_files[filename]; + ofd.ofd_format = ff_res.dffr_file_format; + ofd.ofd_details = ff_res.dffr_details; } { prog_iter_opt | [&prog](auto prog_iter) { @@ -512,6 +520,7 @@ file_collection::watch_logfile(const std::string& filename, default: { auto filename_to_open = filename; + loo.loo_match_details = ff_res.dffr_details; auto eff = detect_mime_type(filename); if (eff) { @@ -790,6 +799,14 @@ file_collection::rescan_files(bool required) pair.second.loo_piper->get_out_pattern().string(), pair.second, required); + if (!pair.second.loo_piper.value().get_demux_id().empty() + && this->fc_other_files.count(pair.first) == 0) + { + auto& ofd = retval.fc_other_files[pair.first]; + ofd.ofd_format = file_format_t::MULTIPLEXED; + ofd.ofd_details + = pair.second.loo_piper.value().get_demux_details(); + } } else { this->expand_filename(fq, pair.first, pair.second, required); if (this->fc_rotated) { @@ -872,6 +889,18 @@ file_collection::copy() return retval; } +void +file_collection::clear() +{ + this->fc_name_to_errors->writeAccess()->clear(); + this->fc_file_names.clear(); + this->fc_files.clear(); + this->fc_renamed_files.clear(); + this->fc_closed_files.clear(); + this->fc_other_files.clear(); + this->fc_new_stats.clear(); +} + size_t file_collection::other_file_format_count(file_format_t ff) const { diff --git a/src/file_collection.hh b/src/file_collection.hh index ec65a0fd..a11e6722 100644 --- a/src/file_collection.hh +++ b/src/file_collection.hh @@ -67,6 +67,7 @@ using safe_scan_progress = safe::Safe; struct other_file_descriptor { file_format_t ofd_format; std::string ofd_description; + std::vector ofd_details; other_file_descriptor(file_format_t format = file_format_t::UNKNOWN, std::string description = "") @@ -188,15 +189,7 @@ struct file_collection { && this->fc_other_files.empty(); } - void clear() - { - this->fc_name_to_errors->writeAccess()->clear(); - this->fc_file_names.clear(); - this->fc_files.clear(); - this->fc_closed_files.clear(); - this->fc_other_files.clear(); - this->fc_new_stats.clear(); - } + void clear(); bool is_below_open_file_limit() const { diff --git a/src/file_format.cc b/src/file_format.cc index 2552bf58..b6da6521 100644 --- a/src/file_format.cc +++ b/src/file_format.cc @@ -39,17 +39,17 @@ #include "config.h" #include "line_buffer.hh" -file_format_t +detect_file_format_result detect_file_format(const std::filesystem::path& filename) { + detect_file_format_result retval = {file_format_t::UNKNOWN}; auto describe_res = archive_manager::describe(filename); if (describe_res.isOk() && describe_res.unwrap().is()) { - return file_format_t::ARCHIVE; + return {file_format_t::ARCHIVE}; } - file_format_t retval = file_format_t::UNKNOWN; auto open_res = lnav::filesystem::open_file(filename, O_RDONLY); if (open_res.isErr()) { log_error("unable to open file for format detection: %s -- %s", @@ -70,7 +70,7 @@ detect_file_format(const std::filesystem::path& filename) if (header_frag.startswith(SQLITE3_HEADER)) { log_info("%s: appears to be a SQLite DB", filename.c_str()); - retval = file_format_t::SQLITE_DB; + retval.dffr_file_format = file_format_t::SQLITE_DB; } else { auto looping = true; lnav::piper::multiplex_matcher mm; @@ -118,7 +118,8 @@ detect_file_format(const std::filesystem::path& filename) log_info("%s: is multiplexed using %s", filename.c_str(), f.f_id.c_str()); - retval = file_format_t::MULTIPLEXED; + retval.dffr_file_format + = file_format_t::MULTIPLEXED; return false; }, [](lnav::piper::multiplex_matcher::not_found nf) { @@ -130,6 +131,7 @@ detect_file_format(const std::filesystem::path& filename) next_range = li.li_file_range; } + retval.dffr_details = std::move(mm.mm_details); } } } diff --git a/src/file_format.hh b/src/file_format.hh index b9fa9be3..9b78bc7a 100644 --- a/src/file_format.hh +++ b/src/file_format.hh @@ -32,10 +32,11 @@ #ifndef lnav_file_format_hh #define lnav_file_format_hh +#include #include +#include "base/lnav.console.hh" #include "fmt/format.h" -#include enum class file_format_t : int { UNKNOWN, @@ -51,7 +52,13 @@ struct external_file_format { std::filesystem::path eff_source_path; }; -file_format_t detect_file_format(const std::filesystem::path& filename); +struct detect_file_format_result { + file_format_t dffr_file_format{file_format_t::UNKNOWN}; + std::vector dffr_details; +}; + +detect_file_format_result detect_file_format( + const std::filesystem::path& filename); std::optional detect_mime_type( const std::filesystem::path& filename); diff --git a/src/files_sub_source.cc b/src/files_sub_source.cc index c009159f..1b0d5623 100644 --- a/src/files_sub_source.cc +++ b/src/files_sub_source.cc @@ -34,6 +34,9 @@ #include "base/fs_util.hh" #include "base/humanize.hh" #include "base/humanize.network.hh" +#include "base/humanize.time.hh" +#include "base/itertools.enumerate.hh" +#include "base/itertools.hh" #include "base/opt_util.hh" #include "base/string_util.hh" #include "config.h" @@ -48,7 +51,7 @@ files_list_selection from_selection(vis_line_t sel_vis) { auto& fc = lnav_data.ld_active_files; - int sel = (int) sel_vis; + int sel = sel_vis; { safe::ReadAccess errs(*fc.fc_name_to_errors); @@ -57,7 +60,8 @@ from_selection(vis_line_t sel_vis) auto iter = errs->begin(); std::advance(iter, sel); - return error_selection::build(sel, iter->first); + return error_selection::build( + sel, std::make_pair(iter->first, iter->second.fei_description)); } sel -= errs->size(); @@ -83,7 +87,10 @@ from_selection(vis_line_t sel_vis) } } // namespace files_model -files_sub_source::files_sub_source() {} +files_sub_source:: +files_sub_source() +{ +} bool files_sub_source::list_input_handle_key(listview_curses& lv, int ch) @@ -181,11 +188,11 @@ files_sub_source::list_input_handle_key(listview_curses& lv, int ch) [&](files_model::error_selection& es) { auto& fc = lnav_data.ld_active_files; - fc.fc_file_names.erase(es.sb_iter); + fc.fc_file_names.erase(es.sb_iter.first); auto name_iter = fc.fc_file_names.begin(); while (name_iter != fc.fc_file_names.end()) { - if (name_iter->first == es.sb_iter) { + if (name_iter->first == es.sb_iter.first) { name_iter = fc.fc_file_names.erase(name_iter); continue; } @@ -196,7 +203,7 @@ files_sub_source::list_input_handle_key(listview_curses& lv, int ch) if (rp_opt) { auto rp = *rp_opt; - if (fmt::to_string(rp.home()) == es.sb_iter) { + if (fmt::to_string(rp.home()) == es.sb_iter.first) { fc.fc_other_files.erase(name_iter->first); name_iter = fc.fc_file_names.erase(name_iter); continue; @@ -205,7 +212,8 @@ files_sub_source::list_input_handle_key(listview_curses& lv, int ch) ++name_iter; } - fc.fc_name_to_errors->writeAccess()->erase(es.sb_iter); + fc.fc_name_to_errors->writeAccess()->erase( + es.sb_iter.first); fc.fc_invalidate_merge = true; lv.reload_data(); }, @@ -246,8 +254,10 @@ files_sub_source::text_value_for_line(textview_curses& tc, std::string& value_out, text_sub_source::line_flags_t flags) { - bool selected - = lnav_data.ld_mode == ln_mode_t::FILES && line == tc.get_selection(); + bool selected = line == tc.get_selection(); + role_t cursor_role = lnav_data.ld_mode == ln_mode_t::FILES + ? role_t::VCR_CURSOR_LINE + : role_t::VCR_DISABLED_CURSOR_LINE; const auto dim = tc.get_dimensions(); const auto& fc = lnav_data.ld_active_files; auto filename_width @@ -280,8 +290,7 @@ files_sub_source::text_value_for_line(textview_curses& tc, } al.append(" ").append(iter->second.fei_description); if (selected) { - al.with_attr_for_all( - VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED)); + al.with_attr_for_all(VC_ROLE.value(cursor_role)); } value_out = al.get_string(); @@ -308,7 +317,7 @@ files_sub_source::text_value_for_line(textview_curses& tc, .append(" ") .append(iter->second.ofd_description); if (selected) { - al.with_attr_for_all(VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED)); + al.with_attr_for_all(VC_ROLE.value(cursor_role)); } if (line == fc.fc_other_files.size() - 1) { al.with_attr_for_all(VC_STYLE.value(text_attrs{A_UNDERLINE})); @@ -323,13 +332,8 @@ files_sub_source::text_value_for_line(textview_curses& tc, const auto& lf = fc.fc_files[line]; auto ld_opt = lnav_data.ld_log_source.find_data(lf); auto fn = fmt::to_string(std::filesystem::path(lf->get_unique_path())); - char start_time[64] = "", end_time[64] = ""; std::vector file_notes; - if (lf->get_format() != nullptr) { - sql_strftime(start_time, sizeof(start_time), lf->front().get_timeval()); - sql_strftime(end_time, sizeof(end_time), lf->back().get_timeval()); - } truncate_to(fn, filename_width); for (const auto& pair : lf->get_notes()) { file_notes.push_back(pair.second); @@ -355,19 +359,13 @@ files_sub_source::text_value_for_line(textview_curses& tc, humanize::file_size(lf->get_index_size(), humanize::alignment::columnar)); } - al.append(" ") - .append(start_time) - .append(" \u2014 ") - .append(end_time) - .append(" ") - .appendf(FMT_STRING("{}"), fmt::join(file_notes, "; ")); + al.append(" ").appendf(FMT_STRING("{}"), fmt::join(file_notes, "; ")); if (selected) { - al.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOCUSED)); + al.with_attr_for_all(VC_ROLE.value(cursor_role)); } value_out = al.get_string(); - this->fss_last_line_len - = filename_width + 23 + strlen(start_time) + strlen(end_time); + this->fss_last_line_len = value_out.length(); } void @@ -455,3 +453,179 @@ files_sub_source::text_handle_mouse( return false; } + +void +files_sub_source::text_selection_changed(textview_curses& tc) +{ + auto sel = files_model::from_selection(tc.get_selection()); + std::vector details; + + sel.match( + [](files_model::no_selection) {}, + + [&details](const files_model::error_selection& es) { + auto path = std::filesystem::path(es.sb_iter.first); + + details.emplace_back( + attr_line_t().appendf(FMT_STRING("Full path: {}"), path)); + details.emplace_back(attr_line_t(" ").append( + lnav::roles::error(es.sb_iter.second))); + }, + [&details](const files_model::other_selection& os) { + auto path = std::filesystem::path(os.sb_iter->first); + + details.emplace_back( + attr_line_t().appendf(FMT_STRING("Full path: {}"), path)); + details.emplace_back(attr_line_t(" ").append("Match Details"_h3)); + for (const auto& msg : os.sb_iter->second.ofd_details) { + auto msg_al = msg.to_attr_line(); + + for (auto& msg_line : msg_al.rtrim().split_lines()) { + msg_line.insert(0, 4, ' '); + details.emplace_back(msg_line); + } + } + }, + [&details](const files_model::file_selection& fs) { + static constexpr auto NAME_WIDTH = 17; + + auto lf = *fs.sb_iter; + auto path = lf->get_filename(); + auto actual_path = lf->get_actual_path(); + auto format = lf->get_format(); + + details.emplace_back( + attr_line_t() + .appendf(FMT_STRING("{}"), path) + .with_attr_for_all(VC_ROLE.value(role_t::VCR_IDENTIFIER))); + const auto notes = lf->get_notes(); + if (!notes.empty()) { + details.emplace_back( + attr_line_t(" ").append("Notes"_h2).append(":")); + for (const auto& pair : notes) { + details.emplace_back(attr_line_t(" ").append( + lnav::roles::warning(pair.second))); + } + } + details.emplace_back(attr_line_t(" ").append("General"_h2)); + if (actual_path) { + if (path != actual_path.value()) { + auto line = attr_line_t() + .append("Actual Path"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(lnav::roles::file( + fmt::to_string(actual_path.value()))); + details.emplace_back(line); + } + } else { + details.emplace_back(attr_line_t().append(" Piped")); + } + details.emplace_back( + attr_line_t() + .append("MIME Type"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(fmt::to_string(lf->get_text_format()))); + details.emplace_back( + attr_line_t() + .append("Last Modified"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(lnav::to_rfc3339_string( + convert_log_time_to_local(lf->get_modified_time()), + 0, + 'T'))); + details.emplace_back( + attr_line_t() + .append("Size"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(humanize::file_size(lf->get_index_size(), + humanize::alignment::none))); + + details.emplace_back(attr_line_t() + .append("Lines"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(lnav::roles::number(fmt::format( + FMT_STRING("{:L}"), lf->size())))); + if (format != nullptr) { + details.emplace_back(attr_line_t() + .append("Time Range"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(lnav::to_rfc3339_string( + lf->front().get_timeval(), 'T')) + .append(" - ") + .append(lnav::to_rfc3339_string( + lf->back().get_timeval(), 'T'))); + details.emplace_back( + attr_line_t() + .append("Duration"_h3) + .right_justify(NAME_WIDTH) + .append(": ") + .append(humanize::time::duration::from_tv( + lf->back().get_timeval() + - lf->front().get_timeval()) + .to_string())); + } + { + auto line + = attr_line_t(" ").append("Log Format"_h2).append(": "); + + if (format != nullptr) { + line.append(lnav::roles::identifier( + format->get_name().to_string())); + } else { + line.append("(none)"_comment); + } + details.emplace_back(line); + } + auto match_msgs = lf->get_format_match_messages(); + + details.emplace_back( + attr_line_t(" ").append("Match Details"_h3)); + for (const auto& msg : match_msgs) { + auto msg_al = msg.to_attr_line(); + + for (auto& msg_line : msg_al.rtrim().split_lines()) { + msg_line.insert(0, 6, ' '); + details.emplace_back(msg_line); + } + } + + { + const auto& meta = lf->get_embedded_metadata(); + + if (!meta.empty()) { + details.emplace_back( + attr_line_t(" ").append("Embedded Metadata:"_h2)); + for (const auto& [index, meta_pair] : + lnav::itertools::enumerate(meta)) + { + details.emplace_back( + attr_line_t(" ") + .appendf(FMT_STRING("[{}]"), index) + .append(" ") + .append(lnav::roles::h3(meta_pair.first))); + details.emplace_back( + attr_line_t(" MIME Type: ") + .append(lnav::roles::symbol(fmt::to_string( + meta_pair.second.m_format)))); + line_range lr{6, -1}; + details.emplace_back(attr_line_t().with_attr( + string_attr{lr, VC_GRAPHIC.value(ACS_HLINE)})); + const auto val_sf = string_fragment::from_str( + meta_pair.second.m_value); + for (const auto val_line : val_sf.split_lines()) { + details.emplace_back( + attr_line_t(" ").append(val_line)); + } + } + } + } + }); + + this->fss_details_source->replace_with(details); +} diff --git a/src/files_sub_source.hh b/src/files_sub_source.hh index 90bcb021..962eafaa 100644 --- a/src/files_sub_source.hh +++ b/src/files_sub_source.hh @@ -31,6 +31,7 @@ #define files_sub_source_hh #include "file_collection.hh" +#include "plain_text_source.hh" #include "textview_curses.hh" class files_sub_source @@ -65,8 +66,11 @@ public: const listview_curses::display_line_content_t&, mouse_event& me) override; + void text_selection_changed(textview_curses& tc) override; + size_t fss_last_line_len{0}; attr_line_t fss_curr_line; + plain_text_source* fss_details_source{nullptr}; }; struct files_overlay_source : public list_overlay_source { @@ -95,7 +99,9 @@ struct selection_base { } }; -struct error_selection : public selection_base {}; +struct error_selection + : public selection_base> {}; struct other_selection : public selection_base< diff --git a/src/filter_status_source.cc b/src/filter_status_source.cc index d284d58f..75147635 100644 --- a/src/filter_status_source.cc +++ b/src/filter_status_source.cc @@ -47,8 +47,10 @@ static auto DELETE_HELP = ANSI_BOLD("D") ": Delete"; static auto FILTERING_HELP = ANSI_BOLD("f") ": "; static auto JUMP_HELP = ANSI_BOLD("ENTER") ": Jump To"; static auto CLOSE_HELP = ANSI_BOLD("X") ": Close"; +static auto FOCUS_DETAILS_HELP = ANSI_BOLD("CTRL-]") ": Focus on details view"; -filter_status_source::filter_status_source() +filter_status_source:: +filter_status_source() { this->tss_fields[TSF_TITLE].set_width(14); this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE); @@ -102,6 +104,7 @@ filter_status_source::statusview_fields() break; case ln_mode_t::FILTER: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: this->tss_fields[TSF_HELP].set_value(EXIT_MSG); break; default: @@ -110,6 +113,7 @@ filter_status_source::statusview_fields() } if (lnav_data.ld_mode == ln_mode_t::FILES + || lnav_data.ld_mode == ln_mode_t::FILE_DETAILS || lnav_data.ld_mode == ln_mode_t::SEARCH_FILES) { this->tss_fields[TSF_FILES_TITLE].set_value( @@ -231,7 +235,8 @@ filter_status_source::update_filtered(text_sub_source* tss) } } -filter_help_status_source::filter_help_status_source() +filter_help_status_source:: +filter_help_status_source() { this->fss_help.set_min_width(10); this->fss_help.set_share(1); @@ -292,10 +297,11 @@ filter_help_status_source::statusview_fields() tss->tss_apply_filters ? "Disable Filtering" : "Enable Filtering"); } - } else if (lnav_data.ld_mode == ln_mode_t::FILES + } else if ((lnav_data.ld_mode == ln_mode_t::FILES + || lnav_data.ld_mode == ln_mode_t::FILE_DETAILS) && lnav_data.ld_session_loaded) { - auto& lv = lnav_data.ld_files_view; + const auto& lv = lnav_data.ld_files_view; auto sel = files_model::from_selection(lv.get_selection()); sel.match( @@ -313,9 +319,16 @@ filter_help_status_source::statusview_fields() if (ld_opt && !ld_opt.value()->ld_visible) { vis_help = "Show"; } + const auto* focus_details_help + = lnav_data.ld_mode == ln_mode_t::FILES + ? FOCUS_DETAILS_HELP + : ""; - this->fss_help.set_value( - " %s%s %s", ENABLE_HELP, vis_help, JUMP_HELP); + this->fss_help.set_value(" %s%s %s %s", + ENABLE_HELP, + vis_help, + JUMP_HELP, + focus_details_help); }); } }; diff --git a/src/formats/formats.am b/src/formats/formats.am index 2c10a0ac..d17ce3d9 100644 --- a/src/formats/formats.am +++ b/src/formats/formats.am @@ -29,7 +29,6 @@ FORMAT_FILES = \ $(srcdir)/%reldir%/openamdb_log.json \ $(srcdir)/%reldir%/openstack_log.json \ $(srcdir)/%reldir%/page_log.json \ - $(srcdir)/%reldir%/papertrail_log.json \ $(srcdir)/%reldir%/pcap_log.json \ $(srcdir)/%reldir%/procstate_log.json \ $(srcdir)/%reldir%/redis_log.json \ diff --git a/src/formats/papertrail_log.json b/src/formats/papertrail_log.json deleted file mode 100644 index b1a9d87e..00000000 --- a/src/formats/papertrail_log.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "https://lnav.org/schemas/format-v1.schema.json", - "papertrail_log": { - "title": "Papertrail Service", - "url": "https://papertrailapp.com/", - "description": "Log format for the papertrail log management service", - "json": true, - "hide-extra": true, - "file-pattern": "pt:.*", - "line-format": [ - { - "field": "display_received_at" - }, - " ", - { - "field": "hostname" - }, - " ", - { - "field": "program" - }, - ": ", - { - "field": "message" - } - ], - "level-field": "severity", - "level": { - "error": "Error", - "debug": "Debug", - "warning": "Warning", - "info": "Info(?:rmational)?|Notice", - "critical": "Crit(?:ical)?", - "fatal": "Emerg(?:ency)?|Alert" - }, - "timestamp-field": "generated_at", - "body-field": "message", - "value": { - "display_received_at": { - "kind": "string" - }, - "program": { - "kind": "string", - "identifier": true - }, - "hostname": { - "kind": "string", - "identifier": true - } - } - } -} \ No newline at end of file diff --git a/src/line_buffer.cc b/src/line_buffer.cc index 714f1323..7631e73b 100644 --- a/src/line_buffer.cc +++ b/src/line_buffer.cc @@ -244,6 +244,13 @@ line_buffer::gz_indexed::open(int fd, lnav::gzip::header& hd) hd.h_mtime.tv_sec = gz_hd.time; hd.h_name = std::string((char*) name); hd.h_comment = std::string((char*) comment); + log_info( + "%d: read gzip header (mtime=%d; name='%s'; " + "comment='%s')", + fd, + hd.h_mtime.tv_sec, + hd.h_name.c_str(), + hd.h_comment.c_str()); break; default: log_error("%d: failed to read gzip header data", fd); diff --git a/src/listview_curses.cc b/src/listview_curses.cc index 894533a9..04ebe033 100644 --- a/src/listview_curses.cc +++ b/src/listview_curses.cc @@ -429,7 +429,7 @@ listview_curses::do_update() } wrap_width = width; - if (this->lv_show_scrollbar) { + if (this->lv_show_scrollbar && wrap_width > 0) { wrap_width -= 1; } @@ -541,6 +541,7 @@ listview_curses::do_update() row_overlay_content[overlay_row].with_attr_for_all( VC_ROLE.value(role_t::VCR_CURSOR_LINE)); } + this->lv_display_lines.push_back( overlay_content{row, overlay_row}); mvwattrline(this->lv_window, diff --git a/src/lnav.cc b/src/lnav.cc index de8a2168..237550f9 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -689,7 +689,21 @@ handle_config_ui_key(int ch, const char* keyseq) switch (lnav_data.ld_mode) { case ln_mode_t::FILES: - retval = lnav_data.ld_files_view.handle_key(ch); + if (ch == KEY_CTRL(']')) { + set_view_mode(ln_mode_t::FILE_DETAILS); + lnav_data.ld_mode = ln_mode_t::FILE_DETAILS; + retval = true; + } else { + retval = lnav_data.ld_files_view.handle_key(ch); + } + break; + case ln_mode_t::FILE_DETAILS: + if (ch == KEY_ESCAPE || ch == KEY_CTRL(']')) { + set_view_mode(ln_mode_t::FILES); + retval = true; + } else { + retval = lnav_data.ld_file_details_view.handle_key(ch); + } break; case ln_mode_t::FILTER: retval = lnav_data.ld_filter_view.handle_key(ch); @@ -727,6 +741,7 @@ handle_config_ui_key(int ch, const char* keyseq) } lnav_data.ld_mode = new_mode.value(); lnav_data.ld_files_view.reload_data(); + lnav_data.ld_file_details_view.reload_data(); lnav_data.ld_filter_view.reload_data(); lnav_data.ld_status[LNS_FILTER].set_needs_update(); } else { @@ -760,6 +775,7 @@ handle_key(int ch, const char* keyseq) case ln_mode_t::FILTER: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: return handle_config_ui_key(ch, keyseq); case ln_mode_t::SPECTRO_DETAILS: { @@ -1349,6 +1365,18 @@ VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'), highlight_source_t::THEME); lnav_data.ld_files_view.set_overlay_source(&lnav_data.ld_files_overlay); + lnav_data.ld_file_details_view.set_title("File Details"); + lnav_data.ld_file_details_view.set_selectable(true); + lnav_data.ld_file_details_view.set_window(lnav_data.ld_window); + lnav_data.ld_file_details_view.set_show_scrollbar(true); + lnav_data.ld_file_details_view.set_supports_marks(true); + lnav_data.ld_file_details_view.get_disabled_highlights().insert( + highlight_source_t::THEME); + lnav_data.ld_file_details_view.tc_cursor_role + = role_t::VCR_DISABLED_CURSOR_LINE; + lnav_data.ld_file_details_view.tc_disabled_cursor_role + = role_t::VCR_DISABLED_CURSOR_LINE; + lnav_data.ld_user_message_view.set_window(lnav_data.ld_window); lnav_data.ld_spectro_details_view.set_title("spectro-details"); @@ -1674,8 +1702,11 @@ VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'), break; case ln_mode_t::SEARCH_FILES: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: lnav_data.ld_files_view.set_needs_update(); + lnav_data.ld_file_details_view.set_needs_update(); lnav_data.ld_files_view.do_update(); + lnav_data.ld_file_details_view.do_update(); break; default: break; @@ -1785,6 +1816,7 @@ VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'), case ln_mode_t::PAGING: case ln_mode_t::FILTER: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: case ln_mode_t::SPECTRO_DETAILS: case ln_mode_t::BUSY: if (old_gen @@ -2054,6 +2086,10 @@ VALUES ('org.lnav.mouse-support', -1, DATETIME('now', '+1 minute'), } } } + + if (rescan_future.valid()) { + rescan_future.get(); + } } catch (readline_curses::error& e) { log_error("error: %s", strerror(e.e_err)); } @@ -2303,13 +2339,19 @@ main(int argc, char* argv[]) SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%' )"; + log_info("performing cleanup"); lnav_data.ld_child_pollers.clear(); + log_info("marking files as closed"); for (auto& lf : lnav_data.ld_active_files.fc_files) { lf->close(); } + log_info("rebuilding after closures"); rebuild_indexes(ui_clock::now()); + log_info("clearing file collection"); + lnav_data.ld_active_files.clear(); + log_info("dropping tables"); lnav_data.ld_vtab_manager = nullptr; std::vector tables_to_drop; @@ -2367,6 +2409,8 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%' #endif lnav_data.ld_db.reset(); + + log_info("cleanup finished"); }); #ifdef HAVE_LIBCURL @@ -2882,8 +2926,14 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%' .add_input_delegate(*filter_source); lnav_data.ld_files_view.set_sub_source(&lnav_data.ld_files_source) .add_input_delegate(lnav_data.ld_files_source); + lnav_data.ld_file_details_view.set_sub_source( + &lnav_data.ld_file_details_source); + lnav_data.ld_files_source.fss_details_source + = &lnav_data.ld_file_details_source; lnav_data.ld_user_message_view.set_sub_source( &lnav_data.ld_user_message_source); + auto overlay_menu = std::make_shared(); + lnav_data.ld_file_details_view.set_overlay_source(overlay_menu.get()); for (int lpc = 0; lpc < LNV__MAX; lpc++) { lnav_data.ld_views[lpc].set_gutter_source(new log_gutter_source()); @@ -3541,6 +3591,8 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%' save_session(); } + + log_info("exiting main loop"); } catch (const std::system_error& e) { if (e.code().value() != EPIPE) { fprintf(stderr, "error: %s\n", e.what()); diff --git a/src/lnav.hh b/src/lnav.hh index 56953c6f..b5291198 100644 --- a/src/lnav.hh +++ b/src/lnav.hh @@ -197,8 +197,10 @@ struct lnav_data_t { textview_curses ld_doc_view; textview_curses ld_filter_view; files_sub_source ld_files_source; + plain_text_source ld_file_details_source; files_overlay_source ld_files_overlay; textview_curses ld_files_view; + textview_curses ld_file_details_view; plain_text_source ld_example_source; textview_curses ld_example_view; plain_text_source ld_match_source; diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 4aa8dab0..14d57683 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -3352,7 +3352,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector& args) attr_line_t al; attr_line_builder alb(al); - switch (detect_res) { + switch (detect_res.dffr_file_format) { case file_format_t::ARCHIVE: { auto describe_res = archive_manager::describe(fn); @@ -3450,7 +3450,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector& args) break; } case file_format_t::SQLITE_DB: { - alb.append(fmt::to_string(detect_res)); + alb.append(fmt::to_string(detect_res.dffr_file_format)); break; } case file_format_t::REMOTE: { diff --git a/src/log_format.cc b/src/log_format.cc index f0a464ed..07a1dd62 100644 --- a/src/log_format.cc +++ b/src/log_format.cc @@ -333,9 +333,10 @@ logline_value::origin_in_full_msg(const char* msg, ssize_t len) const return retval; } -logline_value::logline_value(logline_value_meta lvm, - shared_buffer_ref& sbr, - struct line_range origin) +logline_value:: +logline_value(logline_value_meta lvm, + shared_buffer_ref& sbr, + struct line_range origin) : lv_meta(std::move(lvm)), lv_origin(origin) { if (sbr.get_data() == nullptr) { @@ -4028,16 +4029,24 @@ external_log_format::specialized(int fmt_lock) return retval; } -bool +log_format::match_name_result external_log_format::match_name(const std::string& filename) { if (this->elf_filename_pcre.pp_value == nullptr) { - return true; + return name_matched{}; } - return this->elf_filename_pcre.pp_value->find_in(filename) - .ignore_error() - .has_value(); + if (this->elf_filename_pcre.pp_value->find_in(filename) + .ignore_error() + .has_value()) + { + return name_matched{}; + } + + return name_mismatched{ + this->elf_filename_pcre.pp_value->match_partial(filename), + this->elf_filename_pcre.pp_value->get_pattern(), + }; } auto @@ -4294,8 +4303,8 @@ log_format::find_root_format(const char* name) return nullptr; } -log_format::pattern_for_lines::pattern_for_lines(uint32_t pfl_line, - uint32_t pfl_pat_index) +log_format::pattern_for_lines:: +pattern_for_lines(uint32_t pfl_line, uint32_t pfl_pat_index) : pfl_line(pfl_line), pfl_pat_index(pfl_pat_index) { } diff --git a/src/log_format.hh b/src/log_format.hh index 94cc2469..611c1c34 100644 --- a/src/log_format.hh +++ b/src/log_format.hh @@ -406,7 +406,19 @@ public: */ virtual const intern_string_t get_name() const = 0; - virtual bool match_name(const std::string& filename) { return true; } + struct name_matched {}; + struct name_mismatched { + size_t nm_partial; + std::string nm_pattern; + }; + + using match_name_result + = mapbox::util::variant; + + virtual match_name_result match_name(const std::string& filename) + { + return name_matched{}; + } struct scan_match { uint32_t sm_quality; diff --git a/src/log_format_ext.hh b/src/log_format_ext.hh index d025c750..827839e2 100644 --- a/src/log_format_ext.hh +++ b/src/log_format_ext.hh @@ -140,7 +140,7 @@ public: const intern_string_t get_name() const override { return this->elf_name; } - bool match_name(const std::string& filename) override; + match_name_result match_name(const std::string& filename) override; scan_result_t scan(logfile& lf, std::vector& dst, @@ -446,7 +446,6 @@ public: bool jlf_hide_extra{false}; std::vector jlf_line_format; int jlf_line_format_init_count{0}; - shared_buffer jlf_share_manager; logline_value_vector jlf_line_values; off_t jlf_cached_offset{-1}; @@ -457,6 +456,7 @@ public: string_attrs_t jlf_line_attrs; std::shared_ptr jlf_parse_context; std::shared_ptr jlf_yajl_handle; + shared_buffer jlf_share_manager; private: const intern_string_t elf_name; diff --git a/src/logfile.cc b/src/logfile.cc index 082e0496..635afb4f 100644 --- a/src/logfile.cc +++ b/src/logfile.cc @@ -45,6 +45,7 @@ #include "base/date_time_scanner.cfg.hh" #include "base/fs_util.hh" #include "base/injector.hh" +#include "base/snippet_highlighters.hh" #include "base/string_util.hh" #include "config.h" #include "file_options.hh" @@ -56,6 +57,8 @@ #include "piper.looper.hh" #include "yajlpp/yajlpp_def.hh" +using namespace lnav::roles::literals; + static auto intern_lifetime = intern_string::get_table_lifetime(); static const size_t INDEX_RESERVE_INCREMENT = 1024; @@ -140,6 +143,7 @@ logfile::open(std::filesystem::path filename, lf->lf_indexing = lf->lf_options.loo_is_visible; lf->lf_text_format = lf->lf_options.loo_text_format.value_or(text_format_t::TF_UNKNOWN); + lf->lf_format_match_messages = loo.loo_match_details; const auto& hdr = lf->lf_line_buffer.get_header_data(); if (hdr.valid()) { @@ -149,7 +153,9 @@ logfile::open(std::filesystem::path filename, if (!gzhdr.empty()) { lf->lf_embedded_metadata["net.zlib.gzip.header"] = { text_format_t::TF_JSON, - file_header_handlers.to_string(gzhdr), + file_header_handlers.formatter_for(gzhdr) + .with_config(yajl_gen_beautify, 1) + .to_string(), }; } }, @@ -159,7 +165,9 @@ logfile::open(std::filesystem::path filename, lf->lf_embedded_metadata["org.lnav.piper.header"] = { text_format_t::TF_JSON, - lnav::piper::header_handlers.to_string(phdr), + lnav::piper::header_handlers.formatter_for(phdr) + .with_config(yajl_gen_beautify, 1) + .to_string(), }; log_info("setting file name from piper header: %s", phdr.h_name.c_str()); @@ -196,14 +204,15 @@ logfile::open(std::filesystem::path filename, return Ok(lf); } -logfile::logfile(std::filesystem::path filename, - const logfile_open_options& loo) +logfile:: +logfile(std::filesystem::path filename, const logfile_open_options& loo) : lf_filename(std::move(filename)), lf_options(loo) { this->lf_opids.writeAccess()->los_opid_ranges.reserve(64); } -logfile::~logfile() +logfile::~ +logfile() { log_info("destructing logfile: %s", this->lf_filename.c_str()); } @@ -360,12 +369,33 @@ logfile::process_prefix(shared_buffer_ref& sbr, continue; } - if (!curr->match_name(this->lf_filename)) { + auto match_res = curr->match_name(this->lf_filename); + if (match_res.is()) { + auto nm = match_res.get(); if (li.li_file_range.fr_offset == 0) { log_debug("(%s) does not match file name: %s", curr->get_name().get(), this->lf_filename.c_str()); } + auto regex_al = attr_line_t(nm.nm_pattern); + lnav::snippets::regex_highlighter( + regex_al, -1, line_range{0, (int) regex_al.length()}); + auto note = attr_line_t("pattern: ") + .append(regex_al) + .append("\n ") + .append(lnav::roles::quoted_code( + fmt::to_string(this->get_filename()))) + .append("\n") + .append(nm.nm_partial + 2, ' ') + .append("^ matched up to here"_snippet_border); + auto match_um = lnav::console::user_message::info( + attr_line_t() + .append(lnav::roles::identifier( + curr->get_name().to_string())) + .append(" file name pattern required " + "by format does not match")) + .with_note(note); + this->lf_format_match_messages.emplace_back(match_um); this->lf_mismatched_formats.insert(curr->get_name()); continue; } @@ -414,6 +444,20 @@ logfile::process_prefix(shared_buffer_ref& sbr, " scan with format (%s) matched with quality (%d)", curr->get_name().c_str(), sm.sm_quality); + + auto match_um + = lnav::console::user_message::info( + attr_line_t() + .append(lnav::roles::identifier( + curr->get_name().to_string())) + .append(" matched line ") + .append(lnav::roles::number( + fmt::to_string(starting_index_size)))) + .with_note( + attr_line_t("match quality is ") + .append(lnav::roles::number( + fmt::to_string(sm.sm_quality)))); + this->lf_format_match_messages.emplace_back(match_um); if (best_match) { auto starting_iter = std::next( this->lf_index.begin(), starting_index_size); @@ -469,6 +513,14 @@ logfile::process_prefix(shared_buffer_ref& sbr, this->lf_index.size(), curr->get_name().get()); + auto match_um = lnav::console::user_message::ok( + attr_line_t() + .append(lnav::roles::identifier( + winner.first->get_name().to_string())) + .append(" is the best match for line ") + .append(lnav::roles::number( + fmt::to_string(starting_index_size)))); + this->lf_format_match_messages.emplace_back(match_um); this->lf_text_format = text_format_t::TF_LOG; this->lf_format = curr->specialized(); this->lf_format_quality = winner.second.sm_quality; diff --git a/src/logfile.hh b/src/logfile.hh index 788e4d5e..2a081b4b 100644 --- a/src/logfile.hh +++ b/src/logfile.hh @@ -32,6 +32,7 @@ #ifndef logfile_hh #define logfile_hh +#include #include #include #include @@ -49,7 +50,6 @@ #include "bookmarks.hh" #include "byte_array.hh" #include "file_options.hh" -#include #include "line_buffer.hh" #include "log_format_fwd.hh" #include "logfile_fwd.hh" @@ -420,6 +420,16 @@ public: return this->lf_file_options; } + const std::set& get_mismatched_formats() + { + return this->lf_mismatched_formats; + } + + const std::vector& get_format_match_messages() + { + return this->lf_format_match_messages; + } + protected: /** * Process a line from the file. @@ -489,6 +499,7 @@ private: std::map lf_embedded_metadata; size_t lf_file_options_generation{0}; std::optional> lf_file_options; + std::vector lf_format_match_messages; }; class logline_observer { diff --git a/src/logfile_fwd.hh b/src/logfile_fwd.hh index 66447b2b..ae54c515 100644 --- a/src/logfile_fwd.hh +++ b/src/logfile_fwd.hh @@ -75,6 +75,7 @@ struct logfile_open_options_base { std::optional loo_text_format; std::optional loo_piper; file_location_t loo_init_location{mapbox::util::no_init{}}; + std::vector loo_match_details; }; struct logfile_open_options : public logfile_open_options_base { diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc index e247798b..bd83a770 100644 --- a/src/logfile_sub_source.cc +++ b/src/logfile_sub_source.cc @@ -146,7 +146,8 @@ pretty_pipe_callback(exec_context& ec, const std::string& cmdline, auto_fd& fd) return retval; } -logfile_sub_source::logfile_sub_source() +logfile_sub_source:: +logfile_sub_source() : text_sub_source(1), lss_meta_grepper(*this), lss_location_history(*this) { this->tss_supports_filtering = true; @@ -2007,6 +2008,7 @@ logfile_sub_source::remove_file(std::shared_ptr lf) this->lss_force_rebuild = true; } + this->lss_token_file = nullptr; } std::optional @@ -2303,7 +2305,8 @@ logline_window::end() return {this->lw_source, vl}; } -logline_window::logmsg_info::logmsg_info(logfile_sub_source& lss, vis_line_t vl) +logline_window::logmsg_info:: +logmsg_info(logfile_sub_source& lss, vis_line_t vl) : li_source(lss), li_line(vl) { if (this->li_line < vis_line_t(this->li_source.text_line_count())) { @@ -2394,7 +2397,8 @@ logline_window::logmsg_info::get_metadata() const return &bm_iter->second; } -logline_window::logmsg_info::metadata_edit_guard::~metadata_edit_guard() +logline_window::logmsg_info::metadata_edit_guard::~ +metadata_edit_guard() { auto line_number = std::distance(this->meg_logmsg_info.li_file->begin(), this->meg_logmsg_info.li_logline); diff --git a/src/piper.looper.cc b/src/piper.looper.cc index bf33a988..5b361f87 100644 --- a/src/piper.looper.cc +++ b/src/piper.looper.cc @@ -187,6 +187,14 @@ environ_to_map() return retval; } +auto_pipe& +looper::get_wakeup_pipe() +{ + static auto retval = auto_pipe::for_child_fd(-1).unwrap(); + + return retval; +} + looper:: looper(std::string name, auto_fd stdout_fd, auto_fd stderr_fd, options opts) : l_name(std::move(name)), l_cwd(std::filesystem::current_path().string()), @@ -210,8 +218,11 @@ looper(std::string name, auto_fd stdout_fd, auto_fd stderr_fd, options opts) looper::~ looper() { + char ch = '\0'; + log_info("piper destructed, shutting down: %s", this->l_name.c_str()); this->l_looping = false; + log_perror(write(get_wakeup_pipe().write_end(), &ch, 1)); this->l_future.wait(); } @@ -231,7 +242,7 @@ looper::loop() static constexpr auto FILE_TIMEOUT_MAX = 1000ms; const auto& cfg = injector::get(); - struct pollfd pfd[2]; + struct pollfd pfd[3]; struct { line_buffer lb; file_range last_range; @@ -311,6 +322,11 @@ looper::loop() break; } + auto& wakeup_pfd = pfd[used_pfds++]; + wakeup_pfd.fd = get_wakeup_pipe().read_end().get(); + wakeup_pfd.events = POLLIN; + wakeup_pfd.revents = 0; + auto poll_rc = poll(pfd, used_pfds, poll_timeout); if (poll_rc == 0) { // update the timestamp to keep the file alive from any @@ -329,6 +345,11 @@ looper::loop() } else { last_write = std::chrono::system_clock::now(); } + if (!this->l_looping.load()) { + char ch; + + read(get_wakeup_pipe().read_end(), &ch, 1); + } for (auto& cap : captured_fds) { if (cap.lb.get_fd() == -1) { continue; @@ -469,16 +490,20 @@ looper::loop() fmt::format(FMT_STRING("{:?}"), body_sf).c_str()); auto match_res = mmatcher.match(body_sf); + if (!mmatcher.mm_details.empty()) { + this->l_demux_info.writeAccess()->di_details + = mmatcher.mm_details; + } demux_attempted = match_res.match( [this, &curr_demux_def, &cfg]( multiplex_matcher::found f) { curr_demux_def = cfg.c_demux_definitions.find(f.f_id)->second; { - safe::WriteAccess di( - this->l_demux_id); + safe::WriteAccess di( + this->l_demux_info); - di->assign(f.f_id); + di->di_name = f.f_id; } return true; }, @@ -755,7 +780,7 @@ cleanup() } for (auto& entry : to_remove) { - log_debug("removing piper directory: %s", entry.c_str()); + log_info("removing piper directory: %s", entry.c_str()); std::filesystem::remove_all(entry); } }); diff --git a/src/piper.looper.hh b/src/piper.looper.hh index d1e6c90f..e25c7b2f 100644 --- a/src/piper.looper.hh +++ b/src/piper.looper.hh @@ -30,6 +30,7 @@ #ifndef piper_looper_hh #define piper_looper_hh +#include #include #include #include @@ -37,7 +38,6 @@ #include "base/auto_fd.hh" #include "base/piper.file.hh" #include "base/result.h" -#include #include "safe/safe.h" #include "yajlpp/yajlpp_def.hh" @@ -49,7 +49,12 @@ enum class state { finished, }; -using safe_demux_id = safe::Safe; +struct demux_info { + std::string di_name; + std::vector di_details; +}; + +using safe_demux_info = safe::Safe; struct options { bool o_demux{false}; @@ -86,7 +91,10 @@ public: return this->l_out_dir / "out.*"; } - std::string get_demux_id() const { return *this->l_demux_id.readAccess(); } + demux_info get_demux_info() const + { + return *this->l_demux_info.readAccess(); + } std::string get_url() const { @@ -94,10 +102,7 @@ public: this->l_out_dir.filename().string()); } - int get_loop_count() const - { - return this->l_loop_count.load(); - } + int get_loop_count() const { return this->l_loop_count.load(); } bool is_finished() const { @@ -120,6 +125,8 @@ public: private: void loop(); + static auto_pipe& get_wakeup_pipe(); + std::atomic l_looping{true}; const std::string l_name; const std::string l_cwd; @@ -131,7 +138,7 @@ private: std::future l_future; std::atomic l_finished{0}; std::atomic l_loop_count{0}; - safe_demux_id l_demux_id; + safe_demux_info l_demux_info; }; template @@ -154,7 +161,15 @@ public: return this->h_looper->get_out_pattern(); } - std::string get_demux_id() const { return this->h_looper->get_demux_id(); } + std::string get_demux_id() const + { + return this->h_looper->get_demux_info().di_name; + } + + std::vector get_demux_details() const + { + return this->h_looper->get_demux_info().di_details; + } std::string get_url() const { return this->h_looper->get_url(); } diff --git a/src/plain_text_source.cc b/src/plain_text_source.cc index aa99910e..94ea531c 100644 --- a/src/plain_text_source.cc +++ b/src/plain_text_source.cc @@ -143,6 +143,7 @@ plain_text_source& plain_text_source::replace_with(const std::vector& text_lines) { file_off_t off = 0; + this->tds_lines.clear(); for (const auto& al : text_lines) { this->tds_lines.emplace_back(off, al); off += al.length() + 1; diff --git a/src/readline_callbacks.cc b/src/readline_callbacks.cc index 03b7e2f4..03cebf44 100644 --- a/src/readline_callbacks.cc +++ b/src/readline_callbacks.cc @@ -784,6 +784,7 @@ rl_search_internal(readline_curses* rc, ln_mode_t mode, bool complete = false) case ln_mode_t::PAGING: case ln_mode_t::FILTER: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: case ln_mode_t::EXEC: case ln_mode_t::USER: case ln_mode_t::SPECTRO_DETAILS: @@ -876,6 +877,7 @@ rl_callback_int(readline_curses* rc, bool is_alt) case ln_mode_t::PAGING: case ln_mode_t::FILTER: case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: case ln_mode_t::SPECTRO_DETAILS: case ln_mode_t::BUSY: require(0); diff --git a/src/root-config.json b/src/root-config.json index 048eda63..bfb20bbd 100644 --- a/src/root-config.json +++ b/src/root-config.json @@ -33,6 +33,7 @@ "pattern": "^(?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[+\\-]\\d{2}:\\d{2})) source=[a-zA-Z0-9][a-zA-Z0-9_\\.\\-]* (?.*) kubernetes_host=(?[a-zA-Z0-9][a-zA-Z0-9_\\.\\-]*) kubernetes_pod_name=(?[a-zA-Z0-9][a-zA-Z0-9_\\.\\-]*)" }, "container-with-type": { + "enabled": false, "pattern": "^(?[a-zA-Z][\\w\\-]{3,}) (?[a-zA-Z][\\w\\-]{3,}) (?.*)" } } diff --git a/src/text_overlay_menu.cc b/src/text_overlay_menu.cc index dfa11156..11dbd4bb 100644 --- a/src/text_overlay_menu.cc +++ b/src/text_overlay_menu.cc @@ -33,6 +33,7 @@ #include "config.h" #include "lnav.hh" #include "md4cpp.hh" +#include "sysclip.hh" #include "textview_curses.hh" using namespace md4cpp::literals; @@ -50,7 +51,10 @@ text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row) return retval; } + const auto* tss = tc->get_sub_source(); const auto& sti = tc->tc_selected_text.value(); + const auto supports_filtering + = tss != nullptr && tss->tss_supports_filtering; if (sti.sti_line != row) { return retval; @@ -85,25 +89,29 @@ text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row) 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(" "); + if (is_link || supports_filtering) { + if (is_link) { + al.append(":floppy_disk:"_emoji) + .append(" Open in lnav") + .append(" "); + } else { + al.append(" ").append("\u2714 Filter-in"_ok).append(" "); + } + this->los_menu_items.emplace_back( + menu_line, + line_range{start, start + (int) al.length()}, + [is_link, sti](const std::string& value) { + const 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(); } - this->los_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(" "); @@ -125,28 +133,32 @@ 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)); } + menu_line += 1_vl; { attr_line_t 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->los_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(); + if (is_link || supports_filtering) { + if (is_link) { + al.append(":globe_with_meridians:"_emoji).append(" Open "); + } else { + al.append(" ").append("\u2718 Filter-out"_error).append(" "); + } + this->los_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)); @@ -154,7 +166,15 @@ text_overlay_menu::list_overlay_menu(const listview_curses& lv, vis_line_t row) menu_line, line_range{start, start + (int) al.length()}, [](const std::string& value) { - lnav_data.ld_exec_context.execute("|lnav-copy-text"); + auto clip_res = sysclip::open(sysclip::type_t::GENERAL); + if (clip_res.isErr()) { + log_error("unable to open clipboard: %s", + clip_res.unwrapErr().c_str()); + return; + } + + auto clip_pipe = clip_res.unwrap(); + fwrite(value.c_str(), 1, value.length(), clip_pipe.in()); }); retval.emplace_back(attr_line_t().pad_to(left).append(al)); } diff --git a/src/textview_curses.cc b/src/textview_curses.cc index 0e7a8236..635f4d3c 100644 --- a/src/textview_curses.cc +++ b/src/textview_curses.cc @@ -462,7 +462,6 @@ textview_curses::handle_mouse(mouse_event& me) switch (me.me_state) { case mouse_button_state_t::BUTTON_STATE_PRESSED: { - this->tc_text_selection_active = true; this->tc_press_line = mouse_line; this->tc_press_left = this->lv_left + me.me_press_x; if (!this->lv_selectable) { @@ -470,6 +469,7 @@ textview_curses::handle_mouse(mouse_event& me) } mouse_line.match( [this, &me, sub_delegate, &mouse_line](const main_content& mc) { + this->tc_text_selection_active = true; if (this->vc_enabled) { if (this->tc_supports_marks && me.me_button == mouse_button_t::BUTTON_LEFT @@ -492,9 +492,7 @@ textview_curses::handle_mouse(mouse_event& me) } }, [](const overlay_menu& om) {}, - [](const static_overlay_content& soc) { - - }, + [](const static_overlay_content& soc) {}, [this](const overlay_content& oc) { this->set_overlay_selection(oc.oc_line); }, @@ -574,15 +572,9 @@ textview_curses::handle_mouse(mouse_event& me) sub_delegate->text_handle_mouse(*this, mouse_line, me); } }, - [](const static_overlay_content& soc) { - - }, - [](const overlay_menu& om) { - - }, - [](const overlay_content& oc) { - - }, + [](const static_overlay_content& soc) {}, + [](const overlay_menu& om) {}, + [](const overlay_content& oc) {}, [](const empty_space& es) {}); break; } @@ -658,19 +650,16 @@ textview_curses::handle_mouse(mouse_event& me) if (ov != nullptr && mouse_line.is() && this->tc_selected_text) { - auto* los = dynamic_cast(ov); - if (los != nullptr) { - auto& om = mouse_line.get(); - auto& sti = this->tc_selected_text.value(); + auto& om = mouse_line.get(); + auto& sti = this->tc_selected_text.value(); - for (const auto& mi : los->los_menu_items) { - if (om.om_line == mi.mi_line - && me.is_click_in(mouse_button_t::BUTTON_LEFT, - mi.mi_range)) - { - mi.mi_action(sti.sti_value); - break; - } + for (const auto& mi : ov->los_menu_items) { + if (om.om_line == mi.mi_line + && me.is_click_in(mouse_button_t::BUTTON_LEFT, + mi.mi_range)) + { + mi.mi_action(sti.sti_value); + break; } } } @@ -848,11 +837,11 @@ void textview_curses::execute_search(const std::string& regex_orig) { std::string regex = regex_orig; - std::shared_ptr code; if ((this->tc_search_child == nullptr) || (regex != this->tc_current_search)) { + std::shared_ptr code; this->match_reset(); this->tc_search_child.reset(); diff --git a/src/timeline_source.cc b/src/timeline_source.cc index f3cda50d..b88dca18 100644 --- a/src/timeline_source.cc +++ b/src/timeline_source.cc @@ -175,7 +175,7 @@ timeline_preview_overlay::list_overlay_menu(const listview_curses& lv, } timeline_header_overlay:: -timeline_header_overlay(std::shared_ptr src) +timeline_header_overlay(const std::shared_ptr& src) : gho_src(src) { } diff --git a/src/timeline_source.hh b/src/timeline_source.hh index ac5e3441..38403ad0 100644 --- a/src/timeline_source.hh +++ b/src/timeline_source.hh @@ -187,7 +187,8 @@ public: class timeline_header_overlay : public text_overlay_menu { public: - explicit timeline_header_overlay(std::shared_ptr src); + explicit timeline_header_overlay( + const std::shared_ptr& src); bool list_static_overlay(const listview_curses& lv, int y, diff --git a/src/view_helpers.cc b/src/view_helpers.cc index bae831f8..a2f0397c 100644 --- a/src/view_helpers.cc +++ b/src/view_helpers.cc @@ -617,6 +617,7 @@ handle_winch() lnav_data.ld_match_view.set_needs_update(); lnav_data.ld_filter_view.set_needs_update(); lnav_data.ld_files_view.set_needs_update(); + lnav_data.ld_file_details_view.set_needs_update(); lnav_data.ld_spectro_details_view.set_needs_update(); lnav_data.ld_timeline_details_view.set_needs_update(); lnav_data.ld_user_message_view.set_needs_update(); @@ -627,6 +628,9 @@ handle_winch() void layout_views() { + static constexpr auto FILES_FOCUSED_WIDTH = 40; + static constexpr auto FILES_BLURRED_WIDTH = 20; + static auto* breadcrumb_view = injector::get(); int width, height; getmaxyx(lnav_data.ld_window, height, width); @@ -698,13 +702,31 @@ layout_views() auto config_panel_open = (lnav_data.ld_mode == ln_mode_t::FILTER || lnav_data.ld_mode == ln_mode_t::FILES + || lnav_data.ld_mode == ln_mode_t::FILE_DETAILS || lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS || lnav_data.ld_mode == ln_mode_t::SEARCH_FILES); auto filters_open = (lnav_data.ld_mode == ln_mode_t::FILTER || lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS); auto files_open = (lnav_data.ld_mode == ln_mode_t::FILES + || lnav_data.ld_mode == ln_mode_t::FILE_DETAILS || lnav_data.ld_mode == ln_mode_t::SEARCH_FILES); - int filter_height = config_panel_open ? 5 : 0; + auto files_width = lnav_data.ld_mode == ln_mode_t::FILES + ? FILES_FOCUSED_WIDTH + : FILES_BLURRED_WIDTH; + int filter_height; + + switch (lnav_data.ld_mode) { + case ln_mode_t::FILES: + case ln_mode_t::FILTER: + filter_height = 5; + break; + case ln_mode_t::FILE_DETAILS: + filter_height = 15; + break; + default: + filter_height = 0; + break; + } bool breadcrumb_open = (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS); @@ -809,9 +831,16 @@ layout_views() lnav_data.ld_files_view.set_height(vis_line_t(filter_height)); lnav_data.ld_files_view.set_y(bottom + 2); - lnav_data.ld_files_view.set_width(width); + lnav_data.ld_files_view.set_width(files_width); lnav_data.ld_files_view.set_visible(files_open && vis); + lnav_data.ld_file_details_view.set_height(vis_line_t(filter_height)); + lnav_data.ld_file_details_view.set_y(bottom + 2); + lnav_data.ld_file_details_view.set_x(files_width); + lnav_data.ld_file_details_view.set_width( + std::clamp(width - files_width, 0, width)); + lnav_data.ld_file_details_view.set_visible(files_open && vis); + lnav_data.ld_status[LNS_FILTER_HELP].set_visible(config_panel_open && vis); lnav_data.ld_status[LNS_FILTER_HELP].set_y(bottom + 1); lnav_data.ld_status[LNS_FILTER_HELP].set_width(width); @@ -1265,6 +1294,8 @@ get_textview_for_mode(ln_mode_t mode) case ln_mode_t::SEARCH_FILES: case ln_mode_t::FILES: return &lnav_data.ld_files_view; + case ln_mode_t::FILE_DETAILS: + return &lnav_data.ld_file_details_view; case ln_mode_t::SPECTRO_DETAILS: case ln_mode_t::SEARCH_SPECTRO_DETAILS: return &lnav_data.ld_spectro_details_view; @@ -1435,6 +1466,11 @@ set_view_mode(ln_mode_t mode) lnav_data.ld_view_stack.set_needs_update(); break; } + case ln_mode_t::FILE_DETAILS: { + lnav_data.ld_file_details_view.tc_cursor_role + = role_t::VCR_DISABLED_CURSOR_LINE; + break; + } default: break; } @@ -1443,6 +1479,12 @@ set_view_mode(ln_mode_t mode) breadcrumb_view->focus(); break; } + case ln_mode_t::FILE_DETAILS: { + lnav_data.ld_status[LNS_FILTER].set_needs_update(); + lnav_data.ld_file_details_view.tc_cursor_role + = role_t::VCR_CURSOR_LINE; + break; + } default: break; } @@ -1464,6 +1506,7 @@ all_views() retval.push_back(&lnav_data.ld_example_view); retval.push_back(&lnav_data.ld_preview_view[0]); retval.push_back(&lnav_data.ld_preview_view[1]); + retval.push_back(&lnav_data.ld_file_details_view); retval.push_back(&lnav_data.ld_files_view); retval.push_back(&lnav_data.ld_filter_view); retval.push_back(&lnav_data.ld_user_message_view); @@ -1478,7 +1521,7 @@ void lnav_behavior::mouse_event(int button, bool release, int x, int y) { static auto* breadcrumb_view = injector::get(); - static const std::vector VIEWS = all_views(); + static const auto VIEWS = all_views(); static const auto CLICK_INTERVAL = std::chrono::milliseconds(mouseinterval(-1) * 2); @@ -1552,6 +1595,7 @@ lnav_behavior::mouse_event(int button, bool release, int x, int y) case ln_mode_t::PAGING: break; case ln_mode_t::FILES: + case ln_mode_t::FILE_DETAILS: case ln_mode_t::FILTER: // Clicking on the main view when the config panels are // open should return us to paging. diff --git a/src/view_helpers.hh b/src/view_helpers.hh index e4ec67af..28604265 100644 --- a/src/view_helpers.hh +++ b/src/view_helpers.hh @@ -64,6 +64,7 @@ enum class ln_mode_t : int { BREADCRUMBS, FILTER, FILES, + FILE_DETAILS, SPECTRO_DETAILS, SEARCH_SPECTRO_DETAILS, COMMAND, @@ -96,8 +97,8 @@ std::optional next_cluster( const, const bookmark_type_t* bt, vis_line_t top); -bool moveto_cluster(std::optional ( - bookmark_vector::*f)(vis_line_t) const, +bool moveto_cluster(std::optional (bookmark_vector::*f)( + vis_line_t) const, const bookmark_type_t* bt, vis_line_t top); vis_line_t search_forward_from(textview_curses* tc); diff --git a/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out b/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out index 2501e195..6e26c1df 100644 --- a/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out +++ b/test/expected/test_cli.sh_0b3639753916f71254e8c9cce4ebb8bfd9978d3e.out @@ -5256,7 +5256,7 @@ "control-pattern": "" }, "container-with-type": { - "enabled": true, + "enabled": false, "pattern": "^(?[a-zA-Z][\\w\\-]{3,}) (?[a-zA-Z][\\w\\-]{3,}) (?.*)", "control-pattern": "" }, diff --git a/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out b/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out index dc3277fe..fb1b0cb8 100644 --- a/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out +++ b/test/expected/test_cli.sh_cc06341dd560f927512e92c7c0985ed8b25827ae.out @@ -19,49 +19,50 @@ /log/annotations/org.lnav.test/description -> {test_dir}/configs/installed/anno-test.json:6 /log/annotations/org.lnav.test/handler -> {test_dir}/configs/installed/anno-test.json:8 /log/date-time/convert-zoned-to-local -> root-config.json:18 -/log/demux/container-with-type/pattern -> root-config.json:36 +/log/demux/container-with-type/enabled -> root-config.json:36 +/log/demux/container-with-type/pattern -> root-config.json:37 /log/demux/container/pattern -> root-config.json:29 /log/demux/recv-with-pod/control-pattern -> root-config.json:32 /log/demux/recv-with-pod/pattern -> root-config.json:33 -/tuning/archive-manager/cache-ttl -> root-config.json:43 -/tuning/archive-manager/min-free-space -> root-config.json:42 -/tuning/clipboard/impls/MacOS/find/read -> root-config.json:71 -/tuning/clipboard/impls/MacOS/find/write -> root-config.json:70 -/tuning/clipboard/impls/MacOS/general/read -> root-config.json:67 -/tuning/clipboard/impls/MacOS/general/write -> root-config.json:66 -/tuning/clipboard/impls/MacOS/test -> root-config.json:64 -/tuning/clipboard/impls/NeoVim/general/read -> root-config.json:99 -/tuning/clipboard/impls/NeoVim/general/write -> root-config.json:98 -/tuning/clipboard/impls/NeoVim/test -> root-config.json:96 -/tuning/clipboard/impls/Wayland/general/read -> root-config.json:78 -/tuning/clipboard/impls/Wayland/general/write -> root-config.json:77 -/tuning/clipboard/impls/Wayland/test -> root-config.json:75 -/tuning/clipboard/impls/Windows/general/write -> root-config.json:105 -/tuning/clipboard/impls/Windows/test -> root-config.json:103 -/tuning/clipboard/impls/X11-xclip/general/read -> root-config.json:85 -/tuning/clipboard/impls/X11-xclip/general/write -> root-config.json:84 -/tuning/clipboard/impls/X11-xclip/test -> root-config.json:82 -/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 -/tuning/remote/ssh/command -> root-config.json:47 -/tuning/remote/ssh/config/BatchMode -> root-config.json:49 -/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:127 -/tuning/url-scheme/docker/handler -> root-config.json:124 +/tuning/archive-manager/cache-ttl -> root-config.json:44 +/tuning/archive-manager/min-free-space -> root-config.json:43 +/tuning/clipboard/impls/MacOS/find/read -> root-config.json:72 +/tuning/clipboard/impls/MacOS/find/write -> root-config.json:71 +/tuning/clipboard/impls/MacOS/general/read -> root-config.json:68 +/tuning/clipboard/impls/MacOS/general/write -> root-config.json:67 +/tuning/clipboard/impls/MacOS/test -> root-config.json:65 +/tuning/clipboard/impls/NeoVim/general/read -> root-config.json:100 +/tuning/clipboard/impls/NeoVim/general/write -> root-config.json:99 +/tuning/clipboard/impls/NeoVim/test -> root-config.json:97 +/tuning/clipboard/impls/Wayland/general/read -> root-config.json:79 +/tuning/clipboard/impls/Wayland/general/write -> root-config.json:78 +/tuning/clipboard/impls/Wayland/test -> root-config.json:76 +/tuning/clipboard/impls/Windows/general/write -> root-config.json:106 +/tuning/clipboard/impls/Windows/test -> root-config.json:104 +/tuning/clipboard/impls/X11-xclip/general/read -> root-config.json:86 +/tuning/clipboard/impls/X11-xclip/general/write -> root-config.json:85 +/tuning/clipboard/impls/X11-xclip/test -> root-config.json:83 +/tuning/clipboard/impls/tmux/general/read -> root-config.json:93 +/tuning/clipboard/impls/tmux/general/write -> root-config.json:92 +/tuning/clipboard/impls/tmux/test -> root-config.json:90 +/tuning/external-opener/impls/MacOS/command -> root-config.json:115 +/tuning/external-opener/impls/MacOS/test -> root-config.json:114 +/tuning/external-opener/impls/XDG/command -> root-config.json:119 +/tuning/external-opener/impls/XDG/test -> root-config.json:118 +/tuning/piper/max-size -> root-config.json:58 +/tuning/piper/rotations -> root-config.json:59 +/tuning/piper/ttl -> root-config.json:60 +/tuning/remote/ssh/command -> root-config.json:48 +/tuning/remote/ssh/config/BatchMode -> root-config.json:50 +/tuning/remote/ssh/config/ConnectTimeout -> root-config.json:51 +/tuning/remote/ssh/start-command -> root-config.json:53 +/tuning/remote/ssh/transfer-command -> root-config.json:54 +/tuning/url-scheme/docker-compose/handler -> root-config.json:128 +/tuning/url-scheme/docker/handler -> root-config.json:125 /tuning/url-scheme/hw/handler -> {test_dir}/configs/installed/hw-url-handler.json:6 -/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 +/tuning/url-scheme/journald/handler -> root-config.json:131 +/tuning/url-scheme/piper/handler -> root-config.json:134 +/tuning/url-scheme/podman/handler -> root-config.json:137 /ui/clock-format -> root-config.json:4 /ui/default-colors -> root-config.json:6 /ui/dim-text -> root-config.json:5 diff --git a/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out index 683602c9..c912584a 100644 --- a/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out +++ b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out @@ -1,2 +1,2 @@ -basename(filepath)   descriptor   mimetype   content   -logfile_syslog.1.gz net.zlib.gzip.header application/json {"name":"logfile_syslog.1","mtime":"2007-11-03T09:23:00.000","comment":""} +basename(filepath)   descriptor   mimetype   content   +logfile_syslog.1.gz net.zlib.gzip.header application/json {␊ "name": "logfile_syslog.1",␊ "mtime": "2007-11-03T09:23:00.000",␊ "comment": ""␊}␊