diff options
Diffstat (limited to '')
51 files changed, 3181 insertions, 697 deletions
diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index aa4143f..2ff04c6 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -4,7 +4,9 @@ add_library( ansi_scrubber.cc attr_line.cc attr_line.builder.cc + auto_fd.cc auto_pid.cc + color_spaces.cc date_time_scanner.cc fs_util.cc humanize.cc @@ -18,6 +20,7 @@ add_library( lnav_log.cc network.tcp.cc paths.cc + piper.file.cc snippet_highlighters.cc string_attr_type.cc string_util.cc @@ -25,14 +28,18 @@ add_library( time_util.cc ansi_scrubber.hh + ansi_vars.hh attr_line.hh attr_line.builder.hh auto_fd.hh auto_mem.hh auto_pid.hh bus.hh + color_spaces.hh + date_time_scanner.cfg.hh date_time_scanner.hh enum_util.hh + from_trait.hh fs_util.hh func_util.hh future_util.hh @@ -45,26 +52,32 @@ add_library( is_utf8.hh isc.hh itertools.hh + keycodes.hh + line_range.hh lnav.console.hh lnav.console.into.hh log_level_enum.hh lrucache.hpp + map_util.hh math_util.hh network.tcp.hh paths.hh + piper.file.hh result.h snippet_highlighters.hh string_attr_type.hh strnatcmp.h time_util.hh + types.hh ../third-party/xxHash/xxhash.h ../third-party/xxHash/xxhash.c ) target_include_directories(base PUBLIC . .. ../third-party + ../third-party/date/include ${CMAKE_CURRENT_BINARY_DIR}/..) -target_link_libraries(base cppfmt cppscnlib pcrepp ncurses::libcurses pthread) +target_link_libraries(base cppfmt cppscnlib pcrepp ncurses::libcurses pthread lnavdt datepp) add_executable( test_base diff --git a/src/base/Makefile.am b/src/base/Makefile.am index 4a459a6..105913f 100644 --- a/src/base/Makefile.am +++ b/src/base/Makefile.am @@ -7,6 +7,7 @@ AM_CPPFLAGS = \ -I$(top_srcdir)/src/ \ -I$(top_srcdir)/src/third-party \ -I$(top_srcdir)/src/fmtlib \ + -I$(top_srcdir)/src/third-party/date/include \ -I$(top_srcdir)/src/third-party/scnlib/include \ $(LIBARCHIVE_CFLAGS) \ $(READLINE_CFLAGS) \ @@ -22,15 +23,19 @@ noinst_LIBRARIES = libbase.a noinst_HEADERS = \ ansi_scrubber.hh \ + ansi_vars.hh \ attr_line.hh \ attr_line.builder.hh \ auto_fd.hh \ auto_mem.hh \ auto_pid.hh \ bus.hh \ + color_spaces.hh \ + date_time_scanner.cfg.hh \ date_time_scanner.hh \ enum_util.hh \ file_range.hh \ + from_trait.hh \ fs_util.hh \ func_util.hh \ future_util.hh \ @@ -43,28 +48,35 @@ noinst_HEADERS = \ is_utf8.hh \ isc.hh \ itertools.hh \ + keycodes.hh \ + line_range.hh \ lnav_log.hh \ lnav.console.hh \ lnav.console.into.hh \ lnav.gzip.hh \ log_level_enum.hh \ lrucache.hpp \ + map_util.hh \ math_util.hh \ network.tcp.hh \ opt_util.hh \ paths.hh \ + piper.file.hh \ result.h \ snippet_highlighters.hh \ string_attr_type.hh \ string_util.hh \ strnatcmp.h \ - time_util.hh + time_util.hh \ + types.hh libbase_a_SOURCES = \ ansi_scrubber.cc \ attr_line.cc \ attr_line.builder.cc \ + auto_fd.cc \ auto_pid.cc \ + color_spaces.cc \ date_time_scanner.cc \ fs_util.cc \ humanize.cc \ @@ -78,6 +90,7 @@ libbase_a_SOURCES = \ lnav_log.cc \ network.tcp.cc \ paths.cc \ + piper.file.cc \ snippet_highlighters.cc \ string_attr_type.cc \ string_util.cc \ @@ -103,6 +116,7 @@ test_base_SOURCES = \ test_base_LDADD = \ libbase.a \ ../fmtlib/libcppfmt.a \ + ../third-party/date/src/libdatepp.a \ ../third-party/scnlib/src/libscnlib.a \ ../pcrepp/libpcrepp.a diff --git a/src/base/ansi_scrubber.cc b/src/base/ansi_scrubber.cc index 98f6c96..ec34328 100644 --- a/src/base/ansi_scrubber.cc +++ b/src/base/ansi_scrubber.cc @@ -33,6 +33,8 @@ #include "ansi_scrubber.hh" +#include "ansi_vars.hh" +#include "base/lnav_log.hh" #include "base/opt_util.hh" #include "config.h" #include "pcrepp/pcre2pp.hh" @@ -43,7 +45,7 @@ static const lnav::pcre2pp::code& ansi_regex() { static const auto retval = lnav::pcre2pp::code::from_const( - "\x1b\\[([\\d=;\\?]*)([a-zA-Z])|(?:\\X\x08\\X)+"); + R"(\x1b\[([\d=;\?]*)([a-zA-Z])|\x1b\](\d+);(.*?)(?:\x07|\x1b\\)|(?:\X\x08\X)+|(\x16+))"); return retval; } @@ -120,11 +122,18 @@ void scrub_ansi_string(std::string& str, string_attrs_t* sa) { static thread_local auto md = lnav::pcre2pp::match_data::unitialized(); - const auto& regex = ansi_regex(); - int64_t origin_offset = 0; - int last_origin_offset_end = 0; + static const auto semi_pred = string_fragment::tag1{';'}; - replace(str.begin(), str.end(), '\0', ' '); + const auto& regex = ansi_regex(); + nonstd::optional<std::string> href; + size_t href_start = 0; + string_attrs_t tmp_sa; + size_t cp_dst = std::string::npos; + size_t cp_start = std::string::npos; + int last_origin_end = 0; + int erased = 0; + + std::replace(str.begin(), str.end(), '\0', ' '); auto matcher = regex.capture_from(str).into(md); while (true) { auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK); @@ -140,13 +149,22 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) const auto sf = md[0].value(); auto bs_index_res = sf.codepoint_to_byte_index(1); + if (cp_dst != std::string::npos) { + auto cp_len = sf.sf_begin - cp_start; + memmove(&str[cp_dst], &str[cp_start], cp_len); + cp_dst += cp_len; + } else { + cp_dst = sf.sf_begin; + } + if (sf.length() >= 3 && bs_index_res.isOk() && sf[bs_index_res.unwrap()] == '\b') { - ssize_t fill_index = sf.sf_begin; + ssize_t fill_index = cp_dst; line_range bold_range; line_range ul_range; auto sub_sf = sf; + auto mid_sf = string_fragment(); while (!sub_sf.empty()) { auto lhs_opt = sub_sf.consume_codepoint(); @@ -164,12 +182,13 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) return; } auto rhs_pair = rhs_opt.value(); - sub_sf = rhs_pair.second; if (lhs_pair.first == '_' || rhs_pair.first == '_') { if (sa != nullptr && bold_range.is_valid()) { - sa->emplace_back(bold_range, - VC_STYLE.value(text_attrs{A_BOLD})); + shift_string_attrs( + *sa, bold_range.lr_start, -bold_range.length() * 2); + tmp_sa.emplace_back(bold_range, + VC_STYLE.value(text_attrs{A_BOLD})); bold_range.clear(); } if (ul_range.is_valid()) { @@ -183,9 +202,13 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) ww898::utf::utf8::write(cp, [&str, &fill_index](auto ch) { str[fill_index++] = ch; }); - } else { + } else if (lhs_pair.first == rhs_pair.first + && !fmt::v10::detail::needs_escape(lhs_pair.first)) + { if (sa != nullptr && ul_range.is_valid()) { - sa->emplace_back( + shift_string_attrs( + *sa, ul_range.lr_start, -ul_range.length() * 2); + tmp_sa.emplace_back( ul_range, VC_STYLE.value(text_attrs{A_UNDERLINE})); ul_range.clear(); } @@ -204,57 +227,91 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) log_error("invalid UTF-8 at %d", sf.sf_begin); return; } + } else { + mid_sf = mid_pair.second; + break; } + sub_sf = rhs_pair.second; } - auto output_size = fill_index - sf.sf_begin; - auto erased_size = sf.length() - output_size; - - if (sa != nullptr) { -#if 0 - shift_string_attrs( - *sa, caps->c_begin + sf.length() / 3, -erased_size); -#endif - sa->emplace_back(line_range{last_origin_offset_end, - sf.sf_begin + (int) output_size}, - SA_ORIGIN_OFFSET.value(origin_offset)); - } - + auto output_size = fill_index - cp_dst; if (sa != nullptr && ul_range.is_valid()) { - sa->emplace_back(ul_range, - VC_STYLE.value(text_attrs{A_UNDERLINE})); + shift_string_attrs( + *sa, ul_range.lr_start, -ul_range.length() * 2); + tmp_sa.emplace_back(ul_range, + VC_STYLE.value(text_attrs{A_UNDERLINE})); ul_range.clear(); } if (sa != nullptr && bold_range.is_valid()) { - sa->emplace_back(bold_range, - VC_STYLE.value(text_attrs{A_BOLD})); + shift_string_attrs( + *sa, bold_range.lr_start, -bold_range.length() * 2); + tmp_sa.emplace_back(bold_range, + VC_STYLE.value(text_attrs{A_BOLD})); bold_range.clear(); } - - str.erase(str.begin() + fill_index, str.begin() + sf.sf_end); - last_origin_offset_end = sf.sf_begin + output_size; - origin_offset += erased_size; - matcher.reload_input(str, last_origin_offset_end); + if (sa != nullptr && output_size > 0 && cp_dst > 0) { + tmp_sa.emplace_back( + line_range{ + (int) last_origin_end, + (int) cp_dst + (int) output_size, + }, + SA_ORIGIN_OFFSET.value(erased)); + } + last_origin_end = cp_dst + output_size; + cp_dst = fill_index; + cp_start = sub_sf.sf_begin; + erased += sf.length() - output_size; continue; } - auto seq = md[1].value(); - auto terminator = md[2].value(); struct line_range lr; - bool has_attrs = false; text_attrs attrs; + bool has_attrs = false; nonstd::optional<role_t> role; - size_t lpc; - - switch (terminator[0]) { - case 'm': - for (lpc = seq.sf_begin; - lpc != std::string::npos && lpc < (size_t) seq.sf_end;) - { - auto ansi_code_res = scn::scan_value<int>( - scn::string_view{&str[lpc], &str[seq.sf_end]}); - if (ansi_code_res) { + if (md[3]) { + auto osc_id = scn::scan_value<int32_t>(md[3]->to_string_view()); + + if (osc_id) { + switch (osc_id.value()) { + case 8: + auto split_res = md[4]->split_pair(semi_pred); + if (split_res) { + // auto params = split_res->first; + auto uri = split_res->second; + + if (href) { + if (sa != nullptr) { + tmp_sa.emplace_back( + line_range{ + (int) href_start, + (int) cp_dst, + }, + VC_HYPERLINK.value(href.value())); + } + href = nonstd::nullopt; + } + if (!uri.empty()) { + href = uri.to_string(); + href_start = cp_dst; + } + } + break; + } + } + } else if (md[1]) { + auto seq = md[1].value(); + auto terminator = md[2].value(); + + switch (terminator[0]) { + case 'm': + while (!seq.empty()) { + auto ansi_code_res + = scn::scan_value<int>(seq.to_string_view()); + + if (!ansi_code_res) { + break; + } auto ansi_code = ansi_code_res.value(); if (90 <= ansi_code && ansi_code <= 97) { ansi_code -= 60; @@ -266,6 +323,39 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) if (40 <= ansi_code && ansi_code <= 47) { attrs.ta_bg_color = ansi_code - 40; } + if (ansi_code == 38 || ansi_code == 48) { + auto color_code_pair + = seq.split_when(semi_pred).second.split_pair( + semi_pred); + if (!color_code_pair) { + break; + } + auto color_type = scn::scan_value<int>( + color_code_pair->first.to_string_view()); + if (!color_type.has_value()) { + break; + } + if (color_type.value() == 2) { + } else if (color_type.value() == 5) { + auto color_index_pair + = color_code_pair->second.split_when( + semi_pred); + auto color_index = scn::scan_value<short>( + color_index_pair.first.to_string_view()); + if (!color_index.has_value() + || color_index.value() < 0 + || color_index.value() > 255) + { + break; + } + if (ansi_code == 38) { + attrs.ta_fg_color = color_index.value(); + } else { + attrs.ta_bg_color = color_index.value(); + } + seq = color_index_pair.second; + } + } switch (ansi_code) { case 1: attrs.ta_attrs |= A_BOLD; @@ -283,90 +373,107 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa) attrs.ta_attrs |= A_REVERSE; break; } + auto split_pair = seq.split_when(semi_pred); + seq = split_pair.second; } - lpc = str.find(';', lpc); - if (lpc != std::string::npos) { - lpc += 1; - } - } - has_attrs = true; - break; + has_attrs = true; + break; - case 'C': { - auto spaces_res - = scn::scan_value<unsigned int>(seq.to_string_view()); +#if 0 + case 'C': { + auto spaces_res + = scn::scan_value<unsigned int>(seq.to_string_view()); - if (spaces_res && spaces_res.value() > 0) { - str.insert((std::string::size_type) sf.sf_end, - spaces_res.value(), - ' '); + if (spaces_res && spaces_res.value() > 0) { + str.insert((std::string::size_type) sf.sf_end, + spaces_res.value(), + ' '); + } + break; } - break; - } - case 'H': { - unsigned int row = 0, spaces = 0; + case 'H': { + unsigned int row = 0, spaces = 0; - if (scn::scan(seq.to_string_view(), "{};{}", row, spaces) - && spaces > 1) - { - int ispaces = spaces - 1; - if (ispaces > sf.sf_begin) { - str.insert((unsigned long) sf.sf_end, - ispaces - sf.sf_begin, - ' '); + if (scn::scan(seq.to_string_view(), "{};{}", row, spaces) + && spaces > 1) + { + int ispaces = spaces - 1; + if (ispaces > sf.sf_begin) { + str.insert((unsigned long) sf.sf_end, + ispaces - sf.sf_begin, + ' '); + } } + break; } - break; - } +#endif - case 'O': { - auto role_res = scn::scan_value<int>(seq.to_string_view()); + case 'O': { + auto role_res = scn::scan_value<int>(seq.to_string_view()); - if (role_res) { - role_t role_tmp = (role_t) role_res.value(); - if (role_tmp > role_t::VCR_NONE - && role_tmp < role_t::VCR__MAX) - { - role = role_tmp; - has_attrs = true; + if (role_res) { + role_t role_tmp = (role_t) role_res.value(); + if (role_tmp > role_t::VCR_NONE + && role_tmp < role_t::VCR__MAX) + { + role = role_tmp; + has_attrs = true; + } } + break; } - break; } } - str.erase(str.begin() + sf.sf_begin, str.begin() + sf.sf_end); - if (sa != nullptr) { - shift_string_attrs(*sa, sf.sf_begin, -sf.length()); - - if (has_attrs) { - for (auto rit = sa->rbegin(); rit != sa->rend(); rit++) { - if (rit->sa_range.lr_end != -1) { - continue; + if (md[1] || md[3] || md[5]) { + if (sa != nullptr) { + shift_string_attrs(*sa, sf.sf_begin, -sf.length()); + + if (has_attrs) { + for (auto rit = tmp_sa.rbegin(); rit != tmp_sa.rend(); + rit++) + { + if (rit->sa_range.lr_end != -1) { + continue; + } + rit->sa_range.lr_end = cp_dst; + } + lr.lr_start = cp_dst; + lr.lr_end = -1; + if (!attrs.empty()) { + tmp_sa.emplace_back(lr, VC_STYLE.value(attrs)); } - rit->sa_range.lr_end = sf.sf_begin; + role | [&lr, &tmp_sa](role_t r) { + tmp_sa.emplace_back(lr, VC_ROLE.value(r)); + }; } - lr.lr_start = sf.sf_begin; - lr.lr_end = -1; - if (attrs.ta_attrs || attrs.ta_fg_color || attrs.ta_bg_color) { - sa->emplace_back(lr, VC_STYLE.value(attrs)); + if (cp_dst > 0) { + tmp_sa.emplace_back( + line_range{ + (int) last_origin_end, + (int) cp_dst, + }, + SA_ORIGIN_OFFSET.value(erased)); } - role | [&lr, &sa](role_t r) { - sa->emplace_back(lr, VC_ROLE.value(r)); - }; + last_origin_end = cp_dst; } - sa->emplace_back(line_range{last_origin_offset_end, sf.sf_begin}, - SA_ORIGIN_OFFSET.value(origin_offset)); - last_origin_offset_end = sf.sf_begin; - origin_offset += sf.length(); + erased += sf.length(); } - - matcher.reload_input(str, sf.sf_begin); + cp_start = sf.sf_end; } - if (sa != nullptr && last_origin_offset_end > 0) { - sa->emplace_back(line_range{last_origin_offset_end, (int) str.size()}, - SA_ORIGIN_OFFSET.value(origin_offset)); + if (cp_dst != std::string::npos) { + auto cp_len = str.size() - cp_start; + memmove(&str[cp_dst], &str[cp_start], cp_len); + cp_dst += cp_len; + str.resize(cp_dst); + } + if (sa != nullptr && last_origin_end > 0 && last_origin_end != str.size()) { + tmp_sa.emplace_back(line_range{(int) last_origin_end, (int) str.size()}, + SA_ORIGIN_OFFSET.value(erased)); + } + if (sa != nullptr) { + sa->insert(sa->end(), tmp_sa.begin(), tmp_sa.end()); } } diff --git a/src/base/ansi_scrubber.hh b/src/base/ansi_scrubber.hh index b832e17..ce4eadf 100644 --- a/src/base/ansi_scrubber.hh +++ b/src/base/ansi_scrubber.hh @@ -36,7 +36,6 @@ #include <string> #include "attr_line.hh" -#include "shlex.resolver.hh" #define ANSI_CSI "\x1b[" #define ANSI_CHAR_ATTR "m" @@ -66,10 +65,4 @@ void scrub_ansi_string(std::string& str, string_attrs_t* sa); size_t erase_ansi_escapes(string_fragment input); -/** - * Populate a variable map with strings that contain escape sequences that - * might be useful to script writers. - */ -void add_ansi_vars(std::map<std::string, scoped_value_t>& vars); - #endif diff --git a/src/base/ansi_vars.hh b/src/base/ansi_vars.hh new file mode 100644 index 0000000..e6d8f03 --- /dev/null +++ b/src/base/ansi_vars.hh @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2023, 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_ansi_vars_hh +#define lnav_ansi_vars_hh + +#include "shlex.resolver.hh" + +/** + * Populate a variable map with strings that contain escape sequences that + * might be useful to script writers. + */ +void add_ansi_vars(std::map<std::string, scoped_value_t>& vars); + +#endif diff --git a/src/base/attr_line.builder.cc b/src/base/attr_line.builder.cc index 95416dc..a6071a4 100644 --- a/src/base/attr_line.builder.cc +++ b/src/base/attr_line.builder.cc @@ -28,3 +28,57 @@ */ #include "attr_line.builder.hh" + +attr_line_builder& +attr_line_builder::append_as_hexdump(const string_fragment& sf) +{ + auto byte_off = size_t{0}; + for (auto ch : sf) { + if (byte_off == 8) { + this->append(" "); + } + nonstd::optional<role_t> ro; + if (ch == '\0') { + ro = role_t::VCR_NULL; + } else if (isspace(ch) || iscntrl(ch)) { + ro = role_t::VCR_ASCII_CTRL; + } else if (!isprint(ch)) { + ro = role_t::VCR_NON_ASCII; + } + auto ag = ro.has_value() ? this->with_attr(VC_ROLE.value(ro.value())) + : this->with_default(); + this->appendf(FMT_STRING(" {:0>2x}"), ch); + byte_off += 1; + } + for (; byte_off < 16; byte_off++) { + if (byte_off == 8) { + this->append(" "); + } + this->append(" "); + } + this->append(" "); + byte_off = 0; + for (auto ch : sf) { + if (byte_off == 8) { + this->append(" "); + } + if (ch == '\0') { + auto ag = this->with_attr(VC_ROLE.value(role_t::VCR_NULL)); + this->append("\u22c4"); + } else if (isspace(ch)) { + auto ag = this->with_attr(VC_ROLE.value(role_t::VCR_ASCII_CTRL)); + this->append("_"); + } else if (iscntrl(ch)) { + auto ag = this->with_attr(VC_ROLE.value(role_t::VCR_ASCII_CTRL)); + this->append("\u2022"); + } else if (isprint(ch)) { + this->alb_line.get_string().push_back(ch); + } else { + auto ag = this->with_attr(VC_ROLE.value(role_t::VCR_NON_ASCII)); + this->append("\u00d7"); + } + byte_off += 1; + } + + return *this; +} diff --git a/src/base/attr_line.builder.hh b/src/base/attr_line.builder.hh index 1e62532..9ae2caa 100644 --- a/src/base/attr_line.builder.hh +++ b/src/base/attr_line.builder.hh @@ -40,6 +40,11 @@ public: class attr_guard { public: + explicit attr_guard(attr_line_t& al) + : ag_line(al), ag_start(nonstd::nullopt) + { + } + attr_guard(attr_line_t& al, string_attr_pair sap) : ag_line(al), ag_start(al.get_string().length()), ag_attr(std::move(sap)) @@ -51,7 +56,7 @@ public: attr_guard& operator=(const attr_guard&) = delete; attr_guard(attr_guard&& other) noexcept - : ag_line(other.ag_line), ag_start(other.ag_start), + : ag_line(other.ag_line), ag_start(std::move(other.ag_start)), ag_attr(std::move(other.ag_attr)) { other.ag_start = nonstd::nullopt; @@ -75,6 +80,8 @@ public: string_attr_pair ag_attr; }; + attr_guard with_default() { return attr_guard{this->alb_line}; } + attr_guard with_attr(string_attr_pair sap) { return {this->alb_line, std::move(sap)}; @@ -103,6 +110,14 @@ public: return *this; } + template<typename... Args> + attr_line_builder& appendf(Args... args) + { + this->alb_line.appendf(args...); + + return *this; + } + attr_line_builder& indent(size_t amount) { auto pre = this->with_attr(SA_PREFORMATTED.value()); @@ -112,6 +127,8 @@ public: return *this; } + attr_line_builder& append_as_hexdump(const string_fragment& sf); + private: attr_line_t& alb_line; }; diff --git a/src/base/attr_line.cc b/src/base/attr_line.cc index b65f4ba..c406525 100644 --- a/src/base/attr_line.cc +++ b/src/base/attr_line.cc @@ -200,7 +200,9 @@ attr_line_t::insert(size_t index, this->al_string, starting_line_index, this->al_string.length()); string_fragment last_word; ssize_t line_ch_count = 0; + ssize_t line_indent_count = 0; auto needs_indent = false; + auto last_was_pre = false; while (!text_to_wrap.empty()) { if (needs_indent) { @@ -214,6 +216,7 @@ attr_line_t::insert(size_t index, split_attrs(*this, indent_lr); indent_lr.lr_end += tws->tws_padding_indent; line_ch_count += tws->tws_padding_indent; + line_indent_count += tws->tws_padding_indent; if (!indent_lr.empty()) { this->al_attrs.emplace_back(indent_lr, SA_PREFORMATTED.value()); } @@ -222,18 +225,34 @@ attr_line_t::insert(size_t index, tws->tws_indent + tws->tws_padding_indent); needs_indent = false; } - auto chunk = text_stream::consume(text_to_wrap); - text_to_wrap = chunk.match( + text_stream::chunk next_chunk(mapbox::util::no_init{}); + auto pre_iter = find_string_attr_containing( + this->al_attrs, &SA_PREFORMATTED, text_to_wrap.sf_begin); + if (pre_iter != this->al_attrs.end()) { + auto pre_len = pre_iter->sa_range.lr_end - text_to_wrap.sf_begin; + auto pre_lf = text_to_wrap.find('\n'); + if (pre_lf && pre_lf.value() < pre_len) { + pre_len = pre_lf.value() + 1; + } + + auto pre_pair = text_to_wrap.split_n(pre_len); + next_chunk = text_stream::word{ + pre_pair->first, + pre_pair->second, + }; + } + if (!next_chunk.valid()) { + next_chunk = text_stream::consume(text_to_wrap); + } + + text_to_wrap = next_chunk.match( [&](text_stream::word word) { auto ch_count = word.w_word.utf8_length().unwrapOr(word.w_word.length()); - if ((line_ch_count + ch_count) > usable_width - && find_string_attr_containing(this->al_attrs, - &SA_PREFORMATTED, - text_to_wrap.sf_begin) - == this->al_attrs.end()) + if (line_ch_count > line_indent_count && !last_was_pre + && (line_ch_count + ch_count) > usable_width) { this->insert(word.w_word.sf_begin, 1, '\n'); this->insert(word.w_word.sf_begin + 1, @@ -250,6 +269,7 @@ attr_line_t::insert(size_t index, SA_PREFORMATTED.value()); } line_ch_count = tws->tws_padding_indent + ch_count; + line_indent_count = tws->tws_padding_indent; auto trailing_space_count = 0; if (!last_word.empty()) { trailing_space_count @@ -263,12 +283,18 @@ attr_line_t::insert(size_t index, 1 + tws->tws_indent + tws->tws_padding_indent); } line_ch_count += ch_count; + if (word.w_word.endswith("\n")) { + line_ch_count = 0; + line_indent_count = 0; + needs_indent = true; + } return word.w_remaining; }, [&](text_stream::space space) { if (space.s_value == "\n") { line_ch_count = 0; + line_indent_count = 0; needs_indent = true; return space.s_remaining; } @@ -287,6 +313,7 @@ attr_line_t::insert(size_t index, space.s_value.length()); this->insert(space.s_value.sf_begin, "\n"); line_ch_count = 0; + line_indent_count = 0; needs_indent = true; auto trailing_space_count = 0; @@ -318,9 +345,10 @@ attr_line_t::insert(size_t index, [](text_stream::corrupt corrupt) { return corrupt.c_remaining; }, [](text_stream::eof eof) { return eof.e_remaining; }); - if (chunk.is<text_stream::word>()) { + if (next_chunk.is<text_stream::word>()) { last_word = text_to_wrap; } + last_was_pre = (pre_iter != this->al_attrs.end()); ensure(this->al_string.data() == text_to_wrap.sf_string); ensure(text_to_wrap.sf_begin <= text_to_wrap.sf_end); @@ -429,7 +457,7 @@ attr_line_t::apply_hide() } attr_line_t& -attr_line_t::rtrim() +attr_line_t::rtrim(nonstd::optional<const char*> chars) { auto index = this->al_string.length(); @@ -440,7 +468,12 @@ attr_line_t::rtrim() { break; } - if (!isspace(this->al_string[index - 1])) { + if (chars + && strchr(chars.value(), this->al_string[index - 1]) == nullptr) + { + break; + } + if (!chars && !isspace(this->al_string[index - 1])) { break; } } @@ -462,7 +495,9 @@ attr_line_t::erase(size_t pos, size_t len) this->al_string.erase(pos, len); - shift_string_attrs(this->al_attrs, pos, -((int32_t) len)); + shift_string_attrs(this->al_attrs, + line_range{(int) pos, (int) (pos + len)}, + -((int32_t) len)); auto new_end = std::remove_if( this->al_attrs.begin(), this->al_attrs.end(), [](const auto& attr) { return attr.sa_range.empty(); @@ -506,6 +541,33 @@ line_range::intersection(const line_range& other) const } line_range& +line_range::shift_range(const line_range& cover, int32_t amount) +{ + if (cover.lr_end <= this->lr_start) { + this->lr_start = std::max(0, this->lr_start + amount); + if (this->lr_end != -1) { + this->lr_end = std::max(0, this->lr_end + amount); + } + } else { + if (amount < 0 && cover.contains(*this)) { + this->lr_start = cover.lr_start; + } + if (this->lr_end != -1) { + if (cover.lr_start < this->lr_end) { + if (amount < 0 && amount < (cover.lr_start - this->lr_end)) { + this->lr_end = cover.lr_start; + } else { + this->lr_end + = std::max(this->lr_start, this->lr_end + amount); + } + } + } + } + + return *this; +} + +line_range& line_range::shift(int32_t start, int32_t amount) { if (start == this->lr_start) { @@ -535,3 +597,120 @@ line_range::shift(int32_t start, int32_t amount) return *this; } + +string_attrs_t::const_iterator +find_string_attr(const string_attrs_t& sa, size_t near) +{ + auto nearest = sa.end(); + ssize_t last_diff = INT_MAX; + + for (auto iter = sa.begin(); iter != sa.end(); ++iter) { + const auto& lr = iter->sa_range; + + if (!lr.is_valid() || !lr.contains(near)) { + continue; + } + + ssize_t diff = near - lr.lr_start; + if (diff < last_diff) { + last_diff = diff; + nearest = iter; + } + } + + return nearest; +} + +void +shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount) +{ + for (auto& iter : sa) { + iter.sa_range.shift(start, amount); + } +} + +void +shift_string_attrs(string_attrs_t& sa, const line_range& cover, int32_t amount) +{ + for (auto& iter : sa) { + iter.sa_range.shift_range(cover, amount); + } +} + +struct line_range +find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type) +{ + auto iter = find_string_attr(sa, type); + + if (iter != sa.end()) { + return iter->sa_range; + } + + return line_range(); +} + +void +remove_string_attr(string_attrs_t& sa, const line_range& lr) +{ + string_attrs_t::iterator iter; + + while ((iter = find_string_attr(sa, lr)) != sa.end()) { + sa.erase(iter); + } +} + +void +remove_string_attr(string_attrs_t& sa, string_attr_type_base* type) +{ + for (auto iter = sa.begin(); iter != sa.end();) { + if (iter->sa_type == type) { + iter = sa.erase(iter); + } else { + ++iter; + } + } +} + +string_attrs_t::iterator +find_string_attr(string_attrs_t& sa, const line_range& lr) +{ + string_attrs_t::iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (lr.contains(iter->sa_range)) { + break; + } + } + + return iter; +} + +string_attrs_t::const_iterator +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) { + if (iter->sa_type == type && iter->sa_range.lr_start >= start) { + break; + } + } + + return iter; +} + +nonstd::optional<const string_attr*> +get_string_attr(const string_attrs_t& sa, + const string_attr_type_base* type, + int start) +{ + auto iter = find_string_attr(sa, type, start); + + if (iter == sa.end()) { + return nonstd::nullopt; + } + + return nonstd::make_optional(&(*iter)); +} diff --git a/src/base/attr_line.hh b/src/base/attr_line.hh index c9cb6a8..c294321 100644 --- a/src/base/attr_line.hh +++ b/src/base/attr_line.hh @@ -40,130 +40,10 @@ #include "fmt/format.h" #include "intern_string.hh" +#include "line_range.hh" #include "string_attr_type.hh" #include "string_util.hh" -/** - * Encapsulates a range in a string. - */ -struct line_range { - enum class unit { - bytes, - codepoint, - }; - - int lr_start; - int lr_end; - unit lr_unit; - - explicit line_range(int start = -1, int end = -1, unit u = unit::bytes) - : lr_start(start), lr_end(end), lr_unit(u) - { - } - - bool is_valid() const { return this->lr_start != -1; } - - int length() const - { - return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start; - } - - bool empty() const { return this->length() == 0; } - - void clear() - { - this->lr_start = -1; - this->lr_end = -1; - } - - int end_for_string(const std::string& str) const - { - return this->lr_end == -1 ? str.length() : this->lr_end; - } - - bool contains(int pos) const - { - return this->lr_start <= pos - && (this->lr_end == -1 || pos < this->lr_end); - } - - bool contains(const struct line_range& other) const - { - return this->contains(other.lr_start) - && (this->lr_end == -1 || other.lr_end <= this->lr_end); - } - - bool intersects(const struct line_range& other) const - { - if (this->contains(other.lr_start)) { - return true; - } - if (other.lr_end > 0 && this->contains(other.lr_end - 1)) { - return true; - } - if (other.contains(this->lr_start)) { - return true; - } - - return false; - } - - line_range intersection(const struct line_range& other) const; - - line_range& shift(int32_t start, int32_t amount); - - void ltrim(const char* str) - { - while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) { - this->lr_start += 1; - } - } - - bool operator<(const struct line_range& rhs) const - { - if (this->lr_start < rhs.lr_start) { - return true; - } - if (this->lr_start > rhs.lr_start) { - return false; - } - - // this->lr_start == rhs.lr_start - if (this->lr_end == rhs.lr_end) { - return false; - } - - if (this->lr_end < rhs.lr_end) { - return false; - } - return true; - } - - bool operator==(const struct line_range& rhs) const - { - return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end); - } - - const char* substr(const std::string& str) const - { - if (this->lr_start == -1) { - return str.c_str(); - } - return &(str.c_str()[this->lr_start]); - } - - size_t sublen(const std::string& str) const - { - if (this->lr_start == -1) { - return str.length(); - } - if (this->lr_end == -1) { - return str.length() - this->lr_start; - } - return this->length(); - } -}; - inline line_range to_line_range(const string_fragment& frag) { @@ -214,35 +94,11 @@ struct string_attr_wrapper { /** A map of line ranges to attributes for that range. */ using string_attrs_t = std::vector<string_attr>; -inline string_attrs_t::const_iterator -find_string_attr(const string_attrs_t& sa, - const string_attr_type_base* type, - int start = 0) -{ - string_attrs_t::const_iterator iter; - - for (iter = sa.begin(); iter != sa.end(); ++iter) { - if (iter->sa_type == type && iter->sa_range.lr_start >= start) { - break; - } - } - - return iter; -} - -inline nonstd::optional<const string_attr*> -get_string_attr(const string_attrs_t& sa, - const string_attr_type_base* type, - int start = 0) -{ - auto iter = find_string_attr(sa, type, start); - - if (iter == sa.end()) { - return nonstd::nullopt; - } +string_attrs_t::const_iterator find_string_attr( + const string_attrs_t& sa, const string_attr_type_base* type, int start = 0); - return nonstd::make_optional(&(*iter)); -} +nonstd::optional<const string_attr*> get_string_attr( + const string_attrs_t& sa, const string_attr_type_base* type, int start = 0); template<typename T> inline nonstd::optional<string_attr_wrapper<T>> @@ -276,42 +132,11 @@ find_string_attr_containing(const string_attrs_t& sa, return iter; } -inline string_attrs_t::iterator -find_string_attr(string_attrs_t& sa, const struct line_range& lr) -{ - string_attrs_t::iterator iter; - - for (iter = sa.begin(); iter != sa.end(); ++iter) { - if (lr.contains(iter->sa_range)) { - break; - } - } - - return iter; -} - -inline string_attrs_t::const_iterator -find_string_attr(const string_attrs_t& sa, size_t near) -{ - auto nearest = sa.end(); - ssize_t last_diff = INT_MAX; - - for (auto iter = sa.begin(); iter != sa.end(); ++iter) { - const auto& lr = iter->sa_range; - - if (!lr.is_valid() || !lr.contains(near)) { - continue; - } - - ssize_t diff = near - lr.lr_start; - if (diff < last_diff) { - last_diff = diff; - nearest = iter; - } - } +string_attrs_t::iterator find_string_attr(string_attrs_t& sa, + const struct line_range& lr); - return nearest; -} +string_attrs_t::const_iterator find_string_attr(const string_attrs_t& sa, + size_t near); template<typename T> inline string_attrs_t::const_iterator @@ -341,47 +166,18 @@ rfind_string_attr_if(const string_attrs_t& sa, ssize_t near, T predicate) return nearest; } -inline struct line_range -find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type) -{ - auto iter = find_string_attr(sa, type); +struct line_range find_string_attr_range(const string_attrs_t& sa, + string_attr_type_base* type); - if (iter != sa.end()) { - return iter->sa_range; - } +void remove_string_attr(string_attrs_t& sa, const struct line_range& lr); - return line_range(); -} +void remove_string_attr(string_attrs_t& sa, string_attr_type_base* type); -inline void -remove_string_attr(string_attrs_t& sa, const struct line_range& lr) -{ - string_attrs_t::iterator iter; - - while ((iter = find_string_attr(sa, lr)) != sa.end()) { - sa.erase(iter); - } -} +void shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount); -inline void -remove_string_attr(string_attrs_t& sa, string_attr_type_base* type) -{ - for (auto iter = sa.begin(); iter != sa.end();) { - if (iter->sa_type == type) { - iter = sa.erase(iter); - } else { - ++iter; - } - } -} - -inline void -shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount) -{ - for (auto& iter : sa) { - iter.sa_range.shift(start, amount); - } -} +void shift_string_attrs(string_attrs_t& sa, + const line_range& cover, + int32_t amount); struct text_wrap_settings { text_wrap_settings& with_indent(int indent) @@ -425,6 +221,13 @@ public: return retval.with_ansi_string("%s", str); } + static inline attr_line_t from_ansi_str(const std::string& str) + { + attr_line_t retval; + + return retval.with_ansi_string(str); + } + /** @return The string itself. */ std::string& get_string() { return this->al_string; } @@ -517,15 +320,6 @@ public: return *this; } - attr_line_t& append_quoted(const attr_line_t& al) - { - this->al_string.append("\u201c"); - this->append(al); - this->al_string.append("\u201d"); - - return *this; - } - template<typename S> attr_line_t& append_quoted(S s) { @@ -548,18 +342,31 @@ public: return *this; } - template<typename S> - attr_line_t& append(S str) + attr_line_t& append(const std::string& str) { this->al_string.append(str); return *this; } + attr_line_t& append(const char* str) + { + this->al_string.append(str); + return *this; + } + + template<typename V> + attr_line_t& append(const V& v) + { + this->al_string.append(fmt::to_string(v)); + return *this; + } + template<typename... Args> attr_line_t& appendf(fmt::format_string<Args...> fstr, Args&&... args) { - this->template append( - fmt::vformat(fstr, fmt::make_format_args(args...))); + fmt::vformat_to(std::back_inserter(this->al_string), + fstr, + fmt::make_format_args(args...)); return *this; } @@ -635,6 +442,24 @@ public: return *this; } + template<typename S> + attr_line_t& insert(size_t index, + const std::pair<S, string_attr_pair>& value) + { + size_t start_len = this->al_string.length(); + + this->insert(index, std::move(value.first)); + + line_range lr{ + (int) index, + (int) (index + (this->al_string.length() - start_len)), + }; + + this->al_attrs.emplace_back(lr, value.second); + + return *this; + } + template<typename... Args> attr_line_t& add_header(Args... args) { @@ -657,7 +482,7 @@ public: attr_line_t& erase(size_t pos, size_t len = std::string::npos); - attr_line_t& rtrim(); + attr_line_t& rtrim(nonstd::optional<const char*> chars = nonstd::nullopt); attr_line_t& erase_utf8_chars(size_t start) { @@ -695,6 +520,11 @@ public: return utf8_string_length(this->al_string).unwrapOr(this->length()); } + size_t column_width() const + { + return string_fragment::from_str(this->al_string).column_width(); + } + std::string get_substring(const line_range& lr) const { if (!lr.is_valid()) { diff --git a/src/base/attr_line.tests.cc b/src/base/attr_line.tests.cc index 53b338e..8e23480 100644 --- a/src/base/attr_line.tests.cc +++ b/src/base/attr_line.tests.cc @@ -36,6 +36,15 @@ using namespace lnav::roles::literals; +TEST_CASE("line_range") +{ + line_range lr1{0, 95}; + line_range lr2{0, -1}; + + CHECK(lr2 < lr1); + CHECK(!(lr1 < lr2)); +} + TEST_CASE("attr_line_t::basic-wrapping") { text_wrap_settings tws = {3, 21}; @@ -89,3 +98,18 @@ TEST_CASE("attr_line_t::unicode-wrap") " be wrapped and\n" " indented"); } + +TEST_CASE("attr_line_t::pre-wrap") +{ + auto pre_al = attr_line_t(" Hello, World! ") + .with_attr_for_all(SA_PREFORMATTED.value()); + auto al = attr_line_t("This is a pre-formatted inline -- ") + .append(pre_al) + .append(" -- that should be wrapped"); + + text_wrap_settings tws = {0, 36}; + + auto body = attr_line_t().append(al, &tws); + + printf("body\n%s\n", body.get_string().c_str()); +} diff --git a/src/base/auto_fd.cc b/src/base/auto_fd.cc new file mode 100644 index 0000000..0853c3a --- /dev/null +++ b/src/base/auto_fd.cc @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2023, 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. + * + * @file auto_fd.cc + */ + +#include "auto_fd.hh" + +#include <fcntl.h> +#include <unistd.h> + +#include "lnav_log.hh" + +int +auto_fd::pipe(auto_fd* af) +{ + int retval, fd[2]; + + require(af != nullptr); + + if ((retval = ::pipe(fd)) == 0) { + af[0] = fd[0]; + af[1] = fd[1]; + } + + return retval; +} + +auto_fd +auto_fd::dup_of(int fd) +{ + if (fd == -1) { + return auto_fd{}; + } + + auto new_fd = ::dup(fd); + + if (new_fd == -1) { + throw std::bad_alloc(); + } + + return auto_fd(new_fd); +} + +Result<auto_fd, std::string> +auto_fd::openpt(int flags) +{ + auto rc = posix_openpt(flags); + if (rc == -1) { + return Err(fmt::format(FMT_STRING("posix_openpt() failed: {}"), + strerror(errno))); + } + + return Ok(auto_fd{rc}); +} + +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::dup() const +{ + int new_fd; + + if (this->af_fd == -1 || (new_fd = ::dup(this->af_fd)) == -1) { + throw std::bad_alloc(); + } + + return auto_fd{new_fd}; +} + +auto_fd::~auto_fd() +{ + this->reset(); +} + +void +auto_fd::reset(int fd) +{ + require(fd >= -1); + + if (this->af_fd != fd) { + if (this->af_fd != -1) { + switch (this->af_fd) { + case STDIN_FILENO: + case STDOUT_FILENO: + case STDERR_FILENO: + break; + default: + close(this->af_fd); + break; + } + } + this->af_fd = fd; + } +} + +void +auto_fd::close_on_exec() const +{ + if (this->af_fd == -1) { + return; + } + log_perror(fcntl(this->af_fd, F_SETFD, FD_CLOEXEC)); +} + +void +auto_fd::non_blocking() const +{ + auto fl = fcntl(this->af_fd, F_GETFL, 0); + if (fl < 0) { + return; + } + + log_perror(fcntl(this->af_fd, F_SETFL, fl | O_NONBLOCK)); +} + +auto_fd& +auto_fd::operator=(int fd) +{ + require(fd >= -1); + + this->reset(fd); + return *this; +} + +Result<void, std::string> +auto_fd::write_fully(string_fragment sf) +{ + while (!sf.empty()) { + auto rc = write(this->af_fd, sf.data(), sf.length()); + + if (rc < 0) { + return Err( + fmt::format(FMT_STRING("failed to write {} bytes to FD {}"), + sf.length(), + this->af_fd)); + } + + sf = sf.substr(rc); + } + + return Ok(); +} + +Result<auto_pipe, std::string> +auto_pipe::for_child_fd(int child_fd) +{ + auto_pipe retval(child_fd); + + if (retval.open() == -1) { + return Err(std::string(strerror(errno))); + } + + return Ok(std::move(retval)); +} + +auto_pipe::auto_pipe(int child_fd, int child_flags) + : ap_child_flags(child_flags), ap_child_fd(child_fd) +{ + switch (child_fd) { + case STDIN_FILENO: + this->ap_child_flags = O_RDONLY; + break; + case STDOUT_FILENO: + case STDERR_FILENO: + this->ap_child_flags = O_WRONLY; + break; + } +} + +void +auto_pipe::after_fork(pid_t child_pid) +{ + int new_fd; + + switch (child_pid) { + case -1: + this->close(); + break; + case 0: + if (this->ap_child_flags == O_RDONLY) { + this->write_end().reset(); + if (this->read_end().get() == -1) { + this->read_end() = ::open("/dev/null", O_RDONLY); + } + new_fd = this->read_end().get(); + } else { + this->read_end().reset(); + if (this->write_end().get() == -1) { + this->write_end() = ::open("/dev/null", O_WRONLY); + } + new_fd = this->write_end().get(); + } + if (this->ap_child_fd != -1) { + if (new_fd != this->ap_child_fd) { + dup2(new_fd, this->ap_child_fd); + this->close(); + } + } + break; + default: + if (this->ap_child_flags == O_RDONLY) { + this->read_end().reset(); + } else { + this->write_end().reset(); + } + break; + } +} + +int +auto_pipe::open() +{ + int retval = auto_fd::pipe(this->ap_fd); + this->ap_fd[0].close_on_exec(); + this->ap_fd[1].close_on_exec(); + return retval; +} diff --git a/src/base/auto_fd.hh b/src/base/auto_fd.hh index da4a582..9fb3c91 100644 --- a/src/base/auto_fd.hh +++ b/src/base/auto_fd.hh @@ -32,16 +32,11 @@ #ifndef auto_fd_hh #define auto_fd_hh -#include <exception> -#include <new> #include <string> -#include <errno.h> #include <fcntl.h> -#include <sys/select.h> -#include <unistd.h> -#include "base/lnav_log.hh" +#include "base/intern_string.hh" #include "base/result.h" /** @@ -59,19 +54,7 @@ public: * contains the reader end of the pipe and the second contains the writer. * @return The result of the pipe(2) function. */ - static int pipe(auto_fd* af) - { - int retval, fd[2]; - - require(af != nullptr); - - if ((retval = ::pipe(fd)) == 0) { - af[0] = fd[0]; - af[1] = fd[1]; - } - - return retval; - } + static int pipe(auto_fd* af); /** * dup(2) the given file descriptor and wrap it in an auto_fd. @@ -79,27 +62,16 @@ public: * @param fd The file descriptor to duplicate. * @return A new auto_fd that contains the duplicated file descriptor. */ - static auto_fd dup_of(int fd) - { - if (fd == -1) { - return auto_fd{}; - } + static auto_fd dup_of(int fd); - auto new_fd = ::dup(fd); - - if (new_fd == -1) { - throw std::bad_alloc(); - } - - return auto_fd(new_fd); - } + static Result<auto_fd, std::string> openpt(int flags); /** * Construct an auto_fd to manage the given file descriptor. * * @param fd The file descriptor to be managed. */ - explicit auto_fd(int fd = -1) : af_fd(fd) { require(fd >= -1); } + explicit auto_fd(int fd = -1); /** * Non-const copy constructor. Management of the file descriptor will be @@ -108,7 +80,7 @@ public: * * @param af The source of the file descriptor. */ - auto_fd(auto_fd&& af) noexcept : af_fd(af.release()) {} + auto_fd(auto_fd&& af) noexcept; /** * Const copy constructor. The file descriptor from the source will be @@ -118,21 +90,12 @@ public: */ auto_fd(const auto_fd& af) = delete; - auto_fd dup() const - { - int new_fd; - - if (this->af_fd == -1 || (new_fd = ::dup(this->af_fd)) == -1) { - throw std::bad_alloc(); - } - - return auto_fd{new_fd}; - } + auto_fd dup() const; /** * Destructor that will close the file descriptor managed by this object. */ - ~auto_fd() { this->reset(); } + ~auto_fd(); /** @return The file descriptor as a plain integer. */ operator int() const { return this->af_fd; } @@ -144,13 +107,7 @@ public: * @param fd The file descriptor to store in this object. * @return *this */ - auto_fd& operator=(int fd) - { - require(fd >= -1); - - this->reset(fd); - return *this; - } + auto_fd& operator=(int fd); /** * Transfer management of the given file descriptor to this object. @@ -194,39 +151,21 @@ public: */ int get() const { return this->af_fd; } + bool has_value() const { return this->af_fd != -1; } + /** * Closes the current file descriptor and replaces its value with the given * one. * * @param fd The new file descriptor to be managed. */ - void reset(int fd = -1) - { - require(fd >= -1); - - if (this->af_fd != fd) { - if (this->af_fd != -1) { - switch (this->af_fd) { - case STDIN_FILENO: - case STDOUT_FILENO: - case STDERR_FILENO: - break; - default: - close(this->af_fd); - break; - } - } - this->af_fd = fd; - } - } + void reset(int fd = -1); - void close_on_exec() const - { - if (this->af_fd == -1) { - return; - } - log_perror(fcntl(this->af_fd, F_SETFD, FD_CLOEXEC)); - } + Result<void, std::string> write_fully(string_fragment sf); + + void close_on_exec() const; + + void non_blocking() const; private: int af_fd; /*< The managed file descriptor. */ @@ -234,32 +173,30 @@ private: class auto_pipe { public: - static Result<auto_pipe, std::string> for_child_fd(int child_fd) + static Result<auto_pipe, std::string> for_child_fd(int child_fd); + + template<typename... ARGS> + static Result<std::array<auto_pipe, sizeof...(ARGS)>, std::string> + for_child_fds(ARGS... args) { - auto_pipe retval(child_fd); + std::array<auto_pipe, sizeof...(ARGS)> retval; - if (retval.open() == -1) { - return Err(std::string(strerror(errno))); + size_t index = 0; + for (const auto child_fd : {args...}) { + auto open_res = for_child_fd(child_fd); + if (open_res.isErr()) { + return Err(open_res.unwrapErr()); + } + + retval[index++] = open_res.unwrap(); } return Ok(std::move(retval)); } - explicit auto_pipe(int child_fd = -1, int child_flags = O_RDONLY) - : ap_child_flags(child_flags), ap_child_fd(child_fd) - { - switch (child_fd) { - case STDIN_FILENO: - this->ap_child_flags = O_RDONLY; - break; - case STDOUT_FILENO: - case STDERR_FILENO: - this->ap_child_flags = O_WRONLY; - break; - } - } + explicit auto_pipe(int child_fd = -1, int child_flags = O_RDONLY); - int open() { return auto_fd::pipe(this->ap_fd); } + int open(); void close() { @@ -271,44 +208,7 @@ public: auto_fd& write_end() { return this->ap_fd[1]; } - void after_fork(pid_t child_pid) - { - int new_fd; - - switch (child_pid) { - case -1: - this->close(); - break; - case 0: - if (this->ap_child_flags == O_RDONLY) { - this->write_end().reset(); - if (this->read_end().get() == -1) { - this->read_end() = ::open("/dev/null", O_RDONLY); - } - new_fd = this->read_end().get(); - } else { - this->read_end().reset(); - if (this->write_end().get() == -1) { - this->write_end() = ::open("/dev/null", O_WRONLY); - } - new_fd = this->write_end().get(); - } - if (this->ap_child_fd != -1) { - if (new_fd != this->ap_child_fd) { - dup2(new_fd, this->ap_child_fd); - this->close(); - } - } - break; - default: - if (this->ap_child_flags == O_RDONLY) { - this->read_end().reset(); - } else { - this->write_end().reset(); - } - break; - } - } + void after_fork(pid_t child_pid); int ap_child_flags; int ap_child_fd; diff --git a/src/base/auto_mem.hh b/src/base/auto_mem.hh index e6b456c..b404a1b 100644 --- a/src/base/auto_mem.hh +++ b/src/base/auto_mem.hh @@ -66,6 +66,16 @@ public: return retval; } + static auto_mem calloc(size_t count) + { + return auto_mem(static_cast<T*>(::calloc(count, sizeof(T)))); + } + + static auto_mem malloc(size_t sz) + { + return auto_mem(static_cast<T*>(::malloc(sz))); + } + explicit auto_mem(T* ptr = nullptr) : am_ptr(ptr), am_free_func(default_free) { @@ -241,6 +251,8 @@ public: const char* begin() const { return this->ab_buffer; } + char* next_available() { return &this->ab_buffer[this->ab_size]; } + auto_buffer& push_back(char ch) { if (this->ab_size == this->ab_capacity) { diff --git a/src/base/auto_pid.hh b/src/base/auto_pid.hh index 702af1e..ff44b99 100644 --- a/src/base/auto_pid.hh +++ b/src/base/auto_pid.hh @@ -62,10 +62,7 @@ public: { } - ~auto_pid() noexcept - { - this->reset(); - } + ~auto_pid() noexcept { this->reset(); } auto_pid& operator=(auto_pid&& other) noexcept { @@ -77,10 +74,7 @@ public: auto_pid& operator=(const auto_pid& other) = delete; - pid_t in() const - { - return this->ap_child; - } + pid_t in() const { return this->ap_child; } bool in_child() const { @@ -89,9 +83,7 @@ public: return this->ap_child == 0; } - pid_t release() && - { - return std::exchange(this->ap_child, -1); } + pid_t release() && { return std::exchange(this->ap_child, -1); } int status() const { @@ -107,6 +99,13 @@ public: return WIFEXITED(this->ap_status); } + int term_signal() const + { + static_assert(ProcState == process_state::finished, + "wait_for_child() must be called first"); + return WTERMSIG(this->ap_status); + } + int exit_status() const { static_assert(ProcState == process_state::finished, diff --git a/src/base/color_spaces.cc b/src/base/color_spaces.cc new file mode 100644 index 0000000..74d9fe6 --- /dev/null +++ b/src/base/color_spaces.cc @@ -0,0 +1,176 @@ +/** + * 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 <cmath> + +#include "color_spaces.hh" + +#include "config.h" + +bool +rgb_color::operator<(const rgb_color& rhs) const +{ + if (rc_r < rhs.rc_r) + return true; + if (rhs.rc_r < rc_r) + return false; + if (rc_g < rhs.rc_g) + return true; + if (rhs.rc_g < rc_g) + return false; + return rc_b < rhs.rc_b; +} + +bool +rgb_color::operator>(const rgb_color& rhs) const +{ + return rhs < *this; +} + +bool +rgb_color::operator<=(const rgb_color& rhs) const +{ + return !(rhs < *this); +} + +bool +rgb_color::operator>=(const rgb_color& rhs) const +{ + return !(*this < rhs); +} + +bool +rgb_color::operator==(const rgb_color& rhs) const +{ + return rc_r == rhs.rc_r && rc_g == rhs.rc_g && rc_b == rhs.rc_b; +} + +bool +rgb_color::operator!=(const rgb_color& rhs) const +{ + return !(rhs == *this); +} + +lab_color::lab_color(const rgb_color& rgb) +{ + double r = rgb.rc_r / 255.0, g = rgb.rc_g / 255.0, b = rgb.rc_b / 255.0, x, + y, z; + + r = (r > 0.04045) ? pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = (g > 0.04045) ? pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = (b > 0.04045) ? pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047; + y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000; + z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883; + + x = (x > 0.008856) ? pow(x, 1.0 / 3.0) : (7.787 * x) + 16.0 / 116.0; + y = (y > 0.008856) ? pow(y, 1.0 / 3.0) : (7.787 * y) + 16.0 / 116.0; + z = (z > 0.008856) ? pow(z, 1.0 / 3.0) : (7.787 * z) + 16.0 / 116.0; + + this->lc_l = (116.0 * y) - 16; + this->lc_a = 500.0 * (x - y); + this->lc_b = 200.0 * (y - z); +} + +double +lab_color::deltaE(const lab_color& other) const +{ + double deltaL = this->lc_l - other.lc_l; + double deltaA = this->lc_a - other.lc_a; + double deltaB = this->lc_b - other.lc_b; + double c1 = sqrt(this->lc_a * this->lc_a + this->lc_b * this->lc_b); + double c2 = sqrt(other.lc_a * other.lc_a + other.lc_b * other.lc_b); + double deltaC = c1 - c2; + double deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC; + deltaH = deltaH < 0.0 ? 0.0 : sqrt(deltaH); + double sc = 1.0 + 0.045 * c1; + double sh = 1.0 + 0.015 * c1; + double deltaLKlsl = deltaL / (1.0); + double deltaCkcsc = deltaC / (sc); + double deltaHkhsh = deltaH / (sh); + double i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + + deltaHkhsh * deltaHkhsh; + return i < 0.0 ? 0.0 : sqrt(i); +} + +bool +lab_color::operator<(const lab_color& rhs) const +{ + if (lc_l < rhs.lc_l) + return true; + if (rhs.lc_l < lc_l) + return false; + if (lc_a < rhs.lc_a) + return true; + if (rhs.lc_a < lc_a) + return false; + return lc_b < rhs.lc_b; +} + +bool +lab_color::operator>(const lab_color& rhs) const +{ + return rhs < *this; +} + +bool +lab_color::operator<=(const lab_color& rhs) const +{ + return !(rhs < *this); +} + +bool +lab_color::operator>=(const lab_color& rhs) const +{ + return !(*this < rhs); +} + +bool +lab_color::operator==(const lab_color& rhs) const +{ + return lc_l == rhs.lc_l && lc_a == rhs.lc_a && lc_b == rhs.lc_b; +} + +bool +lab_color::operator!=(const lab_color& rhs) const +{ + return !(rhs == *this); +} + +bool +lab_color::sufficient_contrast(const lab_color& other) const +{ + if (std::abs(this->lc_l - other.lc_l) > 15) { + return true; + } + + return (std::signbit(this->lc_a) != std::signbit(other.lc_a) + || std::signbit(this->lc_b) != std::signbit(other.lc_b)); +} diff --git a/src/base/color_spaces.hh b/src/base/color_spaces.hh new file mode 100644 index 0000000..22c4afc --- /dev/null +++ b/src/base/color_spaces.hh @@ -0,0 +1,96 @@ +/** + * 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 color_spaces_hh +#define color_spaces_hh + +struct rgb_color { + explicit rgb_color(short r = -1, short g = -1, short b = -1) + : rc_r(r), rc_g(g), rc_b(b) + { + } + + bool empty() const + { + return this->rc_r == -1 && this->rc_g == -1 && this->rc_b == -1; + } + + bool operator==(const rgb_color& rhs) const; + + bool operator!=(const rgb_color& rhs) const; + + bool operator<(const rgb_color& rhs) const; + + bool operator>(const rgb_color& rhs) const; + + bool operator<=(const rgb_color& rhs) const; + + bool operator>=(const rgb_color& rhs) const; + + short rc_r; + short rc_g; + short rc_b; +}; + +struct lab_color { + lab_color() : lc_l(0), lc_a(0), lc_b(0) {} + + explicit lab_color(const rgb_color& rgb); + + double deltaE(const lab_color& other) const; + + bool sufficient_contrast(const lab_color& other) const; + + lab_color& operator=(const lab_color& other) + { + this->lc_l = other.lc_l; + this->lc_a = other.lc_a; + this->lc_b = other.lc_b; + + return *this; + } + + bool operator==(const lab_color& rhs) const; + + bool operator!=(const lab_color& rhs) const; + + bool operator<(const lab_color& rhs) const; + + bool operator>(const lab_color& rhs) const; + + bool operator<=(const lab_color& rhs) const; + + bool operator>=(const lab_color& rhs) const; + + double lc_l; + double lc_a; + double lc_b; +}; + +#endif diff --git a/src/base/date_time_scanner.cc b/src/base/date_time_scanner.cc index 72b7e5d..c3a904b 100644 --- a/src/base/date_time_scanner.cc +++ b/src/base/date_time_scanner.cc @@ -34,6 +34,8 @@ #include "date_time_scanner.hh" #include "config.h" +#include "date_time_scanner.cfg.hh" +#include "injector.hh" #include "ptimec.hh" #include "scn/scn.h" @@ -45,17 +47,28 @@ date_time_scanner::ftime(char* dst, { off_t off = 0; - if (time_fmt == nullptr) { - PTIMEC_FORMATS[this->dts_fmt_lock].pf_ffunc(dst, off, len, tm); - if (tm.et_flags & ETF_MILLIS_SET) { - dst[off++] = '.'; - ftime_L(dst, off, len, tm); - } else if (tm.et_flags & ETF_MICROS_SET) { - dst[off++] = '.'; - ftime_f(dst, off, len, tm); - } else if (tm.et_flags & ETF_NANOS_SET) { - dst[off++] = '.'; - ftime_N(dst, off, len, tm); + if (time_fmt == nullptr || this->dts_fmt_lock == -1 + || (tm.et_flags & ETF_MACHINE_ORIENTED)) + { + auto index + = this->dts_fmt_lock != -1 && !(tm.et_flags & ETF_MACHINE_ORIENTED) + ? this->dts_fmt_lock + : PTIMEC_DEFAULT_FMT_INDEX; + PTIMEC_FORMATS[index].pf_ffunc(dst, off, len, tm); + if (tm.et_flags & ETF_SUB_NOT_IN_FORMAT) { + if (tm.et_flags & ETF_MILLIS_SET) { + dst[off++] = '.'; + ftime_L(dst, off, len, tm); + } else if (tm.et_flags & ETF_MICROS_SET) { + dst[off++] = '.'; + ftime_f(dst, off, len, tm); + } else if (tm.et_flags & ETF_NANOS_SET) { + dst[off++] = '.'; + ftime_N(dst, off, len, tm); + } + } + if (index == PTIMEC_DEFAULT_FMT_INDEX && tm.et_flags & ETF_ZONE_SET) { + ftime_z(dst, off, len, tm); } dst[off] = '\0'; } else { @@ -92,6 +105,9 @@ date_time_scanner::scan(const char* time_dest, struct timeval& tv_out, bool convert_local) { + static const auto& cfg + = injector::get<const date_time_scanner_ns::config&>(); + int curr_time_fmt = -1; bool found = false; const char* retval = nullptr; @@ -100,32 +116,34 @@ date_time_scanner::scan(const char* time_dest, time_fmt = PTIMEC_FORMAT_STR; } + this->dts_zoned_to_local = cfg.c_zoned_to_local; while (next_format(time_fmt, curr_time_fmt, this->dts_fmt_lock)) { *tm_out = this->dts_base_tm; tm_out->et_flags = 0; if (time_len > 1 && time_dest[0] == '+' && isdigit(time_dest[1])) { retval = nullptr; - auto epoch_scan_res = scn::scan_value<int64_t>( - scn::string_view{time_dest, time_len}); + auto sv = scn::string_view{time_dest, time_len}; + auto epoch_scan_res = scn::scan_value<int64_t>(sv); if (epoch_scan_res) { time_t gmt = epoch_scan_res.value(); - if (convert_local && this->dts_local_time) { + if (convert_local + && (this->dts_local_time || this->dts_zoned_to_local)) + { localtime_r(&gmt, &tm_out->et_tm); #ifdef HAVE_STRUCT_TM_TM_ZONE tm_out->et_tm.tm_zone = nullptr; #endif tm_out->et_tm.tm_isdst = 0; - gmt = tm2sec(&tm_out->et_tm); + gmt = tm_out->to_timeval().tv_sec; } tv_out.tv_sec = gmt; tv_out.tv_usec = 0; tm_out->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET - | ETF_MACHINE_ORIENTED | ETF_EPOCH_TIME; + | ETF_MACHINE_ORIENTED | ETF_EPOCH_TIME | ETF_ZONE_SET; this->dts_fmt_lock = curr_time_fmt; - this->dts_fmt_len = std::distance(epoch_scan_res.begin(), - epoch_scan_res.end()); + this->dts_fmt_len = sv.length() - epoch_scan_res.range().size(); retval = time_dest + this->dts_fmt_len; found = true; break; @@ -147,10 +165,25 @@ date_time_scanner::scan(const char* time_dest, } if (convert_local && (this->dts_local_time - || tm_out->et_flags & ETF_EPOCH_TIME)) + || tm_out->et_flags & ETF_EPOCH_TIME + || ((tm_out->et_flags & ETF_ZONE_SET + || this->dts_default_zone != nullptr) + && this->dts_zoned_to_local))) { - time_t gmt = tm2sec(&tm_out->et_tm); - + time_t gmt = tm_out->to_timeval().tv_sec; + + if (!(tm_out->et_flags & ETF_ZONE_SET) + && !(tm_out->et_flags & ETF_EPOCH_TIME) + && this->dts_default_zone != nullptr) + { + date::local_seconds stime; + stime += std::chrono::seconds{gmt}; + auto ztime + = date::make_zoned(this->dts_default_zone, stime); + gmt = std::chrono::duration_cast<std::chrono::seconds>( + ztime.get_sys_time().time_since_epoch()) + .count(); + } this->to_localtime(gmt, *tm_out); } const auto& last_tm = this->dts_last_tm.et_tm; @@ -167,8 +200,7 @@ date_time_scanner::scan(const char* time_dest, tv_out.tv_sec += sec_diff; tm_out->et_tm.tm_wday = last_tm.tm_wday; } else { - // log_debug("doing tm2sec"); - tv_out.tv_sec = tm2sec(&tm_out->et_tm); + tv_out = tm_out->to_timeval(); secs2wday(tv_out, &tm_out->et_tm); } tv_out.tv_usec = tm_out->et_nsec / 1000; @@ -198,9 +230,11 @@ date_time_scanner::scan(const char* time_dest, } if (convert_local && (this->dts_local_time - || tm_out->et_flags & ETF_EPOCH_TIME)) + || tm_out->et_flags & ETF_EPOCH_TIME + || (tm_out->et_flags & ETF_ZONE_SET + && this->dts_zoned_to_local))) { - time_t gmt = tm2sec(&tm_out->et_tm); + time_t gmt = tm_out->to_timeval().tv_sec; this->to_localtime(gmt, *tm_out); #ifdef HAVE_STRUCT_TM_TM_ZONE @@ -209,8 +243,7 @@ date_time_scanner::scan(const char* time_dest, tm_out->et_tm.tm_isdst = 0; } - tv_out.tv_sec = tm2sec(&tm_out->et_tm); - tv_out.tv_usec = tm_out->et_nsec / 1000; + tv_out = tm_out->to_timeval(); secs2wday(tv_out, &tm_out->et_tm); this->dts_fmt_lock = curr_time_fmt; @@ -234,7 +267,10 @@ date_time_scanner::scan(const char* time_dest, if (retval != nullptr && static_cast<size_t>(retval - time_dest) < time_len) { /* Try to pull out the milli/micro-second value. */ - if (retval[0] == '.' || retval[0] == ',') { + if (!(tm_out->et_flags + & (ETF_MILLIS_SET | ETF_MICROS_SET | ETF_NANOS_SET)) + && (retval[0] == '.' || retval[0] == ',')) + { off_t off = (retval - time_dest) + 1; if (ptime_N(tm_out, time_dest, off, time_len)) { @@ -243,7 +279,7 @@ date_time_scanner::scan(const char* time_dest, std::chrono::nanoseconds{tm_out->et_nsec}) .count(); this->dts_fmt_len += 10; - tm_out->et_flags |= ETF_NANOS_SET; + tm_out->et_flags |= ETF_NANOS_SET | ETF_SUB_NOT_IN_FORMAT; retval += 10; } else if (ptime_f(tm_out, time_dest, off, time_len)) { tv_out.tv_usec @@ -251,7 +287,7 @@ date_time_scanner::scan(const char* time_dest, std::chrono::nanoseconds{tm_out->et_nsec}) .count(); this->dts_fmt_len += 7; - tm_out->et_flags |= ETF_MICROS_SET; + tm_out->et_flags |= ETF_MICROS_SET | ETF_SUB_NOT_IN_FORMAT; retval += 7; } else if (ptime_L(tm_out, time_dest, off, time_len)) { tv_out.tv_usec @@ -259,7 +295,7 @@ date_time_scanner::scan(const char* time_dest, std::chrono::nanoseconds{tm_out->et_nsec}) .count(); this->dts_fmt_len += 4; - tm_out->et_flags |= ETF_MILLIS_SET; + tm_out->et_flags |= ETF_MILLIS_SET | ETF_SUB_NOT_IN_FORMAT; retval += 4; } } @@ -287,22 +323,27 @@ date_time_scanner::to_localtime(time_t t, exttm& tm_out) if (t < this->dts_local_offset_valid || t >= this->dts_local_offset_expiry) { - time_t new_gmt; - localtime_r(&t, &tm_out.et_tm); + // Clear the gmtoff set by localtime_r() otherwise tm2sec() will + // convert the time back again. #ifdef HAVE_STRUCT_TM_TM_ZONE + tm_out.et_tm.tm_gmtoff = 0; tm_out.et_tm.tm_zone = nullptr; #endif tm_out.et_tm.tm_isdst = 0; - - new_gmt = tm2sec(&tm_out.et_tm); - this->dts_local_offset_cache = t - new_gmt; + auto new_gmt = tm2sec(&tm_out.et_tm); + this->dts_local_offset_cache = new_gmt - t; this->dts_local_offset_valid = t; this->dts_local_offset_expiry = t + (EXPIRE_TIME - 1); this->dts_local_offset_expiry -= this->dts_local_offset_expiry % EXPIRE_TIME; } else { - time_t adjust_gmt = t - this->dts_local_offset_cache; - gmtime_r(&adjust_gmt, &tm_out.et_tm); + time_t adjust_gmt = t + this->dts_local_offset_cache; + secs2tm(adjust_gmt, &tm_out.et_tm); } + tm_out.et_gmtoff = 0; +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm_out.et_tm.tm_gmtoff = 0; + tm_out.et_tm.tm_zone = nullptr; +#endif } diff --git a/src/base/date_time_scanner.cfg.hh b/src/base/date_time_scanner.cfg.hh new file mode 100644 index 0000000..7921aee --- /dev/null +++ b/src/base/date_time_scanner.cfg.hh @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2020, 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. + * + * @file date_time_scanner.hh + */ + +#ifndef lnav_date_time_scanner_cfg_hh +#define lnav_date_time_scanner_cfg_hh + +namespace date_time_scanner_ns { + +struct config { + bool c_zoned_to_local{true}; +}; + +} // namespace date_time_scanner_ns + +#endif diff --git a/src/base/date_time_scanner.hh b/src/base/date_time_scanner.hh index 90eaffa..cf8a0da 100644 --- a/src/base/date_time_scanner.hh +++ b/src/base/date_time_scanner.hh @@ -37,6 +37,7 @@ #include <sys/types.h> +#include "date/tz.h" #include "time_util.hh" /** @@ -59,13 +60,27 @@ struct date_time_scanner { this->dts_last_tm = exttm{}; } + struct lock_state { + int ls_fmt_index{-1}; + int ls_fmt_len{-1}; + }; + /** * Unlock this scanner so that the format is rediscovered. */ - void unlock() + lock_state unlock() { + auto retval = lock_state{this->dts_fmt_lock, this->dts_fmt_len}; + this->dts_fmt_lock = -1; this->dts_fmt_len = -1; + return retval; + } + + void relock(const lock_state& ls) + { + this->dts_fmt_lock = ls.ls_fmt_index; + this->dts_fmt_len = ls.ls_fmt_len; } void set_base_time(time_t base_time, const tm& local_tm); @@ -81,6 +96,7 @@ struct date_time_scanner { bool dts_keep_base_tz{false}; bool dts_local_time{false}; + bool dts_zoned_to_local{true}; time_t dts_base_time{0}; struct exttm dts_base_tm; int dts_fmt_lock{-1}; @@ -90,6 +106,7 @@ struct date_time_scanner { time_t dts_local_offset_cache{0}; time_t dts_local_offset_valid{0}; time_t dts_local_offset_expiry{0}; + const date::time_zone* dts_default_zone{nullptr}; static const int EXPIRE_TIME = 15 * 60; diff --git a/src/base/from_trait.hh b/src/base/from_trait.hh new file mode 100644 index 0000000..05c426e --- /dev/null +++ b/src/base/from_trait.hh @@ -0,0 +1,40 @@ +/** + * 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 from_trait_hh +#define from_trait_hh + +#include <string> + +#include "result.h" + +template<typename R, typename S> +Result<R, std::string> from(S v); + +#endif diff --git a/src/base/fs_util.cc b/src/base/fs_util.cc index f72aaa6..7cbc228 100644 --- a/src/base/fs_util.cc +++ b/src/base/fs_util.cc @@ -29,14 +29,30 @@ #include "fs_util.hh" +#include <stdlib.h> + #include "config.h" #include "fmt/format.h" #include "itertools.hh" +#include "lnav_log.hh" #include "opt_util.hh" namespace lnav { namespace filesystem { +Result<ghc::filesystem::path, std::string> +realpath(const ghc::filesystem::path& path) +{ + char resolved[PATH_MAX]; + auto rc = ::realpath(path.c_str(), resolved); + + if (rc == nullptr) { + return Err(std::string(strerror(errno))); + } + + return Ok(ghc::filesystem::path(resolved)); +} + Result<auto_fd, std::string> create_file(const ghc::filesystem::path& path, int flags, mode_t mode) { @@ -73,7 +89,7 @@ open_temp_file(const ghc::filesystem::path& pattern) int fd; strcpy(pattern_copy, pattern_str.c_str()); - if ((fd = mkstemp(pattern_copy)) == -1) { + if ((fd = mkostemp(pattern_copy, O_CLOEXEC)) == -1) { return Err( fmt::format(FMT_STRING("unable to create temporary file: {} -- {}"), pattern.string(), @@ -102,9 +118,12 @@ read_file(const ghc::filesystem::path& path) } } -Result<void, std::string> -write_file(const ghc::filesystem::path& path, const string_fragment& content) +Result<write_file_result, std::string> +write_file(const ghc::filesystem::path& path, + const string_fragment& content, + std::set<write_file_options> options) { + write_file_result retval; auto tmp_pattern = path; tmp_pattern += ".XXXXXX"; @@ -123,7 +142,25 @@ write_file(const ghc::filesystem::path& path, const string_fragment& content) bytes_written, content.length())); } + std::error_code ec; + if (options.count(write_file_options::backup_existing)) { + if (ghc::filesystem::exists(path, ec)) { + auto backup_path = path; + + backup_path += ".bak"; + ghc::filesystem::rename(path, backup_path, ec); + if (ec) { + return Err( + fmt::format(FMT_STRING("unable to backup file {}: {}"), + path.string(), + ec.message())); + } + + retval.wfr_backup_path = backup_path; + } + } + ghc::filesystem::rename(tmp_pair.first, path, ec); if (ec) { return Err( @@ -132,7 +169,7 @@ write_file(const ghc::filesystem::path& path, const string_fragment& content) ec.message())); } - return Ok(); + return Ok(retval); } std::string @@ -181,3 +218,20 @@ file_lock::file_lock(const ghc::filesystem::path& archive_path) } // namespace filesystem } // namespace lnav + +namespace fmt { + +auto +formatter<ghc::filesystem::path>::format(const ghc::filesystem::path& p, + format_context& ctx) + -> decltype(ctx.out()) const +{ + auto esc_res = fmt::v10::detail::find_escape(&(*p.native().begin()), + &(*p.native().end())); + if (esc_res.end == nullptr) { + return formatter<string_view>::format(p.native(), ctx); + } + + return format_to(ctx.out(), FMT_STRING("{:?}"), p.native()); +} +} // namespace fmt diff --git a/src/base/fs_util.hh b/src/base/fs_util.hh index b9253ff..7553075 100644 --- a/src/base/fs_util.hh +++ b/src/base/fs_util.hh @@ -30,6 +30,7 @@ #ifndef lnav_fs_util_hh #define lnav_fs_util_hh +#include <set> #include <string> #include <vector> @@ -41,6 +42,14 @@ namespace lnav { namespace filesystem { +inline bool +is_glob(const std::string& fn) +{ + return (fn.find('*') != std::string::npos + || fn.find('?') != std::string::npos + || fn.find('[') != std::string::npos); +} + inline int statp(const ghc::filesystem::path& path, struct stat* buf) { @@ -59,6 +68,9 @@ openp(const ghc::filesystem::path& path, int flags, mode_t mode) return open(path.c_str(), flags, mode); } +Result<ghc::filesystem::path, std::string> realpath( + const ghc::filesystem::path& path); + Result<auto_fd, std::string> create_file(const ghc::filesystem::path& path, int flags, mode_t mode); @@ -73,8 +85,18 @@ Result<std::pair<ghc::filesystem::path, auto_fd>, std::string> open_temp_file( Result<std::string, std::string> read_file(const ghc::filesystem::path& path); -Result<void, std::string> write_file(const ghc::filesystem::path& path, - const string_fragment& content); +enum class write_file_options { + backup_existing, +}; + +struct write_file_result { + nonstd::optional<ghc::filesystem::path> wfr_backup_path; +}; + +Result<write_file_result, std::string> write_file( + const ghc::filesystem::path& path, + const string_fragment& content, + std::set<write_file_options> options = {}); std::string build_path(const std::vector<ghc::filesystem::path>& paths); @@ -119,4 +141,12 @@ public: } // namespace filesystem } // namespace lnav +namespace fmt { +template<> +struct formatter<ghc::filesystem::path> : formatter<string_view> { + auto format(const ghc::filesystem::path& p, format_context& ctx) + -> decltype(ctx.out()) const; +}; +} // namespace fmt + #endif diff --git a/src/base/future_util.hh b/src/base/future_util.hh index 8797faa..ad57459 100644 --- a/src/base/future_util.hh +++ b/src/base/future_util.hh @@ -57,22 +57,32 @@ make_ready_future(T&& t) * A queue used to limit the number of futures that are running concurrently. * * @tparam T The result of the futures. - * @tparam MAX_QUEUE_SIZE The maximum number of futures that can be in flight. */ -template<typename T, int MAX_QUEUE_SIZE = 8> +template<typename T> class future_queue { public: + enum class processor_result_t { + ok, + interrupt, + }; + /** * @param processor The function to execute with the result of a future. + * @param max_queue_size The maximum number of futures that can be in + * flight. */ - explicit future_queue(std::function<void(T&)> processor) - : fq_processor(processor){}; - - ~future_queue() + explicit future_queue( + std::function<processor_result_t(std::future<T>&)> processor, + size_t max_queue_size = 8) + : fq_processor(processor), fq_max_queue_size(max_queue_size) { - this->pop_to(); } + future_queue(const future_queue&) = delete; + future_queue& operator=(const future_queue&) = delete; + + ~future_queue() { this->pop_to(); } + /** * Add a future to the queue. If the size of the queue is greater than the * MAX_QUEUE_SIZE, this call will block waiting for the first queued @@ -80,10 +90,10 @@ public: * * @param f The future to add to the queue. */ - void push_back(std::future<T>&& f) + processor_result_t push_back(std::future<T>&& f) { this->fq_deque.emplace_back(std::move(f)); - this->pop_to(MAX_QUEUE_SIZE); + return this->pop_to(this->fq_max_queue_size); } /** @@ -92,17 +102,24 @@ public: * * @param size The new desired size of the queue. */ - void pop_to(size_t size = 0) + processor_result_t pop_to(size_t size = 0) { + processor_result_t retval = processor_result_t::ok; + while (this->fq_deque.size() > size) { - auto v = this->fq_deque.front().get(); - this->fq_processor(v); + if (this->fq_processor(this->fq_deque.front()) + == processor_result_t::interrupt) + { + retval = processor_result_t::interrupt; + } this->fq_deque.pop_front(); } + return retval; } - std::function<void(T&)> fq_processor; + std::function<processor_result_t(std::future<T>&)> fq_processor; std::deque<std::future<T>> fq_deque; + size_t fq_max_queue_size; }; } // namespace futures diff --git a/src/base/humanize.time.cc b/src/base/humanize.time.cc index 68cfe0f..a791a3d 100644 --- a/src/base/humanize.time.cc +++ b/src/base/humanize.time.cc @@ -33,6 +33,7 @@ #include "config.h" #include "fmt/format.h" +#include "math_util.hh" #include "time_util.hh" namespace humanize { @@ -161,18 +162,21 @@ duration::to_string() const millis = -millis; } - uint64_t remaining; + uint64_t remaining = millis.count(); + uint64_t scale = 1; + if (this->d_msecs_resolution > 0) { + remaining = roundup(remaining, this->d_msecs_resolution); + } if (millis >= 10min) { - remaining - = std::chrono::duration_cast<std::chrono::seconds>(millis).count(); + remaining /= curr_interval->length; + scale *= curr_interval->length; curr_interval += 1; - } else { - remaining = millis.count(); } for (; curr_interval != std::end(intervals); curr_interval++) { uint64_t amount; char segment[32]; + auto skip = scale < this->d_msecs_resolution; if (curr_interval->length) { amount = remaining % curr_interval->length; @@ -181,11 +185,16 @@ duration::to_string() const amount = remaining; remaining = 0; } + scale *= curr_interval->length; - if (!amount && !remaining) { + if (amount == 0 && remaining == 0) { break; } + if (skip) { + continue; + } + snprintf(segment, sizeof(segment), curr_interval->format, diff --git a/src/base/humanize.time.hh b/src/base/humanize.time.hh index 96edebd..e1ebf67 100644 --- a/src/base/humanize.time.hh +++ b/src/base/humanize.time.hh @@ -74,12 +74,22 @@ class duration { public: static duration from_tv(const struct timeval& tv); + template<class Rep, class Period> + duration& with_resolution(const std::chrono::duration<Rep, Period>& res) + { + this->d_msecs_resolution + = std::chrono::duration_cast<std::chrono::milliseconds>(res) + .count(); + return *this; + } + std::string to_string() const; private: explicit duration(const struct timeval& tv) : d_timeval(tv) {} struct timeval d_timeval; + uint64_t d_msecs_resolution{1}; }; } // namespace time diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc index 676d2bc..010da52 100644 --- a/src/base/intern_string.cc +++ b/src/base/intern_string.cc @@ -37,6 +37,7 @@ #include "config.h" #include "pcrepp/pcre2pp.hh" +#include "ww898/cp_utf8.hpp" #include "xxHash/xxhash.h" const static int TABLE_SIZE = 4095; @@ -224,7 +225,9 @@ string_fragment::split_lines() const start = index + 1; } } - retval.emplace_back(this->sf_string, start, this->sf_end); + if (retval.empty() || start < this->sf_end) { + retval.emplace_back(this->sf_string, start, this->sf_end); + } return retval; } @@ -301,3 +304,164 @@ string_fragment::to_string_with_case_style(case_style style) const return retval; } + +std::string +string_fragment::to_unquoted_string() const +{ + auto sub_sf = *this; + + if (sub_sf.startswith("r") || sub_sf.startswith("u")) { + sub_sf = sub_sf.consume_n(1).value(); + } + if (sub_sf.length() >= 2 + && ((sub_sf.startswith("\"") && sub_sf.endswith("\"")) + || (sub_sf.startswith("'") && sub_sf.endswith("'")))) + { + std::string retval; + + sub_sf.sf_begin += 1; + sub_sf.sf_end -= 1; + retval.reserve(this->length()); + + auto in_escape = false; + for (auto ch : sub_sf) { + if (in_escape) { + switch (ch) { + case 'n': + retval.push_back('\n'); + break; + case 't': + retval.push_back('\t'); + break; + case 'r': + retval.push_back('\r'); + break; + default: + retval.push_back(ch); + break; + } + in_escape = false; + } else if (ch == '\\') { + in_escape = true; + } else { + retval.push_back(ch); + } + } + + return retval; + } + + return this->to_string(); +} + +uint32_t +string_fragment::front_codepoint() const +{ + size_t index = 0; + auto read_res = ww898::utf::utf8::read( + [this, &index]() { return this->data()[index++]; }); + if (read_res.isErr()) { + return this->data()[0]; + } + return read_res.unwrap(); +} + +Result<ssize_t, const char*> +string_fragment::codepoint_to_byte_index(ssize_t cp_index) const +{ + ssize_t retval = 0; + + while (cp_index > 0) { + if (retval >= this->length()) { + return Err("index is beyond the end of the string"); + } + auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() { + return std::make_pair(this->data()[retval], + this->length() - retval - 1); + })); + + retval += ch_len; + cp_index -= 1; + } + + return Ok(retval); +} + +string_fragment +string_fragment::sub_cell_range(int cell_start, int cell_end) const +{ + int byte_index = this->sf_begin; + nonstd::optional<int> byte_start; + nonstd::optional<int> byte_end; + int cell_index = 0; + + while (byte_index < this->sf_end) { + if (cell_start == cell_index) { + byte_start = byte_index; + } + if (!byte_end && cell_index >= cell_end) { + byte_end = byte_index; + break; + } + auto read_res = ww898::utf::utf8::read( + [this, &byte_index]() { return this->sf_string[byte_index++]; }); + if (read_res.isErr()) { + byte_index += 1; + } else { + auto ch = read_res.unwrap(); + + switch (ch) { + case '\t': + do { + cell_index += 1; + } while (cell_index % 8); + break; + default: + cell_index += wcwidth(read_res.unwrap()); + break; + } + } + } + if (cell_start == cell_index) { + byte_start = byte_index; + } + if (!byte_end) { + byte_end = byte_index; + } + + if (byte_start && byte_end) { + return this->sub_range(byte_start.value(), byte_end.value()); + } + + return string_fragment{}; +} + +size_t +string_fragment::column_width() const +{ + auto index = this->sf_begin; + size_t retval = 0; + + while (index < this->sf_end) { + auto read_res = ww898::utf::utf8::read( + [this, &index]() { return this->sf_string[index++]; }); + if (read_res.isErr()) { + retval += 1; + } else { + auto ch = read_res.unwrap(); + + switch (ch) { + case '\t': + do { + retval += 1; + } while (retval % 8); + break; + default: + retval += wcwidth(read_res.unwrap()); + break; + } + } + } + + return retval; +} diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh index 4ae40da..f77dc49 100644 --- a/src/base/intern_string.hh +++ b/src/base/intern_string.hh @@ -33,7 +33,6 @@ #define intern_string_hh #include <ostream> -#include <string> #include <vector> #include <assert.h> @@ -42,9 +41,9 @@ #include "fmt/format.h" #include "optional.hpp" +#include "result.h" #include "scn/util/string_view.h" #include "strnatcmp.h" -#include "ww898/cp_utf8.hpp" struct string_fragment { using iterator = const char*; @@ -143,6 +142,8 @@ struct string_fragment { Result<ssize_t, const char*> utf8_length() const; + size_t column_width() const; + const char* data() const { return &this->sf_string[this->sf_begin]; } const unsigned char* udata() const @@ -157,16 +158,7 @@ struct string_fragment { char front() const { return this->sf_string[this->sf_begin]; } - uint32_t front_codepoint() const - { - size_t index = 0; - try { - return ww898::utf::utf8::read( - [this, &index]() { return this->data()[index++]; }); - } catch (const std::runtime_error& e) { - return this->data()[0]; - } - } + uint32_t front_codepoint() const; char back() const { return this->sf_string[this->sf_end - 1]; } @@ -183,25 +175,10 @@ struct string_fragment { bool empty() const { return !this->is_valid() || length() == 0; } - Result<ssize_t, const char*> codepoint_to_byte_index(ssize_t cp_index) const - { - ssize_t retval = 0; - - while (cp_index > 0) { - if (retval >= this->length()) { - return Err("index is beyond the end of the string"); - } - auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() { - return std::make_pair(this->data()[retval], - this->length() - retval - 1); - })); - - retval += ch_len; - cp_index -= 1; - } + Result<ssize_t, const char*> codepoint_to_byte_index( + ssize_t cp_index) const; - return Ok(retval); - } + string_fragment sub_cell_range(int cell_start, int cell_end) const; const char& operator[](int index) const { @@ -228,6 +205,22 @@ struct string_fragment { return memcmp(this->data(), sf.data(), sf.length()) == 0; } + bool operator!=(const string_fragment& rhs) const + { + return !(*this == rhs); + } + + bool operator<(const string_fragment& rhs) const + { + auto rc = strncmp( + this->data(), rhs.data(), std::min(this->length(), rhs.length())); + if (rc < 0 || (rc == 0 && this->length() < rhs.length())) { + return true; + } + + return false; + } + bool iequal(const string_fragment& sf) const { if (this->length() != sf.length()) { @@ -290,6 +283,12 @@ struct string_fragment { this->sf_string, this->sf_begin + begin, this->sf_begin + end}; } + bool contains(const string_fragment& sf) const + { + return this->sf_string == sf.sf_string && this->sf_begin <= sf.sf_begin + && sf.sf_end <= this->sf_end; + } + size_t count(char ch) const { size_t retval = 0; @@ -315,7 +314,9 @@ struct string_fragment { } template<typename P> - string_fragment find_left_boundary(size_t start, P&& predicate) const + string_fragment find_left_boundary(size_t start, + P&& predicate, + size_t count = 1) const { assert((int) start <= this->length()); @@ -324,25 +325,33 @@ struct string_fragment { } while (start > 0) { if (predicate(this->data()[start])) { - start += 1; - break; + count -= 1; + if (count == 0) { + start += 1; + break; + } } start -= 1; } return string_fragment{ this->sf_string, - (int) start, + this->sf_begin + (int) start, this->sf_end, }; } template<typename P> - string_fragment find_right_boundary(size_t start, P&& predicate) const + string_fragment find_right_boundary(size_t start, + P&& predicate, + size_t count = 1) const { while ((int) start < this->length()) { if (predicate(this->data()[start])) { - break; + count -= 1; + if (count == 0) { + break; + } } start += 1; } @@ -355,10 +364,14 @@ struct string_fragment { } template<typename P> - string_fragment find_boundaries_around(size_t start, P&& predicate) const + string_fragment find_boundaries_around(size_t start, + P&& predicate, + size_t count = 1) const { - return this->template find_left_boundary(start, predicate) - .find_right_boundary(0, predicate); + auto left = this->template find_left_boundary(start, predicate, count); + + return left.find_right_boundary( + start - left.sf_begin, predicate, count); } nonstd::optional<std::pair<uint32_t, string_fragment>> consume_codepoint() @@ -446,8 +459,10 @@ struct string_fragment { }); } + using split_when_result = std::pair<string_fragment, string_fragment>; + template<typename P> - split_result split_when(P&& predicate) const + split_when_result split_when(P&& predicate) const { int consumed = 0; while (consumed < this->length()) { @@ -458,7 +473,33 @@ struct string_fragment { consumed += 1; } - if (consumed == 0) { + return std::make_pair( + string_fragment{ + this->sf_string, + this->sf_begin, + this->sf_begin + consumed, + }, + string_fragment{ + this->sf_string, + this->sf_begin + consumed + + ((consumed == this->length()) ? 0 : 1), + this->sf_end, + }); + } + + template<typename P> + split_result split_pair(P&& predicate) const + { + int consumed = 0; + while (consumed < this->length()) { + if (predicate(this->data()[consumed])) { + break; + } + + consumed += 1; + } + + if (consumed == this->length()) { return nonstd::nullopt; } @@ -517,6 +558,8 @@ struct string_fragment { return {this->data(), (size_t) this->length()}; } + std::string to_unquoted_string() const; + void clear() { this->sf_begin = 0; @@ -823,6 +866,12 @@ operator==(const string_fragment& left, const intern_string_t& right) && (memcmp(left.data(), right.get(), left.length()) == 0); } +inline string_fragment +operator"" _frag(const char* str, std::size_t len) +{ + return string_fragment::from_byte_range(str, 0, len); +} + namespace std { inline string to_string(const string_fragment& s) @@ -855,6 +904,12 @@ to_string_fragment(const std::string& s) return string_fragment(s.c_str(), 0, s.length()); } +inline string_fragment +to_string_fragment(const scn::string_view& sv) +{ + return string_fragment::from_bytes(sv.data(), sv.length()); +} + struct frag_hasher { size_t operator()(const string_fragment& sf) const { @@ -862,4 +917,11 @@ struct frag_hasher { } }; +struct intern_hasher { + size_t operator()(const intern_string_t& is) const + { + return hash_str(is.c_str(), is.size()); + } +}; + #endif diff --git a/src/base/intern_string.tests.cc b/src/base/intern_string.tests.cc index 8816803..71ff7c2 100644 --- a/src/base/intern_string.tests.cc +++ b/src/base/intern_string.tests.cc @@ -43,6 +43,15 @@ TEST_CASE("string_fragment::startswith") CHECK_FALSE(sf.startswith("abc")); } +TEST_CASE("string_fragment::lt") +{ + auto sf1 = string_fragment::from_const("abc"); + auto sf2 = string_fragment::from_const("abcdef"); + + CHECK(sf1 < sf2); + CHECK_FALSE(sf2 < sf1); +} + TEST_CASE("split_lines") { std::string in1 = "Hello, World!"; @@ -129,6 +138,12 @@ TEST_CASE("find_left_boundary") auto world_sf = sf.find_left_boundary( in1.length() - 3, [](auto ch) { return ch == '\n'; }); CHECK(world_sf.to_string() == "World!\n"); + auto world_sf2 = sf.find_left_boundary( + in1.length() - 3, [](auto ch) { return ch == '\n'; }, 2); + CHECK(world_sf2.to_string() == "Hello,\nWorld!\n"); + auto world_sf3 = sf.find_left_boundary( + in1.length() - 3, [](auto ch) { return ch == '\n'; }, 3); + CHECK(world_sf3.to_string() == "Hello,\nWorld!\n"); auto full_sf = sf.find_left_boundary(3, [](auto ch) { return ch == '\n'; }); CHECK(full_sf.to_string() == in1); @@ -137,16 +152,35 @@ TEST_CASE("find_left_boundary") TEST_CASE("find_right_boundary") { - std::string in1 = "Hello,\nWorld!\n"; - { - auto sf = string_fragment{in1}; + const auto sf = string_fragment::from_const("Hello,\nWorld!\n"); - auto world_sf = sf.find_right_boundary( - in1.length() - 3, [](auto ch) { return ch == '\n'; }); + auto world_sf = sf.find_right_boundary(sf.length() - 3, + string_fragment::tag1{'\n'}); CHECK(world_sf.to_string() == "Hello,\nWorld!"); auto hello_sf = sf.find_right_boundary(3, [](auto ch) { return ch == '\n'; }); CHECK(hello_sf.to_string() == "Hello,"); + auto hello_sf2 + = sf.find_right_boundary(3, string_fragment::tag1{'\n'}, 2); + CHECK(hello_sf2.to_string() == "Hello,\nWorld!"); + } +} + +TEST_CASE("find_boundaries_around") +{ + { + const auto sf = string_fragment::from_const( + R"(Hello, +World! +Goodbye, +World!)"); + + auto all_sf1 + = sf.find_boundaries_around(3, string_fragment::tag1{'\n'}); + CHECK(all_sf1 == "Hello,"); + auto all_sf2 + = sf.find_boundaries_around(3, string_fragment::tag1{'\n'}, 2); + CHECK(all_sf2 == "Hello,\nWorld!"); } } diff --git a/src/base/is_utf8.cc b/src/base/is_utf8.cc index f55dfe0..b8fbab0 100644 --- a/src/base/is_utf8.cc +++ b/src/base/is_utf8.cc @@ -64,7 +64,7 @@ is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator) { const auto* ustr = str.udata(); utf8_scan_result retval; - ssize_t i = 0; + ssize_t i = 0, valid_end = 0; while (i < str.length()) { if (ustr[i] == '\x1b') { @@ -72,20 +72,21 @@ is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator) } if (terminator && ustr[i] == terminator.value()) { - if (retval.usr_message == nullptr) { - retval.usr_valid_frag = str.sub_range(0, i); - } retval.usr_remaining = str.substr(i + 1); break; } + retval.usr_column_width_guess += 1; if (retval.usr_message != nullptr) { i += 1; continue; } - retval.usr_valid_frag = str.sub_range(0, i); + valid_end = i; if (ustr[i] <= 0x7F) /* 00..7F */ { + if (ustr[i] == '\t') { + retval.usr_column_width_guess += 7; + } i += 1; } else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ { if (i + 1 < str.length()) /* Expect a 2nd byte */ { @@ -308,6 +309,8 @@ is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator) } if (retval.usr_message == nullptr) { retval.usr_valid_frag = str.sub_range(0, i); + } else { + retval.usr_valid_frag = str.sub_range(0, valid_end); } return retval; } diff --git a/src/base/is_utf8.hh b/src/base/is_utf8.hh index 56a959f..81d09c6 100644 --- a/src/base/is_utf8.hh +++ b/src/base/is_utf8.hh @@ -40,6 +40,7 @@ struct utf8_scan_result { string_fragment usr_valid_frag{string_fragment::invalid()}; nonstd::optional<string_fragment> usr_remaining; bool usr_has_ansi{false}; + size_t usr_column_width_guess{0}; const char* remaining_ptr(const string_fragment& frag) const { diff --git a/src/base/itertools.hh b/src/base/itertools.hh index 058ceb8..70286fb 100644 --- a/src/base/itertools.hh +++ b/src/base/itertools.hh @@ -31,6 +31,8 @@ #define lnav_itertools_hh #include <algorithm> +#include <deque> +#include <map> #include <memory> #include <set> #include <type_traits> @@ -138,8 +140,16 @@ struct max_with_init { struct sum {}; +struct to_vector {}; + } // namespace details +inline details::to_vector +to_vector() +{ + return details::to_vector{}; +} + template<typename T> inline details::unwrap_or<T> unwrap_or(T value) @@ -619,12 +629,32 @@ operator|(nonstd::optional<T> in, template<typename T, typename F> auto -operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper) - -> std::vector<std::remove_const_t<std::remove_reference_t< - decltype(mapper.m_func(std::declval<typename T::value_type>()))>>> +operator|(const std::set<T>& in, + const lnav::itertools::details::mapper<F>& mapper) + -> std::set<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>> { - using return_type = std::vector<std::remove_const_t<std::remove_reference_t< - decltype(mapper.m_func(std::declval<typename T::value_type>()))>>>; + using return_type = std::set<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>>; + return_type retval; + + std::transform(in.begin(), + in.end(), + std::inserter(retval, retval.begin()), + mapper.m_func); + + return retval; +} + +template<typename T, typename F> +auto +operator|(const std::vector<T>& in, + const lnav::itertools::details::mapper<F>& mapper) + -> std::vector<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>> +{ + using return_type = std::vector<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>>; return_type retval; retval.reserve(in.size()); @@ -636,12 +666,50 @@ operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper) template<typename T, typename F> auto -operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper) +operator|(const std::deque<T>& in, + const lnav::itertools::details::mapper<F>& mapper) + -> std::vector<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>> +{ + using return_type = std::vector<std::remove_const_t< + std::remove_reference_t<decltype(mapper.m_func(std::declval<T>()))>>>; + return_type retval; + + retval.reserve(in.size()); + std::transform( + in.begin(), in.end(), std::back_inserter(retval), mapper.m_func); + + return retval; +} + +template<typename K, typename V, typename F> +auto +operator|(const std::map<K, V>& in, + const lnav::itertools::details::mapper<F>& mapper) -> std::vector< - std::remove_const_t<decltype(((*in.begin()).*mapper.m_func)())>> + std::remove_const_t<std::remove_reference_t<decltype(mapper.m_func( + std::declval<typename std::map<K, V>::value_type>()))>>> { using return_type = std::vector< - std::remove_const_t<decltype(((*in.begin()).*mapper.m_func)())>>; + std::remove_const_t<std::remove_reference_t<decltype(mapper.m_func( + std::declval<typename std::map<K, V>::value_type>()))>>>; + return_type retval; + + retval.reserve(in.size()); + std::transform( + in.begin(), in.end(), std::back_inserter(retval), mapper.m_func); + + return retval; +} + +template<typename T, typename F> +auto +operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper) + -> std::vector<std::remove_const_t< + std::remove_reference_t<decltype(((*in.begin()).*mapper.m_func)())>>> +{ + using return_type = std::vector<std::remove_const_t< + std::remove_reference_t<decltype(((*in.begin()).*mapper.m_func)())>>>; return_type retval; retval.reserve(in.size()); @@ -782,4 +850,16 @@ operator|(nonstd::optional<T> in, return in.value_or(unwrapper.uo_value); } +template<typename T> +std::vector<T> +operator|(std::set<T>&& in, lnav::itertools::details::to_vector tv) +{ + std::vector<T> retval; + + retval.reserve(in.size()); + std::copy(in.begin(), in.end(), std::back_inserter(retval)); + + return retval; +} + #endif diff --git a/src/base/keycodes.hh b/src/base/keycodes.hh new file mode 100644 index 0000000..7f3c3e2 --- /dev/null +++ b/src/base/keycodes.hh @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023, 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_keycodes_hh +#define lnav_keycodes_hh + +inline constexpr int +KEY_CTRL(char k) +{ + return k & 0x1f; +} + +#define KEY_ESCAPE 0x1b +#define KEY_DELETE 0x7f + +#endif diff --git a/src/base/line_range.hh b/src/base/line_range.hh new file mode 100644 index 0000000..70deb25 --- /dev/null +++ b/src/base/line_range.hh @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2023, 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_base_line_range_hh +#define lnav_base_line_range_hh + +#include <string> + +/** + * Encapsulates a range in a string. + */ +struct line_range { + enum class unit { + bytes, + codepoint, + }; + + int lr_start; + int lr_end; + unit lr_unit; + + explicit line_range(int start = -1, int end = -1, unit u = unit::bytes) + : lr_start(start), lr_end(end), lr_unit(u) + { + } + + bool is_valid() const { return this->lr_start != -1; } + + int length() const + { + return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start; + } + + bool empty() const { return this->length() == 0; } + + void clear() + { + this->lr_start = -1; + this->lr_end = -1; + } + + int end_for_string(const std::string& str) const + { + return this->lr_end == -1 ? str.length() : this->lr_end; + } + + bool contains(int pos) const + { + return this->lr_start <= pos + && (this->lr_end == -1 || pos < this->lr_end); + } + + bool contains(const struct line_range& other) const + { + return this->contains(other.lr_start) + && (this->lr_end == -1 || other.lr_end <= this->lr_end); + } + + bool intersects(const struct line_range& other) const + { + if (this->contains(other.lr_start)) { + return true; + } + if (other.lr_end > 0 && this->contains(other.lr_end - 1)) { + return true; + } + if (other.contains(this->lr_start)) { + return true; + } + + return false; + } + + line_range intersection(const struct line_range& other) const; + + line_range& shift(int32_t start, int32_t amount); + + line_range& shift_range(const line_range& cover, int32_t amount); + + void ltrim(const char* str) + { + while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) { + this->lr_start += 1; + } + } + + bool operator<(const struct line_range& rhs) const + { + if (this->lr_start < rhs.lr_start) { + return true; + } + if (this->lr_start > rhs.lr_start) { + return false; + } + + // this->lr_start == rhs.lr_start + if (this->lr_end == rhs.lr_end) { + return false; + } + + // When the start is the same, the longer range has a lower priority + // than the shorter range. + if (rhs.lr_end == -1) { + return false; + } + + if ((this->lr_end == -1) || (this->lr_end > rhs.lr_end)) { + return true; + } + return false; + } + + bool operator==(const struct line_range& rhs) const + { + return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end); + } + + const char* substr(const std::string& str) const + { + if (this->lr_start == -1) { + return str.c_str(); + } + return &(str.c_str()[this->lr_start]); + } + + size_t sublen(const std::string& str) const + { + if (this->lr_start == -1) { + return str.length(); + } + if (this->lr_end == -1) { + return str.length() - this->lr_start; + } + return this->length(); + } +}; + +#endif diff --git a/src/base/lnav.console.cc b/src/base/lnav.console.cc index a34ebac..b9d2d6c 100644 --- a/src/base/lnav.console.cc +++ b/src/base/lnav.console.cc @@ -45,6 +45,45 @@ using namespace lnav::roles::literals; namespace lnav { namespace console { +snippet +snippet::from_content_with_offset(intern_string_t src, + const attr_line_t& content, + size_t offset, + const std::string& errmsg) +{ + auto content_sf = string_fragment::from_str(content.get_string()); + auto line_with_error = content_sf.find_boundaries_around( + offset, string_fragment::tag1{'\n'}); + auto line_with_context = content_sf.find_boundaries_around( + offset, string_fragment::tag1{'\n'}, 3); + auto line_number = content_sf.sub_range(0, offset).count('\n'); + auto erroff_in_line = offset - line_with_error.sf_begin; + + attr_line_t pointer; + + pointer.append(erroff_in_line, ' ') + .append("^ "_snippet_border) + .append(lnav::roles::error(errmsg)) + .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE)); + + snippet retval; + retval.s_content + = content.subline(line_with_context.sf_begin, + line_with_error.sf_end - line_with_context.sf_begin); + if (line_with_error.sf_end >= retval.s_content.get_string().size()) { + retval.s_content.append("\n"); + } + retval.s_content.append(pointer).append( + content.subline(line_with_error.sf_end, + line_with_context.sf_end - line_with_error.sf_end)); + retval.s_location = source_location{ + src, + static_cast<int32_t>(1 + line_number), + }; + + return retval; +} + user_message user_message::raw(const attr_line_t& al) { @@ -238,13 +277,38 @@ curses_color_to_terminal_color(int curses_color) } } +static bool +get_no_color() +{ + return getenv("NO_COLOR") != nullptr; +} + +static bool +get_yes_color() +{ + return getenv("YES_COLOR") != nullptr; +} + +static bool +get_fd_tty(int fd) +{ + return isatty(fd); +} + void println(FILE* file, const attr_line_t& al) { + static const auto IS_NO_COLOR = get_no_color(); + static const auto IS_YES_COLOR = get_yes_color(); + static const auto IS_STDOUT_TTY = get_fd_tty(STDOUT_FILENO); + static const auto IS_STDERR_TTY = get_fd_tty(STDERR_FILENO); + const auto& str = al.get_string(); - if (getenv("NO_COLOR") != nullptr - || (!isatty(fileno(file)) && getenv("YES_COLOR") == nullptr)) + if (IS_NO_COLOR || (file != stdout && file != stderr) + || (((file == stdout && !IS_STDOUT_TTY) + || (file == stderr && !IS_STDERR_TTY)) + && !IS_YES_COLOR)) { fmt::print(file, "{}\n", str); return; @@ -273,6 +337,7 @@ println(FILE* file, const attr_line_t& al) auto line_style = fmt::text_style{}; auto fg_style = fmt::text_style{}; auto start = last_point.value(); + nonstd::optional<std::string> href; for (const auto& attr : al.get_attrs()) { if (!attr.sa_range.contains(start) @@ -282,7 +347,10 @@ println(FILE* file, const attr_line_t& al) } try { - if (attr.sa_type == &VC_BACKGROUND) { + if (attr.sa_type == &VC_HYPERLINK) { + auto saw = string_attr_wrapper<std::string>(&attr); + href = saw.get(); + } else if (attr.sa_type == &VC_BACKGROUND) { auto saw = string_attr_wrapper<int64_t>(&attr); auto color_opt = curses_color_to_terminal_color(saw.get()); @@ -349,6 +417,9 @@ println(FILE* file, const attr_line_t& al) case role_t::VCR_TEXT: case role_t::VCR_IDENTIFIER: break; + case role_t::VCR_ALT_ROW: + line_style |= fmt::emphasis::bold; + break; case role_t::VCR_SEARCH: line_style |= fmt::emphasis::reverse; break; @@ -407,6 +478,7 @@ println(FILE* file, const attr_line_t& al) case role_t::VCR_LIST_GLYPH: line_style |= fmt::fg(fmt::terminal_color::yellow); break; + case role_t::VCR_INLINE_CODE: case role_t::VCR_QUOTED_CODE: default_fg_style = fmt::fg(fmt::terminal_color::white); @@ -442,12 +514,69 @@ println(FILE* file, const attr_line_t& al) line_style |= default_bg_style; } + if (line_style.has_foreground() && line_style.has_background() + && !line_style.get_foreground().is_rgb + && !line_style.get_background().is_rgb + && line_style.get_foreground().value.term_color + == line_style.get_background().value.term_color) + { + auto new_style = fmt::text_style{}; + + if (line_style.has_emphasis()) { + new_style |= line_style.get_emphasis(); + } + new_style |= fmt::fg(line_style.get_foreground()); + if (line_style.get_background().value.term_color + == lnav::enums::to_underlying(fmt::terminal_color::black)) + { + new_style |= fmt::bg(fmt::terminal_color::white); + } else { + new_style |= fmt::bg(fmt::terminal_color::black); + } + line_style = new_style; + } + + if (href) { + fmt::print(file, FMT_STRING("\x1b]8;;{}\x1b\\"), href.value()); + } if (start < str.size()) { auto actual_end = std::min(str.size(), static_cast<size_t>(point)); - fmt::print(file, - line_style, - FMT_STRING("{}"), - str.substr(start, actual_end - start)); + auto sub = std::string{}; + + for (auto lpc = start; lpc < actual_end;) { + auto cp_start = lpc; + auto read_res = ww898::utf::utf8::read( + [&str, &lpc]() { return str[lpc++]; }); + + if (read_res.isErr()) { + sub.append(fmt::format( + FMT_STRING("{:?}"), + fmt::string_view{&str[cp_start], lpc - cp_start})); + continue; + } + + auto ch = read_res.unwrap(); + switch (ch) { + case '\b': + sub.append("\u232b"); + break; + case '\x1b': + sub.append("\u238b"); + break; + case '\x07': + sub.append("\U0001F514"); + break; + + default: + sub.append(&str[cp_start], lpc - cp_start); + break; + } + } + + fmt::print(file, line_style, FMT_STRING("{}"), sub); + } + if (href) { + fmt::print(file, FMT_STRING("\x1b]8;;\x1b\\")); } last_point = point; } diff --git a/src/base/lnav.console.hh b/src/base/lnav.console.hh index ac4c2b0..591093e 100644 --- a/src/base/lnav.console.hh +++ b/src/base/lnav.console.hh @@ -42,6 +42,11 @@ namespace console { void println(FILE* file, const attr_line_t& al); struct snippet { + static snippet from_content_with_offset(intern_string_t src, + const attr_line_t& content, + size_t offset, + const std::string& errmsg); + static snippet from(intern_string_t src, const attr_line_t& content) { snippet retval; @@ -114,6 +119,26 @@ struct user_message { } template<typename C> + user_message& with_context_snippets(C snippets) + { + this->um_snippets.insert(this->um_snippets.begin(), + std::make_move_iterator(std::begin(snippets)), + std::make_move_iterator(std::end(snippets))); + if (this->um_snippets.size() > 1) { + for (auto iter = this->um_snippets.begin(); + iter != this->um_snippets.end();) + { + if (iter->s_content.empty()) { + iter = this->um_snippets.erase(iter); + } else { + ++iter; + } + } + } + return *this; + } + + template<typename C> user_message& with_snippets(C snippets) { this->um_snippets.insert(this->um_snippets.end(), @@ -121,7 +146,8 @@ struct user_message { std::make_move_iterator(std::end(snippets))); if (this->um_snippets.size() > 1) { for (auto iter = this->um_snippets.begin(); - iter != this->um_snippets.end();) { + iter != this->um_snippets.end();) + { if (iter->s_content.empty()) { iter = this->um_snippets.erase(iter); } else { diff --git a/src/base/lnav.gzip.hh b/src/base/lnav.gzip.hh index bd73965..060aad4 100644 --- a/src/base/lnav.gzip.hh +++ b/src/base/lnav.gzip.hh @@ -34,12 +34,27 @@ #include <string> +#include <sys/time.h> + #include "auto_mem.hh" #include "result.h" namespace lnav { namespace gzip { +struct header { + timeval h_mtime{}; + auto_buffer h_extra{auto_buffer::alloc(0)}; + std::string h_name; + std::string h_comment; + + bool empty() const + { + return this->h_mtime.tv_sec == 0 && this->h_extra.empty() + && this->h_name.empty() && this->h_comment.empty(); + } +}; + bool is_gzipped(const char* buffer, size_t len); Result<auto_buffer, std::string> compress(const void* input, size_t len); diff --git a/src/base/lnav_log.cc b/src/base/lnav_log.cc index 2e5dbba..b770698 100644 --- a/src/base/lnav_log.cc +++ b/src/base/lnav_log.cc @@ -190,7 +190,7 @@ log_argv(int argc, char* argv[]) const char* log_path = getenv("LNAV_LOG_PATH"); if (log_path != nullptr) { - lnav_log_file = make_optional_from_nullable(fopen(log_path, "a")); + lnav_log_file = make_optional_from_nullable(fopen(log_path, "ae")); } log_info("argv[%d] =", argc); diff --git a/src/base/map_util.hh b/src/base/map_util.hh new file mode 100644 index 0000000..a3e565e --- /dev/null +++ b/src/base/map_util.hh @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2023, 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_map_util_hh +#define lnav_map_util_hh + +#include <functional> +#include <map> +#include <type_traits> +#include <vector> + +#include "optional.hpp" + +namespace lnav { +namespace map { + +template<typename C> +nonstd::optional< + std::reference_wrapper<std::conditional_t<std::is_const<C>::value, + const typename C::mapped_type, + typename C::mapped_type>>> +find(C& container, const typename C::key_type& key) +{ + auto iter = container.find(key); + if (iter != container.end()) { + return nonstd::make_optional(std::ref(iter->second)); + } + + return nonstd::nullopt; +} + +template<typename K, typename V, typename M = std::map<K, V>> +M +from_vec(const std::vector<std::pair<K, V>>& container) +{ + M retval; + + for (const auto& elem : container) { + retval[elem.first] = elem.second; + } + + return retval; +} + +template<typename K, typename V> +class small : public std::vector<std::pair<K, V>> { +public: + auto insert(const K& key, const V& value) + { + auto pos = this->begin(); + + while (pos != this->end() && pos->first < key) { + ++pos; + } + return this->emplace(pos, std::make_pair(key, value)); + } + + auto find(const K& key) + { + auto retval = this->begin(); + + while (retval != this->end() + && (retval->first < key || key < retval->first)) + { + ++retval; + } + + return retval; + } + + auto find(const K& key) const + { + auto retval = this->begin(); + + while (retval != this->end() + && (retval->first < key || key < retval->first)) + { + ++retval; + } + + return retval; + } + + V& operator[](const K& key) + { + auto iter = this->find(key); + if (iter != this->end()) { + return iter->second; + } + + this->emplace_back(key, V{}); + + return this->back().second; + } +}; + +} // namespace map +} // namespace lnav + +#endif diff --git a/src/base/math_util.hh b/src/base/math_util.hh index 842b319..fcd507c 100644 --- a/src/base/math_util.hh +++ b/src/base/math_util.hh @@ -33,6 +33,7 @@ #include <sys/types.h> #undef rounddown +#undef roundup /** * Round down a number based on a given granularity. @@ -47,6 +48,17 @@ rounddown(Size size, Step step) return size - (size % step); } +template<typename Size, typename Step> +inline auto +roundup(Size size, Step step) +{ + auto retval = size + (step - 1); + + retval -= (retval % step); + + return retval; +} + inline int rounddown_offset(size_t size, int step, int offset) { @@ -70,4 +82,74 @@ abs_diff(T a, T b) return a > b ? a - b : b - a; } +template<typename T> +class clamped { +public: + static clamped from(T value, T min, T max) { return {value, min, max}; } + + clamped& operator+=(T rhs) + { + if (rhs < 0) { + return this->operator-=(-rhs); + } + + if (this->c_value + rhs < this->c_max) { + this->c_value += rhs; + } else { + this->c_value = this->c_max; + } + + return *this; + } + + clamped& operator-=(T rhs) + { + if (rhs < 0) { + return this->operator+=(-rhs); + } + + if (this->c_value - rhs > this->c_min) { + this->c_value -= rhs; + } else { + this->c_value = this->c_min; + } + + return *this; + } + + bool available_to_consume(T rhs) const + { + return (this->c_value - rhs > this->c_min); + } + + bool try_consume(T rhs) + { + if (rhs == 0) { + return false; + } + + if (this->c_value - rhs > this->c_min) { + this->c_value -= rhs; + return true; + } + + return false; + } + + operator T() const { return this->c_value; } + + bool is_min() const { return this->c_value == this->c_min; } + + T get_min() const { return this->c_min; } + + T get_max() const { return this->c_max; } + +private: + clamped(T value, T min, T max) : c_value(value), c_min(min), c_max(max) {} + + T c_value; + T c_min; + T c_max; +}; + #endif diff --git a/src/base/network.tcp.hh b/src/base/network.tcp.hh index 49fc392..c046bb6 100644 --- a/src/base/network.tcp.hh +++ b/src/base/network.tcp.hh @@ -33,6 +33,7 @@ #include <string> #include "auto_fd.hh" +#include "optional.hpp" #include "result.h" namespace network { diff --git a/src/base/piper.file.cc b/src/base/piper.file.cc new file mode 100644 index 0000000..1443604 --- /dev/null +++ b/src/base/piper.file.cc @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2023, 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 "piper.file.hh" + +#include <arpa/inet.h> +#include <unistd.h> + +#include "base/lnav_log.hh" +#include "base/paths.hh" + +namespace lnav { +namespace piper { + +const char HEADER_MAGIC[4] = {'L', 0, 'N', 1}; + +const ghc::filesystem::path& +storage_path() +{ + static auto INSTANCE = lnav::paths::workdir() / "piper"; + + return INSTANCE; +} + +nonstd::optional<auto_buffer> +read_header(int fd, const char* first8) +{ + if (memcmp(first8, HEADER_MAGIC, sizeof(HEADER_MAGIC)) != 0) { + log_trace("first 4 bytes are not a piper header: %02x%02x%02x%02x", + first8[0], + first8[1], + first8[2], + first8[3]); + return nonstd::nullopt; + } + + uint32_t meta_size = ntohl(*((uint32_t*) &first8[4])); + + auto meta_buf = auto_buffer::alloc(meta_size); + if (meta_buf.in() == nullptr) { + log_error("failed to alloc %d bytes for header", meta_size); + return nonstd::nullopt; + } + auto meta_prc = pread(fd, meta_buf.in(), meta_size, 8); + if (meta_prc != meta_size) { + log_error("failed to read piper header: %s", strerror(errno)); + return nonstd::nullopt; + } + meta_buf.resize(meta_size); + + return meta_buf; +} + +} // namespace piper +} // namespace lnav diff --git a/src/piper_proc.hh b/src/base/piper.file.hh index 0caf29e..7a263ea 100644 --- a/src/piper_proc.hh +++ b/src/base/piper.file.hh @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2012, Timothy Stack + * Copyright (c) 2023, Timothy Stack * * All rights reserved. * @@ -25,62 +25,52 @@ * 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. - * - * @file piper_proc.hh */ -#ifndef piper_proc_hh -#define piper_proc_hh +#ifndef lnav_piper_file_hh +#define lnav_piper_file_hh +#include <map> #include <string> -#include <sys/types.h> +#include <sys/time.h> -#include "base/auto_fd.hh" +#include "auto_mem.hh" +#include "ghc/filesystem.hpp" +#include "optional.hpp" +#include "time_util.hh" -/** - * Creates a subprocess that reads data from a pipe and writes it to a file so - * lnav can treat it like any other file and do preads. - * - * TODO: Add support for gzipped files. - */ -class piper_proc { -public: - class error : public std::exception { - public: - error(int err) : e_err(err) {} +namespace lnav { +namespace piper { - int e_err; - }; +struct header { + timeval h_ctime{}; + std::string h_name; + std::string h_cwd; + std::map<std::string, std::string> h_env; - /** - * Forks a subprocess that will read data from the given file descriptor - * and write it to a temporary file. - * - * @param pipefd The file descriptor to read the file contents from. - * @param timestamp True if an ISO 8601 timestamp should be prepended onto - * the lines read from pipefd. - * @param filefd The descriptor for the backing file. - */ - piper_proc(auto_fd pipefd, bool timestamp, auto_fd filefd); + bool operator<(const header& rhs) const + { + if (this->h_ctime < rhs.h_ctime) { + return true; + } - bool has_exited(); + if (this->h_ctime == rhs.h_ctime) { + return this->h_name < rhs.h_name; + } - /** - * Terminates the child process. - */ - virtual ~piper_proc(); + return false; + } +}; - /** @return The file descriptor for the temporary file. */ - auto_fd get_fd() { return this->pp_fd.dup(); } +const ghc::filesystem::path& storage_path(); - pid_t get_child_pid() const { return this->pp_child; } +constexpr size_t HEADER_SIZE = 8; +extern const char HEADER_MAGIC[4]; -private: - /** A file descriptor that refers to the temporary file. */ - auto_fd pp_fd; +nonstd::optional<auto_buffer> read_header(int fd, const char* first8); + +} // namespace piper +} // namespace lnav - /** The child process' pid. */ - pid_t pp_child; -}; #endif diff --git a/src/base/string_attr_type.cc b/src/base/string_attr_type.cc index 9a3950b..fb31036 100644 --- a/src/base/string_attr_type.cc +++ b/src/base/string_attr_type.cc @@ -40,12 +40,13 @@ string_attr_type<void> SA_PREFORMATTED("preformatted"); string_attr_type<std::string> SA_INVALID("invalid"); string_attr_type<std::string> SA_ERROR("error"); string_attr_type<int64_t> SA_LEVEL("level"); -string_attr_type<string_fragment> SA_ORIGIN("origin"); string_attr_type<int64_t> SA_ORIGIN_OFFSET("origin-offset"); string_attr_type<role_t> VC_ROLE("role"); string_attr_type<role_t> VC_ROLE_FG("role-fg"); string_attr_type<text_attrs> VC_STYLE("style"); string_attr_type<int64_t> VC_GRAPHIC("graphic"); +string_attr_type<block_elem_t> VC_BLOCK_ELEM("block-elem"); string_attr_type<int64_t> VC_FOREGROUND("foreground"); string_attr_type<int64_t> VC_BACKGROUND("background"); +string_attr_type<std::string> VC_HYPERLINK("hyperlink"); diff --git a/src/base/string_attr_type.hh b/src/base/string_attr_type.hh index 5bff345..0b8cd85 100644 --- a/src/base/string_attr_type.hh +++ b/src/base/string_attr_type.hh @@ -55,9 +55,11 @@ enum class role_t : int32_t { VCR_ALT_ROW, /*< Highlight for alternating rows in a list */ VCR_HIDDEN, VCR_CURSOR_LINE, + VCR_DISABLED_CURSOR_LINE, VCR_ADJUSTED_TIME, VCR_SKEWED_TIME, VCR_OFFSET_TIME, + VCR_FILE_OFFSET, VCR_INVALID_MSG, VCR_STATUS, /*< Normal status line text. */ VCR_WARN_STATUS, @@ -94,6 +96,9 @@ enum class role_t : int32_t { VCR_DOC_DIRECTIVE, VCR_VARIABLE, VCR_SYMBOL, + VCR_NULL, + VCR_ASCII_CTRL, + VCR_NON_ASCII, VCR_NUMBER, VCR_RE_SPECIAL, VCR_RE_REPEAT, @@ -125,6 +130,13 @@ enum class role_t : int32_t { VCR_FOOTNOTE_BORDER, VCR_FOOTNOTE_TEXT, VCR_SNIPPET_BORDER, + VCR_INDENT_GUIDE, + VCR_INLINE_CODE, + VCR_FUNCTION, + VCR_TYPE, + VCR_SEP_REF_ACC, + VCR_SUGGESTION, + VCR_SELECTED_TEXT, VCR__MAX }; @@ -156,6 +168,11 @@ struct text_attrs { nonstd::optional<short> ta_bg_color; }; +struct block_elem_t { + wchar_t value; + role_t role; +}; + using string_attr_value = mapbox::util::variant<int64_t, role_t, text_attrs, @@ -164,7 +181,8 @@ using string_attr_value = mapbox::util::variant<int64_t, std::shared_ptr<logfile>, bookmark_metadata*, timespec, - string_fragment>; + string_fragment, + block_elem_t>; class string_attr_type_base { public: @@ -211,15 +229,16 @@ extern string_attr_type<void> SA_PREFORMATTED; extern string_attr_type<std::string> SA_INVALID; extern string_attr_type<std::string> SA_ERROR; extern string_attr_type<int64_t> SA_LEVEL; -extern string_attr_type<string_fragment> SA_ORIGIN; extern string_attr_type<int64_t> SA_ORIGIN_OFFSET; extern string_attr_type<role_t> VC_ROLE; extern string_attr_type<role_t> VC_ROLE_FG; extern string_attr_type<text_attrs> VC_STYLE; extern string_attr_type<int64_t> VC_GRAPHIC; +extern string_attr_type<block_elem_t> VC_BLOCK_ELEM; extern string_attr_type<int64_t> VC_FOREGROUND; extern string_attr_type<int64_t> VC_BACKGROUND; +extern string_attr_type<std::string> VC_HYPERLINK; namespace lnav { @@ -233,6 +252,14 @@ preformatted(S str) return std::make_pair(std::move(str), SA_PREFORMATTED.template value()); } +template<typename S> +inline std::pair<S, string_attr_pair> +href(S str, std::string href) +{ + return std::make_pair(std::move(str), + VC_HYPERLINK.template value(std::move(href))); +} + } // namespace attrs } // namespace string @@ -288,6 +315,14 @@ ok(S str) template<typename S> inline std::pair<S, string_attr_pair> +hidden(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_HIDDEN)); +} + +template<typename S> +inline std::pair<S, string_attr_pair> file(S str) { return std::make_pair(std::move(str), @@ -344,6 +379,14 @@ identifier(S str) template<typename S> inline std::pair<S, string_attr_pair> +string(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_STRING)); +} + +template<typename S> +inline std::pair<S, string_attr_pair> hr(S str) { return std::make_pair(std::move(str), @@ -494,6 +537,14 @@ h6(S str) VC_ROLE.template value(role_t::VCR_H6)); } +template<typename S> +inline std::pair<S, string_attr_pair> +suggestion(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_SUGGESTION)); +} + namespace literals { inline std::pair<std::string, string_attr_pair> operator"" _ok(const char* str, @@ -510,6 +561,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _error( VC_ROLE.template value(role_t::VCR_ERROR)); } +inline std::pair<std::string, string_attr_pair> operator"" _warning( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_WARNING)); +} + inline std::pair<std::string, string_attr_pair> operator"" _info( const char* str, std::size_t len) { @@ -517,6 +575,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _info( VC_ROLE.template value(role_t::VCR_INFO)); } +inline std::pair<std::string, string_attr_pair> operator"" _status_title( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_STATUS_TITLE)); +} + inline std::pair<std::string, string_attr_pair> operator"" _symbol( const char* str, std::size_t len) { @@ -629,6 +694,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _code_border( VC_ROLE.template value(role_t::VCR_CODE_BORDER)); } +inline std::pair<std::string, string_attr_pair> operator"" _table_header( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_TABLE_HEADER)); +} + inline std::pair<std::string, string_attr_pair> operator"" _table_border( const char* str, std::size_t len) { @@ -671,6 +743,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _snippet_border( VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER)); } +inline std::pair<std::string, string_attr_pair> operator"" _link( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_HYPERLINK.template value(std::string(str, len))); +} + } // namespace literals } // namespace roles diff --git a/src/base/string_util.cc b/src/base/string_util.cc index 8af686c..39e80f6 100644 --- a/src/base/string_util.cc +++ b/src/base/string_util.cc @@ -199,6 +199,34 @@ is_url(const std::string& fn) } size_t +last_word_str(char* str, size_t len, size_t max_len) +{ + if (len < max_len) { + return len; + } + + size_t last_start = 0; + + for (size_t index = 0; index < len; index++) { + switch (str[index]) { + case '.': + case '-': + case '/': + case ':': + last_start = index + 1; + break; + } + } + + if (last_start == 0) { + return len; + } + + memmove(&str[0], &str[last_start], len - last_start); + return len - last_start; +} + +size_t abbreviate_str(char* str, size_t len, size_t max_len) { size_t last_start = 1; @@ -273,7 +301,7 @@ is_blank(const std::string& str) } std::string -scrub_ws(const char* in) +scrub_ws(const char* in, ssize_t len) { static const std::string TAB_SYMBOL = "\u21e5"; static const std::string LF_SYMBOL = "\u240a"; @@ -281,7 +309,9 @@ scrub_ws(const char* in) std::string retval; - for (size_t lpc = 0; in[lpc]; lpc++) { + for (size_t lpc = 0; (len == -1 && in[lpc]) || (len >= 0 && lpc < len); + lpc++) + { auto ch = in[lpc]; switch (ch) { @@ -302,3 +332,93 @@ scrub_ws(const char* in) return retval; } + +namespace fmt { +auto +formatter<lnav::tainted_string>::format(const lnav::tainted_string& ts, + format_context& ctx) + -> decltype(ctx.out()) const +{ + auto esc_res = fmt::v10::detail::find_escape(&(*ts.ts_str.begin()), + &(*ts.ts_str.end())); + if (esc_res.end == nullptr) { + return formatter<string_view>::format(ts.ts_str, ctx); + } + + return format_to(ctx.out(), FMT_STRING("{:?}"), ts.ts_str); +} +} // namespace fmt + +namespace lnav { +namespace pcre2pp { + +static bool +is_meta(char ch) +{ + switch (ch) { + case '\\': + case '^': + case '$': + case '.': + case '[': + case ']': + case '(': + case ')': + case '*': + case '+': + case '?': + case '{': + case '}': + return true; + default: + return false; + } +} + +static nonstd::optional<const char*> +char_escape_seq(char ch) +{ + switch (ch) { + case '\t': + return "\\t"; + case '\n': + return "\\n"; + } + + return nonstd::nullopt; +} + +std::string +quote(string_fragment str) +{ + std::string retval; + + while (true) { + auto cp_pair_opt = str.consume_codepoint(); + if (!cp_pair_opt) { + break; + } + + auto cp_pair = cp_pair_opt.value(); + if ((cp_pair.first & ~0xff) == 0) { + if (is_meta(cp_pair.first)) { + retval.push_back('\\'); + } else { + auto esc_seq = char_escape_seq(cp_pair.first); + if (esc_seq) { + retval.append(esc_seq.value()); + str = cp_pair_opt->second; + continue; + } + } + } + ww898::utf::utf8::write(cp_pair.first, + [&retval](char ch) { retval.push_back(ch); }); + str = cp_pair_opt->second; + } + + return retval; +} + +} // namespace pcre2pp +} // namespace lnav diff --git a/src/base/string_util.hh b/src/base/string_util.hh index 73a8b87..886fb6c 100644 --- a/src/base/string_util.hh +++ b/src/base/string_util.hh @@ -100,7 +100,12 @@ endswith(const std::string& str, const char (&suffix)[N]) void truncate_to(std::string& str, size_t max_char_len); -std::string scrub_ws(const char* in); +std::string scrub_ws(const char* in, ssize_t len = -1); +inline std::string +scrub_ws(const string_fragment& sf) +{ + return scrub_ws(sf.data(), sf.length()); +} inline std::string trim(const std::string& str) @@ -115,6 +120,16 @@ trim(const std::string& str) return str.substr(start, end - start); } +inline const char* +ltrim(const char* str) +{ + while (isspace(*str)) { + str += 1; + } + + return str; +} + inline std::string rtrim(const std::string& str) { @@ -213,6 +228,8 @@ bool is_blank(const std::string& str); size_t abbreviate_str(char* str, size_t len, size_t max_len); +size_t last_word_str(char* str, size_t len, size_t max_len); + void split_ws(const std::string& str, std::vector<std::string>& toks_out); std::string repeat(const std::string& input, size_t num); @@ -229,4 +246,53 @@ on_blank(const std::string& str, const std::string& def) return str; } +namespace lnav { +class tainted_string { +public: + explicit tainted_string(std::string s) : ts_str(std::move(s)) {} + + bool operator==(const tainted_string& other) const + { + return this->ts_str == other.ts_str; + } + + bool operator!=(const tainted_string& other) const + { + return this->ts_str != other.ts_str; + } + + bool operator<(const tainted_string& other) const + { + return this->ts_str < other.ts_str; + } + + bool empty() const { return this->ts_str.empty(); } + + size_t length() const { return this->ts_str.length(); } + + size_t size() const { return this->ts_str.size(); } + + friend fmt::formatter<lnav::tainted_string>; + +private: + const std::string ts_str; +}; +} // namespace lnav + +namespace fmt { +template<> +struct formatter<lnav::tainted_string> : formatter<string_view> { + auto format(const lnav::tainted_string& ts, + format_context& ctx) -> decltype(ctx.out()) const; +}; +} // namespace fmt + +namespace lnav { +namespace pcre2pp { + +std::string quote(string_fragment sf); + +} +} // namespace lnav + #endif diff --git a/src/base/string_util.tests.cc b/src/base/string_util.tests.cc index 98cf5c8..c27f2a2 100644 --- a/src/base/string_util.tests.cc +++ b/src/base/string_util.tests.cc @@ -87,4 +87,47 @@ TEST_CASE("strnatcmp") CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0); } + { + constexpr const char* n1 = "servers"; + constexpr const char* n2 = "servers.alpha"; + + CHECK(strnatcasecmp(strlen(n1), n1, strlen(n2), n2) < 0); + } + { + static constexpr const char* TOKENS = "[](){}"; + const std::string n1 = "[servers]"; + const std::string n2 = "[servers.alpha]"; + + auto lhs = string_fragment::from_str(n1).trim(TOKENS); + auto rhs = string_fragment::from_str(n2).trim(TOKENS); + CHECK(strnatcasecmp(lhs.length(), lhs.data(), rhs.length(), rhs.data()) + < 0); + } + + { + const std::string a = "10.112.81.15"; + const std::string b = "192.168.202.254"; + + int ipcmp = 0; + auto rc = ipv4cmp(a.length(), a.c_str(), b.length(), b.c_str(), &ipcmp); + CHECK(rc == 1); + CHECK(ipcmp == -1); + } +} + +TEST_CASE("last_word_str") +{ + { + std::string s = "foobar baz"; + + auto rc = last_word_str(&s[0], s.length(), 6); + CHECK(s.length() == rc); + } + { + std::string s = "com.example.foo"; + + auto rc = last_word_str(&s[0], s.length(), 6); + s.resize(rc); + CHECK(s == "foo"); + } } diff --git a/src/base/strnatcmp.c b/src/base/strnatcmp.c index 6773164..0ea332d 100644 --- a/src/base/strnatcmp.c +++ b/src/base/strnatcmp.c @@ -275,13 +275,13 @@ int ipv4cmp(int a_len, nat_char const *a, } for (; ai < a_len; ai++) { - if (!isdigit((unsigned char)a[ai]) || a[ai] != '.') { + if (!isdigit((unsigned char)a[ai]) && a[ai] != '.') { return 0; } } for (; bi < b_len; bi++) { - if (!isdigit((unsigned char)b[bi]) || b[bi] != '.') { + if (!isdigit((unsigned char)b[bi]) && b[bi] != '.') { return 0; } } diff --git a/src/base/time_util.cc b/src/base/time_util.cc index 0d46107..5aee017 100644 --- a/src/base/time_util.cc +++ b/src/base/time_util.cc @@ -30,10 +30,15 @@ */ #include <chrono> +#include <map> #include "time_util.hh" +#include <date/ptz.h> + #include "config.h" +#include "lnav_log.hh" +#include "optional.hpp" namespace lnav { @@ -75,8 +80,66 @@ strftime_rfc3339( return index; } +static nonstd::optional<Posix::time_zone> +get_posix_zone(const char* name) +{ + if (name == nullptr) { + return nonstd::nullopt; + } + + try { + return date::zoned_traits<Posix::time_zone>::locate_zone(name); + } catch (const std::runtime_error& e) { + log_error("invalid TZ value: %s -- %s", name, e.what()); + return nonstd::nullopt; + } +} + +static const date::time_zone* +get_date_zone(const char* name) +{ + if (name == nullptr) { + return date::current_zone(); + } + + try { + return date::locate_zone(name); + } catch (const std::runtime_error& e) { + log_error("invalid TZ value: %s -- %s", name, e.what()); + return date::current_zone(); + } +} + +date::sys_seconds +to_sys_time(date::local_seconds secs) +{ + static const auto* TZ = getenv("TZ"); + static const auto TZ_POSIX_ZONE = get_posix_zone(TZ); + static const auto* TZ_DATE_ZONE = get_date_zone(TZ); + + if (TZ_POSIX_ZONE) { + return TZ_POSIX_ZONE.value().to_sys(secs); + } + + return TZ_DATE_ZONE->to_sys(secs); +} + +date::local_seconds +to_local_time(date::sys_seconds secs) +{ + static const auto* TZ = getenv("TZ"); + static const auto TZ_POSIX_ZONE = get_posix_zone(TZ); + static const auto* TZ_DATE_ZONE = get_date_zone(TZ); + + if (TZ_POSIX_ZONE) { + return TZ_POSIX_ZONE.value().to_local(secs); + } + + return TZ_DATE_ZONE->to_local(secs); } +} // namespace lnav + static time_t BAD_DATE = -1; time_t @@ -225,15 +288,68 @@ secs2tm(lnav::time64_t tim, struct tm* res) return (res); } +exttm +exttm::from_tv(const timeval& tv) +{ + auto retval = exttm{}; + + retval.et_tm = *gmtime(&tv.tv_sec); + retval.et_nsec = tv.tv_usec * 1000; + + return retval; +} + struct timeval exttm::to_timeval() const { struct timeval retval; retval.tv_sec = tm2sec(&this->et_tm); + retval.tv_sec -= this->et_gmtoff; retval.tv_usec = std::chrono::duration_cast<std::chrono::microseconds>( std::chrono::nanoseconds(this->et_nsec)) .count(); return retval; } + +time_range& +time_range::operator|=(const time_range& rhs) +{ + if (rhs.tr_begin < this->tr_begin) { + this->tr_begin = rhs.tr_begin; + } + if (this->tr_end < rhs.tr_end) { + this->tr_end = rhs.tr_end; + } + + return *this; +} + +void +time_range::extend_to(const timeval& tv) +{ + if (tv < this->tr_begin) { + // logs aren't always in time-order + this->tr_begin = tv; + } else if (this->tr_end < tv) { + this->tr_end = tv; + } +} + +std::chrono::milliseconds +time_range::duration() const +{ + auto diff = this->tr_end - this->tr_begin; + + return std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::seconds(diff.tv_sec)) + + std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::microseconds(diff.tv_usec)); +} + +bool +time_range::contains_inclusive(const timeval& tv) const +{ + return (this->tr_begin <= tv) && (tv <= this->tr_end); +} diff --git a/src/base/time_util.hh b/src/base/time_util.hh index ef9687f..5640463 100644 --- a/src/base/time_util.hh +++ b/src/base/time_util.hh @@ -30,6 +30,8 @@ #ifndef lnav_time_util_hh #define lnav_time_util_hh +#include <chrono> + #include <inttypes.h> #include <string.h> #include <sys/time.h> @@ -37,6 +39,7 @@ #include <time.h> #include "config.h" +#include "date/date.h" namespace lnav { @@ -48,6 +51,10 @@ ssize_t strftime_rfc3339(char* buffer, int millis, char sep = ' '); +date::sys_seconds to_sys_time(date::local_seconds secs); + +date::local_seconds to_local_time(date::sys_seconds secs); + } // namespace lnav struct tm* secs2tm(lnav::time64_t tim, struct tm* res); @@ -84,9 +91,15 @@ enum exttm_bits_t { ETB_SECOND_SET, ETB_MACHINE_ORIENTED, ETB_EPOCH_TIME, + ETB_SUB_NOT_IN_FORMAT, ETB_MILLIS_SET, ETB_MICROS_SET, ETB_NANOS_SET, + ETB_ZONE_SET, + ETB_Z_FOR_UTC, + ETB_Z_COLON, + ETB_Z_IS_UTC, + ETB_Z_IS_GMT, }; enum exttm_flags_t { @@ -98,12 +111,20 @@ enum exttm_flags_t { ETF_SECOND_SET = (1UL << ETB_SECOND_SET), ETF_MACHINE_ORIENTED = (1UL << ETB_MACHINE_ORIENTED), ETF_EPOCH_TIME = (1UL << ETB_EPOCH_TIME), + ETF_SUB_NOT_IN_FORMAT = (1UL << ETB_SUB_NOT_IN_FORMAT), ETF_MILLIS_SET = (1UL << ETB_MILLIS_SET), ETF_MICROS_SET = (1UL << ETB_MICROS_SET), ETF_NANOS_SET = (1UL << ETB_NANOS_SET), + ETF_ZONE_SET = (1UL << ETB_ZONE_SET), + ETF_Z_FOR_UTC = (1UL << ETB_Z_FOR_UTC), + ETF_Z_COLON = (1UL << ETB_Z_COLON), + ETF_Z_IS_UTC = (1UL << ETB_Z_IS_UTC), + ETF_Z_IS_GMT = (1UL << ETB_Z_IS_GMT), }; struct exttm { + static exttm from_tv(const timeval& tv); + struct tm et_tm {}; int32_t et_nsec{0}; unsigned int et_flags{0}; @@ -139,6 +160,13 @@ operator<(const struct timeval& left, const struct timeval& right) } inline bool +operator<=(const struct timeval& left, const struct timeval& right) +{ + return left.tv_sec < right.tv_sec + || ((left.tv_sec == right.tv_sec) && (left.tv_usec <= right.tv_usec)); +} + +inline bool operator!=(const struct timeval& left, const struct timeval& right) { return left.tv_sec != right.tv_sec || left.tv_usec != right.tv_usec; @@ -147,7 +175,7 @@ operator!=(const struct timeval& left, const struct timeval& right) inline bool operator==(const struct timeval& left, const struct timeval& right) { - return left.tv_sec == right.tv_sec || left.tv_usec == right.tv_usec; + return left.tv_sec == right.tv_sec && left.tv_usec == right.tv_usec; } inline struct timeval @@ -159,16 +187,31 @@ operator-(const struct timeval& lhs, const struct timeval& rhs) return diff; } +inline struct timeval +operator+(const struct timeval& lhs, const struct timeval& rhs) +{ + struct timeval retval; + + timeradd(&lhs, &rhs, &retval); + return retval; +} + typedef int64_t mstime_t; inline mstime_t +to_mstime(const timeval& tv) +{ + return tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL; +} + +inline mstime_t getmstime() { struct timeval tv; gettimeofday(&tv, nullptr); - return tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL; + return to_mstime(tv); } inline struct timeval @@ -203,4 +246,21 @@ hour_num(time_t ti) return ti / (60 * 60); } +struct time_range { + struct timeval tr_begin; + struct timeval tr_end; + + bool operator<(const time_range& rhs) const + { + return this->tr_begin < rhs.tr_begin; + } + + time_range& operator|=(const time_range& rhs); + + bool contains_inclusive(const timeval& tv) const; + + void extend_to(const timeval& tv); + std::chrono::milliseconds duration() const; +}; + #endif diff --git a/src/base/types.hh b/src/base/types.hh new file mode 100644 index 0000000..ba6f914 --- /dev/null +++ b/src/base/types.hh @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023, 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_base_types_hh +#define lnav_base_types_hh + +struct null_value_t {}; + +#endif |