summaryrefslogtreecommitdiffstats
path: root/src/base
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/base/CMakeLists.txt83
-rw-r--r--src/base/Makefile.am110
-rw-r--r--src/base/README.md1
-rw-r--r--src/base/ansi_scrubber.cc388
-rw-r--r--src/base/ansi_scrubber.hh75
-rw-r--r--src/base/attr_line.builder.cc30
-rw-r--r--src/base/attr_line.builder.hh119
-rw-r--r--src/base/attr_line.cc537
-rw-r--r--src/base/attr_line.hh758
-rw-r--r--src/base/attr_line.tests.cc91
-rw-r--r--src/base/auto_fd.hh318
-rw-r--r--src/base/auto_mem.hh402
-rw-r--r--src/base/auto_pid.cc63
-rw-r--r--src/base/auto_pid.hh175
-rw-r--r--src/base/bus.hh68
-rw-r--r--src/base/date_time_scanner.cc308
-rw-r--r--src/base/date_time_scanner.hh137
-rw-r--r--src/base/enum_util.hh48
-rw-r--r--src/base/file_range.hh78
-rw-r--r--src/base/fs_util.cc183
-rw-r--r--src/base/fs_util.hh122
-rw-r--r--src/base/fs_util.tests.cc56
-rw-r--r--src/base/func_util.hh159
-rw-r--r--src/base/future_util.hh111
-rw-r--r--src/base/humanize.cc110
-rw-r--r--src/base/humanize.file_size.tests.cc51
-rw-r--r--src/base/humanize.hh58
-rw-r--r--src/base/humanize.network.cc76
-rw-r--r--src/base/humanize.network.hh109
-rw-r--r--src/base/humanize.network.tests.cc118
-rw-r--r--src/base/humanize.time.cc208
-rw-r--r--src/base/humanize.time.hh88
-rw-r--r--src/base/humanize.time.tests.cc125
-rw-r--r--src/base/injector.bind.hh173
-rw-r--r--src/base/injector.hh230
-rw-r--r--src/base/intern_string.cc303
-rw-r--r--src/base/intern_string.hh865
-rw-r--r--src/base/intern_string.tests.cc152
-rw-r--r--src/base/is_utf8.cc313
-rw-r--r--src/base/is_utf8.hh59
-rw-r--r--src/base/isc.cc147
-rw-r--r--src/base/isc.hh225
-rw-r--r--src/base/itertools.hh785
-rw-r--r--src/base/lnav.console.cc494
-rw-r--r--src/base/lnav.console.hh176
-rw-r--r--src/base/lnav.console.into.hh51
-rw-r--r--src/base/lnav.gzip.cc135
-rw-r--r--src/base/lnav.gzip.hh54
-rw-r--r--src/base/lnav.gzip.tests.cc67
-rw-r--r--src/base/lnav_log.cc686
-rw-r--r--src/base/lnav_log.hh188
-rw-r--r--src/base/log_level_enum.hh65
-rw-r--r--src/base/lrucache.hpp83
-rw-r--r--src/base/math_util.hh73
-rw-r--r--src/base/network.tcp.cc75
-rw-r--r--src/base/network.tcp.hh80
-rw-r--r--src/base/network.tcp.tests.cc50
-rw-r--r--src/base/opt_util.hh105
-rw-r--r--src/base/paths.cc130
-rw-r--r--src/base/paths.hh58
-rw-r--r--src/base/result.h1032
-rw-r--r--src/base/snippet_highlighters.cc344
-rw-r--r--src/base/snippet_highlighters.hh43
-rw-r--r--src/base/string_attr_type.cc51
-rw-r--r--src/base/string_attr_type.hh679
-rw-r--r--src/base/string_util.cc304
-rw-r--r--src/base/string_util.hh232
-rw-r--r--src/base/string_util.tests.cc90
-rw-r--r--src/base/strnatcmp.c302
-rw-r--r--src/base/strnatcmp.h41
-rw-r--r--src/base/test_base.cc33
-rw-r--r--src/base/time_util.cc239
-rw-r--r--src/base/time_util.hh206
73 files changed, 14781 insertions, 0 deletions
diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt
new file mode 100644
index 0000000..aa4143f
--- /dev/null
+++ b/src/base/CMakeLists.txt
@@ -0,0 +1,83 @@
+add_library(
+ base STATIC
+ ../config.h.in
+ ansi_scrubber.cc
+ attr_line.cc
+ attr_line.builder.cc
+ auto_pid.cc
+ date_time_scanner.cc
+ fs_util.cc
+ humanize.cc
+ humanize.network.cc
+ humanize.time.cc
+ intern_string.cc
+ is_utf8.cc
+ isc.cc
+ lnav.console.cc
+ lnav.gzip.cc
+ lnav_log.cc
+ network.tcp.cc
+ paths.cc
+ snippet_highlighters.cc
+ string_attr_type.cc
+ string_util.cc
+ strnatcmp.c
+ time_util.cc
+
+ ansi_scrubber.hh
+ attr_line.hh
+ attr_line.builder.hh
+ auto_fd.hh
+ auto_mem.hh
+ auto_pid.hh
+ bus.hh
+ date_time_scanner.hh
+ enum_util.hh
+ fs_util.hh
+ func_util.hh
+ future_util.hh
+ humanize.hh
+ humanize.network.hh
+ humanize.time.hh
+ injector.hh
+ injector.bind.hh
+ intern_string.hh
+ is_utf8.hh
+ isc.hh
+ itertools.hh
+ lnav.console.hh
+ lnav.console.into.hh
+ log_level_enum.hh
+ lrucache.hpp
+ math_util.hh
+ network.tcp.hh
+ paths.hh
+ result.h
+ snippet_highlighters.hh
+ string_attr_type.hh
+ strnatcmp.h
+ time_util.hh
+
+ ../third-party/xxHash/xxhash.h
+ ../third-party/xxHash/xxhash.c
+)
+
+target_include_directories(base PUBLIC . .. ../third-party
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(base cppfmt cppscnlib pcrepp ncurses::libcurses pthread)
+
+add_executable(
+ test_base
+ attr_line.tests.cc
+ fs_util.tests.cc
+ humanize.file_size.tests.cc
+ humanize.network.tests.cc
+ humanize.time.tests.cc
+ intern_string.tests.cc
+ lnav.gzip.tests.cc
+ string_util.tests.cc
+ network.tcp.tests.cc
+ test_base.cc)
+target_include_directories(test_base PUBLIC ../third-party/doctest-root)
+target_link_libraries(test_base base pcrepp ZLIB::ZLIB)
+add_test(NAME test_base COMMAND test_base)
diff --git a/src/base/Makefile.am b/src/base/Makefile.am
new file mode 100644
index 0000000..4a459a6
--- /dev/null
+++ b/src/base/Makefile.am
@@ -0,0 +1,110 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ -Wall \
+ -I$(top_srcdir)/src/ \
+ -I$(top_srcdir)/src/third-party \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(SQLITE3_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ $(LIBCURL_CPPFLAGS)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = libbase.a
+
+noinst_HEADERS = \
+ ansi_scrubber.hh \
+ attr_line.hh \
+ attr_line.builder.hh \
+ auto_fd.hh \
+ auto_mem.hh \
+ auto_pid.hh \
+ bus.hh \
+ date_time_scanner.hh \
+ enum_util.hh \
+ file_range.hh \
+ fs_util.hh \
+ func_util.hh \
+ future_util.hh \
+ humanize.hh \
+ humanize.network.hh \
+ humanize.time.hh \
+ injector.hh \
+ injector.bind.hh \
+ intern_string.hh \
+ is_utf8.hh \
+ isc.hh \
+ itertools.hh \
+ lnav_log.hh \
+ lnav.console.hh \
+ lnav.console.into.hh \
+ lnav.gzip.hh \
+ log_level_enum.hh \
+ lrucache.hpp \
+ math_util.hh \
+ network.tcp.hh \
+ opt_util.hh \
+ paths.hh \
+ result.h \
+ snippet_highlighters.hh \
+ string_attr_type.hh \
+ string_util.hh \
+ strnatcmp.h \
+ time_util.hh
+
+libbase_a_SOURCES = \
+ ansi_scrubber.cc \
+ attr_line.cc \
+ attr_line.builder.cc \
+ auto_pid.cc \
+ date_time_scanner.cc \
+ fs_util.cc \
+ humanize.cc \
+ humanize.network.cc \
+ humanize.time.cc \
+ intern_string.cc \
+ is_utf8.cc \
+ isc.cc \
+ lnav.console.cc \
+ lnav.gzip.cc \
+ lnav_log.cc \
+ network.tcp.cc \
+ paths.cc \
+ snippet_highlighters.cc \
+ string_attr_type.cc \
+ string_util.cc \
+ strnatcmp.c \
+ time_util.cc \
+ ../third-party/xxHash/xxhash.h \
+ ../third-party/xxHash/xxhash.c
+
+check_PROGRAMS = \
+ test_base
+
+test_base_SOURCES = \
+ attr_line.tests.cc \
+ fs_util.tests.cc \
+ humanize.file_size.tests.cc \
+ humanize.network.tests.cc \
+ humanize.time.tests.cc \
+ intern_string.tests.cc \
+ lnav.gzip.tests.cc \
+ string_util.tests.cc \
+ test_base.cc
+
+test_base_LDADD = \
+ libbase.a \
+ ../fmtlib/libcppfmt.a \
+ ../third-party/scnlib/src/libscnlib.a \
+ ../pcrepp/libpcrepp.a
+
+TESTS = \
+ test_base
diff --git a/src/base/README.md b/src/base/README.md
new file mode 100644
index 0000000..944dde8
--- /dev/null
+++ b/src/base/README.md
@@ -0,0 +1 @@
+# libbase -- Library of utility functions
diff --git a/src/base/ansi_scrubber.cc b/src/base/ansi_scrubber.cc
new file mode 100644
index 0000000..98f6c96
--- /dev/null
+++ b/src/base/ansi_scrubber.cc
@@ -0,0 +1,388 @@
+/**
+ * Copyright (c) 2013, 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 ansi_scrubber.cc
+ */
+
+#include <algorithm>
+
+#include "ansi_scrubber.hh"
+
+#include "base/opt_util.hh"
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "scn/scn.h"
+#include "view_curses.hh"
+
+static const lnav::pcre2pp::code&
+ansi_regex()
+{
+ static const auto retval = lnav::pcre2pp::code::from_const(
+ "\x1b\\[([\\d=;\\?]*)([a-zA-Z])|(?:\\X\x08\\X)+");
+
+ return retval;
+}
+
+size_t
+erase_ansi_escapes(string_fragment input)
+{
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ const auto& regex = ansi_regex();
+ nonstd::optional<int> move_start;
+ size_t fill_index = 0;
+
+ auto matcher = regex.capture_from(input).into(md);
+ while (true) {
+ auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK);
+
+ if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
+ break;
+ }
+ if (match_res.is<lnav::pcre2pp::matcher::error>()) {
+ log_error("ansi scrub regex failure");
+ break;
+ }
+
+ auto sf = md[0].value();
+ auto bs_index_res = sf.codepoint_to_byte_index(1);
+
+ if (move_start) {
+ auto move_len = sf.sf_begin - move_start.value();
+ memmove(input.writable_data(fill_index),
+ input.data() + move_start.value(),
+ move_len);
+ fill_index += move_len;
+ } else {
+ fill_index = sf.sf_begin;
+ }
+
+ if (sf.length() >= 3 && bs_index_res.isOk()
+ && sf[bs_index_res.unwrap()] == '\b')
+ {
+ static const auto OVERSTRIKE_RE
+ = lnav::pcre2pp::code::from_const(R"((\X)\x08(\X))");
+
+ auto loop_res = OVERSTRIKE_RE.capture_from(sf).for_each(
+ [&fill_index, &input](lnav::pcre2pp::match_data& over_md) {
+ auto lhs = over_md[1].value();
+ if (lhs == "_") {
+ auto rhs = over_md[2].value();
+ memmove(input.writable_data(fill_index),
+ rhs.data(),
+ rhs.length());
+ fill_index += rhs.length();
+ } else {
+ memmove(input.writable_data(fill_index),
+ lhs.data(),
+ lhs.length());
+ fill_index += lhs.length();
+ }
+ });
+ }
+ move_start = md.remaining().sf_begin;
+ }
+
+ memmove(input.writable_data(fill_index),
+ md.remaining().data(),
+ md.remaining().length());
+ fill_index += md.remaining().length();
+
+ return fill_index;
+}
+
+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;
+
+ 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);
+
+ if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
+ break;
+ }
+ if (match_res.is<lnav::pcre2pp::matcher::error>()) {
+ log_error("ansi scrub regex failure");
+ break;
+ }
+
+ const auto sf = md[0].value();
+ auto bs_index_res = sf.codepoint_to_byte_index(1);
+
+ if (sf.length() >= 3 && bs_index_res.isOk()
+ && sf[bs_index_res.unwrap()] == '\b')
+ {
+ ssize_t fill_index = sf.sf_begin;
+ line_range bold_range;
+ line_range ul_range;
+ auto sub_sf = sf;
+
+ while (!sub_sf.empty()) {
+ auto lhs_opt = sub_sf.consume_codepoint();
+ if (!lhs_opt) {
+ return;
+ }
+ auto lhs_pair = lhs_opt.value();
+ auto mid_opt = lhs_pair.second.consume_codepoint();
+ if (!mid_opt) {
+ return;
+ }
+ auto mid_pair = mid_opt.value();
+ auto rhs_opt = mid_pair.second.consume_codepoint();
+ if (!rhs_opt) {
+ 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}));
+ bold_range.clear();
+ }
+ if (ul_range.is_valid()) {
+ ul_range.lr_end += 1;
+ } else {
+ ul_range.lr_start = fill_index;
+ ul_range.lr_end = fill_index + 1;
+ }
+ auto cp = lhs_pair.first == '_' ? rhs_pair.first
+ : lhs_pair.first;
+ ww898::utf::utf8::write(cp, [&str, &fill_index](auto ch) {
+ str[fill_index++] = ch;
+ });
+ } else {
+ if (sa != nullptr && ul_range.is_valid()) {
+ sa->emplace_back(
+ ul_range, VC_STYLE.value(text_attrs{A_UNDERLINE}));
+ ul_range.clear();
+ }
+ if (bold_range.is_valid()) {
+ bold_range.lr_end += 1;
+ } else {
+ bold_range.lr_start = fill_index;
+ bold_range.lr_end = fill_index + 1;
+ }
+ try {
+ ww898::utf::utf8::write(lhs_pair.first,
+ [&str, &fill_index](auto ch) {
+ str[fill_index++] = ch;
+ });
+ } catch (const std::runtime_error& e) {
+ log_error("invalid UTF-8 at %d", sf.sf_begin);
+ return;
+ }
+ }
+ }
+
+ 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));
+ }
+
+ if (sa != nullptr && ul_range.is_valid()) {
+ 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}));
+ 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);
+ continue;
+ }
+
+ auto seq = md[1].value();
+ auto terminator = md[2].value();
+ struct line_range lr;
+ bool has_attrs = false;
+ text_attrs attrs;
+ 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) {
+ auto ansi_code = ansi_code_res.value();
+ if (90 <= ansi_code && ansi_code <= 97) {
+ ansi_code -= 60;
+ attrs.ta_attrs |= A_STANDOUT;
+ }
+ if (30 <= ansi_code && ansi_code <= 37) {
+ attrs.ta_fg_color = ansi_code - 30;
+ }
+ if (40 <= ansi_code && ansi_code <= 47) {
+ attrs.ta_bg_color = ansi_code - 40;
+ }
+ switch (ansi_code) {
+ case 1:
+ attrs.ta_attrs |= A_BOLD;
+ break;
+
+ case 2:
+ attrs.ta_attrs |= A_DIM;
+ break;
+
+ case 4:
+ attrs.ta_attrs |= A_UNDERLINE;
+ break;
+
+ case 7:
+ attrs.ta_attrs |= A_REVERSE;
+ break;
+ }
+ }
+ lpc = str.find(';', lpc);
+ if (lpc != std::string::npos) {
+ lpc += 1;
+ }
+ }
+ has_attrs = true;
+ break;
+
+ 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(),
+ ' ');
+ }
+ break;
+ }
+
+ 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,
+ ' ');
+ }
+ }
+ break;
+ }
+
+ 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;
+ }
+ }
+ 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;
+ }
+ rit->sa_range.lr_end = sf.sf_begin;
+ }
+ 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));
+ }
+ role | [&lr, &sa](role_t r) {
+ sa->emplace_back(lr, VC_ROLE.value(r));
+ };
+ }
+ 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();
+ }
+
+ matcher.reload_input(str, sf.sf_begin);
+ }
+
+ 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));
+ }
+}
+
+void
+add_ansi_vars(std::map<std::string, scoped_value_t>& vars)
+{
+ vars["ansi_csi"] = ANSI_CSI;
+ vars["ansi_norm"] = ANSI_NORM;
+ vars["ansi_bold"] = ANSI_BOLD_START;
+ vars["ansi_underline"] = ANSI_UNDERLINE_START;
+ vars["ansi_black"] = ANSI_COLOR(COLOR_BLACK);
+ vars["ansi_red"] = ANSI_COLOR(COLOR_RED);
+ vars["ansi_green"] = ANSI_COLOR(COLOR_GREEN);
+ vars["ansi_yellow"] = ANSI_COLOR(COLOR_YELLOW);
+ vars["ansi_blue"] = ANSI_COLOR(COLOR_BLUE);
+ vars["ansi_magenta"] = ANSI_COLOR(COLOR_MAGENTA);
+ vars["ansi_cyan"] = ANSI_COLOR(COLOR_CYAN);
+ vars["ansi_white"] = ANSI_COLOR(COLOR_WHITE);
+}
diff --git a/src/base/ansi_scrubber.hh b/src/base/ansi_scrubber.hh
new file mode 100644
index 0000000..b832e17
--- /dev/null
+++ b/src/base/ansi_scrubber.hh
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2013, 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 ansi_scrubber.hh
+ */
+
+#ifndef lnav_ansi_scrubber_hh
+#define lnav_ansi_scrubber_hh
+
+#include <map>
+#include <string>
+
+#include "attr_line.hh"
+#include "shlex.resolver.hh"
+
+#define ANSI_CSI "\x1b["
+#define ANSI_CHAR_ATTR "m"
+#define ANSI_BOLD_PARAM "1"
+#define ANSI_BOLD_START ANSI_CSI ANSI_BOLD_PARAM ANSI_CHAR_ATTR
+#define ANSI_UNDERLINE_START ANSI_CSI "4m"
+#define ANSI_NORM ANSI_CSI "0m"
+#define ANSI_STRIKE_PARAM "9"
+#define ANSI_STRIKE_START ANSI_CSI ANSI_STRIKE_PARAM ANSI_CHAR_ATTR
+
+#define ANSI_BOLD(msg) ANSI_BOLD_START msg ANSI_NORM
+#define ANSI_UNDERLINE(msg) ANSI_UNDERLINE_START msg ANSI_NORM
+
+#define ANSI_ROLE(msg) ANSI_CSI "%dO" msg ANSI_NORM
+#define XANSI_COLOR(col) "3" #col
+#define ANSI_COLOR_PARAM(col) XANSI_COLOR(col)
+#define ANSI_COLOR(col) ANSI_CSI XANSI_COLOR(col) "m"
+
+/**
+ * Check a string for ANSI escape sequences, process them, remove them, and add
+ * any style attributes to the given attribute container.
+ *
+ * @param str The string to check for ANSI escape sequences.
+ * @param sa The container for any style attributes.
+ */
+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/attr_line.builder.cc b/src/base/attr_line.builder.cc
new file mode 100644
index 0000000..95416dc
--- /dev/null
+++ b/src/base/attr_line.builder.cc
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2022, 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 "attr_line.builder.hh"
diff --git a/src/base/attr_line.builder.hh b/src/base/attr_line.builder.hh
new file mode 100644
index 0000000..1e62532
--- /dev/null
+++ b/src/base/attr_line.builder.hh
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2022, 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_attr_line_builder_hh
+#define lnav_attr_line_builder_hh
+
+#include <utility>
+
+#include "attr_line.hh"
+
+class attr_line_builder {
+public:
+ explicit attr_line_builder(attr_line_t& al) : alb_line(al) {}
+
+ class attr_guard {
+ public:
+ attr_guard(attr_line_t& al, string_attr_pair sap)
+ : ag_line(al), ag_start(al.get_string().length()),
+ ag_attr(std::move(sap))
+ {
+ }
+
+ attr_guard(const attr_guard&) = delete;
+
+ attr_guard& operator=(const attr_guard&) = delete;
+
+ attr_guard(attr_guard&& other) noexcept
+ : ag_line(other.ag_line), ag_start(other.ag_start),
+ ag_attr(std::move(other.ag_attr))
+ {
+ other.ag_start = nonstd::nullopt;
+ }
+
+ ~attr_guard()
+ {
+ if (this->ag_start) {
+ this->ag_line.al_attrs.emplace_back(
+ line_range{
+ this->ag_start.value(),
+ (int) this->ag_line.get_string().length(),
+ },
+ this->ag_attr);
+ }
+ }
+
+ private:
+ attr_line_t& ag_line;
+ nonstd::optional<int> ag_start;
+ string_attr_pair ag_attr;
+ };
+
+ attr_guard with_attr(string_attr_pair sap)
+ {
+ return {this->alb_line, std::move(sap)};
+ }
+
+ template<typename... Args>
+ attr_line_builder& overlay_attr(Args... args)
+ {
+ this->alb_line.al_attrs.template emplace_back(args...);
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_builder& overlay_attr_for_char(int index, Args... args)
+ {
+ this->alb_line.al_attrs.template emplace_back(
+ line_range{index, index + 1}, args...);
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_builder& append(Args... args)
+ {
+ this->alb_line.append(args...);
+
+ return *this;
+ }
+
+ attr_line_builder& indent(size_t amount)
+ {
+ auto pre = this->with_attr(SA_PREFORMATTED.value());
+
+ this->append(amount, ' ');
+
+ return *this;
+ }
+
+private:
+ attr_line_t& alb_line;
+};
+
+#endif
diff --git a/src/base/attr_line.cc b/src/base/attr_line.cc
new file mode 100644
index 0000000..b65f4ba
--- /dev/null
+++ b/src/base/attr_line.cc
@@ -0,0 +1,537 @@
+/**
+ * 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.
+ */
+
+#include <algorithm>
+
+#include "attr_line.hh"
+
+#include <stdarg.h>
+
+#include "ansi_scrubber.hh"
+#include "auto_mem.hh"
+#include "config.h"
+#include "lnav_log.hh"
+#include "pcrepp/pcre2pp.hh"
+
+attr_line_t&
+attr_line_t::with_ansi_string(const char* str, ...)
+{
+ auto_mem<char> formatted_str;
+ va_list args;
+
+ va_start(args, str);
+ auto ret = vasprintf(formatted_str.out(), str, args);
+ va_end(args);
+
+ if (ret >= 0 && formatted_str != nullptr) {
+ this->al_string = formatted_str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+ }
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::with_ansi_string(const std::string& str)
+{
+ this->al_string = str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+
+ return *this;
+}
+
+namespace text_stream {
+struct word {
+ string_fragment w_word;
+ string_fragment w_remaining;
+};
+
+struct space {
+ string_fragment s_value;
+ string_fragment s_remaining;
+};
+
+struct corrupt {
+ string_fragment c_value;
+ string_fragment c_remaining;
+};
+
+struct eof {
+ string_fragment e_remaining;
+};
+
+using chunk = mapbox::util::variant<word, space, corrupt, eof>;
+
+chunk
+consume(const string_fragment text)
+{
+ static const auto WORD_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^[^\p{Z}\p{So}\p{C}]+)");
+ static const auto SPACE_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^\s)");
+
+ if (text.empty()) {
+ return eof{text};
+ }
+
+ auto word_find_res
+ = WORD_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (word_find_res) {
+ auto split_res = text.split_n(word_find_res->f_all.length()).value();
+
+ return word{split_res.first, split_res.second};
+ }
+
+ if (isspace(text.front())) {
+ auto split_res = text.split_n(1).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto space_find_res
+ = SPACE_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (space_find_res) {
+ auto split_res = text.split_n(space_find_res->f_all.length()).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto csize_res = ww898::utf::utf8::char_size(
+ [&text]() { return std::make_pair(text.front(), text.length()); });
+
+ if (csize_res.isErr()) {
+ auto split_res = text.split_n(1);
+
+ return corrupt{split_res->first, split_res->second};
+ }
+
+ auto split_res = text.split_n(csize_res.unwrap());
+
+ return word{split_res->first, split_res->second};
+}
+
+} // namespace text_stream
+
+static void
+split_attrs(attr_line_t& al, const line_range& lr)
+{
+ if (lr.empty()) {
+ return;
+ }
+
+ string_attrs_t new_attrs;
+
+ for (auto& attr : al.al_attrs) {
+ if (!lr.intersects(attr.sa_range)) {
+ continue;
+ }
+
+ new_attrs.emplace_back(line_range{lr.lr_end, attr.sa_range.lr_end},
+ std::make_pair(attr.sa_type, attr.sa_value));
+ attr.sa_range.lr_end = lr.lr_start;
+ }
+ for (auto& new_attr : new_attrs) {
+ al.al_attrs.emplace_back(std::move(new_attr));
+ }
+}
+
+attr_line_t&
+attr_line_t::insert(size_t index,
+ const attr_line_t& al,
+ text_wrap_settings* tws)
+{
+ if (index < this->al_string.length()) {
+ shift_string_attrs(this->al_attrs, index, al.al_string.length());
+ }
+
+ this->al_string.insert(index, al.al_string);
+
+ for (const auto& sa : al.al_attrs) {
+ this->al_attrs.emplace_back(sa);
+
+ line_range& lr = this->al_attrs.back().sa_range;
+
+ lr.shift(0, index);
+ if (lr.lr_end == -1) {
+ lr.lr_end = index + al.al_string.length();
+ }
+ }
+
+ if (tws == nullptr) {
+ return *this;
+ }
+
+ auto starting_line_index = this->al_string.rfind('\n', index);
+ if (starting_line_index == std::string::npos) {
+ starting_line_index = 0;
+ } else {
+ starting_line_index += 1;
+ }
+
+ const ssize_t usable_width = tws->tws_width - tws->tws_indent;
+
+ auto text_to_wrap = string_fragment::from_str_range(
+ this->al_string, starting_line_index, this->al_string.length());
+ string_fragment last_word;
+ ssize_t line_ch_count = 0;
+ auto needs_indent = false;
+
+ while (!text_to_wrap.empty()) {
+ if (needs_indent) {
+ this->insert(text_to_wrap.sf_begin,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ text_to_wrap.sf_begin,
+ text_to_wrap.sf_begin + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ line_ch_count += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr, SA_PREFORMATTED.value());
+ }
+ text_to_wrap = text_to_wrap.prepend(
+ this->al_string.data(),
+ 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::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())
+ {
+ this->insert(word.w_word.sf_begin, 1, '\n');
+ this->insert(word.w_word.sf_begin + 1,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ word.w_word.sf_begin + 1,
+ word.w_word.sf_begin + 1 + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr,
+ SA_PREFORMATTED.value());
+ }
+ line_ch_count = tws->tws_padding_indent + ch_count;
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = word.w_word.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_begin, trailing_space_count);
+ }
+ return word.w_remaining
+ .erase_before(this->al_string.data(),
+ trailing_space_count)
+ .prepend(this->al_string.data(),
+ 1 + tws->tws_indent + tws->tws_padding_indent);
+ }
+ line_ch_count += ch_count;
+
+ return word.w_remaining;
+ },
+ [&](text_stream::space space) {
+ if (space.s_value == "\n") {
+ line_ch_count = 0;
+ needs_indent = true;
+ return space.s_remaining;
+ }
+
+ if (line_ch_count > 0) {
+ auto ch_count = space.s_value.utf8_length().unwrapOr(
+ space.s_value.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())
+ {
+ this->erase(space.s_value.sf_begin,
+ space.s_value.length());
+ this->insert(space.s_value.sf_begin, "\n");
+ line_ch_count = 0;
+ needs_indent = true;
+
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = space.s_value.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_end, trailing_space_count);
+ }
+
+ return space.s_remaining
+ .erase_before(
+ this->al_string.data(),
+ space.s_value.length() + trailing_space_count)
+ .prepend(this->al_string.data(), 1);
+ }
+ line_ch_count += ch_count;
+ } else if (find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->erase(space.s_value.sf_begin, space.s_value.length());
+ return space.s_remaining.erase_before(
+ this->al_string.data(), space.s_value.length());
+ }
+
+ return space.s_remaining;
+ },
+ [](text_stream::corrupt corrupt) { return corrupt.c_remaining; },
+ [](text_stream::eof eof) { return eof.e_remaining; });
+
+ if (chunk.is<text_stream::word>()) {
+ last_word = text_to_wrap;
+ }
+
+ ensure(this->al_string.data() == text_to_wrap.sf_string);
+ ensure(text_to_wrap.sf_begin <= text_to_wrap.sf_end);
+ }
+ return *this;
+}
+
+attr_line_t
+attr_line_t::subline(size_t start, size_t len) const
+{
+ if (len == std::string::npos) {
+ len = this->al_string.length() - start;
+ }
+
+ line_range lr{(int) start, (int) (start + len)};
+ attr_line_t retval;
+
+ retval.al_string = this->al_string.substr(start, len);
+ for (const auto& sa : this->al_attrs) {
+ if (!lr.intersects(sa.sa_range)) {
+ continue;
+ }
+
+ auto ilr = lr.intersection(sa.sa_range).shift(0, -lr.lr_start);
+ retval.al_attrs.emplace_back(ilr,
+ std::make_pair(sa.sa_type, sa.sa_value));
+ const auto& last_lr = retval.al_attrs.back().sa_range;
+
+ ensure(last_lr.lr_end <= (int) retval.al_string.length());
+ }
+
+ return retval;
+}
+
+void
+attr_line_t::split_lines(std::vector<attr_line_t>& lines) const
+{
+ size_t pos = 0, next_line;
+
+ while ((next_line = this->al_string.find('\n', pos)) != std::string::npos) {
+ lines.emplace_back(this->subline(pos, next_line - pos));
+ pos = next_line + 1;
+ }
+ lines.emplace_back(this->subline(pos));
+}
+
+attr_line_t&
+attr_line_t::right_justify(unsigned long width)
+{
+ long padding = width - this->length();
+ if (padding > 0) {
+ this->al_string.insert(0, padding, ' ');
+ for (auto& al_attr : this->al_attrs) {
+ if (al_attr.sa_range.lr_start > 0) {
+ al_attr.sa_range.lr_start += padding;
+ }
+ if (al_attr.sa_range.lr_end != -1) {
+ al_attr.sa_range.lr_end += padding;
+ }
+ }
+ }
+
+ return *this;
+}
+
+size_t
+attr_line_t::nearest_text(size_t x) const
+{
+ if (x > 0 && x >= (size_t) this->length()) {
+ if (this->empty()) {
+ x = 0;
+ } else {
+ x = this->length() - 1;
+ }
+ }
+
+ while (x > 0 && isspace(this->al_string[x])) {
+ x -= 1;
+ }
+
+ return x;
+}
+
+void
+attr_line_t::apply_hide()
+{
+ auto& sa = this->al_attrs;
+
+ for (auto& sattr : sa) {
+ if (sattr.sa_type == &SA_HIDDEN && sattr.sa_range.length() > 3) {
+ auto& lr = sattr.sa_range;
+
+ std::for_each(sa.begin(), sa.end(), [&](string_attr& attr) {
+ if (attr.sa_type == &VC_STYLE && lr.contains(attr.sa_range)) {
+ attr.sa_type = &SA_REMOVED;
+ }
+ });
+
+ this->al_string.replace(lr.lr_start, lr.length(), "\xE2\x8B\xAE");
+ shift_string_attrs(sa, lr.lr_start + 1, -(lr.length() - 3));
+ sattr.sa_type = &VC_ROLE;
+ sattr.sa_value = role_t::VCR_HIDDEN;
+ lr.lr_end = lr.lr_start + 3;
+ }
+ }
+}
+
+attr_line_t&
+attr_line_t::rtrim()
+{
+ auto index = this->al_string.length();
+
+ for (; index > 0; index--) {
+ if (find_string_attr_containing(
+ this->al_attrs, &SA_PREFORMATTED, index - 1)
+ != this->al_attrs.end())
+ {
+ break;
+ }
+ if (!isspace(this->al_string[index - 1])) {
+ break;
+ }
+ }
+ if (index > 0 && index < this->al_string.length()) {
+ this->erase(index);
+ }
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::erase(size_t pos, size_t len)
+{
+ if (len == std::string::npos) {
+ len = this->al_string.size() - pos;
+ }
+ if (len == 0) {
+ return *this;
+ }
+
+ this->al_string.erase(pos, len);
+
+ shift_string_attrs(this->al_attrs, pos, -((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();
+ });
+ this->al_attrs.erase(new_end, this->al_attrs.end());
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::pad_to(ssize_t size)
+{
+ const auto curr_len = this->utf8_length_or_length();
+
+ if (curr_len < size) {
+ this->append((size - curr_len), ' ');
+ for (auto& attr : this->al_attrs) {
+ if (attr.sa_range.lr_start == 0 && attr.sa_range.lr_end == curr_len)
+ {
+ attr.sa_range.lr_end = this->al_string.length();
+ }
+ }
+ }
+
+ return *this;
+}
+
+line_range
+line_range::intersection(const line_range& other) const
+{
+ int actual_end;
+
+ if (this->lr_end == -1) {
+ actual_end = other.lr_end;
+ } else if (other.lr_end == -1) {
+ actual_end = this->lr_end;
+ } else {
+ actual_end = std::min(this->lr_end, other.lr_end);
+ }
+ return line_range{std::max(this->lr_start, other.lr_start), actual_end};
+}
+
+line_range&
+line_range::shift(int32_t start, int32_t amount)
+{
+ if (start == this->lr_start) {
+ if (amount > 0) {
+ this->lr_start += amount;
+ }
+ if (this->lr_end != -1) {
+ this->lr_end += amount;
+ if (this->lr_end < this->lr_start) {
+ this->lr_end = this->lr_start;
+ }
+ }
+ } else if (start < 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 (this->lr_end != -1) {
+ if (start < this->lr_end) {
+ if (amount < 0 && amount < (start - this->lr_end)) {
+ this->lr_end = start;
+ } else {
+ this->lr_end = std::max(this->lr_start, this->lr_end + amount);
+ }
+ }
+ }
+
+ return *this;
+}
diff --git a/src/base/attr_line.hh b/src/base/attr_line.hh
new file mode 100644
index 0000000..c9cb6a8
--- /dev/null
+++ b/src/base/attr_line.hh
@@ -0,0 +1,758 @@
+/**
+ * Copyright (c) 2017, 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 attr_line.hh
+ */
+
+#ifndef attr_line_hh
+#define attr_line_hh
+
+#include <new>
+#include <string>
+#include <vector>
+
+#include <limits.h>
+
+#include "fmt/format.h"
+#include "intern_string.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)
+{
+ return line_range{frag.sf_begin, frag.sf_end};
+}
+
+struct string_attr {
+ string_attr(const struct line_range& lr, const string_attr_pair& value)
+ : sa_range(lr), sa_type(value.first), sa_value(value.second)
+ {
+ }
+
+ string_attr() = default;
+
+ bool operator<(const struct string_attr& rhs) const
+ {
+ if (this->sa_range < rhs.sa_range) {
+ return true;
+ }
+ if (this->sa_range == rhs.sa_range && this->sa_type == rhs.sa_type
+ && this->sa_type == &VC_ROLE
+ && this->sa_value.get<role_t>() < rhs.sa_value.get<role_t>())
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ struct line_range sa_range;
+ const string_attr_type_base* sa_type{nullptr};
+ string_attr_value sa_value;
+};
+
+template<typename T>
+struct string_attr_wrapper {
+ explicit string_attr_wrapper(const string_attr* sa) : saw_string_attr(sa) {}
+
+ template<typename U = T>
+ std::enable_if_t<!std::is_void<U>::value, const U&> get() const
+ {
+ return this->saw_string_attr->sa_value.template get<T>();
+ }
+
+ const string_attr* saw_string_attr;
+};
+
+/** 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;
+ }
+
+ return nonstd::make_optional(&(*iter));
+}
+
+template<typename T>
+inline nonstd::optional<string_attr_wrapper<T>>
+get_string_attr(const string_attrs_t& sa,
+ const string_attr_type<T>& type,
+ int start = 0)
+{
+ auto iter = find_string_attr(sa, &type, start);
+
+ if (iter == sa.end()) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(string_attr_wrapper<T>(&(*iter)));
+}
+
+template<typename T>
+inline string_attrs_t::const_iterator
+find_string_attr_containing(const string_attrs_t& sa,
+ const string_attr_type_base* type,
+ T x)
+{
+ string_attrs_t::const_iterator iter;
+
+ for (iter = sa.begin(); iter != sa.end(); ++iter) {
+ if (iter->sa_type == type && iter->sa_range.contains(x)) {
+ break;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ return nearest;
+}
+
+template<typename T>
+inline string_attrs_t::const_iterator
+rfind_string_attr_if(const string_attrs_t& sa, ssize_t near, T predicate)
+{
+ 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.lr_start > near) {
+ continue;
+ }
+
+ if (!predicate(*iter)) {
+ continue;
+ }
+
+ ssize_t diff = near - lr.lr_start;
+ if (diff < last_diff) {
+ last_diff = diff;
+ nearest = iter;
+ }
+ }
+
+ 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);
+
+ if (iter != sa.end()) {
+ return iter->sa_range;
+ }
+
+ return line_range();
+}
+
+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);
+ }
+}
+
+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);
+ }
+}
+
+struct text_wrap_settings {
+ text_wrap_settings& with_indent(int indent)
+ {
+ this->tws_indent = indent;
+ return *this;
+ }
+
+ text_wrap_settings& with_padding_indent(int indent)
+ {
+ this->tws_padding_indent = indent;
+ return *this;
+ }
+
+ text_wrap_settings& with_width(int width)
+ {
+ this->tws_width = width;
+ return *this;
+ }
+
+ int tws_indent{2};
+ int tws_width{80};
+ int tws_padding_indent{0};
+};
+
+/**
+ * A line that has attributes.
+ */
+class attr_line_t {
+public:
+ attr_line_t() = default;
+
+ attr_line_t(std::string str) : al_string(std::move(str)) {}
+
+ attr_line_t(const char* str) : al_string(str) {}
+
+ static inline attr_line_t from_ansi_str(const char* str)
+ {
+ attr_line_t retval;
+
+ return retval.with_ansi_string("%s", str);
+ }
+
+ /** @return The string itself. */
+ std::string& get_string() { return this->al_string; }
+
+ const std::string& get_string() const { return this->al_string; }
+
+ /** @return The attributes for the string. */
+ string_attrs_t& get_attrs() { return this->al_attrs; }
+
+ const string_attrs_t& get_attrs() const { return this->al_attrs; }
+
+ attr_line_t& with_string(const std::string& str)
+ {
+ this->al_string = str;
+ return *this;
+ }
+
+ attr_line_t& with_ansi_string(const char* str, ...);
+
+ attr_line_t& with_ansi_string(const std::string& str);
+
+ attr_line_t& with_attr(const string_attr& sa)
+ {
+ this->al_attrs.push_back(sa);
+ return *this;
+ }
+
+ attr_line_t& ensure_space()
+ {
+ if (!this->al_string.empty() && this->al_string.back() != ' '
+ && this->al_string.back() != '[')
+ {
+ this->append(1, ' ');
+ }
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(S str, const string_attr_pair& value)
+ {
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(str);
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value);
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(const std::pair<S, string_attr_pair>& value)
+ {
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(std::move(value.first));
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value.second);
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append_quoted(const std::pair<S, string_attr_pair>& value)
+ {
+ this->al_string.append("\u201c");
+
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(std::move(value.first));
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value.second);
+
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ attr_line_t& append_quoted(const intern_string_t str)
+ {
+ this->al_string.append("\u201c");
+ this->al_string.append(str.get(), str.size());
+ this->al_string.append("\u201d");
+
+ 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)
+ {
+ this->al_string.append("\u201c");
+ this->append(std::move(s));
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ attr_line_t& append(const intern_string_t str)
+ {
+ this->al_string.append(str.get(), str.size());
+ return *this;
+ }
+
+ attr_line_t& append(const string_fragment& str)
+ {
+ this->al_string.append(str.data(), str.length());
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(S str)
+ {
+ this->al_string.append(str);
+ 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...)));
+ return *this;
+ }
+
+ attr_line_t& with_attr_for_all(const string_attr_pair& sap)
+ {
+ this->al_attrs.emplace_back(line_range{0, -1}, sap);
+ return *this;
+ }
+
+ template<typename C, typename F>
+ attr_line_t& join(const C& container,
+ const string_attr_pair& sap,
+ const F& fill)
+ {
+ bool init = true;
+ for (const auto& elem : container) {
+ if (!init) {
+ this->append(fill);
+ }
+ this->append(std::make_pair(elem, sap));
+ init = false;
+ }
+
+ return *this;
+ }
+
+ template<typename C, typename F>
+ attr_line_t& join(const C& container, const F& fill)
+ {
+ bool init = true;
+ for (const auto& elem : container) {
+ if (!init) {
+ this->append(fill);
+ }
+ this->append(elem);
+ init = false;
+ }
+
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index,
+ const attr_line_t& al,
+ text_wrap_settings* tws = nullptr);
+
+ attr_line_t& append(const attr_line_t& al,
+ text_wrap_settings* tws = nullptr)
+ {
+ return this->insert(this->al_string.length(), al, tws);
+ }
+
+ attr_line_t& append(size_t len, char c)
+ {
+ this->al_string.append(len, c);
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index, size_t len, char c)
+ {
+ this->al_string.insert(index, len, c);
+
+ shift_string_attrs(this->al_attrs, index, len);
+
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index, const char* str)
+ {
+ this->al_string.insert(index, str);
+
+ shift_string_attrs(this->al_attrs, index, strlen(str));
+
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_t& add_header(Args... args)
+ {
+ if (!this->blank()) {
+ this->insert(0, args...);
+ }
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_t& with_default(Args... args)
+ {
+ if (this->blank()) {
+ this->clear();
+ this->append(args...);
+ }
+
+ return *this;
+ }
+
+ attr_line_t& erase(size_t pos, size_t len = std::string::npos);
+
+ attr_line_t& rtrim();
+
+ attr_line_t& erase_utf8_chars(size_t start)
+ {
+ auto byte_index = utf8_char_to_byte_index(this->al_string, start);
+ this->erase(byte_index);
+
+ return *this;
+ }
+
+ attr_line_t& right_justify(unsigned long width);
+
+ attr_line_t& pad_to(ssize_t size);
+
+ ssize_t length() const
+ {
+ size_t retval = this->al_string.length();
+
+ for (const auto& al_attr : this->al_attrs) {
+ retval = std::max(retval, (size_t) al_attr.sa_range.lr_start);
+ if (al_attr.sa_range.lr_end != -1) {
+ retval = std::max(retval, (size_t) al_attr.sa_range.lr_end);
+ }
+ }
+
+ return retval;
+ }
+
+ Result<size_t, const char*> utf8_length() const
+ {
+ return utf8_string_length(this->al_string);
+ }
+
+ ssize_t utf8_length_or_length() const
+ {
+ return utf8_string_length(this->al_string).unwrapOr(this->length());
+ }
+
+ std::string get_substring(const line_range& lr) const
+ {
+ if (!lr.is_valid()) {
+ return "";
+ }
+ return this->al_string.substr(lr.lr_start, lr.sublen(this->al_string));
+ }
+
+ string_fragment to_string_fragment(
+ string_attrs_t::const_iterator iter) const
+ {
+ return string_fragment(this->al_string.c_str(),
+ iter->sa_range.lr_start,
+ iter->sa_range.end_for_string(this->al_string));
+ }
+
+ string_attrs_t::const_iterator find_attr(size_t near) const
+ {
+ near = std::min(near, this->al_string.length() - 1);
+
+ while (near > 0 && isspace(this->al_string[near])) {
+ near -= 1;
+ }
+
+ return find_string_attr(this->al_attrs, near);
+ }
+
+ bool empty() const { return this->length() == 0; }
+
+ bool blank() const { return is_blank(this->al_string); }
+
+ /** Clear the string and the attributes for the string. */
+ attr_line_t& clear()
+ {
+ this->al_string.clear();
+ this->al_attrs.clear();
+
+ return *this;
+ }
+
+ attr_line_t subline(size_t start, size_t len = std::string::npos) const;
+
+ void split_lines(std::vector<attr_line_t>& lines) const;
+
+ std::vector<attr_line_t> split_lines() const
+ {
+ std::vector<attr_line_t> retval;
+
+ this->split_lines(retval);
+ return retval;
+ }
+
+ size_t nearest_text(size_t x) const;
+
+ void apply_hide();
+
+ std::string al_string;
+ string_attrs_t al_attrs;
+};
+
+#endif
diff --git a/src/base/attr_line.tests.cc b/src/base/attr_line.tests.cc
new file mode 100644
index 0000000..53b338e
--- /dev/null
+++ b/src/base/attr_line.tests.cc
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2022, 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 <iostream>
+
+#include "attr_line.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+using namespace lnav::roles::literals;
+
+TEST_CASE("attr_line_t::basic-wrapping")
+{
+ text_wrap_settings tws = {3, 21};
+ attr_line_t to_be_wrapped{"This line, right here, needs to be wrapped."};
+ attr_line_t dst;
+
+ to_be_wrapped.al_attrs.emplace_back(
+ line_range{0, (int) to_be_wrapped.al_string.length()},
+ VC_ROLE.value(role_t::VCR_ERROR));
+ dst.append(to_be_wrapped, &tws);
+
+ CHECK(dst.get_string() ==
+ "This line, right\n"
+ " here, needs to be\n"
+ " wrapped.");
+
+ for (const auto& attr : dst.al_attrs) {
+ printf("attr %d:%d %s\n",
+ attr.sa_range.lr_start,
+ attr.sa_range.lr_end,
+ attr.sa_type->sat_name);
+ }
+}
+
+TEST_CASE("attr_line_t::unicode-wrap")
+{
+ text_wrap_settings tws = {3, 21};
+ attr_line_t prefix;
+
+ prefix.append(" ")
+ .append("\u2022"_list_glyph)
+ .append(" ")
+ .with_attr_for_all(SA_PREFORMATTED.value());
+
+ attr_line_t body;
+ body.append("This is a long line that needs to be wrapped and indented");
+
+ attr_line_t li;
+
+ li.append(prefix)
+ .append(body, &tws)
+ .with_attr_for_all(SA_PREFORMATTED.value());
+
+ attr_line_t dst;
+
+ dst.append(li);
+
+ CHECK(dst.get_string()
+ == " \u2022 This is a long\n"
+ " line that needs to\n"
+ " be wrapped and\n"
+ " indented");
+}
diff --git a/src/base/auto_fd.hh b/src/base/auto_fd.hh
new file mode 100644
index 0000000..da4a582
--- /dev/null
+++ b/src/base/auto_fd.hh
@@ -0,0 +1,318 @@
+/**
+ * Copyright (c) 2007-2012, 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.hh
+ */
+
+#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/result.h"
+
+/**
+ * Resource management class for file descriptors.
+ *
+ * @see auto_ptr
+ */
+class auto_fd {
+public:
+ /**
+ * Wrapper for the posix pipe(2) function that stores the file descriptor
+ * results in an auto_fd array.
+ *
+ * @param af An array of at least two auto_fd elements, where the first
+ * 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;
+ }
+
+ /**
+ * dup(2) the given file descriptor and wrap it in an auto_fd.
+ *
+ * @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{};
+ }
+
+ auto new_fd = ::dup(fd);
+
+ if (new_fd == -1) {
+ throw std::bad_alloc();
+ }
+
+ return auto_fd(new_fd);
+ }
+
+ /**
+ * 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); }
+
+ /**
+ * Non-const copy constructor. Management of the file descriptor will be
+ * transferred from the source to this object and the source will be
+ * cleared.
+ *
+ * @param af The source of the file descriptor.
+ */
+ auto_fd(auto_fd&& af) noexcept : af_fd(af.release()) {}
+
+ /**
+ * Const copy constructor. The file descriptor from the source will be
+ * dup(2)'d and the new descriptor stored in this object.
+ *
+ * @param af The source of the file descriptor.
+ */
+ 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};
+ }
+
+ /**
+ * Destructor that will close the file descriptor managed by this object.
+ */
+ ~auto_fd() { this->reset(); }
+
+ /** @return The file descriptor as a plain integer. */
+ operator int() const { return this->af_fd; }
+
+ /**
+ * Replace the current descriptor with the given one. The current
+ * descriptor will be closed.
+ *
+ * @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;
+ }
+
+ /**
+ * Transfer management of the given file descriptor to this object.
+ *
+ * @param af The old manager of the file descriptor.
+ * @return *this
+ */
+ auto_fd& operator=(auto_fd&& af) noexcept
+ {
+ this->reset(af.release());
+ return *this;
+ }
+
+ /**
+ * Return a pointer that can be passed to functions that require an out
+ * parameter for file descriptors (e.g. openpty).
+ *
+ * @return A pointer to the internal integer.
+ */
+ int* out()
+ {
+ this->reset();
+ return &this->af_fd;
+ }
+
+ /**
+ * Stop managing the file descriptor in this object and return its value.
+ *
+ * @return The file descriptor.
+ */
+ int release()
+ {
+ int retval = this->af_fd;
+
+ this->af_fd = -1;
+ return retval;
+ }
+
+ /**
+ * @return The file descriptor.
+ */
+ int get() const { return this->af_fd; }
+
+ /**
+ * 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 close_on_exec() const
+ {
+ if (this->af_fd == -1) {
+ return;
+ }
+ log_perror(fcntl(this->af_fd, F_SETFD, FD_CLOEXEC));
+ }
+
+private:
+ int af_fd; /*< The managed file descriptor. */
+};
+
+class auto_pipe {
+public:
+ static Result<auto_pipe, std::string> 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));
+ }
+
+ 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;
+ }
+ }
+
+ int open() { return auto_fd::pipe(this->ap_fd); }
+
+ void close()
+ {
+ this->ap_fd[0].reset();
+ this->ap_fd[1].reset();
+ }
+
+ auto_fd& read_end() { return this->ap_fd[0]; }
+
+ 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;
+ }
+ }
+
+ int ap_child_flags;
+ int ap_child_fd;
+ auto_fd ap_fd[2];
+};
+
+#endif
diff --git a/src/base/auto_mem.hh b/src/base/auto_mem.hh
new file mode 100644
index 0000000..e6b456c
--- /dev/null
+++ b/src/base/auto_mem.hh
@@ -0,0 +1,402 @@
+/**
+ * Copyright (c) 2007-2019, 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_mem.hh
+ */
+
+#ifndef lnav_auto_mem_hh
+#define lnav_auto_mem_hh
+
+#include <exception>
+#include <iterator>
+#include <string>
+#include <utility>
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "base/result.h"
+
+using free_func_t = void (*)(void*);
+
+/**
+ * Resource management class for memory allocated by a custom allocator.
+ *
+ * @param T The object type.
+ * @param auto_free The function to call to free the managed object.
+ */
+template<class T, free_func_t default_free = free>
+class auto_mem {
+public:
+ static void noop_free(void*) {}
+
+ static auto_mem<T> leak(T* ptr)
+ {
+ auto_mem<T> retval(noop_free);
+
+ retval = ptr;
+
+ return retval;
+ }
+
+ explicit auto_mem(T* ptr = nullptr)
+ : am_ptr(ptr), am_free_func(default_free)
+ {
+ }
+
+ auto_mem(const auto_mem& am) = delete;
+
+ template<typename F>
+ explicit auto_mem(F free_func) noexcept
+ : am_ptr(nullptr), am_free_func((free_func_t) free_func)
+ {
+ }
+
+ auto_mem(auto_mem&& other) noexcept
+ : am_ptr(other.release()), am_free_func(other.am_free_func)
+ {
+ }
+
+ ~auto_mem() { this->reset(); }
+
+ bool empty() const { return this->am_ptr == nullptr; }
+
+ operator T*() const { return this->am_ptr; }
+
+ T* operator->() { return this->am_ptr; }
+
+ auto_mem& operator=(T* ptr)
+ {
+ this->reset(ptr);
+ return *this;
+ }
+
+ auto_mem& operator=(auto_mem&) = delete;
+
+ auto_mem& operator=(auto_mem&& am) noexcept
+ {
+ this->reset(am.release());
+ this->am_free_func = am.am_free_func;
+ return *this;
+ }
+
+ T* release()
+ {
+ T* retval = this->am_ptr;
+
+ this->am_ptr = nullptr;
+ return retval;
+ }
+
+ T* in() const { return this->am_ptr; }
+
+ T** out()
+ {
+ this->reset();
+ return &this->am_ptr;
+ }
+
+ template<typename F>
+ F get_free_func() const
+ {
+ return (F) this->am_free_func;
+ }
+
+ void reset(T* ptr = nullptr)
+ {
+ if (this->am_ptr != ptr) {
+ if (this->am_ptr != nullptr) {
+ this->am_free_func((void*) this->am_ptr);
+ }
+ this->am_ptr = ptr;
+ }
+ }
+
+private:
+ T* am_ptr;
+ void (*am_free_func)(void*);
+};
+
+template<typename T, void (*free_func)(T*)>
+class static_root_mem {
+public:
+ static_root_mem() { memset(&this->srm_value, 0, sizeof(T)); }
+
+ ~static_root_mem() { free_func(&this->srm_value); }
+
+ const T* operator->() const { return &this->srm_value; }
+
+ const T& in() const { return this->srm_value; }
+
+ T* inout()
+ {
+ free_func(&this->srm_value);
+ memset(&this->srm_value, 0, sizeof(T));
+ return &this->srm_value;
+ }
+
+private:
+ static_root_mem& operator=(T&) { return *this; }
+
+ static_root_mem& operator=(static_root_mem&) { return *this; }
+
+ T srm_value;
+};
+
+class auto_buffer {
+public:
+ using value_type = char;
+
+ static auto_buffer alloc(size_t capacity)
+ {
+ return auto_buffer{capacity == 0 ? nullptr : (char*) malloc(capacity),
+ capacity};
+ }
+
+ static auto_buffer alloc_bitmap(size_t capacity_in_bits)
+ {
+ return alloc((capacity_in_bits + 7) / 8);
+ }
+
+ static auto_buffer from(const char* mem, size_t size)
+ {
+ auto retval = alloc(size);
+
+ retval.resize(size);
+ memcpy(retval.in(), mem, size);
+ return retval;
+ }
+
+ auto_buffer(const auto_buffer&) = delete;
+
+ auto_buffer(auto_buffer&& other) noexcept
+ : ab_buffer(other.ab_buffer), ab_size(other.ab_size),
+ ab_capacity(other.ab_capacity)
+ {
+ other.ab_buffer = nullptr;
+ other.ab_size = 0;
+ other.ab_capacity = 0;
+ }
+
+ ~auto_buffer()
+ {
+ free(this->ab_buffer);
+ this->ab_buffer = nullptr;
+ this->ab_size = 0;
+ this->ab_capacity = 0;
+ }
+
+ auto_buffer& operator=(auto_buffer&) = delete;
+
+ auto_buffer& operator=(auto_buffer&& other) noexcept
+ {
+ free(this->ab_buffer);
+ this->ab_buffer = std::exchange(other.ab_buffer, nullptr);
+ this->ab_size = std::exchange(other.ab_size, 0);
+ this->ab_capacity = std::exchange(other.ab_capacity, 0);
+ return *this;
+ }
+
+ void swap(auto_buffer& other)
+ {
+ std::swap(this->ab_buffer, other.ab_buffer);
+ std::swap(this->ab_size, other.ab_size);
+ std::swap(this->ab_capacity, other.ab_capacity);
+ }
+
+ char* in() { return this->ab_buffer; }
+
+ char* at(size_t offset) { return &this->ab_buffer[offset]; }
+
+ const char* at(size_t offset) const { return &this->ab_buffer[offset]; }
+
+ char* begin() { return this->ab_buffer; }
+
+ const char* begin() const { return this->ab_buffer; }
+
+ auto_buffer& push_back(char ch)
+ {
+ if (this->ab_size == this->ab_capacity) {
+ this->expand_by(256);
+ }
+ this->ab_buffer[this->ab_size] = ch;
+ this->ab_size += 1;
+
+ return *this;
+ }
+
+ void pop_back() { this->ab_size -= 1; }
+
+ bool is_bit_set(size_t bit_offset) const
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ return this->ab_buffer[byte_offset] & bitmask;
+ }
+
+ void set_bit(size_t bit_offset)
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ this->ab_buffer[byte_offset] |= bitmask;
+ }
+
+ void clear_bit(size_t bit_offset)
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ this->ab_buffer[byte_offset] &= ~bitmask;
+ }
+
+ std::reverse_iterator<char*> rbegin()
+ {
+ return std::reverse_iterator<char*>(this->end());
+ }
+
+ std::reverse_iterator<const char*> rbegin() const
+ {
+ return std::reverse_iterator<const char*>(this->end());
+ }
+
+ char* end() { return &this->ab_buffer[this->ab_size]; }
+
+ const char* end() const { return &this->ab_buffer[this->ab_size]; }
+
+ std::reverse_iterator<char*> rend()
+ {
+ return std::reverse_iterator<char*>(this->begin());
+ }
+
+ std::reverse_iterator<const char*> rend() const
+ {
+ return std::reverse_iterator<const char*>(this->begin());
+ }
+
+ std::pair<char*, size_t> release()
+ {
+ auto retval = std::make_pair(this->ab_buffer, this->ab_size);
+
+ this->ab_buffer = nullptr;
+ this->ab_size = 0;
+ this->ab_capacity = 0;
+ return retval;
+ }
+
+ size_t size() const { return this->ab_size; }
+
+ size_t bitmap_size() const { return this->ab_size * 8; }
+
+ bool empty() const { return this->ab_size == 0; }
+
+ bool full() const { return this->ab_size == this->ab_capacity; }
+
+ size_t capacity() const { return this->ab_capacity; }
+
+ size_t available() const { return this->ab_capacity - this->ab_size; }
+
+ void clear() { this->resize(0); }
+
+ auto_buffer& resize(size_t new_size)
+ {
+ assert(new_size <= this->ab_capacity);
+
+ this->ab_size = new_size;
+ return *this;
+ }
+
+ auto_buffer& resize_bitmap(size_t new_size_in_bits, int fill = 0)
+ {
+ auto new_size = (new_size_in_bits + 7) / 8;
+ assert(new_size <= this->ab_capacity);
+
+ auto old_size = std::exchange(this->ab_size, new_size);
+ memset(this->at(old_size), 0, this->ab_size - old_size);
+ return *this;
+ }
+
+ auto_buffer& resize_by(ssize_t amount)
+ {
+ return this->resize(this->ab_size + amount);
+ }
+
+ void expand_to(size_t new_capacity)
+ {
+ if (new_capacity <= this->ab_capacity) {
+ return;
+ }
+ auto* new_buffer = (char*) realloc(this->ab_buffer, new_capacity);
+
+ if (new_buffer == nullptr) {
+ throw std::bad_alloc();
+ }
+
+ this->ab_buffer = new_buffer;
+ this->ab_capacity = new_capacity;
+ }
+
+ void expand_bitmap_to(size_t new_capacity_in_bits)
+ {
+ this->expand_to((new_capacity_in_bits + 7) / 8);
+ }
+
+ void expand_by(size_t amount)
+ {
+ if (amount == 0) {
+ return;
+ }
+
+ this->expand_to(this->ab_capacity + amount);
+ }
+
+ std::string to_string() const { return {this->ab_buffer, this->ab_size}; }
+
+private:
+ auto_buffer(char* buffer, size_t capacity)
+ : ab_buffer(buffer), ab_capacity(capacity)
+ {
+ }
+
+ char* ab_buffer;
+ size_t ab_size{0};
+ size_t ab_capacity;
+};
+
+struct text_auto_buffer {
+ auto_buffer inner;
+};
+
+struct blob_auto_buffer {
+ auto_buffer inner;
+};
+
+#endif
diff --git a/src/base/auto_pid.cc b/src/base/auto_pid.cc
new file mode 100644
index 0000000..a662988
--- /dev/null
+++ b/src/base/auto_pid.cc
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2021, 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 "auto_pid.hh"
+
+#include <unistd.h>
+
+#include "config.h"
+#include "fmt/format.h"
+#include "lnav_log.hh"
+
+namespace lnav {
+namespace pid {
+
+bool in_child = false;
+
+Result<auto_pid<process_state::running>, std::string>
+from_fork()
+{
+ auto pid = ::fork();
+
+ if (pid == -1) {
+ return Err(
+ fmt::format(FMT_STRING("fork() failed: {}"), strerror(errno)));
+ }
+
+ if (pid != 0) {
+ log_debug("started child: %d", pid);
+ } else {
+ in_child = true;
+ }
+
+ return Ok(auto_pid<process_state::running>(pid));
+}
+
+} // namespace pid
+} // namespace lnav
diff --git a/src/base/auto_pid.hh b/src/base/auto_pid.hh
new file mode 100644
index 0000000..702af1e
--- /dev/null
+++ b/src/base/auto_pid.hh
@@ -0,0 +1,175 @@
+/**
+ * Copyright (c) 2013, 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_pid.hh
+ */
+
+#ifndef auto_pid_hh
+#define auto_pid_hh
+
+#include <cerrno>
+#include <csignal>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "base/lnav_log.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+
+enum class process_state {
+ running,
+ finished,
+};
+
+template<process_state ProcState>
+class auto_pid {
+public:
+ explicit auto_pid(pid_t child, int status = 0)
+ : ap_status(status), ap_child(child)
+ {
+ }
+
+ auto_pid(const auto_pid& other) = delete;
+
+ auto_pid(auto_pid&& other) noexcept
+ : ap_status(other.ap_status), ap_child(std::move(other).release())
+ {
+ }
+
+ ~auto_pid() noexcept
+ {
+ this->reset();
+ }
+
+ auto_pid& operator=(auto_pid&& other) noexcept
+ {
+ auto other_status = other.ap_status;
+ this->reset(std::move(other).release());
+ this->ap_status = other_status;
+ return *this;
+ }
+
+ auto_pid& operator=(const auto_pid& other) = delete;
+
+ pid_t in() const
+ {
+ return this->ap_child;
+ }
+
+ bool in_child() const
+ {
+ static_assert(ProcState == process_state::running,
+ "this method is only available in the RUNNING state");
+ return this->ap_child == 0;
+ }
+
+ pid_t release() &&
+ {
+ return std::exchange(this->ap_child, -1); }
+
+ int status() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return this->ap_status;
+ }
+
+ bool was_normal_exit() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return WIFEXITED(this->ap_status);
+ }
+
+ int exit_status() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return WEXITSTATUS(this->ap_status);
+ }
+
+ using poll_result
+ = mapbox::util::variant<auto_pid<process_state::running>,
+ auto_pid<process_state::finished>>;
+
+ poll_result poll() &&
+ {
+ if (this->ap_child != -1) {
+ auto rc = waitpid(this->ap_child, &this->ap_status, WNOHANG);
+
+ if (rc <= 0) {
+ return std::move(*this);
+ }
+ }
+
+ return auto_pid<process_state::finished>(
+ std::exchange(this->ap_child, -1), this->ap_status);
+ }
+
+ auto_pid<process_state::finished> wait_for_child(int options = 0) &&
+ {
+ if (this->ap_child != -1) {
+ while ((waitpid(this->ap_child, &this->ap_status, options)) < 0
+ && (errno == EINTR))
+ {
+ ;
+ }
+ }
+
+ return auto_pid<process_state::finished>(
+ std::exchange(this->ap_child, -1), this->ap_status);
+ }
+
+ void reset(pid_t child = -1) noexcept
+ {
+ if (this->ap_child != child) {
+ this->ap_status = 0;
+ if (ProcState == process_state::running && this->ap_child != -1) {
+ log_debug("sending SIGTERM to child: %d", this->ap_child);
+ kill(this->ap_child, SIGTERM);
+ }
+ this->ap_child = child;
+ }
+ }
+
+private:
+ int ap_status{0};
+ pid_t ap_child;
+};
+
+namespace lnav {
+namespace pid {
+
+extern bool in_child;
+
+Result<auto_pid<process_state::running>, std::string> from_fork();
+} // namespace pid
+} // namespace lnav
+
+#endif
diff --git a/src/base/bus.hh b/src/base/bus.hh
new file mode 100644
index 0000000..dea23ac
--- /dev/null
+++ b/src/base/bus.hh
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2022, 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_bus_hh
+#define lnav_bus_hh
+
+#include <vector>
+
+#include "lnav_log.hh"
+
+template<typename T>
+class bus {
+public:
+ bus() = default;
+
+ virtual ~bus() { require(this->b_components.empty()); }
+
+ bus(const bus<T>&) = delete;
+
+ void attach(T* component)
+ {
+ this->b_components.template emplace_back(component);
+ }
+
+ void detach(T* component)
+ {
+ auto iter = this->b_components.begin();
+ for (; iter != this->b_components.end(); ++iter) {
+ if (*iter == component) {
+ break;
+ }
+ }
+ require(iter != this->b_components.end());
+
+ this->b_components.erase(iter);
+ }
+
+protected:
+ std::vector<T*> b_components;
+};
+
+#endif
diff --git a/src/base/date_time_scanner.cc b/src/base/date_time_scanner.cc
new file mode 100644
index 0000000..72b7e5d
--- /dev/null
+++ b/src/base/date_time_scanner.cc
@@ -0,0 +1,308 @@
+/**
+ * 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.cc
+ */
+
+#include <chrono>
+
+#include "date_time_scanner.hh"
+
+#include "config.h"
+#include "ptimec.hh"
+#include "scn/scn.h"
+
+size_t
+date_time_scanner::ftime(char* dst,
+ size_t len,
+ const char* const time_fmt[],
+ const exttm& tm) const
+{
+ 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);
+ }
+ dst[off] = '\0';
+ } else {
+ off = ftime_fmt(dst, len, time_fmt[this->dts_fmt_lock], tm);
+ }
+
+ return (size_t) off;
+}
+
+bool
+next_format(const char* const fmt[], int& index, int& locked_index)
+{
+ bool retval = true;
+
+ if (locked_index == -1) {
+ index += 1;
+ if (fmt[index] == nullptr) {
+ retval = false;
+ }
+ } else if (index == locked_index) {
+ retval = false;
+ } else {
+ index = locked_index;
+ }
+
+ return retval;
+}
+
+const char*
+date_time_scanner::scan(const char* time_dest,
+ size_t time_len,
+ const char* const time_fmt[],
+ struct exttm* tm_out,
+ struct timeval& tv_out,
+ bool convert_local)
+{
+ int curr_time_fmt = -1;
+ bool found = false;
+ const char* retval = nullptr;
+
+ if (!time_fmt) {
+ time_fmt = PTIMEC_FORMAT_STR;
+ }
+
+ 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});
+ if (epoch_scan_res) {
+ time_t gmt = epoch_scan_res.value();
+
+ if (convert_local && this->dts_local_time) {
+ 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);
+ }
+ 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;
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = std::distance(epoch_scan_res.begin(),
+ epoch_scan_res.end());
+ retval = time_dest + this->dts_fmt_len;
+ found = true;
+ break;
+ }
+ } else if (time_fmt == PTIMEC_FORMAT_STR) {
+ ptime_func func = PTIMEC_FORMATS[curr_time_fmt].pf_func;
+ off_t off = 0;
+
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (!this->dts_keep_base_tz) {
+ tm_out->et_tm.tm_zone = nullptr;
+ }
+#endif
+ if (func(tm_out, time_dest, off, time_len)) {
+ retval = &time_dest[off];
+
+ if (tm_out->et_tm.tm_year < 70) {
+ tm_out->et_tm.tm_year = 80;
+ }
+ if (convert_local
+ && (this->dts_local_time
+ || tm_out->et_flags & ETF_EPOCH_TIME))
+ {
+ time_t gmt = tm2sec(&tm_out->et_tm);
+
+ this->to_localtime(gmt, *tm_out);
+ }
+ const auto& last_tm = this->dts_last_tm.et_tm;
+ if (last_tm.tm_year == tm_out->et_tm.tm_year
+ && last_tm.tm_mon == tm_out->et_tm.tm_mon
+ && last_tm.tm_mday == tm_out->et_tm.tm_mday
+ && last_tm.tm_hour == tm_out->et_tm.tm_hour
+ && last_tm.tm_min == tm_out->et_tm.tm_min)
+ {
+ const auto sec_diff = tm_out->et_tm.tm_sec - last_tm.tm_sec;
+
+ // log_debug("diff %d", sec_diff);
+ tv_out = this->dts_last_tv;
+ 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);
+ secs2wday(tv_out, &tm_out->et_tm);
+ }
+ tv_out.tv_usec = tm_out->et_nsec / 1000;
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = retval - time_dest;
+
+ found = true;
+ break;
+ }
+ } else {
+ off_t off = 0;
+
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (!this->dts_keep_base_tz) {
+ tm_out->et_tm.tm_zone = nullptr;
+ }
+#endif
+ if (ptime_fmt(
+ time_fmt[curr_time_fmt], tm_out, time_dest, off, time_len)
+ && (time_dest[off] == '.' || time_dest[off] == ','
+ || off == (off_t) time_len))
+ {
+ retval = &time_dest[off];
+ if (tm_out->et_tm.tm_year < 70) {
+ tm_out->et_tm.tm_year = 80;
+ }
+ if (convert_local
+ && (this->dts_local_time
+ || tm_out->et_flags & ETF_EPOCH_TIME))
+ {
+ time_t gmt = tm2sec(&tm_out->et_tm);
+
+ this->to_localtime(gmt, *tm_out);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm_out->et_tm.tm_zone = nullptr;
+#endif
+ 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;
+ secs2wday(tv_out, &tm_out->et_tm);
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = retval - time_dest;
+
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found) {
+ retval = nullptr;
+ }
+
+ if (retval != nullptr) {
+ this->dts_last_tm = *tm_out;
+ this->dts_last_tv = tv_out;
+ }
+
+ 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] == ',') {
+ off_t off = (retval - time_dest) + 1;
+
+ if (ptime_N(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 10;
+ tm_out->et_flags |= ETF_NANOS_SET;
+ retval += 10;
+ } else if (ptime_f(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 7;
+ tm_out->et_flags |= ETF_MICROS_SET;
+ retval += 7;
+ } else if (ptime_L(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 4;
+ tm_out->et_flags |= ETF_MILLIS_SET;
+ retval += 4;
+ }
+ }
+ }
+
+ return retval;
+}
+
+void
+date_time_scanner::set_base_time(time_t base_time, const tm& local_tm)
+{
+ this->dts_base_time = base_time;
+ this->dts_base_tm.et_tm = local_tm;
+ this->dts_last_tm = exttm{};
+ this->dts_last_tv = timeval{};
+}
+
+void
+date_time_scanner::to_localtime(time_t t, exttm& tm_out)
+{
+ if (t < (24 * 60 * 60)) {
+ // Don't convert and risk going past the epoch.
+ return;
+ }
+
+ if (t < this->dts_local_offset_valid || t >= this->dts_local_offset_expiry)
+ {
+ time_t new_gmt;
+
+ localtime_r(&t, &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;
+
+ new_gmt = tm2sec(&tm_out.et_tm);
+ this->dts_local_offset_cache = t - new_gmt;
+ 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);
+ }
+}
diff --git a/src/base/date_time_scanner.hh b/src/base/date_time_scanner.hh
new file mode 100644
index 0000000..90eaffa
--- /dev/null
+++ b/src/base/date_time_scanner.hh
@@ -0,0 +1,137 @@
+/**
+ * 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_hh
+#define lnav_date_time_scanner_hh
+
+#include <ctime>
+#include <string>
+
+#include <sys/types.h>
+
+#include "time_util.hh"
+
+/**
+ * Scans a timestamp string to discover the date-time format using the custom
+ * ptimec parser. Once a format is found, it is locked in so that the next
+ * time a timestamp needs to be scanned, the format does not have to be
+ * rediscovered. The discovered date-time format can also be used to convert
+ * an exttm struct to a string using the ftime() method.
+ */
+struct date_time_scanner {
+ date_time_scanner() { this->clear(); }
+
+ void clear()
+ {
+ this->dts_base_time = 0;
+ this->dts_base_tm = exttm{};
+ this->dts_fmt_lock = -1;
+ this->dts_fmt_len = -1;
+ this->dts_last_tv = timeval{};
+ this->dts_last_tm = exttm{};
+ }
+
+ /**
+ * Unlock this scanner so that the format is rediscovered.
+ */
+ void unlock()
+ {
+ this->dts_fmt_lock = -1;
+ this->dts_fmt_len = -1;
+ }
+
+ void set_base_time(time_t base_time, const tm& local_tm);
+
+ /**
+ * Convert a timestamp to local time.
+ *
+ * Calling localtime_r is slow since it wants to lookup the timezone on
+ * every call, so we cache the result and only call it again if the
+ * requested time falls outside of a fifteen minute range.
+ */
+ void to_localtime(time_t t, struct exttm& tm_out);
+
+ bool dts_keep_base_tz{false};
+ bool dts_local_time{false};
+ time_t dts_base_time{0};
+ struct exttm dts_base_tm;
+ int dts_fmt_lock{-1};
+ int dts_fmt_len{-1};
+ struct exttm dts_last_tm {};
+ struct timeval dts_last_tv {};
+ time_t dts_local_offset_cache{0};
+ time_t dts_local_offset_valid{0};
+ time_t dts_local_offset_expiry{0};
+
+ static const int EXPIRE_TIME = 15 * 60;
+
+ const char* scan(const char* time_src,
+ size_t time_len,
+ const char* const time_fmt[],
+ struct exttm* tm_out,
+ struct timeval& tv_out,
+ bool convert_local = true);
+
+ size_t ftime(char* dst,
+ size_t len,
+ const char* const time_fmt[],
+ const struct exttm& tm) const;
+
+ bool convert_to_timeval(const char* time_src,
+ ssize_t time_len,
+ const char* const time_fmt[],
+ struct timeval& tv_out)
+ {
+ struct exttm tm;
+
+ if (time_len == -1) {
+ time_len = strlen(time_src);
+ }
+ if (this->scan(time_src, time_len, time_fmt, &tm, tv_out) != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
+ bool convert_to_timeval(const std::string& time_src, struct timeval& tv_out)
+ {
+ struct exttm tm;
+
+ if (this->scan(time_src.c_str(), time_src.size(), nullptr, &tm, tv_out)
+ != nullptr)
+ {
+ return true;
+ }
+ return false;
+ }
+};
+
+#endif
diff --git a/src/base/enum_util.hh b/src/base/enum_util.hh
new file mode 100644
index 0000000..437292d
--- /dev/null
+++ b/src/base/enum_util.hh
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2019, 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_enum_util_hh
+#define lnav_enum_util_hh
+
+#include <type_traits>
+
+namespace lnav {
+namespace enums {
+
+template<typename E>
+constexpr auto
+to_underlying(E e) noexcept
+{
+ return static_cast<std::underlying_type_t<E>>(e);
+}
+
+} // namespace enums
+} // namespace lnav
+
+#endif
diff --git a/src/base/file_range.hh b/src/base/file_range.hh
new file mode 100644
index 0000000..d9a3de4
--- /dev/null
+++ b/src/base/file_range.hh
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2019, 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_file_range_hh
+#define lnav_file_range_hh
+
+#include <sys/types.h>
+
+#include "intern_string.hh"
+
+using file_off_t = int64_t;
+using file_size_t = uint64_t;
+using file_ssize_t = int64_t;
+
+class file_range {
+public:
+ struct metadata {
+ bool m_valid_utf{true};
+ bool m_has_ansi{false};
+ };
+
+ file_off_t fr_offset{0};
+ file_ssize_t fr_size{0};
+ metadata fr_metadata;
+
+ void clear()
+ {
+ this->fr_offset = 0;
+ this->fr_size = 0;
+ }
+
+ ssize_t next_offset() const { return this->fr_offset + this->fr_size; }
+
+ bool empty() const { return this->fr_size == 0; }
+};
+
+struct source_location {
+ source_location()
+ : sl_source(intern_string::lookup("unknown")), sl_line_number(0)
+ {
+ }
+
+ explicit source_location(intern_string_t source, int32_t line = 0)
+ : sl_source(source), sl_line_number(line)
+ {
+ }
+
+ intern_string_t sl_source;
+ int32_t sl_line_number;
+};
+
+#endif
diff --git a/src/base/fs_util.cc b/src/base/fs_util.cc
new file mode 100644
index 0000000..f72aaa6
--- /dev/null
+++ b/src/base/fs_util.cc
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2022, 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 "fs_util.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+#include "itertools.hh"
+#include "opt_util.hh"
+
+namespace lnav {
+namespace filesystem {
+
+Result<auto_fd, std::string>
+create_file(const ghc::filesystem::path& path, int flags, mode_t mode)
+{
+ auto fd = openp(path, flags | O_CREAT, mode);
+
+ if (fd == -1) {
+ return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+ }
+
+ return Ok(auto_fd(fd));
+}
+
+Result<auto_fd, std::string>
+open_file(const ghc::filesystem::path& path, int flags)
+{
+ auto fd = openp(path, flags);
+
+ if (fd == -1) {
+ return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+ }
+
+ return Ok(auto_fd(fd));
+}
+
+Result<std::pair<ghc::filesystem::path, auto_fd>, std::string>
+open_temp_file(const ghc::filesystem::path& pattern)
+{
+ auto pattern_str = pattern.string();
+ char pattern_copy[pattern_str.size() + 1];
+ int fd;
+
+ strcpy(pattern_copy, pattern_str.c_str());
+ if ((fd = mkstemp(pattern_copy)) == -1) {
+ return Err(
+ fmt::format(FMT_STRING("unable to create temporary file: {} -- {}"),
+ pattern.string(),
+ strerror(errno)));
+ }
+
+ return Ok(std::make_pair(ghc::filesystem::path(pattern_copy), auto_fd(fd)));
+}
+
+Result<std::string, std::string>
+read_file(const ghc::filesystem::path& path)
+{
+ try {
+ ghc::filesystem::ifstream file_stream(path);
+
+ if (!file_stream) {
+ return Err(std::string(strerror(errno)));
+ }
+
+ std::string retval;
+ retval.assign((std::istreambuf_iterator<char>(file_stream)),
+ std::istreambuf_iterator<char>());
+ return Ok(retval);
+ } catch (const std::exception& e) {
+ return Err(std::string(e.what()));
+ }
+}
+
+Result<void, std::string>
+write_file(const ghc::filesystem::path& path, const string_fragment& content)
+{
+ auto tmp_pattern = path;
+ tmp_pattern += ".XXXXXX";
+
+ auto tmp_pair = TRY(open_temp_file(tmp_pattern));
+ auto bytes_written
+ = write(tmp_pair.second.get(), content.data(), content.length());
+ if (bytes_written < 0) {
+ return Err(
+ fmt::format(FMT_STRING("unable to write to temporary file {}: {}"),
+ tmp_pair.first.string(),
+ strerror(errno)));
+ }
+ if (bytes_written != content.length()) {
+ return Err(fmt::format(FMT_STRING("short write to file {}: {} < {}"),
+ tmp_pair.first.string(),
+ bytes_written,
+ content.length()));
+ }
+ std::error_code ec;
+ ghc::filesystem::rename(tmp_pair.first, path, ec);
+ if (ec) {
+ return Err(
+ fmt::format(FMT_STRING("unable to move temporary file {}: {}"),
+ tmp_pair.first.string(),
+ ec.message()));
+ }
+
+ return Ok();
+}
+
+std::string
+build_path(const std::vector<ghc::filesystem::path>& paths)
+{
+ return paths
+ | lnav::itertools::map([](const auto& path) { return path.string(); })
+ | lnav::itertools::append(getenv_opt("PATH").value_or(""))
+ | lnav::itertools::filter_out(&std::string::empty)
+ | lnav::itertools::fold(
+ [](const auto& elem, auto& accum) {
+ if (!accum.empty()) {
+ accum.push_back(':');
+ }
+ return accum.append(elem);
+ },
+ std::string());
+}
+
+Result<struct stat, std::string>
+stat_file(const ghc::filesystem::path& path)
+{
+ struct stat retval;
+
+ if (statp(path, &retval) == 0) {
+ return Ok(retval);
+ }
+
+ return Err(fmt::format(FMT_STRING("failed to find file: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+}
+
+file_lock::file_lock(const ghc::filesystem::path& archive_path)
+{
+ auto lock_path = archive_path;
+
+ lock_path += ".lck";
+ auto open_res
+ = lnav::filesystem::create_file(lock_path, O_RDWR | O_CLOEXEC, 0600);
+ if (open_res.isErr()) {
+ throw std::runtime_error(open_res.unwrapErr());
+ }
+ this->lh_fd = open_res.unwrap();
+}
+
+} // namespace filesystem
+} // namespace lnav
diff --git a/src/base/fs_util.hh b/src/base/fs_util.hh
new file mode 100644
index 0000000..b9253ff
--- /dev/null
+++ b/src/base/fs_util.hh
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2022, 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_fs_util_hh
+#define lnav_fs_util_hh
+
+#include <string>
+#include <vector>
+
+#include "auto_fd.hh"
+#include "ghc/filesystem.hpp"
+#include "intern_string.hh"
+#include "result.h"
+
+namespace lnav {
+namespace filesystem {
+
+inline int
+statp(const ghc::filesystem::path& path, struct stat* buf)
+{
+ return stat(path.c_str(), buf);
+}
+
+inline int
+openp(const ghc::filesystem::path& path, int flags)
+{
+ return open(path.c_str(), flags);
+}
+
+inline int
+openp(const ghc::filesystem::path& path, int flags, mode_t mode)
+{
+ return open(path.c_str(), flags, mode);
+}
+
+Result<auto_fd, std::string> create_file(const ghc::filesystem::path& path,
+ int flags,
+ mode_t mode);
+
+Result<auto_fd, std::string> open_file(const ghc::filesystem::path& path,
+ int flags);
+
+Result<struct stat, std::string> stat_file(const ghc::filesystem::path& path);
+
+Result<std::pair<ghc::filesystem::path, auto_fd>, std::string> open_temp_file(
+ const ghc::filesystem::path& pattern);
+
+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);
+
+std::string build_path(const std::vector<ghc::filesystem::path>& paths);
+
+class file_lock {
+public:
+ class guard {
+ public:
+ explicit guard(file_lock* arc_lock) : g_lock(arc_lock)
+ {
+ this->g_lock->lock();
+ }
+
+ guard(guard&& other) noexcept
+ : g_lock(std::exchange(other.g_lock, nullptr))
+ {
+ }
+
+ ~guard()
+ {
+ if (this->g_lock != nullptr) {
+ this->g_lock->unlock();
+ }
+ }
+
+ guard(const guard&) = delete;
+ guard& operator=(const guard&) = delete;
+ guard& operator=(guard&&) = delete;
+
+ private:
+ file_lock* g_lock;
+ };
+
+ void lock() const { lockf(this->lh_fd, F_LOCK, 0); }
+
+ void unlock() const { lockf(this->lh_fd, F_ULOCK, 0); }
+
+ explicit file_lock(const ghc::filesystem::path& archive_path);
+
+ auto_fd lh_fd;
+};
+
+} // namespace filesystem
+} // namespace lnav
+
+#endif
diff --git a/src/base/fs_util.tests.cc b/src/base/fs_util.tests.cc
new file mode 100644
index 0000000..9ed3377
--- /dev/null
+++ b/src/base/fs_util.tests.cc
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2022, 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 <iostream>
+
+#include "base/fs_util.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("fs_util::build_path")
+{
+ auto* old_path = getenv("PATH");
+ unsetenv("PATH");
+
+ CHECK("" == lnav::filesystem::build_path({}));
+
+ CHECK("/bin:/usr/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ setenv("PATH", "/usr/local/bin", 1);
+ CHECK("/bin:/usr/bin:/usr/local/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ setenv("PATH", "/usr/local/bin:/opt/bin", 1);
+ CHECK("/usr/local/bin:/opt/bin" == lnav::filesystem::build_path({}));
+ CHECK("/bin:/usr/bin:/usr/local/bin:/opt/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ if (old_path != nullptr) {
+ setenv("PATH", old_path, 1);
+ }
+}
diff --git a/src/base/func_util.hh b/src/base/func_util.hh
new file mode 100644
index 0000000..01a2328
--- /dev/null
+++ b/src/base/func_util.hh
@@ -0,0 +1,159 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_func_util_hh
+#define lnav_func_util_hh
+
+#include <functional>
+#include <utility>
+
+template<typename F, typename FrontArg>
+decltype(auto)
+bind_mem(F&& f, FrontArg&& frontArg)
+{
+ return [f = std::forward<F>(f),
+ frontArg = std::forward<FrontArg>(frontArg)](auto&&... backArgs) {
+ return (frontArg->*f)(std::forward<decltype(backArgs)>(backArgs)...);
+ };
+}
+
+struct noop_func {
+ struct anything {
+ template<class T>
+ operator T()
+ {
+ return {};
+ }
+ // optional reference support. Somewhat evil.
+ template<class T>
+ operator T&() const
+ {
+ static T t{};
+ return t;
+ }
+ };
+ template<class... Args>
+ anything operator()(Args&&...) const
+ {
+ return {};
+ }
+};
+
+namespace lnav {
+namespace func {
+
+class scoped_cb {
+public:
+ class guard {
+ public:
+ explicit guard(scoped_cb* owner) : g_owner(owner) {}
+
+ guard(const guard&) = delete;
+ guard& operator=(const guard&) = delete;
+
+ guard(guard&& gu) noexcept : g_owner(std::exchange(gu.g_owner, nullptr))
+ {
+ }
+
+ guard& operator=(guard&& gu) noexcept
+ {
+ this->g_owner = std::exchange(gu.g_owner, nullptr);
+ return *this;
+ }
+
+ ~guard()
+ {
+ if (this->g_owner != nullptr) {
+ this->g_owner->s_callback = {};
+ }
+ }
+
+ private:
+ scoped_cb* g_owner;
+ };
+
+ guard install(std::function<void()> cb)
+ {
+ this->s_callback = std::move(cb);
+
+ return guard{this};
+ }
+
+ void operator()()
+ {
+ if (s_callback) {
+ s_callback();
+ }
+ }
+
+private:
+ std::function<void()> s_callback;
+};
+
+template<typename Fn,
+ typename... Args,
+ std::enable_if_t<std::is_member_pointer<std::decay_t<Fn>>{}, int> = 0>
+constexpr decltype(auto)
+invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+{
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template<typename Fn,
+ typename... Args,
+ std::enable_if_t<!std::is_member_pointer<std::decay_t<Fn>>{}, int> = 0>
+constexpr decltype(auto)
+invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+{
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+template<class F, class... Args>
+struct is_invocable {
+ template<typename U, typename Obj, typename... FuncArgs>
+ static auto test(U&& p)
+ -> decltype((std::declval<Obj>().*p)(std::declval<FuncArgs>()...),
+ void(),
+ std::true_type());
+ template<typename U, typename... FuncArgs>
+ static auto test(U* p) -> decltype((*p)(std::declval<FuncArgs>()...),
+ void(),
+ std::true_type());
+ template<typename U, typename... FuncArgs>
+ static auto test(...) -> decltype(std::false_type());
+
+ static constexpr bool value = decltype(test<F, Args...>(0))::value;
+};
+
+} // namespace func
+} // namespace lnav
+
+#endif
diff --git a/src/base/future_util.hh b/src/base/future_util.hh
new file mode 100644
index 0000000..8797faa
--- /dev/null
+++ b/src/base/future_util.hh
@@ -0,0 +1,111 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_future_util_hh
+#define lnav_future_util_hh
+
+#include <deque>
+#include <future>
+
+namespace lnav {
+namespace futures {
+
+/**
+ * Create a future that is ready to immediately return a result.
+ *
+ * @tparam T The result type of the future.
+ * @param t The value the future should return.
+ * @return The new future.
+ */
+template<class T>
+std::future<std::decay_t<T>>
+make_ready_future(T&& t)
+{
+ std::promise<std::decay_t<T>> pr;
+ auto r = pr.get_future();
+ pr.set_value(std::forward<T>(t));
+ return r;
+}
+
+/**
+ * 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>
+class future_queue {
+public:
+ /**
+ * @param processor The function to execute with the result of a future.
+ */
+ explicit future_queue(std::function<void(T&)> processor)
+ : fq_processor(processor){};
+
+ ~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
+ * future to return a result.
+ *
+ * @param f The future to add to the queue.
+ */
+ void push_back(std::future<T>&& f)
+ {
+ this->fq_deque.emplace_back(std::move(f));
+ this->pop_to(MAX_QUEUE_SIZE);
+ }
+
+ /**
+ * Removes the next future from the queue, waits for the result, and then
+ * repeats until the queue reaches the given size.
+ *
+ * @param size The new desired size of the queue.
+ */
+ void pop_to(size_t size = 0)
+ {
+ while (this->fq_deque.size() > size) {
+ auto v = this->fq_deque.front().get();
+ this->fq_processor(v);
+ this->fq_deque.pop_front();
+ }
+ }
+
+ std::function<void(T&)> fq_processor;
+ std::deque<std::future<T>> fq_deque;
+};
+
+} // namespace futures
+} // namespace lnav
+
+#endif
diff --git a/src/base/humanize.cc b/src/base/humanize.cc
new file mode 100644
index 0000000..5e96bf3
--- /dev/null
+++ b/src/base/humanize.cc
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2019, 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 <vector>
+
+#include "humanize.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+
+namespace humanize {
+
+std::string
+file_size(file_ssize_t value, alignment align)
+{
+ static const double LN1024 = log(1024.0);
+ static const std::vector<const char*> UNITS = {
+ " ",
+ "K",
+ "M",
+ "G",
+ "T",
+ "P",
+ "E",
+ };
+
+ if (value < 0) {
+ return "Unknown";
+ }
+
+ if (value == 0) {
+ switch (align) {
+ case alignment::none:
+ return "0B";
+ case alignment::columnar:
+ return "0.0 B";
+ }
+ }
+
+ auto exp
+ = floor(std::min(log(value) / LN1024, (double) (UNITS.size() - 1)));
+ auto divisor = pow(1024, exp);
+
+ if (align == alignment::none && divisor <= 1) {
+ return fmt::format(FMT_STRING("{}B"), value, UNITS[exp]);
+ }
+ return fmt::format(FMT_STRING("{:.1f}{}B"),
+ divisor == 0 ? value : value / divisor,
+ UNITS[exp]);
+}
+
+const std::string&
+sparkline(double value, nonstd::optional<double> upper_opt)
+{
+ static const std::string ZERO = " ";
+ static const std::string BARS[] = {
+ "\u2581",
+ "\u2582",
+ "\u2583",
+ "\u2584",
+ "\u2585",
+ "\u2586",
+ "\u2587",
+ "\u2588",
+ };
+ static const double BARS_COUNT = std::distance(begin(BARS), end(BARS));
+
+ if (value <= 0.0) {
+ return ZERO;
+ }
+
+ auto upper = upper_opt.value_or(100.0);
+
+ if (value >= upper) {
+ return BARS[(size_t) BARS_COUNT - 1];
+ }
+
+ size_t index = ceil((value / upper) * BARS_COUNT) - 1;
+
+ return BARS[index];
+}
+
+} // namespace humanize
diff --git a/src/base/humanize.file_size.tests.cc b/src/base/humanize.file_size.tests.cc
new file mode 100644
index 0000000..ff70c7e
--- /dev/null
+++ b/src/base/humanize.file_size.tests.cc
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2021, 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 <iostream>
+
+#include "base/humanize.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("humanize::file_size")
+{
+ CHECK(humanize::file_size(0, humanize::alignment::columnar) == "0.0 B");
+ CHECK(humanize::file_size(1, humanize::alignment::columnar) == "1.0 B");
+ CHECK(humanize::file_size(1024, humanize::alignment::columnar) == "1.0KB");
+ CHECK(humanize::file_size(1500, humanize::alignment::columnar) == "1.5KB");
+ CHECK(humanize::file_size(55LL * 784LL * 1024LL * 1024LL,
+ humanize::alignment::columnar)
+ == "42.1GB");
+ CHECK(humanize::file_size(-1LL, humanize::alignment::columnar)
+ == "Unknown");
+ CHECK(humanize::file_size(std::numeric_limits<int64_t>::max(),
+ humanize::alignment::columnar)
+ == "8.0EB");
+}
diff --git a/src/base/humanize.hh b/src/base/humanize.hh
new file mode 100644
index 0000000..3f9f66c
--- /dev/null
+++ b/src/base/humanize.hh
@@ -0,0 +1,58 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_humanize_hh
+#define lnav_humanize_hh
+
+#include <string>
+
+#include <sys/types.h>
+
+#include "file_range.hh"
+
+namespace humanize {
+
+enum class alignment {
+ none,
+ columnar,
+};
+
+/**
+ * Format the given size as a human-friendly string.
+ *
+ * @param value The value to format.
+ * @return The formatted string.
+ */
+std::string file_size(file_ssize_t value, alignment align);
+
+const std::string& sparkline(double value, nonstd::optional<double> upper);
+
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.network.cc b/src/base/humanize.network.cc
new file mode 100644
index 0000000..2bf390d
--- /dev/null
+++ b/src/base/humanize.network.cc
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021, 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 "humanize.network.hh"
+
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+
+namespace humanize {
+namespace network {
+namespace path {
+
+nonstd::optional<::network::path>
+from_str(string_fragment sf)
+{
+ static const auto REMOTE_PATTERN = lnav::pcre2pp::code::from_const(
+ "^(?:(?<username>[\\w\\._\\-]+)@)?"
+ "(?:\\[(?<ipv6>[^\\]]+)\\]|(?<hostname>[^\\[/:]+)):"
+ "(?<path>.*)$");
+ static thread_local auto REMOTE_MATCH_DATA
+ = REMOTE_PATTERN.create_match_data();
+
+ auto match_res = REMOTE_PATTERN.capture_from(sf)
+ .into(REMOTE_MATCH_DATA)
+ .matches()
+ .ignore_error();
+
+ if (!match_res) {
+ return nonstd::nullopt;
+ }
+
+ const auto username = REMOTE_MATCH_DATA["username"].map(
+ [](auto sf) { return sf.to_string(); });
+ const auto ipv6 = REMOTE_MATCH_DATA["ipv6"];
+ const auto hostname = REMOTE_MATCH_DATA["hostname"];
+ const auto locality_hostname = ipv6 ? ipv6.value() : hostname.value();
+ auto path = *REMOTE_MATCH_DATA["path"];
+
+ if (path.empty()) {
+ path = string_fragment::from_const(".");
+ }
+ return ::network::path{
+ {username, locality_hostname.to_string(), nonstd::nullopt},
+ path.to_string(),
+ };
+}
+
+} // namespace path
+} // namespace network
+} // namespace humanize
diff --git a/src/base/humanize.network.hh b/src/base/humanize.network.hh
new file mode 100644
index 0000000..609f57d
--- /dev/null
+++ b/src/base/humanize.network.hh
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2021, 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_humanize_network_hh
+#define lnav_humanize_network_hh
+
+#include <string>
+
+#include "fmt/format.h"
+#include "intern_string.hh"
+#include "network.tcp.hh"
+#include "optional.hpp"
+
+namespace fmt {
+
+template<>
+struct formatter<network::locality> {
+ constexpr auto parse(format_parse_context& ctx)
+ {
+ const auto it = ctx.begin();
+ const auto end = ctx.end();
+
+ // Check if reached the end of the range:
+ if (it != end && *it != '}') {
+ throw format_error("invalid format");
+ }
+
+ // Return an iterator past the end of the parsed range:
+ return it;
+ }
+
+ template<typename FormatContext>
+ auto format(const network::locality& l, FormatContext& ctx)
+ {
+ bool is_ipv6 = l.l_hostname.find(':') != std::string::npos;
+
+ return format_to(ctx.out(),
+ "{}{}{}{}{}",
+ l.l_username.value_or(std::string()),
+ l.l_username ? "@" : "",
+ is_ipv6 ? "[" : "",
+ l.l_hostname,
+ is_ipv6 ? "]" : "");
+ }
+};
+
+template<>
+struct formatter<network::path> {
+ constexpr auto parse(format_parse_context& ctx)
+ {
+ const auto it = ctx.begin();
+ const auto end = ctx.end();
+
+ // Check if reached the end of the range:
+ if (it != end && *it != '}') {
+ throw format_error("invalid format");
+ }
+
+ // Return an iterator past the end of the parsed range:
+ return it;
+ }
+
+ template<typename FormatContext>
+ auto format(const network::path& p, FormatContext& ctx)
+ {
+ return format_to(
+ ctx.out(), "{}:{}", p.p_locality, p.p_path == "." ? "" : p.p_path);
+ }
+};
+
+} // namespace fmt
+
+namespace humanize {
+namespace network {
+namespace path {
+
+nonstd::optional<::network::path> from_str(string_fragment sf);
+
+} // namespace path
+} // namespace network
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.network.tests.cc b/src/base/humanize.network.tests.cc
new file mode 100644
index 0000000..fe2e9d2
--- /dev/null
+++ b/src/base/humanize.network.tests.cc
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2021, 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 <iostream>
+
+#include "base/humanize.network.hh"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("humanize::network::path")
+{
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("foobar"));
+ CHECK(!rp_opt);
+ }
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("dean@foobar/bar"));
+ CHECK(!rp_opt);
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("dean@host1.example.com:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_username.value() == "dean");
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+ }
+
+ {
+ auto rp_opt
+ = humanize::network::path::from_str(string_fragment::from_const(
+ "dean@[fe80::184f:c67:baf1:fe02%en0]:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_username.value() == "dean");
+ CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+
+ CHECK(fmt::format("{}", rp.p_locality)
+ == "dean@[fe80::184f:c67:baf1:fe02%en0]");
+ }
+
+ {
+ auto rp_opt
+ = humanize::network::path::from_str(string_fragment::from_const(
+ "[fe80::184f:c67:baf1:fe02%en0]:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+
+ CHECK(fmt::format("{}", rp.p_locality)
+ == "[fe80::184f:c67:baf1:fe02%en0]");
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("host1.example.com:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("host1.example.com:"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == ".");
+ }
+}
diff --git a/src/base/humanize.time.cc b/src/base/humanize.time.cc
new file mode 100644
index 0000000..68cfe0f
--- /dev/null
+++ b/src/base/humanize.time.cc
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2021, 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 <chrono>
+
+#include "humanize.time.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+#include "time_util.hh"
+
+namespace humanize {
+namespace time {
+
+using namespace std::chrono_literals;
+
+point
+point::from_tv(const timeval& tv)
+{
+ return point(tv);
+}
+
+std::string
+point::as_time_ago() const
+{
+ struct timeval current_time
+ = this->p_recent_point.value_or(current_timeval());
+
+ if (this->p_convert_to_local) {
+ current_time.tv_sec = convert_log_time_to_local(current_time.tv_sec);
+ }
+
+ auto delta
+ = std::chrono::seconds(current_time.tv_sec - this->p_past_point.tv_sec);
+ if (delta < 0s) {
+ return "in the future";
+ }
+ if (delta < 1min) {
+ return "just now";
+ }
+ if (delta < 2min) {
+ return "one minute ago";
+ }
+ if (delta < 1h) {
+ return fmt::format(
+ FMT_STRING("{} minutes ago"),
+ std::chrono::duration_cast<std::chrono::minutes>(delta).count());
+ }
+ if (delta < 2h) {
+ return "one hour ago";
+ }
+ if (delta < 24h) {
+ return fmt::format(
+ FMT_STRING("{} hours ago"),
+ std::chrono::duration_cast<std::chrono::hours>(delta).count());
+ }
+ if (delta < 48h) {
+ return "one day ago";
+ }
+ if (delta < 365 * 24h) {
+ return fmt::format(FMT_STRING("{} days ago"), delta / 24h);
+ }
+ if (delta < (2 * 365 * 24h)) {
+ return "over a year ago";
+ }
+ return fmt::format(FMT_STRING("over {} years ago"), delta / (365 * 24h));
+}
+
+std::string
+point::as_precise_time_ago() const
+{
+ struct timeval now, diff;
+
+ now = this->p_recent_point.value_or(current_timeval());
+ if (this->p_convert_to_local) {
+ now.tv_sec = convert_log_time_to_local(now.tv_sec);
+ }
+
+ timersub(&now, &this->p_past_point, &diff);
+ if (diff.tv_sec < 0) {
+ return this->as_time_ago();
+ } else if (diff.tv_sec <= 1) {
+ return "a second ago";
+ } else if (diff.tv_sec < (10 * 60)) {
+ if (diff.tv_sec < 60) {
+ return fmt::format(FMT_STRING("{:2} seconds ago"), diff.tv_sec);
+ }
+
+ time_t seconds = diff.tv_sec % 60;
+ time_t minutes = diff.tv_sec / 60;
+
+ return fmt::format(FMT_STRING("{:2} minute{} and {:2} second{} ago"),
+ minutes,
+ minutes > 1 ? "s" : "",
+ seconds,
+ seconds == 1 ? "" : "s");
+ } else {
+ return this->as_time_ago();
+ }
+}
+
+duration
+duration::from_tv(const struct timeval& tv)
+{
+ return duration{tv};
+}
+
+std::string
+duration::to_string() const
+{
+ /* 24h22m33s111 */
+
+ static const struct rel_interval {
+ uint64_t length;
+ const char* format;
+ const char* symbol;
+ } intervals[] = {
+ {1000, "%03lld%s", ""},
+ {60, "%lld%s", "s"},
+ {60, "%lld%s", "m"},
+ {24, "%lld%s", "h"},
+ {0, "%lld%s", "d"},
+ };
+
+ const auto* curr_interval = intervals;
+ auto usecs = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::seconds(this->d_timeval.tv_sec))
+ + std::chrono::microseconds(this->d_timeval.tv_usec);
+ auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(usecs);
+ std::string retval;
+ bool neg = false;
+
+ if (millis < 0s) {
+ neg = true;
+ millis = -millis;
+ }
+
+ uint64_t remaining;
+ if (millis >= 10min) {
+ remaining
+ = std::chrono::duration_cast<std::chrono::seconds>(millis).count();
+ curr_interval += 1;
+ } else {
+ remaining = millis.count();
+ }
+
+ for (; curr_interval != std::end(intervals); curr_interval++) {
+ uint64_t amount;
+ char segment[32];
+
+ if (curr_interval->length) {
+ amount = remaining % curr_interval->length;
+ remaining = remaining / curr_interval->length;
+ } else {
+ amount = remaining;
+ remaining = 0;
+ }
+
+ if (!amount && !remaining) {
+ break;
+ }
+
+ snprintf(segment,
+ sizeof(segment),
+ curr_interval->format,
+ amount,
+ curr_interval->symbol);
+ retval.insert(0, segment);
+ if (remaining > 0 && amount < 10 && curr_interval->symbol[0]) {
+ retval.insert(0, "0");
+ }
+ }
+
+ if (neg) {
+ retval.insert(0, "-");
+ }
+
+ return retval;
+}
+
+} // namespace time
+} // namespace humanize
diff --git a/src/base/humanize.time.hh b/src/base/humanize.time.hh
new file mode 100644
index 0000000..96edebd
--- /dev/null
+++ b/src/base/humanize.time.hh
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2021, 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_humanize_time_hh
+#define lnav_humanize_time_hh
+
+#include <string>
+
+#include <sys/time.h>
+
+#include "optional.hpp"
+
+namespace humanize {
+namespace time {
+
+class point {
+public:
+ static point from_tv(const struct timeval& tv);
+
+ point& with_recent_point(const struct timeval& tv)
+ {
+ this->p_recent_point = tv;
+ return *this;
+ }
+
+ point& with_convert_to_local(bool convert_to_local)
+ {
+ this->p_convert_to_local = convert_to_local;
+ return *this;
+ }
+
+ std::string as_time_ago() const;
+
+ std::string as_precise_time_ago() const;
+
+private:
+ explicit point(const struct timeval& tv)
+ : p_past_point{tv.tv_sec, tv.tv_usec}
+ {
+ }
+
+ struct timeval p_past_point;
+ nonstd::optional<struct timeval> p_recent_point;
+ bool p_convert_to_local{false};
+};
+
+class duration {
+public:
+ static duration from_tv(const struct timeval& tv);
+
+ std::string to_string() const;
+
+private:
+ explicit duration(const struct timeval& tv) : d_timeval(tv) {}
+
+ struct timeval d_timeval;
+};
+
+} // namespace time
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.time.tests.cc b/src/base/humanize.time.tests.cc
new file mode 100644
index 0000000..853dd04
--- /dev/null
+++ b/src/base/humanize.time.tests.cc
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2021, 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 <chrono>
+#include <iostream>
+
+#include "config.h"
+#include "doctest/doctest.h"
+#include "humanize.time.hh"
+
+TEST_CASE("time ago")
+{
+ using namespace std::chrono_literals;
+
+ time_t t1 = 1610000000;
+ auto t1_chrono = std::chrono::seconds(t1);
+
+ auto p1 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + 5, 0});
+
+ CHECK(p1.as_time_ago() == "just now");
+ CHECK(p1.as_precise_time_ago() == " 5 seconds ago");
+
+ auto p2 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + 65, 0});
+
+ CHECK(p2.as_time_ago() == "one minute ago");
+ CHECK(p2.as_precise_time_ago() == " 1 minute and 5 seconds ago");
+
+ auto p3 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + (3 * 60 + 5), 0});
+
+ CHECK(p3.as_time_ago() == "3 minutes ago");
+ CHECK(p3.as_precise_time_ago() == " 3 minutes and 5 seconds ago");
+
+ auto p4 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 65min).count(), 0});
+
+ CHECK(p4.as_time_ago() == "one hour ago");
+ CHECK(p4.as_precise_time_ago() == "one hour ago");
+
+ auto p5 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 3h).count(), 0});
+
+ CHECK(p5.as_time_ago() == "3 hours ago");
+ CHECK(p5.as_precise_time_ago() == "3 hours ago");
+
+ auto p6 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 25h).count(), 0});
+
+ CHECK(p6.as_time_ago() == "one day ago");
+ CHECK(p6.as_precise_time_ago() == "one day ago");
+
+ auto p7 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 50h).count(), 0});
+
+ CHECK(p7.as_time_ago() == "2 days ago");
+ CHECK(p7.as_precise_time_ago() == "2 days ago");
+
+ auto p8 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 370 * 24h).count(), 0});
+
+ CHECK(p8.as_time_ago() == "over a year ago");
+ CHECK(p8.as_precise_time_ago() == "over a year ago");
+
+ auto p9 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 800 * 24h).count(), 0});
+
+ CHECK(p9.as_time_ago() == "over 2 years ago");
+ CHECK(p9.as_precise_time_ago() == "over 2 years ago");
+
+ CHECK(humanize::time::point::from_tv({1610000000, 0})
+ .with_recent_point({(time_t) 1612000000, 0})
+ .as_time_ago()
+ == "23 days ago");
+}
+
+TEST_CASE("duration to_string")
+{
+ std::string val;
+
+ val = humanize::time::duration::from_tv({25 * 60 * 60, 123000}).to_string();
+ CHECK(val == "1d01h00m00s");
+ val = humanize::time::duration::from_tv({25 * 60 * 60 + 25 * 60, 123000})
+ .to_string();
+ CHECK(val == "1d01h25m00s");
+ val = humanize::time::duration::from_tv({10, 123000}).to_string();
+ CHECK(val == "10s123");
+ val = humanize::time::duration::from_tv({10, 0}).to_string();
+ CHECK(val == "10s000");
+ val = humanize::time::duration::from_tv({0, 100000}).to_string();
+ CHECK(val == "100");
+ val = humanize::time::duration::from_tv({0, 0}).to_string();
+ CHECK(val == "");
+ val = humanize::time::duration::from_tv({0, -10000}).to_string();
+ CHECK(val == "-010");
+ val = humanize::time::duration::from_tv({-10, 0}).to_string();
+ CHECK(val == "-10s000");
+}
diff --git a/src/base/injector.bind.hh b/src/base/injector.bind.hh
new file mode 100644
index 0000000..f240cd1
--- /dev/null
+++ b/src/base/injector.bind.hh
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2021, 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 injector.bind.hh
+ */
+
+#ifndef lnav_injector_bind_hh
+#define lnav_injector_bind_hh
+
+#include "injector.hh"
+
+namespace injector {
+
+namespace details {
+
+template<typename I, typename R, typename... Args>
+std::function<std::shared_ptr<I>()>
+create_factory(R (*)(Args...))
+{
+ return []() { return std::make_shared<I>(::injector::get<Args>()...); };
+}
+
+template<typename I, std::enable_if_t<has_injectable<I>::value, bool> = true>
+std::function<std::shared_ptr<I>()>
+create_factory()
+{
+ typename I::injectable* i = nullptr;
+
+ return create_factory<I>(i);
+}
+
+template<typename I, std::enable_if_t<!has_injectable<I>::value, bool> = true>
+std::function<std::shared_ptr<I>()>
+create_factory() noexcept
+{
+ return []() { return std::make_shared<I>(); };
+}
+
+} // namespace details
+
+template<typename T, typename... Annotations>
+struct bind : singleton_storage<T, Annotations...> {
+ template<typename I = T,
+ std::enable_if_t<has_injectable<I>::value, bool> = true>
+ static bool to_singleton() noexcept
+ {
+ typename I::injectable* i = nullptr;
+ singleton_storage<T, Annotations...>::ss_owner
+ = create_from_injectable<I>(i)();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+
+ return true;
+ }
+
+ template<typename I = T,
+ std::enable_if_t<!has_injectable<I>::value, bool> = true>
+ static bool to_singleton() noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_owner = std::make_shared<T>();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ struct lifetime {
+ ~lifetime()
+ {
+ singleton_storage<T, Annotations...>::ss_owner = nullptr;
+ singleton_storage<T, Annotations...>::ss_data = nullptr;
+ }
+ };
+
+ template<typename I = T,
+ std::enable_if_t<has_injectable<I>::value, bool> = true>
+ static lifetime to_scoped_singleton() noexcept
+ {
+ typename I::injectable* i = nullptr;
+ singleton_storage<T, Annotations...>::ss_owner
+ = create_from_injectable<I>(i)();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+
+ return {};
+ }
+
+ template<typename... Args>
+ static bool to_instance(T* (*f)(Args...)) noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_data
+ = f(::injector::get<Args>()...);
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ static bool to_instance(T* data) noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_data = data;
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ template<typename I>
+ static bool to() noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_factory
+ = details::create_factory<I>();
+ singleton_storage<T, Annotations...>::ss_scope = scope::none;
+ return true;
+ }
+};
+
+template<typename T>
+struct bind_multiple : multiple_storage<T> {
+ bind_multiple() noexcept = default;
+
+ template<typename I>
+ bind_multiple& add() noexcept
+ {
+ multiple_storage<T>::get_factories()[typeid(I).name()]
+ = details::create_factory<I>();
+
+ return *this;
+ }
+
+ template<typename I, typename... Annotations>
+ bind_multiple& add_singleton() noexcept
+ {
+ auto factory = details::create_factory<I>();
+ auto single = factory();
+
+ if (sizeof...(Annotations) > 0) {
+ bind<T, Annotations...>::to_instance(single.get());
+ }
+ bind<I, Annotations...>::to_instance(single.get());
+ multiple_storage<T>::get_factories()[typeid(I).name()]
+ = [single]() { return single; };
+
+ return *this;
+ }
+};
+
+} // namespace injector
+
+#endif
diff --git a/src/base/injector.hh b/src/base/injector.hh
new file mode 100644
index 0000000..c6848a6
--- /dev/null
+++ b/src/base/injector.hh
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2021, 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 injector.hh
+ */
+
+#ifndef lnav_injector_hh
+#define lnav_injector_hh
+
+#include <map>
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+#include <assert.h>
+
+#include "base/lnav_log.hh"
+
+namespace injector {
+
+enum class scope {
+ undefined,
+ none,
+ singleton,
+};
+
+template<typename Annotation>
+void force_linking(Annotation anno);
+
+template<class...>
+using void_t = void;
+
+template<typename T, typename... Annotations>
+struct with_annotations {
+ T value;
+};
+
+template<class, class = void>
+struct has_injectable : std::false_type {};
+
+template<class T>
+struct has_injectable<T, void_t<typename T::injectable>> : std::true_type {};
+
+template<typename T, typename... Annotations>
+struct singleton_storage {
+ static scope get_scope() { return ss_scope; }
+
+ static T* get()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_data;
+ }
+
+ static std::shared_ptr<T> get_owner()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_owner;
+ }
+
+ static std::shared_ptr<T> create()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_factory();
+ }
+
+protected:
+ static scope ss_scope;
+ static T* ss_data;
+ static std::shared_ptr<T> ss_owner;
+ static std::function<std::shared_ptr<T>()> ss_factory;
+};
+
+template<typename T, typename... Annotations>
+T* singleton_storage<T, Annotations...>::ss_data = nullptr;
+
+template<typename T, typename... Annotations>
+scope singleton_storage<T, Annotations...>::ss_scope = scope::undefined;
+
+template<typename T, typename... Annotations>
+std::shared_ptr<T> singleton_storage<T, Annotations...>::ss_owner;
+
+template<typename T, typename... Annotations>
+std::function<std::shared_ptr<T>()>
+ singleton_storage<T, Annotations...>::ss_factory;
+
+template<typename T>
+struct Impl {
+ using type = T;
+};
+
+template<typename T>
+struct multiple_storage {
+ static std::vector<std::shared_ptr<T>> create()
+ {
+ std::vector<std::shared_ptr<T>> retval;
+
+ for (const auto& pair : get_factories()) {
+ retval.template emplace_back(pair.second());
+ }
+ return retval;
+ }
+
+protected:
+ using factory_map_t
+ = std::map<std::string, std::function<std::shared_ptr<T>()>>;
+
+ static factory_map_t& get_factories()
+ {
+ static factory_map_t retval;
+
+ return retval;
+ }
+};
+
+template<typename T,
+ typename... Annotations,
+ std::enable_if_t<std::is_reference<T>::value, bool> = true>
+T
+get()
+{
+ using plain_t = std::remove_const_t<std::remove_reference_t<T>>;
+
+ return *singleton_storage<plain_t, Annotations...>::get();
+}
+
+template<typename T,
+ typename... Annotations,
+ std::enable_if_t<std::is_pointer<T>::value, bool> = true>
+T
+get()
+{
+ using plain_t = std::remove_const_t<std::remove_pointer_t<T>>;
+
+ return singleton_storage<plain_t, Annotations...>::get();
+}
+
+template<class T>
+struct is_shared_ptr : std::false_type {};
+
+template<class T>
+struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};
+
+template<class T>
+struct is_vector : std::false_type {};
+
+template<class T>
+struct is_vector<std::vector<T>> : std::true_type {};
+
+template<typename I, typename R, typename... IArgs, typename... Args>
+std::function<std::shared_ptr<I>()> create_from_injectable(R (*)(IArgs...),
+ Args&... args);
+
+template<typename T,
+ typename... Args,
+ std::enable_if_t<has_injectable<typename T::element_type>::value, bool>
+ = true,
+ std::enable_if_t<is_shared_ptr<T>::value, bool> = true>
+T
+get(Args&... args)
+{
+ typename T::element_type::injectable* i = nullptr;
+
+ if (singleton_storage<typename T::element_type>::get_scope()
+ == scope::singleton)
+ {
+ return singleton_storage<typename T::element_type>::get_owner();
+ }
+ return create_from_injectable<typename T::element_type>(i, args...)();
+}
+
+template<
+ typename T,
+ typename... Annotations,
+ std::enable_if_t<!has_injectable<typename T::element_type>::value, bool>
+ = true,
+ std::enable_if_t<is_shared_ptr<T>::value, bool> = true>
+T
+get()
+{
+ return singleton_storage<typename T::element_type,
+ Annotations...>::get_owner();
+}
+
+template<typename T, std::enable_if_t<is_vector<T>::value, bool> = true>
+T
+get()
+{
+ return multiple_storage<typename T::value_type::element_type>::create();
+}
+
+template<typename I, typename R, typename... IArgs, typename... Args>
+std::function<std::shared_ptr<I>()>
+create_from_injectable(R (*)(IArgs...), Args&... args)
+{
+ return [&]() {
+ return std::make_shared<I>(args..., ::injector::get<IArgs>()...);
+ };
+}
+
+} // namespace injector
+
+#endif
diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc
new file mode 100644
index 0000000..676d2bc
--- /dev/null
+++ b/src/base/intern_string.cc
@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2014, 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 intern_string.cc
+ */
+
+#include <mutex>
+
+#include "intern_string.hh"
+
+#include <string.h>
+
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "xxHash/xxhash.h"
+
+const static int TABLE_SIZE = 4095;
+
+struct intern_string::intern_table {
+ ~intern_table()
+ {
+ for (auto is : this->it_table) {
+ auto curr = is;
+
+ while (curr != nullptr) {
+ auto next = curr->is_next;
+
+ delete curr;
+ curr = next;
+ }
+ }
+ }
+
+ intern_string* it_table[TABLE_SIZE];
+};
+
+intern_table_lifetime
+intern_string::get_table_lifetime()
+{
+ static intern_table_lifetime retval = std::make_shared<intern_table>();
+
+ return retval;
+}
+
+unsigned long
+hash_str(const char* str, size_t len)
+{
+ return XXH3_64bits(str, len);
+}
+
+const intern_string*
+intern_string::lookup(const char* str, ssize_t len) noexcept
+{
+ unsigned long h;
+ intern_string* curr;
+
+ if (len == -1) {
+ len = strlen(str);
+ }
+ h = hash_str(str, len) % TABLE_SIZE;
+
+ {
+ static std::mutex table_mutex;
+
+ std::lock_guard<std::mutex> lk(table_mutex);
+ auto tab = get_table_lifetime();
+
+ curr = tab->it_table[h];
+ while (curr != nullptr) {
+ if (static_cast<ssize_t>(curr->is_str.size()) == len
+ && strncmp(curr->is_str.c_str(), str, len) == 0)
+ {
+ return curr;
+ }
+ curr = curr->is_next;
+ }
+
+ curr = new intern_string(str, len);
+ curr->is_next = tab->it_table[h];
+ tab->it_table[h] = curr;
+
+ return curr;
+ }
+}
+
+const intern_string*
+intern_string::lookup(const string_fragment& sf) noexcept
+{
+ return lookup(sf.data(), sf.length());
+}
+
+const intern_string*
+intern_string::lookup(const std::string& str) noexcept
+{
+ return lookup(str.c_str(), str.size());
+}
+
+bool
+intern_string::startswith(const char* prefix) const
+{
+ const char* curr = this->is_str.data();
+
+ while (*prefix != '\0' && *prefix == *curr) {
+ prefix += 1;
+ curr += 1;
+ }
+
+ return *prefix == '\0';
+}
+
+string_fragment
+string_fragment::trim(const char* tokens) const
+{
+ string_fragment retval = *this;
+
+ while (retval.sf_begin < retval.sf_end) {
+ bool found = false;
+
+ for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
+ if (retval.sf_string[retval.sf_begin] == tokens[lpc]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ break;
+ }
+
+ retval.sf_begin += 1;
+ }
+ while (retval.sf_begin < retval.sf_end) {
+ bool found = false;
+
+ for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
+ if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ break;
+ }
+
+ retval.sf_end -= 1;
+ }
+
+ return retval;
+}
+
+string_fragment
+string_fragment::trim() const
+{
+ return this->trim(" \t\r\n");
+}
+
+nonstd::optional<string_fragment>
+string_fragment::consume_n(int amount) const
+{
+ if (amount > this->length()) {
+ return nonstd::nullopt;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + amount,
+ this->sf_end,
+ };
+}
+
+string_fragment::split_result
+string_fragment::split_n(int amount) const
+{
+ if (amount > this->length()) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(
+ string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + amount,
+ },
+ string_fragment{
+ this->sf_string,
+ this->sf_begin + amount,
+ this->sf_end,
+ });
+}
+
+std::vector<string_fragment>
+string_fragment::split_lines() const
+{
+ std::vector<string_fragment> retval;
+ int start = this->sf_begin;
+
+ for (auto index = start; index < this->sf_end; index++) {
+ if (this->sf_string[index] == '\n') {
+ retval.emplace_back(this->sf_string, start, index + 1);
+ start = index + 1;
+ }
+ }
+ retval.emplace_back(this->sf_string, start, this->sf_end);
+
+ return retval;
+}
+
+Result<ssize_t, const char*>
+string_fragment::utf8_length() const
+{
+ ssize_t retval = 0;
+
+ for (ssize_t byte_index = this->sf_begin; byte_index < this->sf_end;) {
+ auto ch_size = TRY(ww898::utf::utf8::char_size([this, byte_index]() {
+ return std::make_pair(this->sf_string[byte_index],
+ this->sf_end - byte_index);
+ }));
+ byte_index += ch_size;
+ retval += 1;
+ }
+
+ return Ok(retval);
+}
+
+string_fragment::case_style
+string_fragment::detect_text_case_style() const
+{
+ static const auto LOWER_RE
+ = lnav::pcre2pp::code::from_const(R"(^[^A-Z]+$)");
+ static const auto UPPER_RE
+ = lnav::pcre2pp::code::from_const(R"(^[^a-z]+$)");
+ static const auto CAMEL_RE
+ = lnav::pcre2pp::code::from_const(R"(^(?:[A-Z][a-z0-9]+)+$)");
+
+ if (LOWER_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::lower;
+ }
+ if (UPPER_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::upper;
+ }
+ if (CAMEL_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::camel;
+ }
+
+ return case_style::mixed;
+}
+
+std::string
+string_fragment::to_string_with_case_style(case_style style) const
+{
+ std::string retval;
+
+ switch (style) {
+ case case_style::lower: {
+ for (auto ch : *this) {
+ retval.append(1, std::tolower(ch));
+ }
+ break;
+ }
+ case case_style::upper: {
+ for (auto ch : *this) {
+ retval.append(1, std::toupper(ch));
+ }
+ break;
+ }
+ case case_style::camel: {
+ retval = this->to_string();
+ if (!this->empty()) {
+ retval[0] = toupper(retval[0]);
+ }
+ break;
+ }
+ case case_style::mixed: {
+ return this->to_string();
+ }
+ }
+
+ return retval;
+}
diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh
new file mode 100644
index 0000000..4ae40da
--- /dev/null
+++ b/src/base/intern_string.hh
@@ -0,0 +1,865 @@
+/**
+ * Copyright (c) 2014, 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 intern_string.hh
+ */
+
+#ifndef intern_string_hh
+#define intern_string_hh
+
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <assert.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "fmt/format.h"
+#include "optional.hpp"
+#include "scn/util/string_view.h"
+#include "strnatcmp.h"
+#include "ww898/cp_utf8.hpp"
+
+struct string_fragment {
+ using iterator = const char*;
+
+ static string_fragment invalid()
+ {
+ string_fragment retval;
+
+ retval.invalidate();
+ return retval;
+ }
+
+ static string_fragment from_c_str(const char* str)
+ {
+ return string_fragment{str, 0, str != nullptr ? (int) strlen(str) : 0};
+ }
+
+ static string_fragment from_c_str(const unsigned char* str)
+ {
+ return string_fragment{
+ str, 0, str != nullptr ? (int) strlen((char*) str) : 0};
+ }
+
+ template<typename T, std::size_t N>
+ static string_fragment from_const(const T (&str)[N])
+ {
+ return string_fragment{str, 0, (int) N - 1};
+ }
+
+ static string_fragment from_str(const std::string& str)
+ {
+ return string_fragment{str.c_str(), 0, (int) str.size()};
+ }
+
+ static string_fragment from_substr(const std::string& str,
+ size_t offset,
+ size_t length)
+ {
+ return string_fragment{
+ str.c_str(), (int) offset, (int) (offset + length)};
+ }
+
+ static string_fragment from_str_range(const std::string& str,
+ size_t begin,
+ size_t end)
+ {
+ return string_fragment{str.c_str(), (int) begin, (int) end};
+ }
+
+ static string_fragment from_bytes(const char* bytes, size_t len)
+ {
+ return string_fragment{bytes, 0, (int) len};
+ }
+
+ static string_fragment from_bytes(const unsigned char* bytes, size_t len)
+ {
+ return string_fragment{(const char*) bytes, 0, (int) len};
+ }
+
+ static string_fragment from_memory_buffer(const fmt::memory_buffer& buf)
+ {
+ return string_fragment{buf.data(), 0, (int) buf.size()};
+ }
+
+ static string_fragment from_byte_range(const char* bytes,
+ size_t begin,
+ size_t end)
+ {
+ return string_fragment{bytes, (int) begin, (int) end};
+ }
+
+ explicit string_fragment(const char* str = "", int begin = 0, int end = -1)
+ : sf_string(str), sf_begin(begin), sf_end(end == -1 ? strlen(str) : end)
+ {
+ }
+
+ explicit string_fragment(const unsigned char* str,
+ int begin = 0,
+ int end = -1)
+ : sf_string((const char*) str), sf_begin(begin),
+ sf_end(end == -1 ? strlen((const char*) str) : end)
+ {
+ }
+
+ string_fragment(const std::string& str)
+ : sf_string(str.c_str()), sf_begin(0), sf_end(str.length())
+ {
+ }
+
+ bool is_valid() const
+ {
+ return this->sf_begin != -1 && this->sf_begin <= this->sf_end;
+ }
+
+ int length() const { return this->sf_end - this->sf_begin; }
+
+ Result<ssize_t, const char*> utf8_length() const;
+
+ const char* data() const { return &this->sf_string[this->sf_begin]; }
+
+ const unsigned char* udata() const
+ {
+ return (const unsigned char*) &this->sf_string[this->sf_begin];
+ }
+
+ char* writable_data(int offset = 0)
+ {
+ return (char*) &this->sf_string[this->sf_begin + offset];
+ }
+
+ 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];
+ }
+ }
+
+ char back() const { return this->sf_string[this->sf_end - 1]; }
+
+ void pop_back()
+ {
+ if (!this->empty()) {
+ this->sf_end -= 1;
+ }
+ }
+
+ iterator begin() const { return &this->sf_string[this->sf_begin]; }
+
+ iterator end() const { return &this->sf_string[this->sf_end]; }
+
+ 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;
+ }
+
+ return Ok(retval);
+ }
+
+ const char& operator[](int index) const
+ {
+ return this->sf_string[sf_begin + index];
+ }
+
+ bool operator==(const std::string& str) const
+ {
+ if (this->length() != (int) str.length()) {
+ return false;
+ }
+
+ return memcmp(
+ &this->sf_string[this->sf_begin], str.c_str(), str.length())
+ == 0;
+ }
+
+ bool operator==(const string_fragment& sf) const
+ {
+ if (this->length() != sf.length()) {
+ return false;
+ }
+
+ return memcmp(this->data(), sf.data(), sf.length()) == 0;
+ }
+
+ bool iequal(const string_fragment& sf) const
+ {
+ if (this->length() != sf.length()) {
+ return false;
+ }
+
+ return strnatcasecmp(
+ this->length(), this->data(), sf.length(), sf.data())
+ == 0;
+ }
+
+ bool operator==(const char* str) const
+ {
+ size_t len = strlen(str);
+
+ return len == (size_t) this->length()
+ && strncmp(this->data(), str, this->length()) == 0;
+ }
+
+ bool operator!=(const char* str) const { return !(*this == str); }
+
+ bool startswith(const char* prefix) const
+ {
+ const auto* iter = this->begin();
+
+ while (*prefix != '\0' && iter < this->end() && *prefix == *iter) {
+ prefix += 1;
+ iter += 1;
+ }
+
+ return *prefix == '\0';
+ }
+
+ bool endswith(const char* suffix) const
+ {
+ int suffix_len = strlen(suffix);
+
+ if (suffix_len > this->length()) {
+ return false;
+ }
+
+ const auto* curr = this->end() - suffix_len;
+ while (*suffix != '\0' && *curr == *suffix) {
+ suffix += 1;
+ curr += 1;
+ }
+
+ return *suffix == '\0';
+ }
+
+ string_fragment substr(int begin) const
+ {
+ return string_fragment{
+ this->sf_string, this->sf_begin + begin, this->sf_end};
+ }
+
+ string_fragment sub_range(int begin, int end) const
+ {
+ return string_fragment{
+ this->sf_string, this->sf_begin + begin, this->sf_begin + end};
+ }
+
+ size_t count(char ch) const
+ {
+ size_t retval = 0;
+
+ for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) {
+ if (this->sf_string[lpc] == ch) {
+ retval += 1;
+ }
+ }
+
+ return retval;
+ }
+
+ nonstd::optional<size_t> find(char ch) const
+ {
+ for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) {
+ if (this->sf_string[lpc] == ch) {
+ return lpc - this->sf_begin;
+ }
+ }
+
+ return nonstd::nullopt;
+ }
+
+ template<typename P>
+ string_fragment find_left_boundary(size_t start, P&& predicate) const
+ {
+ assert((int) start <= this->length());
+
+ if (start > 0 && start == this->length()) {
+ start -= 1;
+ }
+ while (start > 0) {
+ if (predicate(this->data()[start])) {
+ start += 1;
+ break;
+ }
+ start -= 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ (int) start,
+ this->sf_end,
+ };
+ }
+
+ template<typename P>
+ string_fragment find_right_boundary(size_t start, P&& predicate) const
+ {
+ while ((int) start < this->length()) {
+ if (predicate(this->data()[start])) {
+ break;
+ }
+ start += 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + (int) start,
+ };
+ }
+
+ template<typename P>
+ string_fragment find_boundaries_around(size_t start, P&& predicate) const
+ {
+ return this->template find_left_boundary(start, predicate)
+ .find_right_boundary(0, predicate);
+ }
+
+ nonstd::optional<std::pair<uint32_t, string_fragment>> consume_codepoint()
+ const
+ {
+ auto cp = this->front_codepoint();
+ auto index_res = this->codepoint_to_byte_index(1);
+
+ if (index_res.isErr()) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(cp, this->substr(index_res.unwrap()));
+ }
+
+ template<typename P>
+ nonstd::optional<string_fragment> consume(P predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (!predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + consumed,
+ this->sf_end,
+ };
+ }
+
+ nonstd::optional<string_fragment> consume_n(int amount) const;
+
+ template<typename P>
+ string_fragment skip(P predicate) const
+ {
+ int offset = 0;
+ while (offset < this->length() && predicate(this->data()[offset])) {
+ offset += 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + offset,
+ this->sf_end,
+ };
+ }
+
+ using split_result
+ = nonstd::optional<std::pair<string_fragment, string_fragment>>;
+
+ template<typename P>
+ split_result split_while(P&& predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (!predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ 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,
+ this->sf_end,
+ });
+ }
+
+ template<typename P>
+ split_result split_when(P&& predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ 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 + 1,
+ this->sf_end,
+ });
+ }
+
+ split_result split_n(int amount) const;
+
+ std::vector<string_fragment> split_lines() const;
+
+ struct tag1 {
+ const char t_value;
+
+ bool operator()(char ch) const { return this->t_value == ch; }
+ };
+
+ struct quoted_string_body {
+ bool qs_in_escape{false};
+
+ bool operator()(char ch)
+ {
+ if (this->qs_in_escape) {
+ this->qs_in_escape = false;
+ return true;
+ } else if (ch == '\\') {
+ this->qs_in_escape = true;
+ return true;
+ } else if (ch == '"') {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ };
+
+ const char* to_string(char* buf) const
+ {
+ memcpy(buf, this->data(), this->length());
+ buf[this->length()] = '\0';
+
+ return buf;
+ }
+
+ std::string to_string() const
+ {
+ return {this->data(), (size_t) this->length()};
+ }
+
+ void clear()
+ {
+ this->sf_begin = 0;
+ this->sf_end = 0;
+ }
+
+ void invalidate()
+ {
+ this->sf_begin = -1;
+ this->sf_end = -1;
+ }
+
+ string_fragment trim(const char* tokens) const;
+ string_fragment trim() const;
+
+ string_fragment prepend(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin + amount,
+ this->sf_end + amount,
+ };
+ }
+
+ string_fragment erase_before(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin - amount,
+ this->sf_end - amount,
+ };
+ }
+
+ string_fragment erase(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin,
+ this->sf_end - amount,
+ };
+ }
+
+ template<typename A>
+ const char* to_c_str(A allocator) const
+ {
+ auto* retval = allocator.allocate(this->length() + 1);
+ memcpy(retval, this->data(), this->length());
+ retval[this->length()] = '\0';
+ return retval;
+ }
+
+ template<typename A>
+ string_fragment to_owned(A allocator) const
+ {
+ return string_fragment{
+ this->template to_c_str(allocator),
+ 0,
+ this->length(),
+ };
+ }
+
+ scn::string_view to_string_view() const
+ {
+ return scn::string_view{this->begin(), this->end()};
+ }
+
+ enum class case_style {
+ lower,
+ upper,
+ camel,
+ mixed,
+ };
+
+ case_style detect_text_case_style() const;
+
+ std::string to_string_with_case_style(case_style style) const;
+
+ const char* sf_string;
+ int sf_begin;
+ int sf_end;
+};
+
+inline bool
+operator==(const std::string& left, const string_fragment& right)
+{
+ return right == left;
+}
+
+inline bool
+operator<(const char* left, const string_fragment& right)
+{
+ int rc = strncmp(left, right.data(), right.length());
+ return rc < 0;
+}
+
+inline void
+operator+=(std::string& left, const string_fragment& right)
+{
+ left.append(right.data(), right.length());
+}
+
+inline bool
+operator<(const string_fragment& left, const char* right)
+{
+ return strncmp(left.data(), right, left.length()) < 0;
+}
+
+inline std::ostream&
+operator<<(std::ostream& os, const string_fragment& sf)
+{
+ os.write(sf.data(), sf.length());
+ return os;
+}
+
+class intern_string {
+public:
+ static const intern_string* lookup(const char* str, ssize_t len) noexcept;
+
+ static const intern_string* lookup(const string_fragment& sf) noexcept;
+
+ static const intern_string* lookup(const std::string& str) noexcept;
+
+ const char* get() const { return this->is_str.c_str(); };
+
+ size_t size() const { return this->is_str.size(); }
+
+ std::string to_string() const { return this->is_str; }
+
+ string_fragment to_string_fragment() const
+ {
+ return string_fragment{this->is_str};
+ }
+
+ bool startswith(const char* prefix) const;
+
+ struct intern_table;
+ static std::shared_ptr<intern_table> get_table_lifetime();
+
+private:
+ friend intern_table;
+
+ intern_string(const char* str, ssize_t len)
+ : is_next(nullptr), is_str(str, (size_t) len)
+ {
+ }
+
+ intern_string* is_next;
+ std::string is_str;
+};
+
+using intern_table_lifetime = std::shared_ptr<intern_string::intern_table>;
+
+class intern_string_t {
+public:
+ using iterator = const char*;
+
+ intern_string_t(const intern_string* is = nullptr) : ist_interned_string(is)
+ {
+ }
+
+ const intern_string* unwrap() const { return this->ist_interned_string; }
+
+ void clear() { this->ist_interned_string = nullptr; };
+
+ bool empty() const { return this->ist_interned_string == nullptr; }
+
+ const char* get() const
+ {
+ if (this->empty()) {
+ return "";
+ }
+ return this->ist_interned_string->get();
+ }
+
+ const char* c_str() const { return this->get(); }
+
+ iterator begin() const { return this->get(); }
+
+ iterator end() const { return this->get() + this->size(); }
+
+ size_t size() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return 0;
+ }
+ return this->ist_interned_string->size();
+ }
+
+ size_t hash() const
+ {
+ auto ptr = (uintptr_t) this->ist_interned_string;
+
+ return ptr;
+ }
+
+ std::string to_string() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return "";
+ }
+ return this->ist_interned_string->to_string();
+ }
+
+ string_fragment to_string_fragment() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return string_fragment{"", 0, 0};
+ }
+ return this->ist_interned_string->to_string_fragment();
+ }
+
+ bool operator<(const intern_string_t& rhs) const
+ {
+ return strcmp(this->get(), rhs.get()) < 0;
+ }
+
+ bool operator==(const intern_string_t& rhs) const
+ {
+ return this->ist_interned_string == rhs.ist_interned_string;
+ }
+
+ bool operator!=(const intern_string_t& rhs) const
+ {
+ return !(*this == rhs);
+ }
+
+ bool operator==(const char* rhs) const
+ {
+ return strcmp(this->get(), rhs) == 0;
+ }
+
+ bool operator!=(const char* rhs) const
+ {
+ return strcmp(this->get(), rhs) != 0;
+ }
+
+ static bool case_lt(const intern_string_t& lhs, const intern_string_t& rhs)
+ {
+ return strnatcasecmp(lhs.size(), lhs.get(), rhs.size(), rhs.get()) < 0;
+ }
+
+private:
+ const intern_string* ist_interned_string;
+};
+
+unsigned long hash_str(const char* str, size_t len);
+
+namespace fmt {
+template<>
+struct formatter<string_fragment> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(const string_fragment& sf, FormatContext& ctx)
+ {
+ return formatter<string_view>::format(
+ string_view{sf.data(), (size_t) sf.length()}, ctx);
+ }
+};
+
+template<>
+struct formatter<intern_string_t> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(const intern_string_t& is, FormatContext& ctx)
+ {
+ return formatter<string_view>::format(
+ string_view{is.get(), (size_t) is.size()}, ctx);
+ }
+};
+} // namespace fmt
+
+namespace std {
+template<>
+struct hash<const intern_string_t> {
+ std::size_t operator()(const intern_string_t& ist) const
+ {
+ return ist.hash();
+ }
+};
+} // namespace std
+
+inline bool
+operator<(const char* left, const intern_string_t& right)
+{
+ int rc = strncmp(left, right.get(), right.size());
+ return rc < 0;
+}
+
+inline bool
+operator<(const intern_string_t& left, const char* right)
+{
+ return strncmp(left.get(), right, left.size()) < 0;
+}
+
+inline bool
+operator==(const intern_string_t& left, const string_fragment& sf)
+{
+ return ((int) left.size() == sf.length())
+ && (memcmp(left.get(), sf.data(), left.size()) == 0);
+}
+
+inline bool
+operator==(const string_fragment& left, const intern_string_t& right)
+{
+ return (left.length() == (int) right.size())
+ && (memcmp(left.data(), right.get(), left.length()) == 0);
+}
+
+namespace std {
+inline string
+to_string(const string_fragment& s)
+{
+ return {s.data(), (size_t) s.length()};
+}
+
+inline string
+to_string(const intern_string_t& s)
+{
+ return s.to_string();
+}
+} // namespace std
+
+inline string_fragment
+to_string_fragment(const string_fragment& s)
+{
+ return s;
+}
+
+inline string_fragment
+to_string_fragment(const intern_string_t& s)
+{
+ return string_fragment(s.get(), 0, s.size());
+}
+
+inline string_fragment
+to_string_fragment(const std::string& s)
+{
+ return string_fragment(s.c_str(), 0, s.length());
+}
+
+struct frag_hasher {
+ size_t operator()(const string_fragment& sf) const
+ {
+ return hash_str(sf.data(), sf.length());
+ }
+};
+
+#endif
diff --git a/src/base/intern_string.tests.cc b/src/base/intern_string.tests.cc
new file mode 100644
index 0000000..8816803
--- /dev/null
+++ b/src/base/intern_string.tests.cc
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2021, 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 <cctype>
+#include <iostream>
+
+#include "intern_string.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("string_fragment::startswith")
+{
+ std::string empty;
+ auto sf = string_fragment{empty};
+
+ CHECK_FALSE(sf.startswith("abc"));
+}
+
+TEST_CASE("split_lines")
+{
+ std::string in1 = "Hello, World!";
+ std::string in2 = "Hello, World!\nGoodbye, World!";
+
+ {
+ auto sf = string_fragment(in1);
+ auto split = sf.split_lines();
+
+ CHECK(1 == split.size());
+ CHECK(in1 == split[0].to_string());
+ }
+
+ {
+ auto sf = string_fragment::from_str_range(in1, 7, -1);
+ auto split = sf.split_lines();
+
+ CHECK(1 == split.size());
+ CHECK("World!" == split[0].to_string());
+ }
+
+ {
+ auto sf = string_fragment(in2);
+ auto split = sf.split_lines();
+
+ CHECK(2 == split.size());
+ CHECK("Hello, World!\n" == split[0].to_string());
+ CHECK("Goodbye, World!" == split[1].to_string());
+ }
+}
+
+TEST_CASE("consume")
+{
+ auto is_eq = string_fragment::tag1{'='};
+ auto is_dq = string_fragment::tag1{'"'};
+ auto is_colon = string_fragment::tag1{':'};
+
+ const char* pair = "foo = bar";
+ auto sf = string_fragment(pair);
+
+ auto split_sf = sf.split_while(isalnum);
+
+ CHECK(split_sf.has_value());
+ CHECK(split_sf->first.to_string() == "foo");
+ CHECK(split_sf->second.to_string() == " = bar");
+
+ auto value_frag = split_sf->second.skip(isspace).consume(is_eq);
+
+ CHECK(value_frag.has_value());
+ CHECK(value_frag->to_string() == " bar");
+
+ auto stripped_value_frag = value_frag->consume(isspace);
+
+ CHECK(stripped_value_frag.has_value());
+ CHECK(stripped_value_frag->to_string() == "bar");
+
+ auto no_value = sf.consume(is_colon);
+ CHECK(!no_value.has_value());
+
+ const char* qs = R"("foo \" bar")";
+ auto qs_sf = string_fragment{qs};
+
+ auto qs_body = qs_sf.consume(is_dq);
+ string_fragment::quoted_string_body qsb;
+ auto split_body = qs_body->split_while(qsb);
+
+ CHECK(split_body.has_value());
+ CHECK(split_body->first.to_string() == "foo \\\" bar");
+ CHECK(split_body->second.to_string() == "\"");
+
+ auto empty = split_body->second.consume(is_dq);
+
+ CHECK(empty.has_value());
+ CHECK(empty->empty());
+}
+
+TEST_CASE("find_left_boundary")
+{
+ std::string in1 = "Hello,\nWorld!\n";
+
+ {
+ auto sf = string_fragment{in1};
+
+ auto world_sf = sf.find_left_boundary(
+ in1.length() - 3, [](auto ch) { return ch == '\n'; });
+ CHECK(world_sf.to_string() == "World!\n");
+ auto full_sf
+ = sf.find_left_boundary(3, [](auto ch) { return ch == '\n'; });
+ CHECK(full_sf.to_string() == in1);
+ }
+}
+
+TEST_CASE("find_right_boundary")
+{
+ std::string in1 = "Hello,\nWorld!\n";
+
+ {
+ auto sf = string_fragment{in1};
+
+ auto world_sf = sf.find_right_boundary(
+ in1.length() - 3, [](auto ch) { return ch == '\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,");
+ }
+}
diff --git a/src/base/is_utf8.cc b/src/base/is_utf8.cc
new file mode 100644
index 0000000..f55dfe0
--- /dev/null
+++ b/src/base/is_utf8.cc
@@ -0,0 +1,313 @@
+/*
+ * is_utf8 is distributed under the following terms:
+ *
+ * Copyright (c) 2013 Palard Julien. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 "is_utf8.hh"
+
+#include "config.h"
+
+/*
+ Check if the given unsigned char * is a valid utf-8 sequence.
+
+ Return value :
+ If the string is valid utf-8, 0 is returned.
+ Else the position, starting from 1, is returned.
+
+ Source:
+ http://www.unicode.org/versions/Unicode7.0.0/UnicodeStandard-7.0.pdf
+ page 124, 3.9 "Unicode Encoding Forms", "UTF-8"
+
+
+ Table 3-7. Well-Formed UTF-8 Byte Sequences
+ -----------------------------------------------------------------------------
+ | Code Points | First Byte | Second Byte | Third Byte | Fourth Byte |
+ | U+0000..U+007F | 00..7F | | | |
+ | U+0080..U+07FF | C2..DF | 80..BF | | |
+ | U+0800..U+0FFF | E0 | A0..BF | 80..BF | |
+ | U+1000..U+CFFF | E1..EC | 80..BF | 80..BF | |
+ | U+D000..U+D7FF | ED | 80..9F | 80..BF | |
+ | U+E000..U+FFFF | EE..EF | 80..BF | 80..BF | |
+ | U+10000..U+3FFFF | F0 | 90..BF | 80..BF | 80..BF |
+ | U+40000..U+FFFFF | F1..F3 | 80..BF | 80..BF | 80..BF |
+ | U+100000..U+10FFFF | F4 | 80..8F | 80..BF | 80..BF |
+ -----------------------------------------------------------------------------
+
+ Returns the first erroneous byte position, and give in
+ `faulty_bytes` the number of actually existing bytes taking part in this
+ error.
+*/
+utf8_scan_result
+is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator)
+{
+ const auto* ustr = str.udata();
+ utf8_scan_result retval;
+ ssize_t i = 0;
+
+ while (i < str.length()) {
+ if (ustr[i] == '\x1b') {
+ retval.usr_has_ansi = true;
+ }
+
+ 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;
+ }
+
+ if (retval.usr_message != nullptr) {
+ i += 1;
+ continue;
+ }
+
+ retval.usr_valid_frag = str.sub_range(0, i);
+ if (ustr[i] <= 0x7F) /* 00..7F */ {
+ i += 1;
+ } else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ {
+ if (i + 1 < str.length()) /* Expect a 2nd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between C2 and DF, expecting a "
+ "2nd byte between 80 and BF";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between C2 and DF, expecting a 2nd "
+ "byte.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 2;
+ } else if (ustr[i] == 0xE0) /* E0 A0..BF 80..BF */ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0xA0 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of E0, expecting a 2nd byte "
+ "between A0 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of E0, expecting a 3nd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of E0, expecting two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] >= 0xE1
+ && ustr[i] <= 0xEC) /* E1..EC 80..BF 80..BF */
+ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting the "
+ "2nd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting the "
+ "3rd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting two "
+ "following bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] == 0xED) /* ED 80..9F 80..BF */ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x9F) {
+ retval.usr_message
+ = "After a first byte of ED, expecting 2nd byte "
+ "between 80 and 9F.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of ED, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of ED, expecting two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] >= 0xEE
+ && ustr[i] <= 0xEF) /* EE..EF 80..BF 80..BF */
+ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between EE and EF, expecting 2nd "
+ "byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between EE and EF, expecting 3rd "
+ "byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between EE and EF, two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] == 0xF0) /* F0 90..BF 80..BF 80..BF */ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x90 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 2nd byte "
+ "between 90 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 4th byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F0, expecting three following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else if (ustr[i] >= 0xF1
+ && ustr[i] <= 0xF3) /* F1..F3 80..BF 80..BF 80..BF */
+ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "2nd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "3rd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "4th byte between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting three "
+ "following bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else if (ustr[i] == 0xF4) /* F4 80..8F 80..BF 80..BF */ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x8F) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 2nd byte "
+ "between 80 and 8F.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 4th byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F4, expecting three following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else {
+ retval.usr_message
+ = "Expecting bytes in the following ranges: 00..7F C2..F4.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ }
+ if (retval.usr_message == nullptr) {
+ retval.usr_valid_frag = str.sub_range(0, i);
+ }
+ return retval;
+}
diff --git a/src/base/is_utf8.hh b/src/base/is_utf8.hh
new file mode 100644
index 0000000..56a959f
--- /dev/null
+++ b/src/base/is_utf8.hh
@@ -0,0 +1,59 @@
+/*
+ * is_utf8 is distributed under the following terms:
+ *
+ * Copyright (c) 2013 Palard Julien. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 _IS_UTF8_H
+#define _IS_UTF8_H
+
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "intern_string.hh"
+#include "optional.hpp"
+
+struct utf8_scan_result {
+ const char* usr_message{nullptr};
+ size_t usr_faulty_bytes{0};
+ string_fragment usr_valid_frag{string_fragment::invalid()};
+ nonstd::optional<string_fragment> usr_remaining;
+ bool usr_has_ansi{false};
+
+ const char* remaining_ptr(const string_fragment& frag) const
+ {
+ if (this->usr_remaining) {
+ return this->usr_remaining->begin();
+ } else {
+ return nullptr;
+ }
+ }
+ bool is_valid() const { return this->usr_message == nullptr; }
+};
+
+utf8_scan_result is_utf8(string_fragment frag,
+ nonstd::optional<unsigned char> terminator
+ = nonstd::nullopt);
+
+#endif /* _IS_UTF8_H */
diff --git a/src/base/isc.cc b/src/base/isc.cc
new file mode 100644
index 0000000..7dbce11
--- /dev/null
+++ b/src/base/isc.cc
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2021, 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 isc.cc
+ */
+
+#include <algorithm>
+
+#include "isc.hh"
+
+#include "config.h"
+
+namespace isc {
+
+void
+service_base::start()
+{
+ log_debug("starting service thread for: %s", this->s_name.c_str());
+ this->s_thread = std::thread(&service_base::run, this);
+ this->s_started = true;
+}
+
+void*
+service_base::run()
+{
+ log_info("BEGIN isc thread: %s", this->s_name.c_str());
+ while (this->s_looping) {
+ mstime_t current_time = getmstime();
+ auto timeout = this->compute_timeout(current_time);
+
+ this->s_port.process_for(timeout);
+ this->s_children.cleanup_children();
+
+ try {
+ this->loop_body();
+ } catch (const std::exception& e) {
+ log_error("%s: loop_body() failed with -- %s",
+ this->s_name.c_str(),
+ e.what());
+ this->s_looping = false;
+ } catch (...) {
+ log_error("%s: loop_body() failed with non-standard exception",
+ this->s_name.c_str());
+ this->s_looping = false;
+ }
+ }
+ if (!this->s_children.empty()) {
+ log_debug("stopping children of service: %s", this->s_name.c_str());
+ this->s_children.stop_children();
+ }
+ this->stopped();
+ log_info("END isc thread: %s", this->s_name.c_str());
+
+ return nullptr;
+}
+
+void
+service_base::stop()
+{
+ if (this->s_started) {
+ log_debug("stopping service thread: %s", this->s_name.c_str());
+ if (this->s_looping) {
+ this->s_looping = false;
+ this->s_port.send(empty_msg());
+ }
+ log_debug("waiting for service thread: %s", this->s_name.c_str());
+ this->s_thread.join();
+ log_debug("joined service thread: %s", this->s_name.c_str());
+ this->s_started = false;
+ }
+}
+
+supervisor::supervisor(service_list servs, service_base* parent)
+ : s_service_list(std::move(servs)), s_parent(parent)
+{
+ for (auto& serv : this->s_service_list) {
+ serv->start();
+ }
+}
+
+supervisor::~supervisor()
+{
+ this->stop_children();
+}
+
+void
+supervisor::stop_children()
+{
+ for (auto& serv : this->s_service_list) {
+ serv->stop();
+ }
+ this->cleanup_children();
+}
+
+void
+supervisor::cleanup_children()
+{
+ this->s_service_list.erase(
+ std::remove_if(this->s_service_list.begin(),
+ this->s_service_list.end(),
+ [this](auto& child) {
+ if (child->is_looping()) {
+ return false;
+ }
+
+ child->stop();
+ if (this->s_parent != nullptr) {
+ this->s_parent->child_finished(child);
+ }
+ return true;
+ }),
+ this->s_service_list.end());
+}
+
+void
+supervisor::add_child_service(std::shared_ptr<service_base> new_service)
+{
+ this->s_service_list.emplace_back(new_service);
+ new_service->start();
+}
+
+} // namespace isc
diff --git a/src/base/isc.hh b/src/base/isc.hh
new file mode 100644
index 0000000..339dcaf
--- /dev/null
+++ b/src/base/isc.hh
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2021, 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 isc.hh
+ */
+
+#include <atomic>
+#include <chrono>
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+#include <utility>
+
+#include "injector.hh"
+#include "safe/safe.h"
+#include "time_util.hh"
+
+#ifndef lnav_isc_hh
+# define lnav_isc_hh
+
+namespace isc {
+
+struct msg {
+ std::function<void()> m_callback;
+};
+
+inline msg
+empty_msg()
+{
+ return {[]() {}};
+}
+
+class msg_port {
+public:
+ msg_port() = default;
+
+ void send(msg&& m)
+ {
+ safe::WriteAccess<safe_message_list, std::unique_lock> writable_msgs(
+ this->mp_messages);
+
+ writable_msgs->emplace_back(m);
+ this->sp_cond.notify_all();
+ }
+
+ template<class Rep, class Period>
+ void process_for(const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ std::deque<msg> tmp_msgs;
+
+ {
+ safe::WriteAccess<safe_message_list, std::unique_lock>
+ writable_msgs(this->mp_messages);
+
+ if (writable_msgs->empty() && rel_time.count() > 0) {
+ this->sp_cond.template wait_for(writable_msgs.lock, rel_time);
+ }
+
+ tmp_msgs.swap(*writable_msgs);
+ }
+ while (!tmp_msgs.empty()) {
+ auto& m = tmp_msgs.front();
+
+ m.m_callback();
+ tmp_msgs.pop_front();
+ }
+ }
+
+private:
+ using message_list = std::deque<msg>;
+ using safe_message_list = safe::Safe<message_list>;
+
+ std::condition_variable sp_cond;
+ safe_message_list mp_messages;
+};
+
+class service_base;
+using service_list = std::vector<std::shared_ptr<service_base>>;
+
+struct supervisor {
+ explicit supervisor(service_list servs = {},
+ service_base* parent = nullptr);
+
+ ~supervisor();
+
+ bool empty() const { return this->s_service_list.empty(); }
+
+ void add_child_service(std::shared_ptr<service_base> new_service);
+
+ void stop_children();
+
+ void cleanup_children();
+
+protected:
+ service_list s_service_list;
+ service_base* s_parent;
+};
+
+class service_base : public std::enable_shared_from_this<service_base> {
+public:
+ explicit service_base(std::string name)
+ : s_name(std::move(name)), s_children({}, this)
+ {
+ }
+
+ virtual ~service_base() = default;
+
+ bool is_looping() const { return this->s_looping; }
+
+ msg_port& get_port() { return this->s_port; }
+
+ friend supervisor;
+
+private:
+ void start();
+
+ void stop();
+
+protected:
+ virtual void* run();
+ virtual void loop_body() {}
+ virtual void child_finished(std::shared_ptr<service_base> child) {}
+ virtual void stopped() {}
+ virtual std::chrono::milliseconds compute_timeout(
+ mstime_t current_time) const
+ {
+ using namespace std::literals::chrono_literals;
+
+ return 1s;
+ }
+
+ const std::string s_name;
+ bool s_started{false};
+ std::thread s_thread;
+ std::atomic<bool> s_looping{true};
+ msg_port s_port;
+ supervisor s_children;
+};
+
+template<typename T>
+class service : public service_base {
+public:
+ explicit service(std::string sub_name = "")
+ : service_base(std::string(__PRETTY_FUNCTION__) + " " + sub_name)
+ {
+ }
+
+ template<typename F>
+ void send(F msg)
+ {
+ this->s_port.send({[lifetime = this->shared_from_this(), this, msg]() {
+ msg(*(static_cast<T*>(this)));
+ }});
+ }
+
+ template<typename F, class Rep, class Period>
+ void send_and_wait(F msg,
+ const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ msg_port reply_port;
+
+ this->s_port.send(
+ {[lifetime = this->shared_from_this(), this, &reply_port, msg]() {
+ msg(*(static_cast<T*>(this)));
+ reply_port.send(empty_msg());
+ }});
+ reply_port.template process_for(rel_time);
+ }
+};
+
+template<typename T, typename Service, typename... Annotations>
+struct to {
+ void send(std::function<void(T&)> cb)
+ {
+ auto& service = injector::get<T&, Service>();
+
+ service.send(cb);
+ }
+
+ template<class Rep, class Period>
+ void send_and_wait(std::function<void(T)> cb,
+ const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ auto& service = injector::get<T&, Service>();
+
+ service.send_and_wait(cb, rel_time);
+ }
+
+ void send_and_wait(std::function<void(T)> cb)
+ {
+ using namespace std::literals::chrono_literals;
+
+ this->send_and_wait(cb, 48h);
+ }
+};
+
+} // namespace isc
+
+#endif
diff --git a/src/base/itertools.hh b/src/base/itertools.hh
new file mode 100644
index 0000000..058ceb8
--- /dev/null
+++ b/src/base/itertools.hh
@@ -0,0 +1,785 @@
+/**
+ * Copyright (c) 2022, 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_itertools_hh
+#define lnav_itertools_hh
+
+#include <algorithm>
+#include <memory>
+#include <set>
+#include <type_traits>
+#include <vector>
+
+#include "func_util.hh"
+#include "optional.hpp"
+
+namespace lnav {
+namespace itertools {
+
+struct empty {};
+
+struct not_empty {};
+
+struct full {
+ size_t f_max_size;
+};
+
+namespace details {
+
+template<typename T>
+struct unwrap_or {
+ T uo_value;
+};
+
+template<typename P>
+struct find_if {
+ P fi_predicate;
+};
+
+template<typename T>
+struct find {
+ T f_value;
+};
+
+struct first {};
+
+struct second {};
+
+template<typename F>
+struct filter_in {
+ F f_func;
+};
+
+template<typename F>
+struct filter_out {
+ F f_func;
+};
+
+template<typename C>
+struct sort_by {
+ C sb_cmp;
+};
+
+struct sorted {};
+
+template<typename F>
+struct mapper {
+ F m_func;
+};
+
+template<typename F>
+struct flat_mapper {
+ F fm_func;
+};
+
+template<typename F>
+struct for_eacher {
+ F fe_func;
+};
+
+template<typename R, typename T>
+struct folder {
+ R f_func;
+ T f_init;
+};
+
+template<typename T>
+struct prepend {
+ T p_value;
+};
+
+template<typename T>
+struct append {
+ T p_value;
+};
+
+struct nth {
+ nonstd::optional<size_t> a_index;
+};
+
+struct skip {
+ size_t a_count;
+};
+
+struct unique {};
+
+struct max_value {};
+
+template<typename T>
+struct max_with_init {
+ T m_init;
+};
+
+struct sum {};
+
+} // namespace details
+
+template<typename T>
+inline details::unwrap_or<T>
+unwrap_or(T value)
+{
+ return details::unwrap_or<T>{
+ value,
+ };
+}
+
+template<typename P>
+inline details::find_if<P>
+find_if(P predicate)
+{
+ return details::find_if<P>{
+ predicate,
+ };
+}
+
+template<typename T>
+inline details::find<T>
+find(T value)
+{
+ return details::find<T>{
+ value,
+ };
+}
+
+inline details::first
+first()
+{
+ return details::first{};
+}
+
+inline details::second
+second()
+{
+ return details::second{};
+}
+
+inline details::nth
+nth(nonstd::optional<size_t> index)
+{
+ return details::nth{
+ index,
+ };
+}
+
+inline details::skip
+skip(size_t count)
+{
+ return details::skip{
+ count,
+ };
+}
+
+template<typename F>
+inline details::filter_in<F>
+filter_in(F func)
+{
+ return details::filter_in<F>{
+ func,
+ };
+}
+
+template<typename F>
+inline details::filter_out<F>
+filter_out(F func)
+{
+ return details::filter_out<F>{
+ func,
+ };
+}
+
+template<typename T>
+inline details::prepend<T>
+prepend(T value)
+{
+ return details::prepend<T>{
+ std::move(value),
+ };
+}
+
+template<typename T>
+inline details::append<T>
+append(T value)
+{
+ return details::append<T>{
+ std::move(value),
+ };
+}
+
+template<typename C>
+inline details::sort_by<C>
+sort_with(C cmp)
+{
+ return details::sort_by<C>{cmp};
+}
+
+template<typename C, typename T>
+inline auto
+sort_by(T C::*m)
+{
+ return sort_with(
+ [m](const C& lhs, const C& rhs) { return lhs.*m < rhs.*m; });
+}
+
+template<typename F>
+inline details::mapper<F>
+map(F func)
+{
+ return details::mapper<F>{func};
+}
+
+template<typename F>
+inline details::flat_mapper<F>
+flat_map(F func)
+{
+ return details::flat_mapper<F>{func};
+}
+
+template<typename F>
+inline details::for_eacher<F>
+for_each(F func)
+{
+ return details::for_eacher<F>{func};
+}
+
+inline auto
+deref()
+{
+ return map([](auto iter) { return *iter; });
+}
+
+template<typename R, typename T>
+inline details::folder<R, T>
+fold(R func, T init)
+{
+ return details::folder<R, T>{func, init};
+}
+
+inline details::unique
+unique()
+{
+ return details::unique{};
+}
+
+inline details::sorted
+sorted()
+{
+ return details::sorted{};
+}
+
+template<typename T, typename... Args>
+T
+chain(const T& value1, const Args&... args)
+{
+ T retval;
+
+ for (const auto& arg : {value1, args...}) {
+ for (const auto& elem : arg) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+inline details::max_value
+max()
+{
+ return details::max_value{};
+}
+
+template<typename T>
+inline details::max_with_init<T>
+max(T init)
+{
+ return details::max_with_init<T>{init};
+}
+
+inline details::sum
+sum()
+{
+ return details::sum{};
+}
+
+} // namespace itertools
+} // namespace lnav
+
+template<typename C, typename P>
+nonstd::optional<std::conditional_t<
+ std::is_const<typename std::remove_reference_t<C>>::value,
+ typename std::remove_reference_t<C>::const_iterator,
+ typename std::remove_reference_t<C>::iterator>>
+operator|(C&& in, const lnav::itertools::details::find_if<P>& finder)
+{
+ for (auto iter = in.begin(); iter != in.end(); ++iter) {
+ if (lnav::func::invoke(finder.fi_predicate, *iter)) {
+ return nonstd::make_optional(iter);
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C, typename T>
+nonstd::optional<size_t>
+operator|(const C& in, const lnav::itertools::details::find<T>& finder)
+{
+ size_t retval = 0;
+ for (const auto& elem : in) {
+ if (elem == finder.f_value) {
+ return nonstd::make_optional(retval);
+ }
+ retval += 1;
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C>
+nonstd::optional<typename C::const_iterator>
+operator|(const C& in, const lnav::itertools::details::nth indexer)
+{
+ if (!indexer.a_index.has_value()) {
+ return nonstd::nullopt;
+ }
+
+ if (indexer.a_index.value() < in.size()) {
+ auto iter = in.begin();
+
+ std::advance(iter, indexer.a_index.value());
+ return nonstd::make_optional(iter);
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C>
+std::vector<typename C::key_type>
+operator|(const C& in, const lnav::itertools::details::first indexer)
+{
+ std::vector<typename C::key_type> retval;
+
+ for (const auto& pair : in) {
+ retval.emplace_back(pair.first);
+ }
+
+ return retval;
+}
+
+template<typename C>
+nonstd::optional<typename C::value_type>
+operator|(const C& in, const lnav::itertools::details::max_value maxer)
+{
+ nonstd::optional<typename C::value_type> retval;
+
+ for (const auto& elem : in) {
+ if (!retval) {
+ retval = elem;
+ continue;
+ }
+
+ if (elem > retval.value()) {
+ retval = elem;
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename T>
+typename C::value_type
+operator|(const C& in, const lnav::itertools::details::max_with_init<T> maxer)
+{
+ typename C::value_type retval = (typename C::value_type) maxer.m_init;
+
+ for (const auto& elem : in) {
+ if (elem > retval) {
+ retval = elem;
+ }
+ }
+
+ return retval;
+}
+
+template<typename C>
+typename C::value_type
+operator|(const C& in, const lnav::itertools::details::sum summer)
+{
+ typename C::value_type retval{0};
+
+ for (const auto& elem : in) {
+ retval += elem;
+ }
+
+ return retval;
+}
+
+template<typename C>
+C
+operator|(const C& in, const lnav::itertools::details::skip& skipper)
+{
+ C retval;
+
+ if (skipper.a_count < in.size()) {
+ auto iter = in.begin();
+ std::advance(iter, skipper.a_count);
+ for (; iter != in.end(); ++iter) {
+ retval.emplace_back(*iter);
+ }
+ }
+
+ return retval;
+}
+
+template<typename T, typename F>
+std::vector<T*>
+operator|(const std::vector<std::unique_ptr<T>>& in,
+ const lnav::itertools::details::filter_in<F>& filterer)
+{
+ std::vector<T*> retval;
+
+ for (const auto& elem : in) {
+ if (lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem.get());
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename F>
+C
+operator|(const C& in, const lnav::itertools::details::filter_in<F>& filterer)
+{
+ C retval;
+
+ for (const auto& elem : in) {
+ if (lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename F>
+C
+operator|(const C& in, const lnav::itertools::details::filter_out<F>& filterer)
+{
+ C retval;
+
+ for (const auto& elem : in) {
+ if (!lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename T>
+C
+operator|(C in, const lnav::itertools::details::prepend<T>& prepender)
+{
+ in.emplace(in.begin(), prepender.p_value);
+
+ return in;
+}
+
+template<typename C, typename T>
+C
+operator|(C in, const lnav::itertools::details::append<T>& appender)
+{
+ in.emplace_back(appender.p_value);
+
+ return in;
+}
+
+template<typename C, typename R, typename T>
+T
+operator|(const C& in, const lnav::itertools::details::folder<R, T>& folder)
+{
+ auto accum = folder.f_init;
+
+ for (const auto& elem : in) {
+ accum = folder.f_func(elem, accum);
+ }
+
+ return accum;
+}
+
+template<typename C>
+std::set<typename C::value_type>
+operator|(C&& in, const lnav::itertools::details::unique& sorter)
+{
+ return {in.begin(), in.end()};
+}
+
+template<typename T, typename C>
+T
+operator|(T in, const lnav::itertools::details::sort_by<C>& sorter)
+{
+ std::sort(in.begin(), in.end(), sorter.sb_cmp);
+
+ return in;
+}
+
+template<typename T>
+T
+operator|(T in, const lnav::itertools::details::sorted& sorter)
+{
+ std::sort(in.begin(), in.end());
+
+ return in;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::flat_mapper<F>& mapper) ->
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(lnav::func::invoke(mapper.fm_func, in.value()))>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return lnav::func::invoke(mapper.fm_func, in.value());
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+void
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::for_eacher<F>& eacher)
+{
+ if (!in) {
+ return;
+ }
+
+ lnav::func::invoke(eacher.fe_func, in.value());
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+void
+operator|(std::vector<std::shared_ptr<T>>& in,
+ const lnav::itertools::details::for_eacher<F>& eacher)
+{
+ for (auto& elem : in) {
+ lnav::func::invoke(eacher.fe_func, *elem);
+ }
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(lnav::func::invoke(mapper.m_func, in.value()))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(lnav::func::invoke(mapper.m_func, in.value()));
+}
+
+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>()))>>>
+{
+ using return_type = std::vector<std::remove_const_t<std::remove_reference_t<
+ decltype(mapper.m_func(std::declval<typename T::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<decltype(((*in.begin()).*mapper.m_func)())>>
+{
+ using return_type = std::vector<
+ std::remove_const_t<decltype(((*in.begin()).*mapper.m_func)())>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back((elem.*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<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>
+{
+ using return_type
+ = std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ std::transform(
+ in.begin(),
+ in.end(),
+ std::back_inserter(retval),
+ [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); });
+
+ return retval;
+}
+
+template<typename T, typename F>
+auto
+operator|(const std::set<T>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>
+{
+ using return_type
+ = std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ std::transform(
+ in.begin(),
+ in.end(),
+ std::back_inserter(retval),
+ [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); });
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(const std::vector<std::shared_ptr<T>>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<typename std::remove_reference_t<
+ typename std::remove_const_t<decltype(((*in.front()).*mapper.m_func))>>>
+{
+ using return_type = std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<decltype((
+ (*in.front()).*mapper.m_func))>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back(((*elem).*mapper.m_func));
+ }
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(const std::vector<T>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(((in.front()).*mapper.m_func))>>>
+{
+ using return_type = std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<decltype((
+ (in.front()).*mapper.m_func))>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back(elem.*mapper.m_func);
+ }
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<typename std::remove_reference_t<
+ typename std::remove_const_t<decltype(((in.value()).*mapper.m_func))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional((in.value()).*mapper.m_func);
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(((*in.value()).*mapper.m_func))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional((*in.value()).*mapper.m_func);
+}
+
+template<typename T>
+T
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::unwrap_or<T>& unwrapper)
+{
+ return in.value_or(unwrapper.uo_value);
+}
+
+#endif
diff --git a/src/base/lnav.console.cc b/src/base/lnav.console.cc
new file mode 100644
index 0000000..a34ebac
--- /dev/null
+++ b/src/base/lnav.console.cc
@@ -0,0 +1,494 @@
+/**
+ * Copyright (c) 2022, 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 <algorithm>
+
+#include "lnav.console.hh"
+
+#include "config.h"
+#include "fmt/color.h"
+#include "itertools.hh"
+#include "lnav.console.into.hh"
+#include "log_level_enum.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "snippet_highlighters.hh"
+#include "view_curses.hh"
+
+using namespace lnav::roles::literals;
+
+namespace lnav {
+namespace console {
+
+user_message
+user_message::raw(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::raw;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::error(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::error;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::info(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::info;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::ok(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::ok;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::warning(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::warning;
+ retval.um_message.append(al);
+ return retval;
+}
+
+attr_line_t
+user_message::to_attr_line(std::set<render_flags> flags) const
+{
+ auto indent = 1;
+ attr_line_t retval;
+
+ if (this->um_level == level::warning) {
+ indent = 3;
+ }
+
+ if (flags.count(render_flags::prefix)) {
+ switch (this->um_level) {
+ case level::raw:
+ break;
+ case level::ok:
+ retval.append(lnav::roles::ok("\u2714 "));
+ break;
+ case level::info:
+ retval.append("\u24d8 info"_info).append(": ");
+ break;
+ case level::warning:
+ retval.append(lnav::roles::warning("\u26a0 warning"))
+ .append(": ");
+ break;
+ case level::error:
+ retval.append(lnav::roles::error("\u2718 error")).append(": ");
+ break;
+ }
+ }
+
+ retval.append(this->um_message).append("\n");
+ if (!this->um_reason.empty()) {
+ bool first_line = true;
+ for (const auto& line : this->um_reason.split_lines()) {
+ auto role = this->um_level == level::error ? role_t::VCR_ERROR
+ : role_t::VCR_WARNING;
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(indent, ' ')
+ .append("reason", VC_ROLE.value(role))
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" | ", VC_ROLE.value(role))
+ .append(indent, ' ');
+ }
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+ if (!this->um_snippets.empty()) {
+ for (const auto& snip : this->um_snippets) {
+ attr_line_t header;
+
+ header.append(" --> "_snippet_border)
+ .append(lnav::roles::file(snip.s_location.sl_source.get()));
+ if (snip.s_location.sl_line_number > 0) {
+ header.append(":").appendf(FMT_STRING("{}"),
+ snip.s_location.sl_line_number);
+ }
+ retval.append(header).append("\n");
+ if (!snip.s_content.blank()) {
+ auto snippet_lines = snip.s_content.split_lines();
+ auto longest_line_length = snippet_lines
+ | lnav::itertools::map(&attr_line_t::utf8_length_or_length)
+ | lnav::itertools::max(40);
+
+ for (auto& line : snippet_lines) {
+ line.pad_to(longest_line_length);
+ retval.append(" | "_snippet_border)
+ .append(line)
+ .append("\n");
+ }
+ }
+ }
+ }
+ if (!this->um_notes.empty()) {
+ for (const auto& note : this->um_notes) {
+ bool first_line = true;
+ for (const auto& line : note.split_lines()) {
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(" ="_snippet_border)
+ .append(indent, ' ')
+ .append("note"_snippet_border)
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" ").append(indent, ' ');
+ }
+
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+ }
+ if (!this->um_help.empty()) {
+ bool first_line = true;
+ for (const auto& line : this->um_help.split_lines()) {
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(" ="_snippet_border)
+ .append(indent, ' ')
+ .append("help"_snippet_border)
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" ");
+ }
+
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+
+ return retval;
+}
+
+static nonstd::optional<fmt::terminal_color>
+curses_color_to_terminal_color(int curses_color)
+{
+ switch (curses_color) {
+ case COLOR_BLACK:
+ return fmt::terminal_color::black;
+ case COLOR_CYAN:
+ return fmt::terminal_color::cyan;
+ case COLOR_WHITE:
+ return fmt::terminal_color::white;
+ case COLOR_MAGENTA:
+ return fmt::terminal_color::magenta;
+ case COLOR_BLUE:
+ return fmt::terminal_color::blue;
+ case COLOR_YELLOW:
+ return fmt::terminal_color::yellow;
+ case COLOR_GREEN:
+ return fmt::terminal_color::green;
+ case COLOR_RED:
+ return fmt::terminal_color::red;
+ default:
+ return nonstd::nullopt;
+ }
+}
+
+void
+println(FILE* file, const attr_line_t& al)
+{
+ const auto& str = al.get_string();
+
+ if (getenv("NO_COLOR") != nullptr
+ || (!isatty(fileno(file)) && getenv("YES_COLOR") == nullptr))
+ {
+ fmt::print(file, "{}\n", str);
+ return;
+ }
+
+ std::set<size_t> points = {0, static_cast<size_t>(al.length())};
+
+ for (const auto& attr : al.get_attrs()) {
+ if (!attr.sa_range.is_valid()) {
+ continue;
+ }
+ points.insert(attr.sa_range.lr_start);
+ if (attr.sa_range.lr_end > 0) {
+ points.insert(attr.sa_range.lr_end);
+ }
+ }
+
+ nonstd::optional<size_t> last_point;
+ for (const auto& point : points) {
+ if (!last_point) {
+ last_point = point;
+ continue;
+ }
+ auto default_fg_style = fmt::text_style{};
+ auto default_bg_style = fmt::text_style{};
+ auto line_style = fmt::text_style{};
+ auto fg_style = fmt::text_style{};
+ auto start = last_point.value();
+
+ for (const auto& attr : al.get_attrs()) {
+ if (!attr.sa_range.contains(start)
+ && !attr.sa_range.contains(point - 1))
+ {
+ continue;
+ }
+
+ try {
+ if (attr.sa_type == &VC_BACKGROUND) {
+ auto saw = string_attr_wrapper<int64_t>(&attr);
+ auto color_opt = curses_color_to_terminal_color(saw.get());
+
+ if (color_opt) {
+ line_style |= fmt::bg(color_opt.value());
+ }
+ } else if (attr.sa_type == &VC_FOREGROUND) {
+ auto saw = string_attr_wrapper<int64_t>(&attr);
+ auto color_opt = curses_color_to_terminal_color(saw.get());
+
+ if (color_opt) {
+ fg_style = fmt::fg(color_opt.value());
+ }
+ } else if (attr.sa_type == &VC_STYLE) {
+ auto saw = string_attr_wrapper<text_attrs>(&attr);
+ auto style = saw.get();
+
+ if (style.ta_attrs & A_REVERSE) {
+ line_style |= fmt::emphasis::reverse;
+ }
+ if (style.ta_attrs & A_BOLD) {
+ line_style |= fmt::emphasis::bold;
+ }
+ if (style.ta_attrs & A_UNDERLINE) {
+ line_style |= fmt::emphasis::underline;
+ }
+ if (style.ta_fg_color) {
+ auto color_opt = curses_color_to_terminal_color(
+ style.ta_fg_color.value());
+
+ if (color_opt) {
+ fg_style = fmt::fg(color_opt.value());
+ }
+ }
+ if (style.ta_bg_color) {
+ auto color_opt = curses_color_to_terminal_color(
+ style.ta_bg_color.value());
+
+ if (color_opt) {
+ line_style |= fmt::bg(color_opt.value());
+ }
+ }
+ } else if (attr.sa_type == &SA_LEVEL) {
+ auto level = static_cast<log_level_t>(
+ attr.sa_value.get<int64_t>());
+
+ switch (level) {
+ case LEVEL_FATAL:
+ case LEVEL_CRITICAL:
+ case LEVEL_ERROR:
+ line_style |= fmt::fg(fmt::terminal_color::red);
+ break;
+ case LEVEL_WARNING:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ default:
+ break;
+ }
+ } else if (attr.sa_type == &VC_ROLE) {
+ auto saw = string_attr_wrapper<role_t>(&attr);
+ auto role = saw.get();
+
+ switch (role) {
+ case role_t::VCR_TEXT:
+ case role_t::VCR_IDENTIFIER:
+ break;
+ case role_t::VCR_SEARCH:
+ line_style |= fmt::emphasis::reverse;
+ break;
+ case role_t::VCR_ERROR:
+ line_style |= fmt::fg(fmt::terminal_color::red)
+ | fmt::emphasis::bold;
+ break;
+ case role_t::VCR_WARNING:
+ case role_t::VCR_RE_REPEAT:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_COMMENT:
+ line_style |= fmt::fg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_SNIPPET_BORDER:
+ line_style |= fmt::fg(fmt::terminal_color::cyan);
+ break;
+ case role_t::VCR_OK:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_INFO:
+ case role_t::VCR_STATUS:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_KEYWORD:
+ case role_t::VCR_RE_SPECIAL:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::cyan);
+ break;
+ case role_t::VCR_STRING:
+ line_style |= fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_VARIABLE:
+ line_style |= fmt::emphasis::underline;
+ break;
+ case role_t::VCR_SYMBOL:
+ case role_t::VCR_NUMBER:
+ case role_t::VCR_FILE:
+ line_style |= fmt::emphasis::bold;
+ break;
+ case role_t::VCR_H1:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_H2:
+ line_style |= fmt::emphasis::bold;
+ break;
+ case role_t::VCR_H3:
+ case role_t::VCR_H4:
+ case role_t::VCR_H5:
+ case role_t::VCR_H6:
+ line_style |= fmt::emphasis::underline;
+ break;
+ case role_t::VCR_LIST_GLYPH:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_QUOTED_CODE:
+ default_fg_style
+ = fmt::fg(fmt::terminal_color::white);
+ default_bg_style
+ = fmt::bg(fmt::terminal_color::black);
+ break;
+ case role_t::VCR_LOW_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_MED_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_HIGH_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::red);
+ break;
+ default:
+ // log_debug("missing role handler %d", (int) role);
+ break;
+ }
+ }
+ } catch (const fmt::format_error& e) {
+ log_error("style error: %s", e.what());
+ }
+ }
+
+ if (!line_style.has_foreground() && fg_style.has_foreground()) {
+ line_style |= fg_style;
+ }
+ if (!line_style.has_foreground() && default_fg_style.has_foreground()) {
+ line_style |= default_fg_style;
+ }
+ if (!line_style.has_background() && default_bg_style.has_background()) {
+ line_style |= default_bg_style;
+ }
+
+ 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));
+ }
+ last_point = point;
+ }
+ fmt::print(file, "\n");
+}
+
+void
+print(FILE* file, const user_message& um)
+{
+ auto al = um.to_attr_line();
+
+ if (endswith(al.get_string(), "\n")) {
+ al.erase(al.length() - 1);
+ }
+ println(file, al);
+}
+
+user_message
+to_user_message(intern_string_t src, const lnav::pcre2pp::compile_error& ce)
+{
+ attr_line_t pcre_error_content{ce.ce_pattern};
+
+ lnav::snippets::regex_highlighter(pcre_error_content,
+ pcre_error_content.length(),
+ line_range{
+ 0,
+ (int) pcre_error_content.length(),
+ });
+ pcre_error_content.append("\n")
+ .append(ce.ce_offset, ' ')
+ .append(lnav::roles::error("^ "))
+ .append(lnav::roles::error(ce.get_message()))
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ return user_message::error(
+ attr_line_t()
+ .append_quoted(ce.ce_pattern)
+ .append(" is not a valid regular expression"))
+ .with_reason(ce.get_message())
+ .with_snippet(lnav::console::snippet::from(src, pcre_error_content));
+}
+
+} // namespace console
+} // namespace lnav
diff --git a/src/base/lnav.console.hh b/src/base/lnav.console.hh
new file mode 100644
index 0000000..ac4c2b0
--- /dev/null
+++ b/src/base/lnav.console.hh
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2022, 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_console_hh
+#define lnav_console_hh
+
+#include <set>
+#include <vector>
+
+#include "base/attr_line.hh"
+#include "base/file_range.hh"
+
+namespace lnav {
+namespace console {
+
+void println(FILE* file, const attr_line_t& al);
+
+struct snippet {
+ static snippet from(intern_string_t src, const attr_line_t& content)
+ {
+ snippet retval;
+
+ retval.s_location.sl_source = src;
+ retval.s_content = content;
+ return retval;
+ }
+
+ static snippet from(source_location loc, const attr_line_t& content)
+ {
+ snippet retval;
+
+ retval.s_location = loc;
+ retval.s_content = content;
+ return retval;
+ }
+
+ snippet& with_line(int32_t line)
+ {
+ this->s_location.sl_line_number = line;
+ return *this;
+ }
+
+ source_location s_location;
+ attr_line_t s_content;
+};
+
+struct user_message {
+ enum class level {
+ raw,
+ ok,
+ info,
+ warning,
+ error,
+ };
+
+ static user_message raw(const attr_line_t& al);
+
+ static user_message error(const attr_line_t& al);
+
+ static user_message warning(const attr_line_t& al);
+
+ static user_message info(const attr_line_t& al);
+
+ static user_message ok(const attr_line_t& al);
+
+ user_message& with_reason(const attr_line_t& al)
+ {
+ this->um_reason = al;
+ this->um_reason.rtrim();
+ return *this;
+ }
+
+ user_message& with_reason(const user_message& um)
+ {
+ return this->with_reason(um.to_attr_line({}));
+ }
+
+ user_message& with_errno_reason()
+ {
+ this->um_reason = strerror(errno);
+ return *this;
+ }
+
+ user_message& with_snippet(const snippet& sn)
+ {
+ this->um_snippets.emplace_back(sn);
+ return *this;
+ }
+
+ template<typename C>
+ user_message& with_snippets(C snippets)
+ {
+ this->um_snippets.insert(this->um_snippets.end(),
+ 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;
+ }
+
+ user_message& with_note(const attr_line_t& al)
+ {
+ if (!al.blank()) {
+ this->um_notes.emplace_back(al);
+ }
+
+ return *this;
+ }
+
+ user_message& with_help(const attr_line_t& al)
+ {
+ if (al.blank()) {
+ this->um_help.clear();
+ } else {
+ this->um_help = al;
+ this->um_help.rtrim();
+ }
+
+ return *this;
+ }
+
+ enum class render_flags {
+ prefix,
+ };
+
+ attr_line_t to_attr_line(std::set<render_flags> flags
+ = {render_flags::prefix}) const;
+
+ level um_level{level::ok};
+ attr_line_t um_message;
+ std::vector<snippet> um_snippets;
+ attr_line_t um_reason;
+ std::vector<attr_line_t> um_notes;
+ attr_line_t um_help;
+};
+
+void print(FILE* file, const user_message& um);
+
+} // namespace console
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.console.into.hh b/src/base/lnav.console.into.hh
new file mode 100644
index 0000000..206d563
--- /dev/null
+++ b/src/base/lnav.console.into.hh
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2022, 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_console_into_hh
+#define lnav_console_into_hh
+
+#include "intern_string.hh"
+#include "lnav.console.hh"
+
+namespace lnav {
+namespace pcre2pp {
+
+struct compile_error;
+
+}
+
+namespace console {
+
+user_message to_user_message(intern_string_t src,
+ const pcre2pp::compile_error& ce);
+
+}
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.gzip.cc b/src/base/lnav.gzip.cc
new file mode 100644
index 0000000..6b31dad
--- /dev/null
+++ b/src/base/lnav.gzip.cc
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2021, 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 lnav.gzip.cc
+ */
+
+#include "lnav.gzip.hh"
+
+#include <zlib.h>
+
+#include "config.h"
+#include "fmt/format.h"
+
+namespace lnav {
+namespace gzip {
+
+bool
+is_gzipped(const char* buffer, size_t len)
+{
+ return len > 2 && buffer[0] == '\037' && buffer[1] == '\213';
+}
+
+Result<auto_buffer, std::string>
+compress(const void* input, size_t len)
+{
+ auto retval = auto_buffer::alloc(len + 4096);
+
+ z_stream zs;
+ zs.zalloc = Z_NULL;
+ zs.zfree = Z_NULL;
+ zs.opaque = Z_NULL;
+ zs.avail_in = (uInt) len;
+ zs.next_in = (Bytef*) input;
+ zs.avail_out = (uInt) retval.capacity();
+ zs.next_out = (Bytef*) retval.in();
+ zs.total_out = 0;
+
+ auto rc = deflateInit2(
+ &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY);
+ if (rc != Z_OK) {
+ return Err(fmt::format(
+ FMT_STRING("unable to initialize compressor -- {}"), zError(rc)));
+ }
+ rc = deflate(&zs, Z_FINISH);
+ if (rc != Z_STREAM_END) {
+ return Err(fmt::format(FMT_STRING("unable to compress data -- {}"),
+ zError(rc)));
+ }
+ rc = deflateEnd(&zs);
+ if (rc != Z_OK) {
+ return Err(fmt::format(
+ FMT_STRING("unable to finalize compression -- {}"), zError(rc)));
+ }
+ return Ok(std::move(retval.resize(zs.total_out)));
+}
+
+Result<auto_buffer, std::string>
+uncompress(const std::string& src, const void* buffer, size_t size)
+{
+ auto uncomp = auto_buffer::alloc(size * 2);
+ z_stream strm;
+ int err;
+
+ strm.next_in = (Bytef*) buffer;
+ strm.msg = Z_NULL;
+ strm.avail_in = size;
+ strm.total_in = 0;
+ strm.total_out = 0;
+ strm.zalloc = Z_NULL;
+ strm.zfree = Z_NULL;
+
+ if ((err = inflateInit2(&strm, (16 + MAX_WBITS))) != Z_OK) {
+ return Err(fmt::format(FMT_STRING("invalid gzip data: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+
+ bool done = false;
+
+ while (!done) {
+ if (strm.total_out >= uncomp.size()) {
+ uncomp.expand_by(size / 2);
+ }
+
+ strm.next_out = (Bytef*) (uncomp.in() + strm.total_out);
+ strm.avail_out = uncomp.capacity() - strm.total_out;
+
+ // Inflate another chunk.
+ err = inflate(&strm, Z_SYNC_FLUSH);
+ if (err == Z_STREAM_END) {
+ done = true;
+ } else if (err != Z_OK) {
+ inflateEnd(&strm);
+ return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+ }
+
+ if (inflateEnd(&strm) != Z_OK) {
+ return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+
+ return Ok(std::move(uncomp.resize(strm.total_out)));
+}
+
+} // namespace gzip
+} // namespace lnav
diff --git a/src/base/lnav.gzip.hh b/src/base/lnav.gzip.hh
new file mode 100644
index 0000000..bd73965
--- /dev/null
+++ b/src/base/lnav.gzip.hh
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021, 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 lnav.gzip.hh
+ */
+
+#ifndef lnav_gzip_hh
+#define lnav_gzip_hh
+
+#include <string>
+
+#include "auto_mem.hh"
+#include "result.h"
+
+namespace lnav {
+namespace gzip {
+
+bool is_gzipped(const char* buffer, size_t len);
+
+Result<auto_buffer, std::string> compress(const void* input, size_t len);
+
+Result<auto_buffer, std::string> uncompress(const std::string& src,
+ const void* buffer,
+ size_t size);
+
+} // namespace gzip
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.gzip.tests.cc b/src/base/lnav.gzip.tests.cc
new file mode 100644
index 0000000..2f6939e
--- /dev/null
+++ b/src/base/lnav.gzip.tests.cc
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2021, 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 <iostream>
+
+#include <zlib.h>
+
+#include "base/lnav.gzip.hh"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("lnav::gzip::uncompress")
+{
+ {
+ auto u_res = lnav::gzip::uncompress("empty", nullptr, 0);
+
+ CHECK(u_res.isErr());
+ CHECK(u_res.unwrapErr()
+ == "unable to uncompress: empty -- stream error");
+ }
+
+ {
+ auto u_res = lnav::gzip::uncompress("garbage", "abc", 3);
+
+ CHECK(u_res.isErr());
+ CHECK(u_res.unwrapErr()
+ == "unable to uncompress: garbage -- incorrect header check");
+ }
+}
+
+TEST_CASE("lnav::gzip::roundtrip")
+{
+ const char msg[] = "Hello, World!";
+
+ auto c_res = lnav::gzip::compress(msg, sizeof(msg));
+ auto buf = c_res.unwrap();
+ auto u_res = lnav::gzip::uncompress("test", buf.in(), buf.size());
+ auto buf2 = u_res.unwrap();
+
+ CHECK(std::string(msg) == std::string(buf2.in()));
+}
diff --git a/src/base/lnav_log.cc b/src/base/lnav_log.cc
new file mode 100644
index 0000000..2e5dbba
--- /dev/null
+++ b/src/base/lnav_log.cc
@@ -0,0 +1,686 @@
+/**
+ * Copyright (c) 2014, 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 lnav_log.cc
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_EXECINFO_H
+# include <execinfo.h>
+#endif
+#if BACKWARD_HAS_DW == 1
+# include "backward-cpp/backward.hpp"
+#endif
+
+#include <algorithm>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+#define PCRE2_CODE_UNIT_WIDTH 8
+#include <pcre2.h>
+#include <sys/param.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+# include <ncursesw/termcap.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+# include <termcap.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+# include <ncurses/termcap.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+# include <termcap.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+# include <termcap.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+#include "auto_mem.hh"
+#include "enum_util.hh"
+#include "lnav_log.hh"
+#include "opt_util.hh"
+
+static const size_t BUFFER_SIZE = 256 * 1024;
+static const size_t MAX_LOG_LINE_SIZE = 2 * 1024;
+
+static const char* CRASH_MSG
+ = "\n"
+ "\n"
+ "==== GURU MEDITATION ====\n"
+ "Unfortunately, lnav has crashed, sorry for the inconvenience.\n"
+ "\n"
+ "You can help improve lnav by sending the following file "
+ "to " PACKAGE_BUGREPORT
+ " :\n"
+ " %s\n"
+ "=========================\n";
+
+nonstd::optional<FILE*> lnav_log_file;
+lnav_log_level_t lnav_log_level = lnav_log_level_t::DEBUG;
+const char* lnav_log_crash_dir;
+nonstd::optional<const struct termios*> lnav_log_orig_termios;
+// NOTE: This mutex is leaked so that it is not destroyed during exit.
+// Otherwise, any attempts to log will fail.
+static std::mutex*
+lnav_log_mutex()
+{
+ static auto* retval = new std::mutex();
+
+ return retval;
+}
+
+static std::vector<log_state_dumper*>&
+DUMPER_LIST()
+{
+ static auto* retval = new std::vector<log_state_dumper*>();
+
+ return *retval;
+}
+static std::vector<log_crash_recoverer*> CRASH_LIST;
+
+struct thid {
+ static uint32_t COUNTER;
+
+ thid() noexcept : t_id(COUNTER++) {}
+
+ uint32_t t_id;
+};
+
+uint32_t thid::COUNTER = 0;
+
+thread_local thid current_thid;
+thread_local std::string thread_log_prefix;
+
+static struct {
+ size_t lr_length;
+ off_t lr_frag_start;
+ off_t lr_frag_end;
+ char lr_data[BUFFER_SIZE];
+} log_ring = {0, BUFFER_SIZE, 0, {}};
+
+static const char* LEVEL_NAMES[] = {
+ "T",
+ "D",
+ "I",
+ "W",
+ "E",
+};
+
+static char*
+log_alloc()
+{
+ off_t data_end = log_ring.lr_length + MAX_LOG_LINE_SIZE;
+
+ if (data_end >= (off_t) BUFFER_SIZE) {
+ const char* new_start = &log_ring.lr_data[MAX_LOG_LINE_SIZE];
+
+ new_start = (const char*) memchr(
+ new_start, '\n', log_ring.lr_length - MAX_LOG_LINE_SIZE);
+ log_ring.lr_frag_start = new_start - log_ring.lr_data;
+ log_ring.lr_frag_end = log_ring.lr_length;
+ log_ring.lr_length = 0;
+
+ assert(log_ring.lr_frag_start >= 0);
+ assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE);
+ } else if (data_end >= log_ring.lr_frag_start) {
+ const char* new_start = &log_ring.lr_data[log_ring.lr_frag_start];
+
+ new_start = (const char*) memchr(
+ new_start, '\n', log_ring.lr_frag_end - log_ring.lr_frag_start);
+ assert(new_start != nullptr);
+ log_ring.lr_frag_start = new_start - log_ring.lr_data;
+ assert(log_ring.lr_frag_start >= 0);
+ assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE);
+ }
+
+ return &log_ring.lr_data[log_ring.lr_length];
+}
+
+void
+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"));
+ }
+
+ log_info("argv[%d] =", argc);
+ for (int lpc = 0; lpc < argc; lpc++) {
+ log_info(" [%d] = %s", lpc, argv[lpc]);
+ }
+}
+
+void
+log_set_thread_prefix(std::string prefix)
+{
+ // thread_log_prefix = std::move(prefix);
+}
+
+void
+log_host_info()
+{
+ char cwd[MAXPATHLEN];
+ char jittarget[128];
+ struct utsname un;
+ struct rusage ru;
+ uint32_t pcre_jit;
+
+ uname(&un);
+ pcre2_config(PCRE2_CONFIG_JIT, &pcre_jit);
+ pcre2_config(PCRE2_CONFIG_JITTARGET, jittarget);
+
+ log_info("uname:");
+ log_info(" sysname=%s", un.sysname);
+ log_info(" nodename=%s", un.nodename);
+ log_info(" machine=%s", un.machine);
+ log_info(" release=%s", un.release);
+ log_info(" version=%s", un.version);
+ log_info("PCRE:");
+ log_info(" jit=%d", pcre_jit);
+ log_info(" jittarget=%s", jittarget);
+ log_info("Environment:");
+ log_info(" HOME=%s", getenv("HOME"));
+ log_info(" XDG_CONFIG_HOME=%s", getenv("XDG_CONFIG_HOME"));
+ log_info(" LANG=%s", getenv("LANG"));
+ log_info(" PATH=%s", getenv("PATH"));
+ log_info(" TERM=%s", getenv("TERM"));
+ log_info(" TZ=%s", getenv("TZ"));
+ log_info("Process:");
+ log_info(" pid=%d", getpid());
+ log_info(" ppid=%d", getppid());
+ log_info(" pgrp=%d", getpgrp());
+ log_info(" uid=%d", getuid());
+ log_info(" gid=%d", getgid());
+ log_info(" euid=%d", geteuid());
+ log_info(" egid=%d", getegid());
+ if (getcwd(cwd, sizeof(cwd)) == nullptr) {
+ log_info(" ERROR: getcwd failed");
+ } else {
+ log_info(" cwd=%s", cwd);
+ }
+ log_info("Executable:");
+ log_info(" version=%s", VCS_PACKAGE_STRING);
+
+ getrusage(RUSAGE_SELF, &ru);
+ log_rusage(lnav_log_level_t::INFO, ru);
+}
+
+void
+log_rusage_raw(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const struct rusage& ru)
+{
+ log_msg(level, src_file, line_number, "rusage:");
+ log_msg(level,
+ src_file,
+ line_number,
+ " utime=%d.%06d",
+ ru.ru_utime.tv_sec,
+ ru.ru_utime.tv_usec);
+ log_msg(level,
+ src_file,
+ line_number,
+ " stime=%d.%06d",
+ ru.ru_stime.tv_sec,
+ ru.ru_stime.tv_usec);
+ log_msg(level, src_file, line_number, " maxrss=%ld", ru.ru_maxrss);
+ log_msg(level, src_file, line_number, " ixrss=%ld", ru.ru_ixrss);
+ log_msg(level, src_file, line_number, " idrss=%ld", ru.ru_idrss);
+ log_msg(level, src_file, line_number, " isrss=%ld", ru.ru_isrss);
+ log_msg(level, src_file, line_number, " minflt=%ld", ru.ru_minflt);
+ log_msg(level, src_file, line_number, " majflt=%ld", ru.ru_majflt);
+ log_msg(level, src_file, line_number, " nswap=%ld", ru.ru_nswap);
+ log_msg(level, src_file, line_number, " inblock=%ld", ru.ru_inblock);
+ log_msg(level, src_file, line_number, " oublock=%ld", ru.ru_oublock);
+ log_msg(level, src_file, line_number, " msgsnd=%ld", ru.ru_msgsnd);
+ log_msg(level, src_file, line_number, " msgrcv=%ld", ru.ru_msgrcv);
+ log_msg(level, src_file, line_number, " nsignals=%ld", ru.ru_nsignals);
+ log_msg(level, src_file, line_number, " nvcsw=%ld", ru.ru_nvcsw);
+ log_msg(level, src_file, line_number, " nivcsw=%ld", ru.ru_nivcsw);
+}
+
+void
+log_msg(lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const char* fmt,
+ ...)
+{
+ struct timeval curr_time;
+ struct tm localtm;
+ ssize_t prefix_size;
+ va_list args;
+ ssize_t rc;
+
+ if (level < lnav_log_level) {
+ return;
+ }
+
+ std::lock_guard<std::mutex> log_lock(*lnav_log_mutex());
+
+ {
+ // get the base name of the file. NB: can't use basename() since it
+ // can modify its argument
+ const char* last_slash = src_file;
+
+ for (int lpc = 0; src_file[lpc]; lpc++) {
+ if (src_file[lpc] == '/' || src_file[lpc] == '\\') {
+ last_slash = &src_file[lpc + 1];
+ }
+ }
+
+ src_file = last_slash;
+ }
+
+ va_start(args, fmt);
+ gettimeofday(&curr_time, nullptr);
+ localtime_r(&curr_time.tv_sec, &localtm);
+ auto line = log_alloc();
+ prefix_size = snprintf(line,
+ MAX_LOG_LINE_SIZE,
+ "%4d-%02d-%02dT%02d:%02d:%02d.%03d %s t%u %s:%d ",
+ localtm.tm_year + 1900,
+ localtm.tm_mon + 1,
+ localtm.tm_mday,
+ localtm.tm_hour,
+ localtm.tm_min,
+ localtm.tm_sec,
+ (int) (curr_time.tv_usec / 1000),
+ LEVEL_NAMES[lnav::enums::to_underlying(level)],
+ current_thid.t_id,
+ src_file,
+ line_number);
+#if 0
+ if (!thread_log_prefix.empty()) {
+ prefix_size += snprintf(
+ &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size,
+ "%s ",
+ thread_log_prefix.c_str());
+ }
+#endif
+ rc = vsnprintf(
+ &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size, fmt, args);
+ if (rc >= (ssize_t) (MAX_LOG_LINE_SIZE - prefix_size)) {
+ rc = MAX_LOG_LINE_SIZE - prefix_size - 1;
+ }
+ line[prefix_size + rc] = '\n';
+ log_ring.lr_length += prefix_size + rc + 1;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, prefix_size + rc + 1, file);
+ fflush(file);
+ };
+ va_end(args);
+}
+
+void
+log_msg_extra(const char* fmt, ...)
+{
+ std::lock_guard<std::mutex> mg(*lnav_log_mutex());
+ va_list args;
+
+ va_start(args, fmt);
+ auto line = log_alloc();
+ auto rc = vsnprintf(line, MAX_LOG_LINE_SIZE - 1, fmt, args);
+ log_ring.lr_length += rc;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, rc, file);
+ fflush(file);
+ };
+ va_end(args);
+}
+
+void
+log_msg_extra_complete()
+{
+ std::lock_guard<std::mutex> mg(*lnav_log_mutex());
+ auto line = log_alloc();
+ line[0] = '\n';
+ log_ring.lr_length += 1;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, 1, file);
+ fflush(file);
+ };
+}
+
+void
+log_backtrace(lnav_log_level_t level)
+{
+#ifdef HAVE_EXECINFO_H
+ int frame_count;
+ void* frames[128];
+
+ frame_count = backtrace(frames, 128);
+ auto bt = backtrace_symbols(frames, frame_count);
+ for (int lpc = 0; lpc < frame_count; lpc++) {
+ log_msg(level, __FILE__, __LINE__, "%s", bt[lpc]);
+ }
+#endif
+}
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-result"
+static void
+sigabrt(int sig, siginfo_t* info, void* ctx)
+{
+ char crash_path[1024], latest_crash_path[1024];
+ int fd;
+#ifdef HAVE_EXECINFO_H
+ int frame_count;
+ void* frames[128];
+#endif
+ struct tm localtm;
+ time_t curr_time;
+
+ if (lnav_log_crash_dir == nullptr) {
+ printf("%*s", (int) log_ring.lr_length, log_ring.lr_data);
+ return;
+ }
+
+ log_error("Received signal: %d", sig);
+
+#ifdef HAVE_EXECINFO_H
+ frame_count = backtrace(frames, 128);
+#endif
+ curr_time = time(nullptr);
+ localtime_r(&curr_time, &localtm);
+ snprintf(crash_path,
+ sizeof(crash_path),
+ "%s/crash-%4d-%02d-%02d-%02d-%02d-%02d.%d.log",
+ lnav_log_crash_dir,
+ localtm.tm_year + 1900,
+ localtm.tm_mon + 1,
+ localtm.tm_mday,
+ localtm.tm_hour,
+ localtm.tm_min,
+ localtm.tm_sec,
+ getpid());
+ snprintf(latest_crash_path,
+ sizeof(latest_crash_path),
+ "%s/latest-crash.log",
+ lnav_log_crash_dir);
+ if ((fd = open(crash_path, O_CREAT | O_TRUNC | O_RDWR, 0600)) != -1) {
+ if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) {
+ (void) write(fd,
+ &log_ring.lr_data[log_ring.lr_frag_start],
+ log_ring.lr_frag_end - log_ring.lr_frag_start);
+ }
+ (void) write(fd, log_ring.lr_data, log_ring.lr_length);
+#ifdef HAVE_EXECINFO_H
+ backtrace_symbols_fd(frames, frame_count, fd);
+#endif
+#if BACKWARD_HAS_DW == 1
+ {
+ ucontext_t* uctx = static_cast<ucontext_t*>(ctx);
+ void* error_addr = nullptr;
+
+# ifdef REG_RIP // x86_64
+ error_addr
+ = reinterpret_cast<void*>(uctx->uc_mcontext.gregs[REG_RIP]);
+# elif defined(REG_EIP) // x86_32
+ error_addr
+ = reinterpret_cast<void*>(uctx->uc_mcontext.gregs[REG_EIP]);
+# endif
+
+ backward::StackTrace st;
+
+ if (error_addr) {
+ st.load_from(error_addr,
+ 32,
+ reinterpret_cast<void*>(uctx),
+ info->si_addr);
+ } else {
+ st.load_here(32, reinterpret_cast<void*>(uctx), info->si_addr);
+ }
+ backward::TraceResolver tr;
+
+ tr.load_stacktrace(st);
+ for (size_t lpc = 0; lpc < st.size(); lpc++) {
+ auto trace = tr.resolve(st[lpc]);
+ char buf[1024];
+
+ snprintf(buf,
+ sizeof(buf),
+ "Frame %lu:%s:%s (%s:%d)\n",
+ lpc,
+ trace.object_filename.c_str(),
+ trace.object_function.c_str(),
+ trace.source.filename.c_str(),
+ trace.source.line);
+ write(fd, buf, strlen(buf));
+ }
+ }
+#endif
+ log_ring.lr_length = 0;
+ log_ring.lr_frag_start = BUFFER_SIZE;
+ log_ring.lr_frag_end = 0;
+
+ log_host_info();
+
+ for (auto lsd : DUMPER_LIST()) {
+ lsd->log_state();
+ }
+
+ if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) {
+ write(fd,
+ &log_ring.lr_data[log_ring.lr_frag_start],
+ log_ring.lr_frag_end - log_ring.lr_frag_start);
+ }
+ write(fd, log_ring.lr_data, log_ring.lr_length);
+ if (getenv("DUMP_CRASH") != nullptr) {
+ char buffer[1024];
+ int rc;
+
+ lseek(fd, 0, SEEK_SET);
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ write(STDERR_FILENO, buffer, rc);
+ }
+ }
+ close(fd);
+
+ remove(latest_crash_path);
+ symlink(crash_path, latest_crash_path);
+ }
+
+ lnav_log_orig_termios | [](auto termios) {
+ for (auto lcr : CRASH_LIST) {
+ lcr->log_crash_recover();
+ }
+
+ tcsetattr(STDOUT_FILENO, TCSAFLUSH, termios);
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+ };
+ fprintf(stderr, CRASH_MSG, crash_path);
+
+#ifndef ATTACH_ON_SIGNAL
+ if (isatty(STDIN_FILENO)) {
+ char response;
+
+ fprintf(stderr, "\nWould you like to attach a debugger? (y/N) ");
+ fflush(stderr);
+
+ if (scanf("%c", &response) > 0 && tolower(response) == 'y') {
+ pid_t lnav_pid = getpid();
+ pid_t child_pid;
+
+ switch ((child_pid = fork())) {
+ case 0: {
+ char pid_str[32];
+
+ snprintf(pid_str, sizeof(pid_str), "--pid=%d", lnav_pid);
+ execlp("gdb", "gdb", pid_str, nullptr);
+
+ snprintf(pid_str, sizeof(pid_str), "%d", lnav_pid);
+ execlp("lldb", "lldb", "--attach-pid", pid_str, nullptr);
+
+ fprintf(stderr, "Could not attach gdb or lldb, exiting.\n");
+ _exit(1);
+ break;
+ }
+
+ case -1: {
+ break;
+ }
+
+ default: {
+ int status;
+
+ while (wait(&status) < 0) {
+ }
+ break;
+ }
+ }
+ }
+ }
+#endif
+
+ _exit(1);
+}
+#pragma GCC diagnostic pop
+
+void
+log_install_handlers()
+{
+ const size_t stack_size = 8 * 1024 * 1024;
+ const int sigs[] = {
+ SIGABRT,
+ SIGSEGV,
+ SIGBUS,
+ SIGILL,
+ SIGFPE,
+ };
+ static auto_mem<void> stack_content;
+
+ stack_t ss;
+
+ stack_content = malloc(stack_size);
+ ss.ss_sp = stack_content;
+ ss.ss_size = stack_size;
+ ss.ss_flags = 0;
+ sigaltstack(&ss, nullptr);
+ for (const auto sig : sigs) {
+ struct sigaction sa;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER | SA_RESETHAND;
+ sigfillset(&sa.sa_mask);
+ sigdelset(&sa.sa_mask, sig);
+ sa.sa_sigaction = sigabrt;
+
+ sigaction(sig, &sa, nullptr);
+ }
+}
+
+void
+log_abort()
+{
+ raise(SIGABRT);
+ _exit(1);
+}
+
+void
+log_pipe_err(int fd)
+{
+ std::thread reader([fd]() {
+ char buffer[1024];
+ bool done = false;
+
+ while (!done) {
+ int rc = read(fd, buffer, sizeof(buffer));
+
+ switch (rc) {
+ case -1:
+ case 0:
+ done = true;
+ break;
+ default:
+ while (buffer[rc - 1] == '\n' || buffer[rc - 1] == '\r') {
+ rc -= 1;
+ }
+
+ log_error("%.*s", rc, buffer);
+ break;
+ }
+ }
+
+ close(fd);
+ });
+
+ reader.detach();
+}
+
+log_state_dumper::log_state_dumper()
+{
+ DUMPER_LIST().push_back(this);
+}
+
+log_state_dumper::~log_state_dumper()
+{
+ auto iter = std::find(DUMPER_LIST().begin(), DUMPER_LIST().end(), this);
+ if (iter != DUMPER_LIST().end()) {
+ DUMPER_LIST().erase(iter);
+ }
+}
+
+log_crash_recoverer::log_crash_recoverer()
+{
+ CRASH_LIST.push_back(this);
+}
+
+log_crash_recoverer::~log_crash_recoverer()
+{
+ auto iter = std::find(CRASH_LIST.begin(), CRASH_LIST.end(), this);
+
+ if (iter != CRASH_LIST.end()) {
+ CRASH_LIST.erase(iter);
+ }
+}
diff --git a/src/base/lnav_log.hh b/src/base/lnav_log.hh
new file mode 100644
index 0000000..551b4f8
--- /dev/null
+++ b/src/base/lnav_log.hh
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2014, 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 lnav_log.hh
+ */
+
+#ifndef lnav_log_hh
+#define lnav_log_hh
+
+#include <cstdint>
+#include <string>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#ifndef lnav_dead2
+# define lnav_dead2 __attribute__((noreturn))
+#endif
+
+#include "optional.hpp"
+
+struct termios;
+
+enum class lnav_log_level_t : uint32_t {
+ TRACE,
+ DEBUG,
+ INFO,
+ WARNING,
+ ERROR,
+};
+
+void log_argv(int argc, char* argv[]);
+void log_host_info();
+void log_rusage_raw(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const struct rusage& ru);
+void log_msg(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const char* fmt,
+ ...);
+void log_msg_extra(const char* fmt, ...);
+void log_msg_extra_complete();
+void log_install_handlers();
+void log_abort() lnav_dead2;
+void log_pipe_err(int fd);
+void log_set_thread_prefix(std::string prefix);
+void log_backtrace(lnav_log_level_t level);
+
+struct log_state_dumper {
+public:
+ log_state_dumper();
+
+ virtual ~log_state_dumper();
+
+ virtual void log_state(){
+
+ };
+
+ log_state_dumper(const log_state_dumper&) = delete;
+ log_state_dumper& operator=(const log_state_dumper&) = delete;
+};
+
+struct log_crash_recoverer {
+public:
+ log_crash_recoverer();
+
+ virtual ~log_crash_recoverer();
+
+ virtual void log_crash_recover() = 0;
+};
+
+extern nonstd::optional<FILE*> lnav_log_file;
+extern const char* lnav_log_crash_dir;
+extern nonstd::optional<const struct termios*> lnav_log_orig_termios;
+extern enum lnav_log_level_t lnav_log_level;
+
+#define log_msg_wrapper(level, fmt...) \
+ do { \
+ if (lnav_log_level <= level) { \
+ log_msg(level, __FILE__, __LINE__, fmt); \
+ } \
+ } while (false)
+
+#define log_rusage(level, ru) log_rusage_raw(level, __FILE__, __LINE__, ru);
+
+#define log_error(fmt...) log_msg_wrapper(lnav_log_level_t::ERROR, fmt);
+
+#define log_warning(fmt...) log_msg_wrapper(lnav_log_level_t::WARNING, fmt);
+
+#define log_info(fmt...) log_msg_wrapper(lnav_log_level_t::INFO, fmt);
+
+#define log_debug(fmt...) log_msg_wrapper(lnav_log_level_t::DEBUG, fmt);
+
+#define log_trace(fmt...) log_msg_wrapper(lnav_log_level_t::TRACE, fmt);
+
+#define require(e) ((void) ((e) ? 0 : lnav_require(#e, __FILE__, __LINE__)))
+#define lnav_require(e, file, line) \
+ (log_msg( \
+ lnav_log_level_t::ERROR, file, line, "failed precondition `%s'", e), \
+ log_abort(), \
+ 1)
+
+#define require_true(lhs) \
+ ((void) ((lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__)))
+#define require_false(lhs) \
+ ((void) ((!lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__)))
+#define lnav_require_unary(e, lhs, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "failed precondition `%s' (lhs=%s)", \
+ e, \
+ std::to_string(lhs).c_str()), \
+ log_abort(), \
+ 1)
+
+#define require_ge(lhs, rhs) \
+ ((void) ((lhs >= rhs) \
+ ? 0 \
+ : lnav_require_binary( \
+ #lhs " >= " #rhs, lhs, rhs, __FILE__, __LINE__)))
+#define require_gt(lhs, rhs) \
+ ((void) ((lhs > rhs) ? 0 \
+ : lnav_require_binary( \
+ #lhs " > " #rhs, lhs, rhs, __FILE__, __LINE__)))
+#define require_lt(lhs, rhs) \
+ ((void) ((lhs < rhs) ? 0 \
+ : lnav_require_binary( \
+ #lhs " < " #rhs, lhs, rhs, __FILE__, __LINE__)))
+
+#define lnav_require_binary(e, lhs, rhs, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "failed precondition `%s' (lhs=%s; rhs=%s)", \
+ e, \
+ std::to_string(lhs).c_str(), \
+ std::to_string(rhs).c_str()), \
+ log_abort(), \
+ 1)
+
+#define ensure(e) ((void) ((e) ? 0 : lnav_ensure(#e, __FILE__, __LINE__)))
+#define lnav_ensure(e, file, line) \
+ (log_msg( \
+ lnav_log_level_t::ERROR, file, line, "failed postcondition `%s'", e), \
+ log_abort(), \
+ 1)
+
+#define log_perror(e) \
+ ((void) ((e != -1) ? 0 : lnav_log_perror(#e, __FILE__, __LINE__)))
+#define lnav_log_perror(e, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "syscall failed `%s' -- %s", \
+ e, \
+ strerror(errno)), \
+ 1)
+
+#endif
diff --git a/src/base/log_level_enum.hh b/src/base/log_level_enum.hh
new file mode 100644
index 0000000..983fd5c
--- /dev/null
+++ b/src/base/log_level_enum.hh
@@ -0,0 +1,65 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_log_level_enum_hh
+#define lnav_log_level_enum_hh
+
+/**
+ * The logging level identifiers for a line(s).
+ */
+enum log_level_t : int {
+ LEVEL_UNKNOWN,
+ LEVEL_TRACE,
+ LEVEL_DEBUG5,
+ LEVEL_DEBUG4,
+ LEVEL_DEBUG3,
+ LEVEL_DEBUG2,
+ LEVEL_DEBUG,
+ LEVEL_INFO,
+ LEVEL_STATS,
+ LEVEL_NOTICE,
+ LEVEL_WARNING,
+ LEVEL_ERROR,
+ LEVEL_CRITICAL,
+ LEVEL_FATAL,
+ LEVEL_INVALID,
+
+ LEVEL__MAX,
+
+ LEVEL_IGNORE = 0x10, /*< Ignore */
+ LEVEL_TIME_SKEW = 0x20, /*< Received after timestamp. */
+ LEVEL_MARK = 0x40, /*< Bookmarked line. */
+ LEVEL_CONTINUED = 0x80, /*< Continuation of multiline entry. */
+
+ /** Mask of flags for the level field. */
+ LEVEL__FLAGS
+ = (LEVEL_IGNORE | LEVEL_TIME_SKEW | LEVEL_MARK | LEVEL_CONTINUED)
+};
+
+#endif
diff --git a/src/base/lrucache.hpp b/src/base/lrucache.hpp
new file mode 100644
index 0000000..8bcbad6
--- /dev/null
+++ b/src/base/lrucache.hpp
@@ -0,0 +1,83 @@
+/*
+ * File: lrucache.hpp
+ * Author: Alexander Ponomarev
+ *
+ * Created on June 20, 2013, 5:09 PM
+ */
+
+#ifndef _LRUCACHE_HPP_INCLUDED_
+#define _LRUCACHE_HPP_INCLUDED_
+
+#include <map>
+#include <list>
+#include <cstddef>
+#include <stdexcept>
+
+#include "optional.hpp"
+
+namespace cache {
+
+template<typename key_t, typename value_t>
+class lru_cache {
+public:
+ typedef typename std::pair<key_t, value_t> key_value_pair_t;
+ typedef typename std::list<key_value_pair_t>::iterator list_iterator_t;
+
+ lru_cache(size_t max_size) :
+ _max_size(max_size) {
+ }
+
+ void put(const key_t& key, const value_t& value) {
+ auto it = _cache_items_map.find(key);
+ _cache_items_list.push_front(key_value_pair_t(key, value));
+ if (it != _cache_items_map.end()) {
+ _cache_items_list.erase(it->second);
+ _cache_items_map.erase(it);
+ }
+ _cache_items_map[key] = _cache_items_list.begin();
+
+ if (_cache_items_map.size() > _max_size) {
+ auto last = _cache_items_list.end();
+ last--;
+ _cache_items_map.erase(last->first);
+ _cache_items_list.pop_back();
+ }
+ }
+
+ nonstd::optional<value_t> get(const key_t& key) {
+ auto it = _cache_items_map.find(key);
+ if (it == _cache_items_map.end()) {
+ return nonstd::nullopt;
+ }
+
+ _cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second);
+ return it->second->second;
+ }
+
+ bool exists(const key_t& key) const {
+ return _cache_items_map.find(key) != _cache_items_map.end();
+ }
+
+ size_t size() const {
+ return _cache_items_map.size();
+ }
+
+ void set_max_size(size_t max_size) {
+ this->_max_size = max_size;
+ }
+
+ void clear() {
+ this->_cache_items_map.clear();
+ this->_cache_items_list.clear();
+ }
+
+private:
+ std::list<key_value_pair_t> _cache_items_list;
+ std::map<key_t, list_iterator_t> _cache_items_map;
+ size_t _max_size;
+};
+
+} // namespace cache
+
+#endif /* _LRUCACHE_HPP_INCLUDED_ */
+
diff --git a/src/base/math_util.hh b/src/base/math_util.hh
new file mode 100644
index 0000000..842b319
--- /dev/null
+++ b/src/base/math_util.hh
@@ -0,0 +1,73 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_math_util_hh
+#define lnav_math_util_hh
+
+#include <sys/types.h>
+
+#undef rounddown
+
+/**
+ * Round down a number based on a given granularity.
+ *
+ * @param
+ * @param step The granularity.
+ */
+template<typename Size, typename Step>
+inline int
+rounddown(Size size, Step step)
+{
+ return size - (size % step);
+}
+
+inline int
+rounddown_offset(size_t size, int step, int offset)
+{
+ return size - ((size - offset) % step);
+}
+
+inline size_t
+roundup_size(size_t size, int step)
+{
+ size_t retval = size + step;
+
+ retval -= (retval % step);
+
+ return retval;
+}
+
+template<typename T>
+T
+abs_diff(T a, T b)
+{
+ return a > b ? a - b : b - a;
+}
+
+#endif
diff --git a/src/base/network.tcp.cc b/src/base/network.tcp.cc
new file mode 100644
index 0000000..0194b6f
--- /dev/null
+++ b/src/base/network.tcp.cc
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2021, 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 "network.tcp.hh"
+
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include "auto_mem.hh"
+#include "config.h"
+#include "fmt/format.h"
+
+namespace network {
+namespace tcp {
+
+Result<auto_fd, std::string>
+connect(const char* hostname, const char* servname)
+{
+ struct addrinfo hints;
+ auto_mem<addrinfo> ai(freeaddrinfo);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ auto rc = getaddrinfo(hostname, servname, &hints, ai.out());
+
+ if (rc != 0) {
+ return Err(fmt::format(FMT_STRING("unable to resolve {}:{} -- {}"),
+ hostname,
+ servname,
+ gai_strerror(rc)));
+ }
+
+ auto retval = auto_fd(socket(ai->ai_family, ai->ai_socktype, 0));
+
+ rc = ::connect(retval, ai->ai_addr, ai->ai_addrlen);
+ if (rc != 0) {
+ return Err(fmt::format(FMT_STRING("unable to connect to {}:{} -- {}"),
+ hostname,
+ servname,
+ strerror(rc)));
+ }
+
+ return Ok(std::move(retval));
+}
+
+} // namespace tcp
+} // namespace network
diff --git a/src/base/network.tcp.hh b/src/base/network.tcp.hh
new file mode 100644
index 0000000..49fc392
--- /dev/null
+++ b/src/base/network.tcp.hh
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2021, 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_network_tcp_hh
+#define lnav_network_tcp_hh
+
+#include <string>
+
+#include "auto_fd.hh"
+#include "result.h"
+
+namespace network {
+
+struct locality {
+ locality(nonstd::optional<std::string> username,
+ std::string hostname,
+ nonstd::optional<std::string> service)
+ : l_username(std::move(username)), l_hostname(std::move(hostname)),
+ l_service(std::move(service))
+ {
+ }
+
+ nonstd::optional<std::string> l_username;
+ std::string l_hostname;
+ nonstd::optional<std::string> l_service;
+};
+
+struct path {
+ locality p_locality;
+ std::string p_path;
+
+ path(locality loc, std::string path)
+ : p_locality(std::move(loc)), p_path(std::move(path))
+ {
+ }
+
+ path home() const
+ {
+ return {
+ this->p_locality,
+ ".",
+ };
+ }
+};
+
+namespace tcp {
+
+Result<auto_fd, std::string> connect(const char* hostname,
+ const char* servname);
+
+}
+} // namespace network
+
+#endif
diff --git a/src/base/network.tcp.tests.cc b/src/base/network.tcp.tests.cc
new file mode 100644
index 0000000..76846ca
--- /dev/null
+++ b/src/base/network.tcp.tests.cc
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2021, 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 <iostream>
+
+#include "config.h"
+#include "doctest/doctest.h"
+#include "network.tcp.hh"
+
+TEST_CASE("bad hostname")
+{
+ auto connect_res = network::tcp::connect("foobar.bazzer", "http");
+ CHECK(connect_res.unwrapErr() ==
+ "unable to resolve foobar.bazzer:http -- nodename nor servname "
+ "provided, or not known");
+}
+
+TEST_CASE("bad servname")
+{
+ auto connect_res = network::tcp::connect("www.cnn.com", "non-existent");
+ CHECK(connect_res.unwrapErr() ==
+ "unable to resolve www.cnn.com:non-existent -- nodename nor "
+ "servname provided, or not known");
+}
diff --git a/src/base/opt_util.hh b/src/base/opt_util.hh
new file mode 100644
index 0000000..aea038e
--- /dev/null
+++ b/src/base/opt_util.hh
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2019, 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_opt_util_hh
+#define lnav_opt_util_hh
+
+#include <stdlib.h>
+
+#include "optional.hpp"
+
+namespace detail {
+
+template<class T>
+typename std::enable_if<std::is_void<T>::value, T>::type
+void_or_nullopt()
+{
+ return;
+}
+
+template<class T>
+typename std::enable_if<not std::is_void<T>::value, T>::type
+void_or_nullopt()
+{
+ return nonstd::nullopt;
+}
+
+template<class T>
+struct is_optional : std::false_type {
+};
+
+template<class T>
+struct is_optional<nonstd::optional<T>> : std::true_type {
+};
+} // namespace detail
+
+template<class T,
+ class F,
+ std::enable_if_t<detail::is_optional<std::decay_t<T>>::value, int> = 0>
+auto
+operator|(T&& t, F f)
+ -> decltype(detail::void_or_nullopt<decltype(f(std::forward<T>(t).
+ operator*()))>())
+{
+ using return_type = decltype(f(std::forward<T>(t).operator*()));
+ if (t)
+ return f(std::forward<T>(t).operator*());
+ else
+ return detail::void_or_nullopt<return_type>();
+}
+
+template<class T>
+optional_constexpr nonstd::optional<typename std::decay<T>::type>
+make_optional_from_nullable(T&& v)
+{
+ if (v != nullptr) {
+ return nonstd::optional<typename std::decay<T>::type>(
+ std::forward<T>(v));
+ }
+ return nonstd::nullopt;
+}
+
+template<template<typename, typename...> class C, typename T>
+nonstd::optional<T>
+cget(const C<T>& container, size_t index)
+{
+ if (index < container.size()) {
+ return container[index];
+ }
+
+ return nonstd::nullopt;
+}
+
+inline nonstd::optional<const char*>
+getenv_opt(const char* name)
+{
+ return make_optional_from_nullable(getenv(name));
+}
+
+#endif
diff --git a/src/base/paths.cc b/src/base/paths.cc
new file mode 100644
index 0000000..ca53a07
--- /dev/null
+++ b/src/base/paths.cc
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2021, 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 "config.h"
+
+#ifdef __CYGWIN__
+# include <iostream>
+# include <sstream>
+#endif
+
+#include "fmt/format.h"
+#include "paths.hh"
+
+namespace lnav {
+namespace paths {
+
+#ifdef __CYGWIN__
+char*
+windows_to_unix_file_path(char* input)
+{
+ if (input == nullptr) {
+ return nullptr;
+ }
+ std::string file_path;
+ file_path.assign(input);
+
+ // Replace the slashes
+ std::replace(file_path.begin(),
+ file_path.end(),
+ WINDOWS_FILE_PATH_SEPARATOR,
+ UNIX_FILE_PATH_SEPARATOR);
+
+ // Convert the drive letter to lowercase
+ std::transform(
+ file_path.begin(),
+ file_path.begin() + 1,
+ file_path.begin(),
+ [](unsigned char character) { return std::tolower(character); });
+
+ // Remove the colon
+ const auto drive_letter = file_path.substr(0, 1);
+ const auto remaining_path = file_path.substr(2, file_path.size() - 2);
+ file_path = drive_letter + remaining_path;
+
+ std::stringstream stringstream;
+ stringstream << "/cygdrive/";
+ stringstream << file_path;
+
+ return const_cast<char*>(stringstream.str().c_str());
+}
+#endif
+
+ghc::filesystem::path
+dotlnav()
+{
+#ifdef __CYGWIN__
+ auto home_env = windows_to_unix_file_path(getenv("APPDATA"));
+#else
+ auto home_env = getenv("HOME");
+#endif
+ auto xdg_config_home = getenv("XDG_CONFIG_HOME");
+
+ if (home_env != nullptr) {
+ auto home_path = ghc::filesystem::path(home_env);
+
+ if (ghc::filesystem::is_directory(home_path)) {
+ auto home_lnav = home_path / ".lnav";
+
+ if (ghc::filesystem::is_directory(home_lnav)) {
+ return home_lnav;
+ }
+
+ if (xdg_config_home != nullptr) {
+ auto xdg_path = ghc::filesystem::path(xdg_config_home);
+
+ if (ghc::filesystem::is_directory(xdg_path)) {
+ return xdg_path / "lnav";
+ }
+ }
+
+ auto home_config = home_path / ".config";
+
+ if (ghc::filesystem::is_directory(home_config)) {
+ return home_config / "lnav";
+ }
+
+ return home_lnav;
+ }
+ }
+
+ return ghc::filesystem::current_path();
+}
+
+ghc::filesystem::path
+workdir()
+{
+ auto subdir_name = fmt::format(FMT_STRING("lnav-user-{}-work"), getuid());
+ auto tmp_path = ghc::filesystem::temp_directory_path();
+
+ return tmp_path / ghc::filesystem::path(subdir_name);
+}
+
+} // namespace paths
+} // namespace lnav
diff --git a/src/base/paths.hh b/src/base/paths.hh
new file mode 100644
index 0000000..6c43a2a
--- /dev/null
+++ b/src/base/paths.hh
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2021, 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_paths_hh
+#define lnav_paths_hh
+
+#include "ghc/filesystem.hpp"
+
+namespace lnav {
+namespace paths {
+
+#ifdef __CYGWIN__
+static const char WINDOWS_FILE_PATH_SEPARATOR = '\\';
+static const char UNIX_FILE_PATH_SEPARATOR = '/';
+
+char* windows_to_unix_file_path(char* input);
+#endif
+
+/**
+ * Compute the path to a file in the user's '.lnav' directory.
+ *
+ * @param sub The path to the file in the '.lnav' directory.
+ * @return The full path
+ */
+ghc::filesystem::path dotlnav();
+
+ghc::filesystem::path workdir();
+
+} // namespace paths
+} // namespace lnav
+
+#endif
diff --git a/src/base/result.h b/src/base/result.h
new file mode 100644
index 0000000..e3e8ac5
--- /dev/null
+++ b/src/base/result.h
@@ -0,0 +1,1032 @@
+/*
+ Mathieu Stefani, 03 mai 2016
+
+ This header provides a Result type that can be used to replace exceptions in
+ code that has to handle error.
+
+ Result<T, E> can be used to return and propagate an error to the caller.
+ Result<T, E> is an algebraic data type that can either Ok(T) to represent
+ success or Err(E) to represent an error.
+*/
+
+#pragma once
+
+#include <exception>
+#include <functional>
+#include <type_traits>
+
+#include <stdio.h>
+
+namespace types {
+template<typename T>
+struct Ok {
+ Ok(const T& val) : val(val) {}
+ Ok(T&& val) : val(std::move(val)) {}
+
+ T val;
+};
+
+template<>
+struct Ok<void> {};
+
+template<typename E>
+struct Err {
+ Err(const E& val) : val(val) {}
+ Err(E&& val) : val(std::move(val)) {}
+
+ E val;
+};
+
+template<>
+struct Err<void> {};
+}; // namespace types
+
+template<typename T, typename CleanT = typename std::decay<T>::type>
+types::Ok<CleanT>
+Ok(T&& val)
+{
+ return types::Ok<CleanT>(std::forward<T>(val));
+}
+
+inline types::Ok<void>
+Ok()
+{
+ return {};
+}
+
+template<typename E, typename CleanE = typename std::decay<E>::type>
+types::Err<CleanE>
+Err(E&& val)
+{
+ return types::Err<CleanE>(std::forward<E>(val));
+}
+
+inline types::Err<void>
+Err()
+{
+ return {};
+}
+
+template<typename T, typename E>
+struct Result;
+
+namespace details {
+
+template<typename...>
+struct void_t {
+ typedef void type;
+};
+
+namespace impl {
+template<typename Func>
+struct result_of;
+
+template<typename Ret, typename Cls, typename... Args>
+struct result_of<Ret (Cls::*)(Args...)> : public result_of<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct result_of<std::function<Ret(Args...)>> {
+ typedef Ret type;
+};
+} // namespace impl
+
+template<typename Func>
+struct result_of : public impl::result_of<decltype(&Func::operator())> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct result_of<Ret (Cls::*)(Args...) const> {
+ typedef Ret type;
+};
+
+template<typename Ret, typename... Args>
+struct result_of<Ret (*)(Args...)> {
+ typedef Ret type;
+};
+
+template<typename R>
+struct ResultOkType {
+ typedef typename std::decay<R>::type type;
+};
+
+template<typename T, typename E>
+struct ResultOkType<Result<T, E>> {
+ typedef T type;
+};
+
+template<typename R>
+struct ResultErrType {
+ typedef R type;
+};
+
+template<typename T, typename E>
+struct ResultErrType<Result<T, E>> {
+ typedef typename std::remove_reference<E>::type type;
+};
+
+template<typename R>
+struct IsResult : public std::false_type {};
+template<typename T, typename E>
+struct IsResult<Result<T, E>> : public std::true_type {};
+
+namespace ok {
+
+namespace impl {
+
+template<typename T>
+struct Map;
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg) const> : public Map<Ret(Arg)> {};
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg)> : public Map<Ret(Arg)> {};
+
+// General implementation
+template<typename Ret, typename Arg>
+struct Map<Ret(Arg)> {
+ static_assert(
+ !IsResult<Ret>::value,
+ "Can not map a callback returning a Result, use then instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<Ret, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<T, Arg>::value || std::is_convertible<T, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isOk()) {
+ auto res = func(result.storage().template get<T>());
+ return types::Ok<Ret>(std::move(res));
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for callback returning void
+template<typename Arg>
+struct Map<void(Arg)> {
+ template<typename T, typename E, typename Func>
+ static Result<void, E> map(const Result<T, E>& result, Func func)
+ {
+ if (result.isOk()) {
+ func(result.storage().template get<T>());
+ return types::Ok<void>();
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for a void Result
+template<typename Ret>
+struct Map<Ret(void)> {
+ template<typename T, typename E, typename Func>
+ static Result<Ret, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not map a void callback on a non-void Result");
+
+ if (result.isOk()) {
+ auto ret = func();
+ return types::Ok<Ret>(std::move(ret));
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for callback returning void on a void Result
+template<>
+struct Map<void(void)> {
+ template<typename T, typename E, typename Func>
+ static Result<void, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not map a void callback on a non-void Result");
+
+ if (result.isOk()) {
+ func();
+ return types::Ok<void>();
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// General specialization for a callback returning a Result
+template<typename U, typename E, typename Arg>
+struct Map<Result<U, E>(Arg)> {
+ template<typename T, typename Func>
+ static Result<U, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<T, Arg>::value || std::is_convertible<T, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isOk()) {
+ auto res = func(result.storage().template get<T>());
+ return res;
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for a void callback returning a Result
+template<typename U, typename E>
+struct Map<Result<U, E>(void)> {
+ template<typename T, typename Func>
+ static Result<U, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isOk()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Map;
+
+template<typename Ret, typename... Args>
+struct Map<Ret (*)(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Map<Ret(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Map<Ret (Cls::*)(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Map<Ret (Cls::*)(Args...) const> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Map<std::function<Ret(Args...)>> : public impl::Map<Ret(Args...)> {};
+
+} // namespace ok
+
+namespace err {
+
+namespace impl {
+
+template<typename T>
+struct Map;
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg) const> {
+ static_assert(
+ !IsResult<Ret>::value,
+ "Can not map a callback returning a Result, use orElse instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, Ret> map(const Result<T, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return types::Err<Ret>(res);
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, Ret> map(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return types::Err<Ret>(res);
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Map : public impl::Map<decltype(&Func::operator())> {};
+
+} // namespace err
+
+namespace And {
+
+namespace impl {
+
+template<typename Func>
+struct Then;
+
+template<typename Ret, typename... Args>
+struct Then<Ret (*)(Args...)> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...)> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...) const> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Then<Ret(Arg)> {
+ static_assert(std::is_same<Ret, void>::value,
+ "then() should not return anything, use map() instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, E> then(const Result<T, E>& result, Func func)
+ {
+ if (result.isOk()) {
+ func(result.storage().template get<T>());
+ }
+ return result;
+ }
+};
+
+template<typename Ret>
+struct Then<Ret(void)> {
+ static_assert(std::is_same<Ret, void>::value,
+ "then() should not return anything, use map() instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, E> then(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isOk()) {
+ func();
+ }
+
+ return result;
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Then : public impl::Then<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Then<Ret (*)(Args...)> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Then<Ret(Arg)> : public impl::Then<Ret(Arg)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...)> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...) const> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Then<std::function<Ret(Args...)>> : public impl::Then<Ret(Args...)> {};
+
+} // namespace And
+
+namespace Or {
+
+namespace impl {
+
+template<typename Func>
+struct Else;
+
+template<typename Ret, typename... Args>
+struct Else<Ret (*)(Args...)> : public Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...)> : public Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...) const> : public Else<Ret(Args...)> {};
+
+template<typename T, typename F, typename Arg>
+struct Else<Result<T, F>(Arg)> {
+ template<typename E, typename Func>
+ static Result<T, F> orElse(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<E, Arg>::value || std::is_convertible<E, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return res;
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, F> orElse(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return res;
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+template<typename T, typename F>
+struct Else<Result<T, F>(void)> {
+ template<typename E, typename Func>
+ static Result<T, F> orElse(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isErr()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, F> orElse(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Else : public impl::Else<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Else<Ret (*)(Args...)> : public impl::Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...)> : public impl::Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...) const> : public impl::Else<Ret(Args...)> {};
+
+} // namespace Or
+
+namespace Other {
+
+namespace impl {
+
+template<typename Func>
+struct Wise;
+
+template<typename Ret, typename... Args>
+struct Wise<Ret (*)(Args...)> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...)> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...) const> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Wise<Ret(Arg)> {
+ template<typename T, typename E, typename Func>
+ static Result<T, E> otherwise(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<E, Arg>::value || std::is_convertible<E, Arg>::value,
+ "Incompatible types detected");
+
+ static_assert(
+ std::is_same<Ret, void>::value,
+ "callback should not return anything, use mapError() for that");
+
+ if (result.isErr()) {
+ func(result.storage().template get<E>());
+ }
+ return result;
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Wise : public impl::Wise<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Wise<Ret (*)(Args...)> : public impl::Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...)> : public impl::Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...) const> : public impl::Wise<Ret(Args...)> {};
+
+} // namespace Other
+
+template<typename T, typename E, typename Func>
+decltype(auto)
+map(const Result<T, E>& result, Func func)
+{
+ return ok::Map<Func>::map(result, func);
+}
+
+template<typename T,
+ typename E,
+ typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+Ret
+mapError(const Result<T, E>& result, Func func)
+{
+ return err::Map<Func>::map(result, func);
+}
+
+template<typename T, typename E, typename Func>
+Result<T, E>
+then(const Result<T, E>& result, Func func)
+{
+ return And::Then<Func>::then(result, func);
+}
+
+template<typename T, typename E, typename Func>
+Result<T, E>
+otherwise(const Result<T, E>& result, Func func)
+{
+ return Other::Wise<Func>::otherwise(result, func);
+}
+
+template<typename T,
+ typename E,
+ typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+Ret
+orElse(const Result<T, E>& result, Func func)
+{
+ return Or::Else<Func>::orElse(result, func);
+}
+
+struct ok_tag {};
+struct err_tag {};
+
+template<typename T, typename E>
+struct Storage {
+ static constexpr size_t Size = sizeof(T) > sizeof(E) ? sizeof(T)
+ : sizeof(E);
+ static constexpr size_t Align = sizeof(T) > sizeof(E) ? alignof(T)
+ : alignof(E);
+
+ typedef typename std::aligned_storage<Size, Align>::type type;
+
+ Storage() : initialized_(false) {}
+
+ void construct(types::Ok<T> ok)
+ {
+ new (&storage_) T(std::move(ok.val));
+ initialized_ = true;
+ }
+ void construct(types::Err<E> err)
+ {
+ new (&storage_) E(err.val);
+ initialized_ = true;
+ }
+
+ template<typename U>
+ void rawConstruct(U&& val)
+ {
+ typedef typename std::decay<U>::type CleanU;
+
+ new (&storage_) CleanU(std::forward<U>(val));
+ initialized_ = true;
+ }
+
+ template<typename U>
+ const U& get() const
+ {
+ return *reinterpret_cast<const U*>(&storage_);
+ }
+
+ template<typename U>
+ U& get()
+ {
+ return *reinterpret_cast<U*>(&storage_);
+ }
+
+ void destroy(ok_tag)
+ {
+ if (initialized_) {
+ get<T>().~T();
+ initialized_ = false;
+ }
+ }
+
+ void destroy(err_tag)
+ {
+ if (initialized_) {
+ get<E>().~E();
+ initialized_ = false;
+ }
+ }
+
+ type storage_;
+ bool initialized_;
+};
+
+template<typename E>
+struct Storage<void, E> {
+ typedef typename std::aligned_storage<sizeof(E), alignof(E)>::type type;
+
+ void construct(types::Ok<void>) { initialized_ = true; }
+
+ void construct(types::Err<E> err)
+ {
+ new (&storage_) E(err.val);
+ initialized_ = true;
+ }
+
+ template<typename U>
+ void rawConstruct(U&& val)
+ {
+ typedef typename std::decay<U>::type CleanU;
+
+ new (&storage_) CleanU(std::forward<U>(val));
+ initialized_ = true;
+ }
+
+ void destroy(ok_tag) { initialized_ = false; }
+ void destroy(err_tag)
+ {
+ if (initialized_) {
+ get<E>().~E();
+ initialized_ = false;
+ }
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<!std::is_same<U, void>::value>>
+ const U& get() const
+ {
+ return *reinterpret_cast<const U*>(&storage_);
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<!std::is_same<U, void>::value>>
+ typename std::add_lvalue_reference<U>::type get()
+ {
+ return *reinterpret_cast<U*>(&storage_);
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<std::is_same<U, void>::value>>
+ void get()
+ {
+ }
+
+ type storage_;
+ bool initialized_;
+};
+
+template<typename T, typename E>
+struct Constructor {
+ static void move(Storage<T, E>&& src, Storage<T, E>& dst, ok_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<T>()));
+ src.destroy(ok_tag());
+ }
+
+ static void copy(const Storage<T, E>& src, Storage<T, E>& dst, ok_tag)
+ {
+ dst.rawConstruct(src.template get<T>());
+ }
+
+ static void move(Storage<T, E>&& src, Storage<T, E>& dst, err_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<E>()));
+ src.destroy(err_tag());
+ }
+
+ static void copy(const Storage<T, E>& src, Storage<T, E>& dst, err_tag)
+ {
+ dst.rawConstruct(src.template get<E>());
+ }
+};
+
+template<typename E>
+struct Constructor<void, E> {
+ static void move(Storage<void, E>&& src, Storage<void, E>& dst, ok_tag) {}
+
+ static void copy(const Storage<void, E>& src, Storage<void, E>& dst, ok_tag)
+ {
+ }
+
+ static void move(Storage<void, E>&& src, Storage<void, E>& dst, err_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<E>()));
+ src.destroy(err_tag());
+ }
+
+ static void copy(const Storage<void, E>& src,
+ Storage<void, E>& dst,
+ err_tag)
+ {
+ dst.rawConstruct(src.template get<E>());
+ }
+};
+
+} // namespace details
+
+namespace local_concept {
+
+template<typename T, typename = void>
+struct EqualityComparable : std::false_type {};
+
+template<typename T>
+struct EqualityComparable<
+ T,
+ typename std::enable_if<
+ true,
+ typename details::void_t<decltype(std::declval<T>()
+ == std::declval<T>())>::type>::type>
+ : std::true_type {};
+
+} // namespace local_concept
+
+template<typename T, typename E>
+struct Result {
+ static_assert(!std::is_same<E, void>::value,
+ "void error type is not allowed");
+
+ typedef details::Storage<T, E> storage_type;
+
+ Result(types::Ok<T> ok) : ok_(true) { storage_.construct(std::move(ok)); }
+
+ Result(types::Err<E> err) : ok_(false)
+ {
+ storage_.construct(std::move(err));
+ }
+
+ Result(Result&& other)
+ {
+ if (other.isOk()) {
+ details::Constructor<T, E>::move(
+ std::move(other.storage_), storage_, details::ok_tag());
+ ok_ = true;
+ } else {
+ details::Constructor<T, E>::move(
+ std::move(other.storage_), storage_, details::err_tag());
+ ok_ = false;
+ }
+ }
+
+ Result(const Result& other)
+ {
+ if (other.isOk()) {
+ details::Constructor<T, E>::copy(
+ other.storage_, storage_, details::ok_tag());
+ ok_ = true;
+ } else {
+ details::Constructor<T, E>::copy(
+ other.storage_, storage_, details::err_tag());
+ ok_ = false;
+ }
+ }
+
+ ~Result()
+ {
+ if (ok_)
+ storage_.destroy(details::ok_tag());
+ else
+ storage_.destroy(details::err_tag());
+ }
+
+ bool isOk() const { return ok_; }
+
+ bool isErr() const { return !ok_; }
+
+ T expect(const char* str)
+ {
+ if (!isOk()) {
+ ::fprintf(stderr, "%s\n", str);
+ abort();
+ }
+ return expect_impl(std::is_same<T, void>());
+ }
+
+ template<typename Func>
+ auto map(Func func)
+ {
+ using return_type = decltype(func(T{}));
+
+ if (this->isOk()) {
+ auto value = std::move(this->storage().template get<T>());
+ auto res = func(std::move(value));
+ return Result<return_type, E>(
+ types::Ok<return_type>(std::move(res)));
+ }
+
+ return Result<return_type, E>(
+ types::Err<E>(this->storage().template get<E>()));
+ }
+
+ template<typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+ Ret mapError(Func func) const
+ {
+ return details::mapError(*this, func);
+ }
+
+ template<typename Func>
+ Result<void, E> then(Func func)
+ {
+ if (this->isOk()) {
+ func(std::move(this->storage().template get<T>()));
+
+ return Ok();
+ }
+
+ return Err(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func>
+ Result<typename std::result_of<Func>::type, E> then(Func func)
+ {
+ if (this->isOk()) {
+ return Ok(func(std::move(this->storage().template get<T>())));
+ }
+
+ return Err(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func>
+ void otherwise(Func func)
+ {
+ if (this->isOk()) {
+ return;
+ }
+
+ func(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+ Ret orElse(Func func) const
+ {
+ return details::orElse(*this, func);
+ }
+
+ storage_type& storage() { return storage_; }
+
+ const storage_type& storage() const { return storage_; }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, T>::type unwrapOr(
+ const U& defaultValue) const
+ {
+ if (isOk()) {
+ return storage().template get<T>();
+ }
+ return defaultValue;
+ }
+
+ template<typename Func>
+ auto unwrapOrElse(Func func) const
+ {
+ if (isOk()) {
+ return storage().template get<T>();
+ }
+ return func(this->storage().template get<E>());
+ }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, U>::type unwrap()
+ const
+ {
+ if (isOk()) {
+ return std::move(storage().template get<U>());
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, U>::type unwrap()
+ {
+ if (isOk()) {
+ return std::move(storage().template get<U>());
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ template<typename U = T>
+ typename std::enable_if<std::is_same<U, void>::value, U>::type unwrap()
+ const
+ {
+ if (isOk()) {
+ return;
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ E unwrapErr() const
+ {
+ if (isErr()) {
+ return storage().template get<E>();
+ }
+
+ ::fprintf(stderr, "Attempting to unwrapErr an ok Result\n");
+ abort();
+ }
+
+private:
+ T expect_impl(std::true_type) const {}
+ T expect_impl(std::false_type)
+ {
+ return std::move(storage_.template get<T>());
+ }
+
+ bool ok_;
+ storage_type storage_;
+};
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, const Result<T, E>& rhs)
+{
+ static_assert(local_concept::EqualityComparable<T>::value,
+ "T must be EqualityComparable for Result to be comparable");
+ static_assert(local_concept::EqualityComparable<E>::value,
+ "E must be EqualityComparable for Result to be comparable");
+
+ if (lhs.isOk() && rhs.isOk()) {
+ return lhs.storage().template get<T>()
+ == rhs.storage().template get<T>();
+ }
+ if (lhs.isErr() && rhs.isErr()) {
+ return lhs.storage().template get<E>()
+ == rhs.storage().template get<E>();
+ }
+}
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, types::Ok<T> ok)
+{
+ static_assert(local_concept::EqualityComparable<T>::value,
+ "T must be EqualityComparable for Result to be comparable");
+
+ if (!lhs.isOk())
+ return false;
+
+ return lhs.storage().template get<T>() == ok.val;
+}
+
+template<typename E>
+bool
+operator==(const Result<void, E>& lhs, types::Ok<void>)
+{
+ return lhs.isOk();
+}
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, types::Err<E> err)
+{
+ static_assert(local_concept::EqualityComparable<E>::value,
+ "E must be EqualityComparable for Result to be comparable");
+ if (!lhs.isErr())
+ return false;
+
+ return lhs.storage().template get<E>() == err.val;
+}
+
+#define TRY(...) \
+ ({ \
+ auto res = __VA_ARGS__; \
+ if (!res.isOk()) { \
+ typedef typename ::details::ResultErrType<decltype(res)>::type E; \
+ return ::types::Err<E>(res.storage().template get<E>()); \
+ } \
+ res.unwrap(); \
+ })
diff --git a/src/base/snippet_highlighters.cc b/src/base/snippet_highlighters.cc
new file mode 100644
index 0000000..058fa41
--- /dev/null
+++ b/src/base/snippet_highlighters.cc
@@ -0,0 +1,344 @@
+/**
+ * Copyright (c) 2022, 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 "snippet_highlighters.hh"
+
+#include "attr_line.builder.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "view_curses.hh"
+
+namespace lnav {
+namespace snippets {
+
+static bool
+is_bracket(const std::string& str, int index, bool is_lit)
+{
+ if (index == 0) {
+ return true;
+ }
+
+ if (is_lit && str[index - 1] == '\\') {
+ return true;
+ }
+ if (!is_lit && str[index - 1] != '\\') {
+ return true;
+ }
+ return false;
+}
+
+static void
+find_matching_bracket(
+ attr_line_t& al, int x, line_range sub, char left, char right)
+{
+ bool is_lit = (left == 'Q');
+ attr_line_builder alb(al);
+ const auto& line = al.get_string();
+ int depth = 0;
+
+ if (x < sub.lr_start || x > sub.lr_end) {
+ return;
+ }
+
+ if (line[x] == right && is_bracket(line, x, is_lit)) {
+ for (int lpc = x - 1; lpc >= sub.lr_start; lpc--) {
+ if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ if (line[x] == left && is_bracket(line, x, is_lit)) {
+ for (int lpc = x + 1; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ nonstd::optional<int> first_left;
+
+ depth = 0;
+
+ for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ if (!first_left) {
+ first_left = lpc;
+ }
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth > 0) {
+ depth -= 1;
+ } else {
+ auto lr = line_range(is_lit ? lpc - 1 : lpc, lpc + 1);
+ alb.overlay_attr(
+ lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ }
+ }
+
+ if (depth > 0) {
+ auto lr
+ = line_range(is_lit ? first_left.value() - 1 : first_left.value(),
+ first_left.value() + 1);
+ alb.overlay_attr(lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+}
+
+static bool
+check_re_prev(const std::string& line, int x)
+{
+ bool retval = false;
+
+ if ((x > 0 && line[x - 1] != ')' && line[x - 1] != ']' && line[x - 1] != '*'
+ && line[x - 1] != '?' && line[x - 1] != '+')
+ && (x < 2 || line[x - 2] != '\\'))
+ {
+ retval = true;
+ }
+
+ return retval;
+}
+
+static char
+safe_read(const std::string& str, std::string::size_type index)
+{
+ if (index < str.length()) {
+ return str.at(index);
+ }
+
+ return 0;
+}
+
+void
+regex_highlighter(attr_line_t& al, int x, line_range sub)
+{
+ static const char* brackets[] = {
+ "[]",
+ "{}",
+ "()",
+ "QE",
+
+ nullptr,
+ };
+
+ const auto& line = al.get_string();
+ attr_line_builder alb(al);
+ bool backslash_is_quoted = false;
+
+ for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) {
+ if (lpc == 0 || line[lpc - 1] != '\\') {
+ switch (line[lpc]) {
+ case '^':
+ case '$':
+ case '*':
+ case '+':
+ case '|':
+ case '.':
+ alb.overlay_attr_for_char(
+ lpc, VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+
+ if ((line[lpc] == '*' || line[lpc] == '+')
+ && check_re_prev(line, lpc))
+ {
+ alb.overlay_attr_for_char(
+ lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT));
+ }
+ break;
+ case '?': {
+ struct line_range lr(lpc, lpc + 1);
+
+ if (lpc == sub.lr_start || (lpc - sub.lr_start) == 0) {
+ alb.overlay_attr_for_char(
+ lpc,
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(
+ lpc, VC_ROLE.value(role_t::VCR_ERROR));
+ } else if (line[lpc - 1] == '(') {
+ switch (line[lpc + 1]) {
+ case ':':
+ case '!':
+ case '#':
+ lr.lr_end += 1;
+ break;
+ }
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_OK));
+ if (line[lpc + 1] == '<') {
+ alb.overlay_attr(
+ line_range(lpc + 1, lpc + 2),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ } else {
+ alb.overlay_attr(lr,
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+
+ if (check_re_prev(line, lpc)) {
+ alb.overlay_attr_for_char(
+ lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT));
+ }
+ }
+ break;
+ }
+ case '>': {
+ static const auto CAP_RE
+ = lnav::pcre2pp::code::from_const(R"(\(\?\<\w+$)");
+
+ auto capture_start
+ = string_fragment::from_str_range(
+ line, sub.lr_start, lpc)
+ .find_left_boundary(lpc - sub.lr_start - 1,
+ string_fragment::tag1{'('});
+
+ auto cap_find_res
+ = CAP_RE.find_in(capture_start).ignore_error();
+
+ if (cap_find_res) {
+ alb.overlay_attr(
+ line_range(capture_start.sf_begin
+ + cap_find_res->f_all.sf_begin + 3,
+ capture_start.sf_begin
+ + cap_find_res->f_all.sf_end),
+ VC_ROLE.value(role_t::VCR_IDENTIFIER));
+ alb.overlay_attr(line_range(lpc, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ break;
+ }
+
+ case '(':
+ case ')':
+ case '{':
+ case '}':
+ case '[':
+ case ']':
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ }
+ if (lpc > 0 && line[lpc - 1] == '\\') {
+ if (backslash_is_quoted) {
+ backslash_is_quoted = false;
+ continue;
+ }
+ switch (line[lpc]) {
+ case '\\':
+ backslash_is_quoted = true;
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ break;
+ case 'd':
+ case 'D':
+ case 'h':
+ case 'H':
+ case 'N':
+ case 'R':
+ case 's':
+ case 'S':
+ case 'v':
+ case 'V':
+ case 'w':
+ case 'W':
+ case 'X':
+
+ case 'A':
+ case 'b':
+ case 'B':
+ case 'G':
+ case 'Z':
+ case 'z':
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ break;
+ case ' ':
+ alb.overlay_attr(
+ line_range(lpc - 1, lpc + 1),
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ break;
+ case '0':
+ case 'x':
+ if (safe_read(line, lpc + 1) == '{') {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ } else if (isdigit(safe_read(line, lpc + 1))
+ && isdigit(safe_read(line, lpc + 2)))
+ {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 3),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ } else {
+ alb.overlay_attr(
+ line_range(lpc - 1, lpc + 1),
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ break;
+ case 'Q':
+ case 'E':
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ default:
+ if (isdigit(line[lpc])) {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ break;
+ }
+ }
+ }
+
+ for (int lpc = 0; brackets[lpc]; lpc++) {
+ find_matching_bracket(al, x, sub, brackets[lpc][0], brackets[lpc][1]);
+ }
+}
+
+} // namespace snippets
+} // namespace lnav
diff --git a/src/base/snippet_highlighters.hh b/src/base/snippet_highlighters.hh
new file mode 100644
index 0000000..0da6291
--- /dev/null
+++ b/src/base/snippet_highlighters.hh
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2022, 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_snippet_highlighters_hh
+#define lnav_snippet_highlighters_hh
+
+#include "attr_line.hh"
+
+namespace lnav {
+namespace snippets {
+
+void regex_highlighter(attr_line_t& al, int x, line_range sub);
+
+} // namespace snippets
+} // namespace lnav
+
+#endif
diff --git a/src/base/string_attr_type.cc b/src/base/string_attr_type.cc
new file mode 100644
index 0000000..9a3950b
--- /dev/null
+++ b/src/base/string_attr_type.cc
@@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+
+#include "string_attr_type.hh"
+
+#include "config.h"
+
+string_attr_type<void> SA_ORIGINAL_LINE("original_line");
+string_attr_type<void> SA_BODY("body");
+string_attr_type<void> SA_HIDDEN("hidden");
+string_attr_type<const intern_string_t> SA_FORMAT("format");
+string_attr_type<void> SA_REMOVED("removed");
+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<int64_t> VC_FOREGROUND("foreground");
+string_attr_type<int64_t> VC_BACKGROUND("background");
diff --git a/src/base/string_attr_type.hh b/src/base/string_attr_type.hh
new file mode 100644
index 0000000..5bff345
--- /dev/null
+++ b/src/base/string_attr_type.hh
@@ -0,0 +1,679 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_string_attr_type_hh
+#define lnav_string_attr_type_hh
+
+#include <string>
+#include <utility>
+
+#include <stdint.h>
+
+#include "base/intern_string.hh"
+#include "mapbox/variant.hpp"
+
+class logfile;
+struct bookmark_metadata;
+
+/** Roles that can be mapped to curses attributes using attrs_for_role() */
+enum class role_t : int32_t {
+ VCR_NONE = -1,
+
+ VCR_TEXT, /*< Raw text. */
+ VCR_IDENTIFIER,
+ VCR_SEARCH, /*< A search hit. */
+ VCR_OK,
+ VCR_INFO,
+ VCR_ERROR, /*< An error message. */
+ VCR_WARNING, /*< A warning message. */
+ VCR_ALT_ROW, /*< Highlight for alternating rows in a list */
+ VCR_HIDDEN,
+ VCR_CURSOR_LINE,
+ VCR_ADJUSTED_TIME,
+ VCR_SKEWED_TIME,
+ VCR_OFFSET_TIME,
+ VCR_INVALID_MSG,
+ VCR_STATUS, /*< Normal status line text. */
+ VCR_WARN_STATUS,
+ VCR_ALERT_STATUS, /*< Alert status line text. */
+ VCR_ACTIVE_STATUS, /*< */
+ VCR_ACTIVE_STATUS2, /*< */
+ VCR_STATUS_TITLE,
+ VCR_STATUS_SUBTITLE,
+ VCR_STATUS_INFO,
+ VCR_STATUS_STITCH_TITLE_TO_SUB,
+ VCR_STATUS_STITCH_SUB_TO_TITLE,
+ VCR_STATUS_STITCH_SUB_TO_NORMAL,
+ VCR_STATUS_STITCH_NORMAL_TO_SUB,
+ VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ VCR_STATUS_STITCH_NORMAL_TO_TITLE,
+ VCR_STATUS_TITLE_HOTKEY,
+ VCR_STATUS_DISABLED_TITLE,
+ VCR_STATUS_HOTKEY,
+ VCR_INACTIVE_STATUS,
+ VCR_INACTIVE_ALERT_STATUS,
+ VCR_SCROLLBAR,
+ VCR_SCROLLBAR_ERROR,
+ VCR_SCROLLBAR_WARNING,
+ VCR_FOCUSED,
+ VCR_DISABLED_FOCUSED,
+ VCR_POPUP,
+ VCR_COLOR_HINT,
+
+ VCR_QUOTED_CODE,
+ VCR_CODE_BORDER,
+ VCR_KEYWORD,
+ VCR_STRING,
+ VCR_COMMENT,
+ VCR_DOC_DIRECTIVE,
+ VCR_VARIABLE,
+ VCR_SYMBOL,
+ VCR_NUMBER,
+ VCR_RE_SPECIAL,
+ VCR_RE_REPEAT,
+ VCR_FILE,
+
+ VCR_DIFF_DELETE, /*< Deleted line in a diff. */
+ VCR_DIFF_ADD, /*< Added line in a diff. */
+ VCR_DIFF_SECTION, /*< Section marker in a diff. */
+
+ VCR_LOW_THRESHOLD,
+ VCR_MED_THRESHOLD,
+ VCR_HIGH_THRESHOLD,
+
+ VCR_H1,
+ VCR_H2,
+ VCR_H3,
+ VCR_H4,
+ VCR_H5,
+ VCR_H6,
+
+ VCR_HR,
+ VCR_HYPERLINK,
+ VCR_LIST_GLYPH,
+ VCR_BREADCRUMB,
+ VCR_TABLE_BORDER,
+ VCR_TABLE_HEADER,
+ VCR_QUOTE_BORDER,
+ VCR_QUOTED_TEXT,
+ VCR_FOOTNOTE_BORDER,
+ VCR_FOOTNOTE_TEXT,
+ VCR_SNIPPET_BORDER,
+
+ VCR__MAX
+};
+
+struct text_attrs {
+ bool empty() const
+ {
+ return this->ta_attrs == 0 && !this->ta_fg_color && !this->ta_bg_color;
+ }
+
+ text_attrs operator|(const text_attrs& other) const
+ {
+ return text_attrs{
+ this->ta_attrs | other.ta_attrs,
+ this->ta_fg_color ? this->ta_fg_color : other.ta_fg_color,
+ this->ta_bg_color ? this->ta_bg_color : other.ta_bg_color,
+ };
+ }
+
+ bool operator==(const text_attrs& other) const
+ {
+ return this->ta_attrs == other.ta_attrs
+ && this->ta_fg_color == other.ta_fg_color
+ && this->ta_bg_color == other.ta_bg_color;
+ }
+
+ int32_t ta_attrs{0};
+ nonstd::optional<short> ta_fg_color;
+ nonstd::optional<short> ta_bg_color;
+};
+
+using string_attr_value = mapbox::util::variant<int64_t,
+ role_t,
+ text_attrs,
+ const intern_string_t,
+ std::string,
+ std::shared_ptr<logfile>,
+ bookmark_metadata*,
+ timespec,
+ string_fragment>;
+
+class string_attr_type_base {
+public:
+ explicit string_attr_type_base(const char* name) noexcept : sat_name(name)
+ {
+ }
+
+ const char* const sat_name;
+};
+
+using string_attr_pair
+ = std::pair<const string_attr_type_base*, string_attr_value>;
+
+template<typename T>
+class string_attr_type : public string_attr_type_base {
+public:
+ using value_type = T;
+
+ explicit string_attr_type(const char* name) noexcept
+ : string_attr_type_base(name)
+ {
+ }
+
+ template<typename U = T>
+ std::enable_if_t<!std::is_void<U>::value, string_attr_pair> value(
+ const U& val) const
+ {
+ return std::make_pair(this, val);
+ }
+
+ template<typename U = T>
+ std::enable_if_t<std::is_void<U>::value, string_attr_pair> value() const
+ {
+ return std::make_pair(this, string_attr_value{});
+ }
+};
+
+extern string_attr_type<void> SA_ORIGINAL_LINE;
+extern string_attr_type<void> SA_BODY;
+extern string_attr_type<void> SA_HIDDEN;
+extern string_attr_type<const intern_string_t> SA_FORMAT;
+extern string_attr_type<void> SA_REMOVED;
+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<int64_t> VC_FOREGROUND;
+extern string_attr_type<int64_t> VC_BACKGROUND;
+
+namespace lnav {
+
+namespace string {
+namespace attrs {
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+preformatted(S str)
+{
+ return std::make_pair(std::move(str), SA_PREFORMATTED.template value());
+}
+
+} // namespace attrs
+} // namespace string
+
+namespace roles {
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+error(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_ERROR));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+warning(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_WARNING));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+status(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_STATUS));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+inactive_status(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_INACTIVE_STATUS));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+status_title(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_STATUS_TITLE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+ok(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_OK));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+file(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FILE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+symbol(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_SYMBOL));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+keyword(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_KEYWORD));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+variable(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_VARIABLE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+number(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_NUMBER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+comment(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_COMMENT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+identifier(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_IDENTIFIER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+hr(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_HR));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+hyperlink(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_HYPERLINK));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+list_glyph(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_LIST_GLYPH));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+breadcrumb(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_BREADCRUMB));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quoted_code(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTED_CODE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+code_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_CODE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+snippet_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+table_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_TABLE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+table_header(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_TABLE_HEADER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quote_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quoted_text(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTED_TEXT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+footnote_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+footnote_text(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_TEXT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h1(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H1));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h2(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H2));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h3(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H3));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h4(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H4));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h5(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H5));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h6(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H6));
+}
+
+namespace literals {
+
+inline std::pair<std::string, string_attr_pair> operator"" _ok(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_OK));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _error(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_ERROR));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _info(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_INFO));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _symbol(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_SYMBOL));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _keyword(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_KEYWORD));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _variable(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_VARIABLE));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _comment(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_COMMENT));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hotkey(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_STATUS_HOTKEY));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h1(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H1));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h2(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H2));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h3(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H3));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h4(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H4));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h5(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H5));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hr(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_HR));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hyperlink(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_HYPERLINK));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _list_glyph(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_LIST_GLYPH));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _breadcrumb(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_BREADCRUMB));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quoted_code(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTED_CODE));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _code_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_CODE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _table_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_TABLE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quote_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quoted_text(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTED_TEXT));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _footnote_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _footnote_text(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _snippet_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER));
+}
+
+} // namespace literals
+
+} // namespace roles
+} // namespace lnav
+
+#endif
diff --git a/src/base/string_util.cc b/src/base/string_util.cc
new file mode 100644
index 0000000..8af686c
--- /dev/null
+++ b/src/base/string_util.cc
@@ -0,0 +1,304 @@
+/**
+ * Copyright (c) 2019, 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 <algorithm>
+#include <iterator>
+#include <regex>
+#include <sstream>
+
+#include "string_util.hh"
+
+#include "config.h"
+#include "is_utf8.hh"
+#include "lnav_log.hh"
+
+void
+scrub_to_utf8(char* buffer, size_t length)
+{
+ while (true) {
+ auto frag = string_fragment::from_bytes(buffer, length);
+ auto scan_res = is_utf8(frag);
+
+ if (scan_res.is_valid()) {
+ break;
+ }
+ for (size_t lpc = 0; lpc < scan_res.usr_faulty_bytes; lpc++) {
+ buffer[scan_res.usr_valid_frag.sf_end + lpc] = '?';
+ }
+ }
+}
+
+void
+quote_content(auto_buffer& buf, const string_fragment& sf, char quote_char)
+{
+ for (char ch : sf) {
+ if (ch == quote_char) {
+ buf.push_back('\\').push_back(ch);
+ continue;
+ }
+ switch (ch) {
+ case '\\':
+ buf.push_back('\\').push_back('\\');
+ break;
+ case '\n':
+ buf.push_back('\\').push_back('n');
+ break;
+ case '\t':
+ buf.push_back('\\').push_back('t');
+ break;
+ case '\r':
+ buf.push_back('\\').push_back('r');
+ break;
+ case '\a':
+ buf.push_back('\\').push_back('a');
+ break;
+ case '\b':
+ buf.push_back('\\').push_back('b');
+ break;
+ default:
+ buf.push_back(ch);
+ break;
+ }
+ }
+}
+
+size_t
+unquote_content(char* dst, const char* str, size_t len, char quote_char)
+{
+ size_t index = 0;
+
+ for (size_t lpc = 0; lpc < len; lpc++, index++) {
+ dst[index] = str[lpc];
+ if (str[lpc] == quote_char) {
+ lpc += 1;
+ } else if (str[lpc] == '\\' && (lpc + 1) < len) {
+ switch (str[lpc + 1]) {
+ case 'n':
+ dst[index] = '\n';
+ break;
+ case 'r':
+ dst[index] = '\r';
+ break;
+ case 't':
+ dst[index] = '\t';
+ break;
+ default:
+ dst[index] = str[lpc + 1];
+ break;
+ }
+ lpc += 1;
+ }
+ }
+ dst[index] = '\0';
+
+ return index;
+}
+
+size_t
+unquote(char* dst, const char* str, size_t len)
+{
+ if (str[0] == 'r' || str[0] == 'u') {
+ str += 1;
+ len -= 1;
+ }
+ char quote_char = str[0];
+
+ require(str[0] == '\'' || str[0] == '"');
+
+ return unquote_content(dst, &str[1], len - 2, quote_char);
+}
+
+size_t
+unquote_w3c(char* dst, const char* str, size_t len)
+{
+ size_t index = 0;
+
+ require(str[0] == '\'' || str[0] == '"');
+
+ for (size_t lpc = 1; lpc < (len - 1); lpc++, index++) {
+ dst[index] = str[lpc];
+ if (str[lpc] == '"') {
+ lpc += 1;
+ }
+ }
+ dst[index] = '\0';
+
+ return index;
+}
+
+void
+truncate_to(std::string& str, size_t max_char_len)
+{
+ static const std::string ELLIPSIS = "\u22ef";
+
+ if (str.length() < max_char_len) {
+ return;
+ }
+
+ auto str_char_len_res = utf8_string_length(str);
+
+ if (str_char_len_res.isErr()) {
+ // XXX
+ return;
+ }
+
+ auto str_char_len = str_char_len_res.unwrap();
+ if (str_char_len <= max_char_len) {
+ return;
+ }
+
+ if (max_char_len < 3) {
+ str = ELLIPSIS;
+ return;
+ }
+
+ auto chars_to_remove = (str_char_len - max_char_len) + 1;
+ auto midpoint = str_char_len / 2;
+ auto chars_to_keep_at_front = midpoint - (chars_to_remove / 2);
+ auto bytes_to_keep_at_front
+ = utf8_char_to_byte_index(str, chars_to_keep_at_front);
+ auto remove_up_to_bytes = utf8_char_to_byte_index(
+ str, chars_to_keep_at_front + chars_to_remove);
+ auto bytes_to_remove = remove_up_to_bytes - bytes_to_keep_at_front;
+ str.erase(bytes_to_keep_at_front, bytes_to_remove);
+ str.insert(bytes_to_keep_at_front, ELLIPSIS);
+}
+
+bool
+is_url(const std::string& fn)
+{
+ static const auto url_re = std::regex("^(file|https?|ftps?|scp|sftp):.*");
+
+ return std::regex_match(fn, url_re);
+}
+
+size_t
+abbreviate_str(char* str, size_t len, size_t max_len)
+{
+ size_t last_start = 1;
+
+ if (len < max_len) {
+ return len;
+ }
+
+ for (size_t index = 0; index < len; index++) {
+ switch (str[index]) {
+ case '.':
+ case '-':
+ case '/':
+ case ':':
+ memmove(&str[last_start], &str[index], len - index);
+ len -= (index - last_start);
+ index = last_start + 1;
+ last_start = index + 1;
+
+ if (len < max_len) {
+ return len;
+ }
+ break;
+ }
+ }
+
+ return len;
+}
+
+void
+split_ws(const std::string& str, std::vector<std::string>& toks_out)
+{
+ std::stringstream ss(str);
+ std::string buf;
+
+ while (ss >> buf) {
+ toks_out.push_back(buf);
+ }
+}
+
+std::string
+repeat(const std::string& input, size_t num)
+{
+ std::ostringstream os;
+ std::fill_n(std::ostream_iterator<std::string>(os), num, input);
+ return os.str();
+}
+
+std::string
+center_str(const std::string& subject, size_t width)
+{
+ std::string retval = subject;
+
+ truncate_to(retval, width);
+
+ auto retval_length = utf8_string_length(retval).unwrapOr(retval.length());
+ auto total_fill = width - retval_length;
+ auto before = total_fill / 2;
+ auto after = total_fill - before;
+
+ retval.insert(0, before, ' ');
+ retval.append(after, ' ');
+
+ return retval;
+}
+
+bool
+is_blank(const std::string& str)
+{
+ return std::all_of(
+ str.begin(), str.end(), [](const auto ch) { return isspace(ch); });
+}
+
+std::string
+scrub_ws(const char* in)
+{
+ static const std::string TAB_SYMBOL = "\u21e5";
+ static const std::string LF_SYMBOL = "\u240a";
+ static const std::string CR_SYMBOL = "\u240d";
+
+ std::string retval;
+
+ for (size_t lpc = 0; in[lpc]; lpc++) {
+ auto ch = in[lpc];
+
+ switch (ch) {
+ case '\t':
+ retval.append(TAB_SYMBOL);
+ break;
+ case '\n':
+ retval.append(LF_SYMBOL);
+ break;
+ case '\r':
+ retval.append(CR_SYMBOL);
+ break;
+ default:
+ retval.append(1, ch);
+ break;
+ }
+ }
+
+ return retval;
+}
diff --git a/src/base/string_util.hh b/src/base/string_util.hh
new file mode 100644
index 0000000..73a8b87
--- /dev/null
+++ b/src/base/string_util.hh
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2019, 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_string_util_hh
+#define lnav_string_util_hh
+
+#include <string>
+#include <vector>
+
+#include <string.h>
+
+#include "auto_mem.hh"
+#include "intern_string.hh"
+#include "ww898/cp_utf8.hpp"
+
+void scrub_to_utf8(char* buffer, size_t length);
+
+inline bool
+is_line_ending(char ch)
+{
+ return ch == '\r' || ch == '\n';
+}
+
+void quote_content(auto_buffer& buf,
+ const string_fragment& sf,
+ char quote_char);
+
+size_t unquote_content(char* dst, const char* str, size_t len, char quote_char);
+
+size_t unquote(char* dst, const char* str, size_t len);
+
+size_t unquote_w3c(char* dst, const char* str, size_t len);
+
+inline bool
+startswith(const char* str, const char* prefix)
+{
+ return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+inline bool
+startswith(const std::string& str, const char* prefix)
+{
+ return startswith(str.c_str(), prefix);
+}
+
+inline bool
+startswith(const std::string& str, const std::string& prefix)
+{
+ return startswith(str.c_str(), prefix.c_str());
+}
+
+inline bool
+endswith(const char* str, const char* suffix)
+{
+ size_t len = strlen(str), suffix_len = strlen(suffix);
+
+ if (suffix_len > len) {
+ return false;
+ }
+
+ return strcmp(&str[len - suffix_len], suffix) == 0;
+}
+
+template<int N>
+inline bool
+endswith(const std::string& str, const char (&suffix)[N])
+{
+ if (N - 1 > str.length()) {
+ return false;
+ }
+
+ return strcmp(&str[str.size() - (N - 1)], suffix) == 0;
+}
+
+void truncate_to(std::string& str, size_t max_char_len);
+
+std::string scrub_ws(const char* in);
+
+inline std::string
+trim(const std::string& str)
+{
+ std::string::size_type start, end;
+
+ for (start = 0; start < str.size() && isspace(str[start]); start++)
+ ;
+ for (end = str.size(); end > 0 && isspace(str[end - 1]); end--)
+ ;
+
+ return str.substr(start, end - start);
+}
+
+inline std::string
+rtrim(const std::string& str)
+{
+ std::string::size_type end;
+
+ for (end = str.size(); end > 0 && isspace(str[end - 1]); end--)
+ ;
+
+ return str.substr(0, end);
+}
+
+inline std::string
+tolower(const char* str)
+{
+ std::string retval;
+
+ for (int lpc = 0; str[lpc]; lpc++) {
+ retval.push_back(::tolower(str[lpc]));
+ }
+
+ return retval;
+}
+
+inline std::string
+tolower(const std::string& str)
+{
+ return tolower(str.c_str());
+}
+
+inline std::string
+toupper(const char* str)
+{
+ std::string retval;
+
+ for (int lpc = 0; str[lpc]; lpc++) {
+ retval.push_back(::toupper(str[lpc]));
+ }
+
+ return retval;
+}
+
+inline std::string
+toupper(const std::string& str)
+{
+ return toupper(str.c_str());
+}
+
+inline ssize_t
+utf8_char_to_byte_index(const std::string& str, ssize_t ch_index)
+{
+ ssize_t retval = 0;
+
+ while (ch_index > 0) {
+ auto ch_len
+ = ww898::utf::utf8::char_size([&str, retval]() {
+ return std::make_pair(str[retval], str.length() - retval - 1);
+ }).unwrapOr(1);
+
+ retval += ch_len;
+ ch_index -= 1;
+ }
+
+ return retval;
+}
+
+inline Result<size_t, const char*>
+utf8_string_length(const char* str, ssize_t len = -1)
+{
+ size_t retval = 0;
+
+ if (len == -1) {
+ len = strlen(str);
+ }
+
+ for (ssize_t byte_index = 0; byte_index < len;) {
+ auto ch_size
+ = TRY(ww898::utf::utf8::char_size([str, len, byte_index]() {
+ return std::make_pair(str[byte_index], len - byte_index);
+ }));
+ byte_index += ch_size;
+ retval += 1;
+ }
+
+ return Ok(retval);
+}
+
+inline Result<size_t, const char*>
+utf8_string_length(const std::string& str)
+{
+ return utf8_string_length(str.c_str(), str.length());
+}
+
+bool is_url(const std::string& fn);
+
+bool is_blank(const std::string& str);
+
+size_t abbreviate_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);
+
+std::string center_str(const std::string& subject, size_t width);
+
+inline std::string
+on_blank(const std::string& str, const std::string& def)
+{
+ if (is_blank(str)) {
+ return def;
+ }
+
+ return str;
+}
+
+#endif
diff --git a/src/base/string_util.tests.cc b/src/base/string_util.tests.cc
new file mode 100644
index 0000000..98cf5c8
--- /dev/null
+++ b/src/base/string_util.tests.cc
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2021, 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 <iostream>
+
+#include "base/string_util.hh"
+
+#include "base/strnatcmp.h"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("endswith")
+{
+ std::string hw("hello");
+
+ CHECK(endswith(hw, "f") == false);
+ CHECK(endswith(hw, "lo") == true);
+}
+
+TEST_CASE("truncate_to")
+{
+ const std::string orig = "0123456789abcdefghijklmnopqrstuvwxyz";
+ std::string str;
+
+ truncate_to(str, 10);
+ CHECK(str == "");
+ str = "abc";
+ truncate_to(str, 10);
+ CHECK(str == "abc");
+ str = orig;
+ truncate_to(str, 10);
+ CHECK(str == "01234\u22efwxyz");
+ str = orig;
+ truncate_to(str, 1);
+ CHECK(str == "\u22ef");
+ str = orig;
+ truncate_to(str, 2);
+ CHECK(str == "\u22ef");
+ str = orig;
+ truncate_to(str, 3);
+ CHECK(str == "0\u22efz");
+ str = orig;
+ truncate_to(str, 4);
+ CHECK(str == "01\u22efz");
+ str = orig;
+ truncate_to(str, 5);
+ CHECK(str == "01\u22efyz");
+}
+
+TEST_CASE("strnatcmp")
+{
+ {
+ constexpr const char* n1 = "010";
+ constexpr const char* n2 = "020";
+
+ CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0);
+ }
+ {
+ constexpr const char* n1 = "2";
+ constexpr const char* n2 = "10";
+
+ CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0);
+ }
+}
diff --git a/src/base/strnatcmp.c b/src/base/strnatcmp.c
new file mode 100644
index 0000000..6773164
--- /dev/null
+++ b/src/base/strnatcmp.c
@@ -0,0 +1,302 @@
+/* -*- mode: c; c-file-style: "k&r" -*-
+
+ strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
+ Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+
+/* partial change history:
+ *
+ * 2004-10-10 mbp: Lift out character type dependencies into macros.
+ *
+ * Eric Sosman pointed out that ctype functions take a parameter whose
+ * value must be that of an unsigned int, even on platforms that have
+ * negative chars in their default char type.
+ */
+
+#include <assert.h>
+#include <ctype.h>
+
+#include "strnatcmp.h"
+
+
+/* These are defined as macros to make it easier to adapt this code to
+ * different characters types or comparison functions. */
+static inline int
+nat_isdigit(nat_char a)
+{
+ return isdigit((unsigned char) a);
+}
+
+
+static inline int
+nat_isspace(nat_char a)
+{
+ return isspace((unsigned char) a);
+}
+
+
+static inline nat_char
+nat_toupper(nat_char a)
+{
+ return toupper((unsigned char) a);
+}
+
+
+
+static int
+compare_right(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out)
+{
+ int bias = 0;
+
+ /* The longest run of digits wins. That aside, the greatest
+ value wins, but we can't know that it will until we've scanned
+ both numbers to know that they have the same magnitude, so we
+ remember it in BIAS. */
+ for (;; a++, b++, a_len--, b_len--, (*len_out)++) {
+ if (a_len == 0 && b_len == 0)
+ return bias;
+ if (a_len == 0)
+ return -1;
+ if (b_len == 0)
+ return 1;
+ if (!nat_isdigit(*a) && !nat_isdigit(*b))
+ return bias;
+ else if (!nat_isdigit(*a))
+ return -1;
+ else if (!nat_isdigit(*b))
+ return +1;
+ else if (*a < *b) {
+ if (!bias)
+ bias = -1;
+ } else if (*a > *b) {
+ if (!bias)
+ bias = +1;
+ } else if (!*a && !*b)
+ return bias;
+ }
+
+ return 0;
+}
+
+static int
+compare_left(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out)
+{
+ /* Compare two left-aligned numbers: the first to have a
+ different value wins. */
+ for (;; a++, b++, a_len--, b_len--, (*len_out)++) {
+ if (a_len == 0 && b_len == 0)
+ return 0;
+ if (a_len == 0)
+ return -1;
+ if (b_len == 0)
+ return 1;
+ if (!nat_isdigit(*a) && !nat_isdigit(*b))
+ return 0;
+ else if (!nat_isdigit(*a))
+ return -1;
+ else if (!nat_isdigit(*b))
+ return +1;
+ else if (*a < *b)
+ return -1;
+ else if (*a > *b)
+ return +1;
+ }
+
+ return 0;
+}
+
+static int strnatcmp0(int a_len, nat_char const *a,
+ int b_len, nat_char const *b,
+ int fold_case)
+{
+ int ai, bi;
+ nat_char ca, cb;
+ int fractional, result;
+
+ assert(a && b);
+ ai = bi = 0;
+ while (1) {
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+
+ /* skip over leading spaces or zeros */
+ while (nat_isspace(ca)) {
+ ai += 1;
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ }
+
+ while (nat_isspace(cb)) {
+ bi += 1;
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+ }
+
+ /* process run of digits */
+ if (nat_isdigit(ca) && nat_isdigit(cb)) {
+ int num_len = 0;
+
+ fractional = (ca == '0' || cb == '0');
+
+ if (fractional) {
+ if ((result = compare_left(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len)) != 0) {
+ return result;
+ }
+ } else {
+ if ((result = compare_right(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len)) != 0) {
+ return result;
+ }
+ }
+
+ ai += num_len;
+ bi += num_len;
+ continue;
+ }
+
+ if (!ca && !cb) {
+ /* The strings compare the same. Perhaps the caller
+ will want to call strcmp to break the tie. */
+ return 0;
+ }
+
+ if (fold_case) {
+ ca = nat_toupper(ca);
+ cb = nat_toupper(cb);
+ }
+
+ if (ca < cb)
+ return -1;
+ else if (ca > cb)
+ return +1;
+
+ ++ai;
+ ++bi;
+ }
+}
+
+int ipv4cmp(int a_len, nat_char const *a,
+ int b_len, nat_char const *b,
+ int *res_out)
+{
+ int ai, bi;
+ nat_char ca, cb;
+ int fractional, result = 0;
+
+ assert(a && b);
+ ai = bi = 0;
+ while (result == 0) {
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+
+ /* skip over leading spaces or zeros */
+ while (nat_isspace(ca)) {
+ ai += 1;
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ }
+
+ while (nat_isspace(cb)) {
+ bi += 1;
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+ }
+
+ /* process run of digits */
+ if (nat_isdigit(ca) && nat_isdigit(cb)) {
+ int num_len = 0;
+
+ fractional = (ca == '0' || cb == '0');
+
+ if (fractional) {
+ result = compare_left(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len);
+ } else {
+ result = compare_right(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len);
+ }
+
+ ai += num_len;
+ bi += num_len;
+ continue;
+ }
+
+ if (!ca && !cb) {
+ /* The strings compare the same. Perhaps the caller
+ will want to call strcmp to break the tie. */
+ *res_out = result;
+ return 1;
+ }
+
+ if (ca != '.' || cb != '.') {
+ return 0;
+ }
+
+ ++ai;
+ ++bi;
+ }
+
+ for (; ai < a_len; ai++) {
+ if (!isdigit((unsigned char)a[ai]) || a[ai] != '.') {
+ return 0;
+ }
+ }
+
+ for (; bi < b_len; bi++) {
+ if (!isdigit((unsigned char)b[bi]) || b[bi] != '.') {
+ return 0;
+ }
+ }
+
+ *res_out = result;
+ return 1;
+}
+
+int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b)
+{
+ return strnatcmp0(a_len, a, b_len, b, 0);
+}
+
+/* Compare, recognizing numeric string and ignoring case. */
+int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b)
+{
+ return strnatcmp0(a_len, a, b_len, b, 1);
+}
diff --git a/src/base/strnatcmp.h b/src/base/strnatcmp.h
new file mode 100644
index 0000000..24b0c53
--- /dev/null
+++ b/src/base/strnatcmp.h
@@ -0,0 +1,41 @@
+/* -*- mode: c; c-file-style: "k&r" -*-
+
+ strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
+ Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* CUSTOMIZATION SECTION
+ *
+ * You can change this typedef, but must then also change the inline
+ * functions in strnatcmp.c */
+typedef char nat_char;
+
+int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b);
+
+int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b);
+
+int ipv4cmp(int a_len, nat_char const *a, int b_len, nat_char const *b, int *res_out);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/base/test_base.cc b/src/base/test_base.cc
new file mode 100644
index 0000000..654cd57
--- /dev/null
+++ b/src/base/test_base.cc
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2021, 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 "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
diff --git a/src/base/time_util.cc b/src/base/time_util.cc
new file mode 100644
index 0000000..0d46107
--- /dev/null
+++ b/src/base/time_util.cc
@@ -0,0 +1,239 @@
+/**
+ * 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 time_util.cc
+ */
+
+#include <chrono>
+
+#include "time_util.hh"
+
+#include "config.h"
+
+namespace lnav {
+
+ssize_t
+strftime_rfc3339(
+ char* buffer, size_t buffer_size, lnav::time64_t tim, int millis, char sep)
+{
+ struct tm gmtm;
+ int year, month, index = 0;
+
+ secs2tm(tim, &gmtm);
+ year = gmtm.tm_year + 1900;
+ month = gmtm.tm_mon + 1;
+ buffer[index++] = '0' + ((year / 1000) % 10);
+ buffer[index++] = '0' + ((year / 100) % 10);
+ buffer[index++] = '0' + ((year / 10) % 10);
+ buffer[index++] = '0' + ((year / 1) % 10);
+ buffer[index++] = '-';
+ buffer[index++] = '0' + ((month / 10) % 10);
+ buffer[index++] = '0' + ((month / 1) % 10);
+ buffer[index++] = '-';
+ buffer[index++] = '0' + ((gmtm.tm_mday / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_mday / 1) % 10);
+ buffer[index++] = sep;
+ buffer[index++] = '0' + ((gmtm.tm_hour / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_hour / 1) % 10);
+ buffer[index++] = ':';
+ buffer[index++] = '0' + ((gmtm.tm_min / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_min / 1) % 10);
+ buffer[index++] = ':';
+ buffer[index++] = '0' + ((gmtm.tm_sec / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_sec / 1) % 10);
+ buffer[index++] = '.';
+ buffer[index++] = '0' + ((millis / 100) % 10);
+ buffer[index++] = '0' + ((millis / 10) % 10);
+ buffer[index++] = '0' + ((millis / 1) % 10);
+ buffer[index] = '\0';
+
+ return index;
+}
+
+}
+
+static time_t BAD_DATE = -1;
+
+time_t
+tm2sec(const struct tm* t)
+{
+ int year;
+ time_t days, secs;
+ const int dayoffset[12]
+ = {306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275};
+
+ year = t->tm_year;
+
+ if (year < 70) {
+ return BAD_DATE;
+ }
+ if ((sizeof(time_t) <= 4) && (year >= 138)) {
+ year = 137;
+ }
+
+ /* shift new year to 1st March in order to make leap year calc easy */
+
+ if (t->tm_mon < 2) {
+ year--;
+ }
+
+ /* Find number of days since 1st March 1900 (in the Gregorian calendar). */
+
+ days = year * 365 + year / 4 - year / 100 + (year / 100 + 3) / 4;
+ days += dayoffset[t->tm_mon] + t->tm_mday - 1;
+ days -= 25508; /* 1 jan 1970 is 25508 days since 1 mar 1900 */
+
+ secs = ((days * 24 + t->tm_hour) * 60 + t->tm_min) * 60 + t->tm_sec;
+
+ if (secs < 0) {
+ return BAD_DATE;
+ } /* must have overflowed */
+ else
+ {
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (t->tm_zone) {
+ secs -= t->tm_gmtoff;
+ }
+#endif
+ return secs;
+ } /* must be a valid time */
+}
+
+static const int SECSPERMIN = 60;
+static const int SECSPERHOUR = 60 * SECSPERMIN;
+static const int SECSPERDAY = 24 * SECSPERHOUR;
+static const int YEAR_BASE = 1900;
+static const int EPOCH_WDAY = 4;
+static const int DAYSPERWEEK = 7;
+static const int EPOCH_YEAR = 1970;
+
+#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0)
+
+static const int year_lengths[2] = {365, 366};
+
+const unsigned short int mon_yday[2][13] = {
+ /* Normal years. */
+ {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
+ /* Leap years. */
+ {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
+
+void
+secs2wday(const struct timeval& tv, struct tm* res)
+{
+ long days, rem;
+ time_t lcltime;
+
+ /* base decision about std/dst time on current time */
+ lcltime = tv.tv_sec;
+
+ days = ((long) lcltime) / SECSPERDAY;
+ rem = ((long) lcltime) % SECSPERDAY;
+ while (rem < 0) {
+ rem += SECSPERDAY;
+ --days;
+ }
+
+ /* compute day of week */
+ if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0) {
+ res->tm_wday += DAYSPERWEEK;
+ }
+}
+
+struct tm*
+secs2tm(lnav::time64_t tim, struct tm* res)
+{
+ long days, rem;
+ lnav::time64_t lcltime;
+ int y;
+ int yleap;
+ const unsigned short int* ip;
+
+ /* base decision about std/dst time on current time */
+ lcltime = tim;
+
+ days = ((long) lcltime) / SECSPERDAY;
+ rem = ((long) lcltime) % SECSPERDAY;
+ while (rem < 0) {
+ rem += SECSPERDAY;
+ --days;
+ }
+
+ /* compute hour, min, and sec */
+ res->tm_hour = (int) (rem / SECSPERHOUR);
+ rem %= SECSPERHOUR;
+ res->tm_min = (int) (rem / SECSPERMIN);
+ res->tm_sec = (int) (rem % SECSPERMIN);
+
+ /* compute day of week */
+ if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0)
+ res->tm_wday += DAYSPERWEEK;
+
+ /* compute year & day of year */
+ y = EPOCH_YEAR;
+ if (days >= 0) {
+ for (;;) {
+ yleap = isleap(y);
+ if (days < year_lengths[yleap])
+ break;
+ y++;
+ days -= year_lengths[yleap];
+ }
+ } else {
+ do {
+ --y;
+ yleap = isleap(y);
+ days += year_lengths[yleap];
+ } while (days < 0);
+ }
+
+ res->tm_year = y - YEAR_BASE;
+ res->tm_yday = days;
+ ip = mon_yday[isleap(y)];
+ for (y = 11; days < (long int) ip[y]; --y)
+ continue;
+ days -= ip[y];
+ res->tm_mon = y;
+ res->tm_mday = days + 1;
+
+ res->tm_isdst = 0;
+
+ return (res);
+}
+
+struct timeval
+exttm::to_timeval() const
+{
+ struct timeval retval;
+
+ retval.tv_sec = tm2sec(&this->et_tm);
+ retval.tv_usec = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds(this->et_nsec))
+ .count();
+
+ return retval;
+}
diff --git a/src/base/time_util.hh b/src/base/time_util.hh
new file mode 100644
index 0000000..ef9687f
--- /dev/null
+++ b/src/base/time_util.hh
@@ -0,0 +1,206 @@
+/**
+ * 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.
+ */
+
+#ifndef lnav_time_util_hh
+#define lnav_time_util_hh
+
+#include <inttypes.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "config.h"
+
+namespace lnav {
+
+using time64_t = uint64_t;
+
+ssize_t strftime_rfc3339(char* buffer,
+ size_t buffer_size,
+ lnav::time64_t tim,
+ int millis,
+ char sep = ' ');
+
+} // namespace lnav
+
+struct tm* secs2tm(lnav::time64_t tim, struct tm* res);
+/**
+ * Convert the time stored in a 'tm' struct into epoch time.
+ *
+ * @param t The 'tm' structure to convert to epoch time.
+ * @return The given time in seconds since the epoch.
+ */
+time_t tm2sec(const struct tm* t);
+void secs2wday(const struct timeval& tv, struct tm* res);
+
+inline time_t
+convert_log_time_to_local(time_t value)
+{
+ struct tm tm;
+
+ localtime_r(&value, &tm);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm.tm_zone = NULL;
+#endif
+ tm.tm_isdst = 0;
+ return tm2sec(&tm);
+}
+
+constexpr lnav::time64_t MAX_TIME_T = 4000000000LL;
+
+enum exttm_bits_t {
+ ETB_YEAR_SET,
+ ETB_MONTH_SET,
+ ETB_DAY_SET,
+ ETB_HOUR_SET,
+ ETB_MINUTE_SET,
+ ETB_SECOND_SET,
+ ETB_MACHINE_ORIENTED,
+ ETB_EPOCH_TIME,
+ ETB_MILLIS_SET,
+ ETB_MICROS_SET,
+ ETB_NANOS_SET,
+};
+
+enum exttm_flags_t {
+ ETF_YEAR_SET = (1UL << ETB_YEAR_SET),
+ ETF_MONTH_SET = (1UL << ETB_MONTH_SET),
+ ETF_DAY_SET = (1UL << ETB_DAY_SET),
+ ETF_HOUR_SET = (1UL << ETB_HOUR_SET),
+ ETF_MINUTE_SET = (1UL << ETB_MINUTE_SET),
+ ETF_SECOND_SET = (1UL << ETB_SECOND_SET),
+ ETF_MACHINE_ORIENTED = (1UL << ETB_MACHINE_ORIENTED),
+ ETF_EPOCH_TIME = (1UL << ETB_EPOCH_TIME),
+ ETF_MILLIS_SET = (1UL << ETB_MILLIS_SET),
+ ETF_MICROS_SET = (1UL << ETB_MICROS_SET),
+ ETF_NANOS_SET = (1UL << ETB_NANOS_SET),
+};
+
+struct exttm {
+ struct tm et_tm {};
+ int32_t et_nsec{0};
+ unsigned int et_flags{0};
+ long et_gmtoff{0};
+
+ exttm() { memset(&this->et_tm, 0, sizeof(this->et_tm)); }
+
+ bool operator==(const exttm& other) const
+ {
+ return memcmp(this, &other, sizeof(exttm)) == 0;
+ }
+
+ struct timeval to_timeval() const;
+};
+
+inline bool
+operator<(const struct timeval& left, time_t right)
+{
+ return left.tv_sec < right;
+}
+
+inline bool
+operator<(time_t left, const struct timeval& right)
+{
+ return left < right.tv_sec;
+}
+
+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;
+}
+
+inline bool
+operator==(const struct timeval& left, const struct timeval& right)
+{
+ return left.tv_sec == right.tv_sec || left.tv_usec == right.tv_usec;
+}
+
+inline struct timeval
+operator-(const struct timeval& lhs, const struct timeval& rhs)
+{
+ struct timeval diff;
+
+ timersub(&lhs, &rhs, &diff);
+ return diff;
+}
+
+typedef int64_t mstime_t;
+
+inline mstime_t
+getmstime()
+{
+ struct timeval tv;
+
+ gettimeofday(&tv, nullptr);
+
+ return tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL;
+}
+
+inline struct timeval
+current_timeval()
+{
+ struct timeval retval;
+
+ gettimeofday(&retval, nullptr);
+
+ return retval;
+}
+
+inline struct timespec
+current_timespec()
+{
+ struct timespec retval;
+
+ clock_gettime(CLOCK_REALTIME, &retval);
+
+ return retval;
+}
+
+inline time_t
+day_num(time_t ti)
+{
+ return ti / (24 * 60 * 60);
+}
+
+inline time_t
+hour_num(time_t ti)
+{
+ return ti / (60 * 60);
+}
+
+#endif