commit dd28afc1ef77169d04b34ae40aebe58ba67c984d Author: oleg Date: Thu Apr 9 11:00:31 2026 +0300 init commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..2686bc6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +Language: Cpp +ColumnLimit: 120 +IndentPPDirectives: AfterHash diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5eac261 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,136 @@ +cmake_minimum_required(VERSION 3.13) +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_MESSAGE_LOG_LEVEL TRACE) +set(CMAKE_VERBOSE_MAKEFILE OFF) +set(CMAKE_CXX_COMPILER /usr/bin/clang++ CACHE INTERNAL "cxx compiler") +option(BUILD_SHARED_LIBS "Build using shared libraries" OFF) + +include(FetchContent) +set(FETCHCONTENT_QUIET FALSE) + +FetchContent_Declare( + tpl + GIT_REPOSITORY https://gitlab.com/eidheim/tiny-process-library.git + GIT_TAG 8bbb5a211c5c9df8ee69301da9d22fb977b27dc1 + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(tpl) +FetchContent_Declare( + ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git + GIT_TAG v6.0.0 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + PATCH_COMMAND git apply --check ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-empty-container.patch 2>/dev/null && git apply ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-empty-container.patch || true COMMAND git apply --check ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-window.patch 2>/dev/null && git apply ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-window.patch || true +) + +FetchContent_MakeAvailable(ftxui) +FetchContent_Declare( + sqlite_modern + GIT_REPOSITORY https://github.com/SqliteModernCpp/sqlite_modern_cpp + GIT_TAG 6e3009973025e0016d5573529067714201338c80 + GIT_PROGRESS TRUE +) + +FetchContent_GetProperties(sqlite_modern) +if(NOT sqlite_modern_POPULATED) + FetchContent_Populate(sqlite_modern) +endif() + + +option(STATIC "Set to ON to build xlnt as a static library instead of a shared library" OFF) +FetchContent_Declare( + xlnt + GIT_REPOSITORY https://github.com/xlnt-community/xlnt.git + GIT_TAG e165887739147027e7fbab918280b88f9efa5ffb + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(xlnt) + +# Suppress warnings in xlnt +if(TARGET xlnt) + target_compile_options(xlnt PRIVATE + -Wno-unsafe-buffer-usage-in-libc-call + -Wno-unsafe-buffer-usage + -Wno-undefined-reinterpret-cast + -Wno-extra-semi-stmt + -Wno-sign-conversion + -Wno-old-style-cast + -Wno-switch-default + -Wno-nrvo + -Wno-reserved-identifier + -Wno-unused-but-set-variable + -Wno-missing-prototypes + -Wno-character-conversion + -Wno-implicit-int-float-conversion + -Wno-float-equal + -Wno-global-constructors + -Wno-unique-object-duplication + ) +endif() + +set(FMT_TEST OFF CACHE BOOL "" FORCE) +set(FMT_DOC OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 11.1.4 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(fmt) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + json + GIT_REPOSITORY https://github.com/nlohmann/json + GIT_TAG v3.12.0 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(json) +set(SPDLOG_FMT_EXTERNAL OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog + GIT_TAG v1.15.3 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(spdlog) + +project(canscope) +file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) +add_executable(${CMAKE_PROJECT_NAME} ${SOURCES}) +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/thirdparty + ${ftxui_SOURCE_DIR}/include + ${tpl_SOURCE_DIR}/include + ${xlnt_SOURCE_DIR}/include + ${fmt_SOURCE_DIR}/include + ${json_SOURCE_DIR}/include + ${sqlite_modern_SOURCE_DIR}/hdr +) + +target_link_directories(${CMAKE_PROJECT_NAME} + PRIVATE + ${ftxui_BINARY_DIR} + ${tpl_BINARY_DIR} + ${xlnt_BINARY_DIR} + ${fmt_BINARY_DIR} + ${json_BINARY_DIR} + #{sqlite_modern_BINARY_DIR} +) + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ftxui-component ftxui-screen ftxui-dom tiny-process-library xlnt sqlite3 spdlog::spdlog systemd z ${Boost_LIBRARIES}) +# target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC sqlite3 ${Boost_LIBRARIES}) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fd8b3c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# {canscope} + +CAN bus sniffer and SAE J1939 protocol analyzer. Reads CAN frames from an external process (e.g. `candump`), decodes them using a J1939 Digital Annex (xlsx), and presents results in an interactive terminal UI or as JSON output. + +## Features + +- **TUI mode** -- full-screen interactive terminal interface (FTXUI). Multiple display modes per CAN ID: deployed, brief, verbose, manual, little-endian +- **Headless mode** -- JSON output to stdout or file, for scripting and automation +- **Recording** -- decoded J1939 SPN values saved to SQLite database with gzip compression and batch flushing +- **J1939 decoding** -- PGN/SPN lookup, bit-level value extraction from payload +- **CAN playback** -- replay recorded CAN frames +- **Custom SPN configuration** -- per-parameter settings, parameter export +- **Real-time** -- 30 fps UI refresh + +## Build + +**Requirements:** +- clang++ with C++23 support +- CMake >= 3.13 +- System libraries: boost (signals2, spirit, phoenix), sqlite3, systemd, zlib + +The rest of the dependencies are fetched automatically via CMake FetchContent (FTXUI, tiny-process-library, sqlite_modern_cpp, xlnt, fmt, nlohmann/json, spdlog). + +```bash +cmake -B build -S . && cmake --build build -j$(nproc) +``` + +Binary: `build/canscope` + +## Usage + +```bash +# TUI mode (default) +./build/canscope -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx + +# Headless -- JSON to stdout +./build/canscope -hl -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx + +# Headless -- JSON to file +./build/canscope -hl -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx -of output.json + +# Record to SQLite database +./build/canscope -rec -db recording.db -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx + +# Record + TUI +./build/canscope -rec -db recording.db -tui -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx +``` + +### CLI flags + +| Flag | Long form | Description | +|------|-----------|-------------| +| `-j1939` | `--j1939-document` | **(required)** J1939 Digital Annex xlsx file | +| `-e` | `--execute-command` | Command to read CAN frames from (e.g. `"candump can0"`) | +| `-hl` | `--headless` | Headless mode (no TUI) | +| `-of` | `--output-file` | Output file path (headless mode) | +| `-rec` | `--record` | Record decoded values to SQLite | +| `-db` | `--database` | SQLite database path (required with `-rec`) | +| `-tui` | | Show TUI alongside recording | +| `-h` | `--help` | Show help | + +## Architecture + +``` +candump / other CAN source + | stdout + v + aggregator_task ──> shared JSON (mutex-protected) + | + diff_task (33ms) + | + "new_entry" signal + / \ + TUI headless/recorder +``` + +### Key components + +| File | Role | +|------|------| +| `main.cpp` | Entry point, CLI parsing (clipp), async task orchestration | +| `xlsx.cpp` | Parses J1939 xlsx into in-memory SQLite (pgns, spns, spn_fragments) | +| `can_frame.cpp` | Decodes CAN frame against SQLite DB, extracts SPN values | +| `parsers.hpp` | Boost.Spirit Qi grammars for J1939 field formats | +| `signals.hpp` | Type-safe signal map (boost::signals2) | +| `recorder.cpp` | SQLite recording with gzip compression | +| `mainform.cpp` | Root FTXUI component | +| `canid_unit.cpp` | Per-CAN-ID display component | +| `headless.cpp` | Headless JSON output handler | + +### Patterns + +- Components communicate through a type-safe signal map (`signals_map_t`) +- `nlohmann::json` as universal data interchange between all layers +- Factory functions via `extern` declarations instead of header includes +- `std::jthread` / `std::stop_token` for async task management +- SIGINT gracefully stops all background tasks diff --git a/src/.clangd b/src/.clangd new file mode 100644 index 0000000..e69de29 diff --git a/src/bitstream.hpp b/src/bitstream.hpp new file mode 100644 index 0000000..8482433 --- /dev/null +++ b/src/bitstream.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +class BitStream { + +public: + uint8_t *toByteArray() { return m_data_; } + + void openBytes(uint8_t *bytes, size_t size) { + m_data_ = bytes; + m_size_ = size; + } + + void open(size_t size) { m_data_ = reinterpret_cast(std::malloc(size)); } + void close() { std::free(m_data_); } + + void write(size_t index, size_t bits_size, size_t data) { + index += bits_size; + while (data) { + m_data_[index / UINT8_WIDTH] = chbit_(m_data_[index / UINT8_WIDTH], index % UINT8_WIDTH, data & 1u); + data >>= 1u; + index--; + } + } + + size_t read(size_t index, size_t bits_size) { + size_t dat = 0; + for (int32_t i = index; i < index + bits_size; i++) { + dat = dat * 2 + getbit_(m_data_[i / 8], i % 8); + } + + return dat; + } + +private: + bool getbit_(char x, int y) { return (x >> (7 - y)) & 1; } + size_t chbit_(size_t x, size_t i, bool v) { + if (v) { + return x | (1u << (7u - i)); + } + + return x & ~(1u << (7u - i)); + } + + uint8_t *m_data_; + size_t m_size_; +}; diff --git a/src/can_data.hpp b/src/can_data.hpp new file mode 100644 index 0000000..d6bc599 --- /dev/null +++ b/src/can_data.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Mutex protecting the J1939 SQLite database from concurrent access +extern std::mutex g_j1939_db_mtx; + +struct can_frame_data_s { + std::vector payload; + int32_t size = 0; +}; + +struct can_frame_diff_s { + bool is_new_interface = false; + bool is_new_canid = false; + std::vector payload_changed; +}; + +struct can_frame_update_s { + std::string iface; + std::string canid; + can_frame_data_s data; + can_frame_diff_s diff; + std::shared_ptr verbose; + std::shared_ptr brief; +}; + +// Convert verbose processFrame JSON to export format for a single CAN ID +nlohmann::json verboseToExportJson(const nlohmann::json &verbose); diff --git a/src/can_frame.cpp b/src/can_frame.cpp new file mode 100644 index 0000000..a485937 --- /dev/null +++ b/src/can_frame.cpp @@ -0,0 +1,144 @@ +#include "can_data.hpp" +#include + +#define FMT_HEADER_ONLY +#include +#include + +// For sqlite +#include "sqlite_modern_cpp.h" + +std::pair processFrame(sqlite::database &db, const std::string &iface, + const std::string &canid, const std::vector &data) { + nlohmann::json verbose, brief; + db << R"(SELECT pgn, pg_label, pg_acronym, pg_descr, edp, dp, pf, ps, pg_datalen, pg_priority FROM pgns WHERE pgn = ?;)" + << [&]() -> int32_t { + int32_t ret; + std::stringstream{} << std::hex << canid.substr(2, 4) >> ret; + return ret; + }() >> [&](int32_t pgn, const std::string &pg_label, const std::string &pg_acronym, const std::string &pg_descr, + int32_t edp, int32_t dp, int32_t pf, int32_t ps, int32_t pd_datalen, int32_t pg_priority) { + int32_t pgn_int; + std::stringstream{} << std::hex << canid.substr(2, 4) >> pgn_int; + + // Make header + verbose = { + {"Interface", iface}, {"Can ID", canid}, {"PGN", canid.substr(2, 4)}, {"PGN (integer)", pgn_int}, + {"Acronym", pg_acronym}, {"Label", pg_label}, {"Description", pg_descr}, + }; + + // Get SPNs + nlohmann::json::array_t spns_array; + db << R"(SELECT spn, spn_name, spn_pos, spn_length, resolution, offset, data_range, min_value, max_value, units, slot_id, slot_name, spn_type FROM spns WHERE pgn = ?;)" + << pgn_int >> + [&](int32_t spn, const std::string &spn_name, const std::string &spn_pos, int32_t spn_length, double resolution, + int32_t offset, const std::string &data_range, double min, double max, const std::string &unit, + const std::string &slot_id, const std::string &slot_name, const std::string &spn_type) { + nlohmann::json spn_json = { + {"SPN (integer)", spn}, + {"SPN name", spn_name}, + {"SPN position", spn_pos}, + {"SPN length (bits)", spn_length}, + {"Resolution", resolution}, + {"Offset", offset}, + {"Minimum value", min}, + {"Maximum value", max}, + {"Unit", unit}, + {"SLOT id", slot_id}, + {"SPN type", spn_type}, + }; + + // Get parts + size_t result = 0u, iter = 0u, total_size_bits = 0u; + nlohmann::json::array_t parts_array; + db << "SELECT byte_offset,bit_offset,size FROM spn_fragments WHERE spn = ? AND pgn = ?" << spn << pgn >> + [&, byte_array = data](int32_t byte_offset, int32_t bit_offset, int32_t size_bits) { + result <<= size_bits; + total_size_bits += size_bits; + for (uint32_t i = 0; i < ((size_bits / UINT8_WIDTH) + (size_bits % UINT8_WIDTH ? 1 : 0)); ++i) { + const uint8_t &byte = byte_array[byte_offset + i]; + result |= (((byte << (i * UINT8_WIDTH)) & + (size_bits % UINT8_WIDTH + ? ~((0xff << (size_bits % UINT8_WIDTH + bit_offset + i * UINT8_WIDTH)) | + ~(0xff << (bit_offset + i * UINT8_WIDTH))) + : (0xff << (size_bits % UINT8_WIDTH + bit_offset + i * UINT8_WIDTH)) | + ~(0xff << (bit_offset + i * UINT8_WIDTH)))) >> + (bit_offset)); + } + + parts_array.push_back(nlohmann::json::parse( + fmt::format(R"({{"{}":{{"byte_offset":{},"bit_offset":{},"size_bits":{},"parse_result":"{}"}}}})", + fmt::format("Fragment#{}", iter++), byte_offset, bit_offset, size_bits, + fmt::format("{:#x}", result)))); + }; + + double result_real = result * resolution + offset; + spn_json["Fragments"] = parts_array; + spn_json["Value"] = result_real; + spns_array.push_back(spn_json); + }; + + verbose["SPNs"] = spns_array; + }; + + if (!verbose.is_null()) { + brief = { + {"PGN", verbose["PGN"]}, + {"Label", verbose["Label"]}, + {"Acronym", verbose["Acronym"]}, + }; + + nlohmann::json::array_t spns_array; + for (const auto &spn : verbose["SPNs"]) { + spns_array.push_back(fmt::format("{}: {:.6g} {}", spn["SPN name"].get(), spn["Value"].get(), + spn["Unit"].get())); + } + + brief["SPNs"] = spns_array; + } + + return {verbose, brief}; +} + +nlohmann::json verboseToExportJson(const nlohmann::json &verbose) { + nlohmann::json::array_t spns; + + if (verbose.is_null() || !verbose.contains("SPNs")) return spns; + + std::string pgn = verbose.contains("PGN") ? verbose["PGN"].get() : ""; + + for (const auto &v : verbose["SPNs"]) { + nlohmann::json spn = { + {"name", v.value("SPN name", "")}, + {"offset", v.value("Offset", 0)}, + {"resolution", v.value("Resolution", 0.0)}, + {"max", v.value("Maximum value", 0.0)}, + {"min", v.value("Minimum value", 0.0)}, + {"pgn", pgn}, + {"value", v.value("Value", 0.0)}, + {"unit", v.value("Unit", "")}, + }; + + nlohmann::json::array_t frags; + if (v.contains("Fragments")) { + for (const auto &[k, frag] : v["Fragments"].items()) { + auto frag_key = fmt::format("Fragment#{}", k); + if (frag.contains(frag_key)) { + frags.push_back({{ + fmt::format("fragment#{}", k), + { + {"byte_pos", frag[frag_key].value("byte_offset", 0)}, + {"bit_pos", frag[frag_key].value("bit_offset", 0)}, + {"bit_size", frag[frag_key].value("size_bits", 0)}, + }, + }}); + } + } + } + + spn["fragments"] = std::move(frags); + spns.push_back(std::move(spn)); + } + + return spns; +} diff --git a/src/can_player_dialog.cpp b/src/can_player_dialog.cpp new file mode 100644 index 0000000..2d53683 --- /dev/null +++ b/src/can_player_dialog.cpp @@ -0,0 +1,692 @@ +#include "can_data.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define FMT_HEADER_ONLY +#include + +#include "signals.hpp" +#include "sqlite_modern_cpp.h" +#include "utils.hpp" +#include "json/expander.hpp" +#include "json/json.hpp" + +ftxui::Component makeCanPlayerDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &is_ready) { + + class Impl : public ftxui::ComponentBase { + public: + Impl(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &is_ready) { + static sqlite::database *database = nullptr; + static float canbus_player_focus_relative = 0; + + auto pgnContainer = ftxui::Container::Vertical({}); + + struct pgn_parameters_s { + bool selected, is_running, pinned, forward = false; + uint32_t pgn, priority, datalen; + std::string label, acronym, descr, address, ifname, period_ms; + std::vector payload; + std::string forward_canid; + + struct { + std::unique_ptr mtx; + std::future fut; + std::unique_ptr ss; + } concurrent; + }; + + struct spn_parameters_s { + struct fragment_s { + int32_t byte_offset, bit_offset, size; + }; + + bool checked, little_endian; + float resolution, offset, min, max, current; + std::string unit; + int32_t slider_percent; + size_t raw; + std::vector fragments; + struct pgn_parameters_s *pg_ref = nullptr; + }; + + static std::map pgs; + static std::set received_pgns; + static const auto send_frame = [](const pgn_parameters_s &pg) { + struct can_frame frame = {}; + + const int can_socket = ::socket(AF_CAN, SOCK_RAW, CAN_RAW); + if (can_socket < 0) { + return; + } + + ifreq ifr{}; + + std::strcpy(ifr.ifr_name, pg.ifname.c_str()); + if (::ioctl(can_socket, SIOCGIFINDEX, &ifr) < 0) { + ::close(can_socket); + return; + } + + sockaddr_can addr = {}; + addr.can_family = AF_CAN; + addr.can_ifindex = ifr.ifr_ifindex; + ::setsockopt(can_socket, SOL_CAN_RAW, CAN_RAW_FILTER, nullptr, 0); + + if (::bind(can_socket, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(can_socket); + return; + } + + uint32_t address; + std::stringstream{} << std::hex << pg.address >> address; + + frame.can_id = ((pg.priority & 0x7u) << 26u) | ((pg.pgn & 0x3FFFFu) << 8u) | (address & 0xFFu); + frame.can_dlc = pg.datalen; + frame.can_id |= CAN_EFF_FLAG; + std::memcpy(frame.data, pg.payload.data(), pg.payload.size()); + ::write(can_socket, &frame, sizeof(frame)); + ::close(can_socket); + }; + + static const auto calculate_spn = [](spn_parameters_s &spn_params) { + // Calculate value from slider percentage + spn_params.current = + ((spn_params.slider_percent / 100.0f) * (spn_params.max - spn_params.min)) + spn_params.offset; + + // Round and clamp by min/max + spn_params.current = std::clamp(std::round(spn_params.current), spn_params.min, spn_params.max); + spn_params.raw = (spn_params.current - spn_params.offset) / spn_params.resolution; + auto raw = spn_params.raw; + + // Swap bytes if needed + if (spn_params.little_endian) { + // Get size + size_t size = 0; + for (const auto &frag : spn_params.fragments) { + size += frag.size; + } + + size /= UINT8_WIDTH; + if (size > 1) { + auto swapped = std::shared_ptr(new uint8_t[size], [](auto *p) { delete[] p; }); + for (size_t i = 0; i < size; ++i) { + swapped.get()[i] = reinterpret_cast(&raw)[size - i - 1]; + } + + raw = *reinterpret_cast(swapped.get()); + } + } + + // Magic here + { + std::lock_guard lock(*spn_params.pg_ref->concurrent.mtx); + for (const auto &fragment : spn_params.fragments) { + for (int32_t i = 0; i < fragment.size / UINT8_WIDTH + (fragment.size % UINT8_WIDTH ? 1 : 0); i++) { + + uint8_t &byte = spn_params.pg_ref->payload[i + fragment.byte_offset]; + + // Reset bits in this byte depending on fragment size and fragment bit offset + byte &= fragment.size % UINT8_WIDTH + ? (static_cast(0xffu << (fragment.size % UINT8_WIDTH + fragment.bit_offset)) | + static_cast(~(0xffu << fragment.bit_offset))) + : 0x00u; + byte |= static_cast((raw >> (i * UINT8_WIDTH)) << fragment.bit_offset); + } + + raw >>= fragment.size; + } + } + }; + + static const auto stop_pg = [](pgn_parameters_s &pg) { + if (pg.is_running) { + pg.concurrent.ss->request_stop(); + pg.concurrent.fut.wait(); + pg.concurrent.ss = std::make_unique(); + pg.is_running = false; + } + }; + + // Static connections + { + static struct on_stopped_connection_s { + on_stopped_connection_s(signals_map_t &smap) { + smap.get("canplayer_stopped")->connect([]() { + for (auto &[_, pg] : pgs) { + stop_pg(pg); + } + }); + } + } on_stopped_connection(smap); + + static struct forward_connection_s { + forward_connection_s(signals_map_t &smap) { + smap.get &)>("new_entries_batch") + ->connect([](const std::vector &batch) { + for (const auto &entry : batch) { + uint32_t pgn_num = 0; + if (entry.canid.size() >= 6) { + auto pgn_str = entry.canid.substr(entry.canid.size() >= 8 ? entry.canid.size() - 6 : 2, 4); + std::stringstream ss; + ss << std::hex << pgn_str; + ss >> pgn_num; + } + received_pgns.insert(pgn_num); + if (pgs.contains(pgn_num) && pgs[pgn_num].forward && pgs[pgn_num].is_running) { + auto &pg = pgs[pgn_num]; + std::lock_guard lock(*pg.concurrent.mtx); + pg.payload = entry.data.payload; + pg.payload.resize(pg.datalen, 0); + pg.forward_canid = entry.canid; + send_frame(pg); + } + } + }); + } + } forward_connection(smap); + + static struct database_ready_connection_s { + database_ready_connection_s(signals_map_t &smap, ftxui::ScreenInteractive *scr, ftxui::Component pgnContainer, + bool &is_ready) { + smap.get("j1939_database_ready") + ->connect([scr, &is_ready, pgnContainer](sqlite::database &db) { + scr->Post([scr, &is_ready, pgnContainer, &db]() { + std::lock_guard db_lock(g_j1939_db_mtx); + db << "SELECT pgn, pg_label, pg_acronym, pg_descr, pg_datalen, pg_priority FROM pgns" >> + [&, pgnContainer](uint32_t pgn, const std::string &label, const std::string &acronym, + const std::string &descr, uint32_t datalen, uint32_t priority) { + if (!pgs.contains(pgn)) { + pgs.insert(std::pair{ + pgn, + pgn_parameters_s{ + .selected = false, + .pinned = false, + .pgn = pgn, + .priority = priority, + .datalen = datalen, + .label = label, + .acronym = acronym, + .descr = descr, + .address = "0xFF", + .ifname = "vcan0", + .period_ms = "1000", + .payload = std::vector(datalen), + .concurrent = + { + .mtx = std::make_unique(), + .ss = std::make_unique(), + }, + }, + }); + } + + auto &pg_ref = pgs[pgn]; + auto spnContainer = ftxui::Container::Vertical({}); + db << fmt::format("SELECT id, pgn, spn, spn_name FROM spns WHERE pgn = {};", pgn) >> + [spnContainer, &db](int32_t id, int32_t pgn, int32_t spn, const std::string &spn_name) { + static std::map spns; + db << fmt::format( + "SELECT min_value, max_value, resolution, offset, units FROM spns WHERE spn = {};", + spn) >> + [&](float min, float max, float resolution, float offset, const std::string &unit) { + db << fmt::format("SELECT COUNT(*) FROM spn_fragments WHERE spn = {}", spn) >> + [&](int32_t count) { + std::vector fragments; + fragments.resize(count); + auto *fragment_ptr = fragments.data(); + + // Fill fragments array + db << fmt::format("SELECT byte_offset, bit_offset, size FROM spn_fragments " + "WHERE spn = {}", + spn) >> + [&fragment_ptr](int32_t byte_offset, int32_t bit_offset, int32_t size) { + *(fragment_ptr++) = { + .byte_offset = byte_offset, + .bit_offset = bit_offset, + .size = size, + }; + }; + + spns.insert_or_assign(spn, spn_parameters_s{ + .checked = false, + .little_endian = false, + .resolution = resolution, + .offset = offset, + .min = min, + .max = max, + .current = 0.0f, + .unit = unit, + .slider_percent = 0, + .fragments = fragments, + .pg_ref = &pgs[pgn], + }); + }; + }; + + auto &spn_params = spns[spn]; + spnContainer->Add({ + ftxui::Container::Horizontal({ + ftxui::Container::Vertical({ + ftxui::Checkbox({ + .checked = &spns[spn].checked, + .transform = + [spn_name](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::separatorEmpty(), + ftxui::text(state.state ? "▼ " : "▶ "), + ftxui::text(spn_name), + }) | + (state.focused + ? (ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11)) + : ftxui::nothing) | + ftxui::flex; + }, + }), + + ftxui::Maybe( + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + ftxui::Elements separators(4u, ftxui::separatorEmpty()); + return ftxui::hbox(separators); + }), + + ftxui::Container::Vertical({ + ftxui::Slider(ftxui::SliderOption{ + .value = &spn_params.slider_percent, + .min = 0, + .max = 100, + .increment = 1, + .on_change = + [&spn_params = + spns[spn]]() { calculate_spn(spn_params); }, + }) | ftxui::Renderer([](ftxui::Element inner) { + return ftxui::hbox({ + ftxui::text("Value: ") | ftxui::bold | + ftxui::color(ftxui::Color::Yellow), + ftxui::text("["), + inner, + ftxui::text("]"), + }) | + ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 100u); + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + return ftxui::text("Endianness: ") | ftxui::bold | + ftxui::color(ftxui::Color::Yellow); + }), + + ftxui::Checkbox({ + .checked = &spn_params.little_endian, + .transform = + [&spn_params](const ftxui::EntryState &state) { + auto el = ftxui::hbox({ + ftxui::text("<"), + ftxui::text("little") | + (spn_params.little_endian + ? (ftxui::bold | + ftxui::color(ftxui::Color::Red)) + : ftxui::nothing), + ftxui::text(" | "), + ftxui::text("big") | + (!spn_params.little_endian + ? (ftxui::bold | + ftxui::color(ftxui::Color::Red)) + : ftxui::nothing), + ftxui::text(">"), + }); + + if (state.focused || state.active) { + el = el | ftxui::bold | + ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + .on_change = + [&spn_params = spns[spn]]() { + calculate_spn(spn_params); + }, + }), + }), + + ftxui::Renderer([]() { + return ftxui::vbox({ + ftxui::separatorEmpty(), + ftxui::text("SPN info:") | + ftxui::color(ftxui::Color::Cyan) | ftxui::bold, + }); + }), + + From( + [spn]() -> nlohmann::json { + auto &spn_params = spns[spn]; + auto fragments = nlohmann::json::array({}); + + for (const auto &frag : spn_params.fragments) { + fragments.push_back( + nlohmann::json{{"byte_offset", frag.byte_offset}, + {"bit_offset", frag.bit_offset}, + {"size", frag.size}}); + } + + return { + {"fragments", fragments}, + {"min", spn_params.min}, + {"max", spn_params.max}, + {"resolution", spn_params.resolution}, + {"offset", spn_params.offset}, + }; + }(), + false, -100, ExpanderImpl::Root()) | + ftxui::Renderer([](ftxui::Element inner) { + return ftxui::hbox({ + ftxui::separatorEmpty(), + ftxui::separatorEmpty(), + inner, + }); + }), + + ftxui::Renderer([]() { return ftxui::separatorEmpty(); }), + ftxui::Renderer([&spn_params = spns[spn]]() { + return ftxui::vbox({ + ftxui::hbox({ + ftxui::text("Value: ") | ftxui::bold | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(fmt::format("{} {}", spn_params.current, + spn_params.unit)), + }), + + ftxui::hbox({ + ftxui::text("Raw: ") | ftxui::bold | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(fmt::format( + "{} (hex:{}) (bin:{})", spn_params.raw, + fmt::format("{0:#x}", spn_params.raw), + fmt::format("{0:#b}", spn_params.raw))), + }), + + ftxui::hbox({ + ftxui::text("PG payload: ") | ftxui::bold | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(fmt::format( + "[{}]", + [&]() { + std::string ret; + for (const auto &byte : + spn_params.pg_ref->payload) { + ret += fmt::format("{0:#010b} ", byte); + } + + return ret; + }())), + }), + + ftxui::separatorEmpty(), + }); + }), + }), + }), + + &spns[spn].checked), + }), + }), + }); + }; + + pgnContainer->Add(ftxui::Container::Vertical({ + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .checked = &pg_ref.selected, + .transform = [&pg_ref = + pgs[pgn]](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::text(state.state ? "▼ " : "▶ "), + ftxui::text(fmt::format("0x{:x} - {}", pg_ref.pgn, pg_ref.label)) | + (pg_ref.is_running ? ftxui::color(ftxui::Color::Green) + : ftxui::nothing), + ftxui::filler(), + }) | + (state.focused ? (ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11)) + : ftxui::nothing) | + ftxui::flex; + }, + }), + }), + + ftxui::Maybe( + ftxui::Container::Vertical({ + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::text(" >[send_frame]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) + : ftxui::nothing), + }); + }, + + .on_change = + [&pg = pgs[pgn]]() { + std::lock_guard lock(*pg.concurrent.mtx); + send_frame(pg); + }, + }), + + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[run]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) + : ftxui::nothing); + }, + + .on_change = + [&pg = pgs[pgn]]() { + if (!pg.is_running) { + pg.concurrent.fut = std::async( + std::launch::async, + [&pg](std::stop_token st) { + int32_t period_ms; + std::stringstream{} << pg.period_ms >> period_ms; + period_ms = std::clamp(period_ms, 50, INT32_MAX); + + while (!st.stop_requested()) { + { + std::lock_guard lock(*pg.concurrent.mtx); + send_frame(pg); + } + + std::this_thread::sleep_for( + std::chrono::milliseconds(period_ms)); + } + }, + + pg.concurrent.ss->get_token()); + pg.is_running = true; + } + }, + }), + + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[stop]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) + : ftxui::nothing); + }, + + .on_change = [&pg = pgs[pgn]]() { stop_pg(pg); }, + }), + }), + + ftxui::Renderer([]() { return ftxui::separator(); }), + ftxui::Maybe( + ftxui::Checkbox({ + .checked = &pg_ref.forward, + .transform = [&pg_ref](const ftxui::EntryState &state) -> ftxui::Element { + auto el = + ftxui::text(pg_ref.forward ? " [X] forward " : " [ ] forward ") | + ftxui::color(pg_ref.forward ? ftxui::Color::Green + : ftxui::Color::Cyan); + if (state.focused || state.active) + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + return el; + }, + }), + + [pgn]() { return received_pgns.contains(pgn); }), + ftxui::Renderer([]() { return ftxui::separatorEmpty(); }), + + ftxui::Input({ + .content = &pg_ref.address, + .placeholder = "0xFF", + .multiline = false, + }) | ftxui::Renderer([](ftxui::Element inner) { + return ftxui::hbox({ + ftxui::separatorEmpty(), + ftxui::text("Address (hex): ") | ftxui::color(ftxui::Color::Magenta) | + ftxui::bold, + ftxui::hbox({ + inner, + ftxui::filler(), + }), + }); + }), + + ftxui::Input({ + .content = &pg_ref.ifname, + .placeholder = "vcan0", + .multiline = false, + }) | ftxui::Renderer([](ftxui::Element inner) { + return ftxui::hbox({ + ftxui::separatorEmpty(), + ftxui::text("CAN interface name: ") | ftxui::color(ftxui::Color::Magenta) | + ftxui::bold, + ftxui::hbox({ + inner, + ftxui::filler(), + }), + }); + }), + + ftxui::Input({ + .content = &pg_ref.period_ms, + .placeholder = "1000", + .multiline = false, + }) | ftxui::Renderer([](ftxui::Element inner) { + return ftxui::hbox({ + ftxui::separatorEmpty(), + ftxui::text("Send period (ms): ") | ftxui::color(ftxui::Color::Magenta) | + ftxui::bold, + ftxui::hbox({ + inner, + ftxui::filler(), + }), + }); + }), + + ftxui::Renderer([]() { return ftxui::separatorEmpty(); }), + spnContainer, + }) | ftxui::border, + &pg_ref.selected), + })); + }; + + database = &db; + is_ready = true; + scr->Post(ftxui::Event::Custom); + }); + }); + } + } database_ready_connection(smap, scr, pgnContainer, is_ready); + } + + auto main = ftxui::Container::Vertical({ + (pgnContainer | ftxui::Renderer([](ftxui::Element inner) { + return inner | ftxui::focusPositionRelative(0, canbus_player_focus_relative) | ftxui::vscroll_indicator | + ftxui::frame | ftxui::flex; + })) | + ftxui::CatchEvent([pgnContainer](ftxui::Event event) { + static const auto increment_focus = [pgnContainer = pgnContainer]() { + if (auto n = pgnContainer->ChildCount(); n > 0) + canbus_player_focus_relative = + std::clamp(canbus_player_focus_relative + 1.0f / static_cast(n), 0.0f, 1.0f); + }; + + static const auto decrement_focus = [pgnContainer = pgnContainer]() { + if (auto n = pgnContainer->ChildCount(); n > 0) + canbus_player_focus_relative = + std::clamp(canbus_player_focus_relative - 1.0f / static_cast(n), 0.0f, 1.0f); + }; + + if (!database) { + return true; + } + + if (event.is_mouse()) { + switch (static_cast(event.mouse().button)) { + case ftxui::Mouse::Button::WheelDown: { + increment_focus(); + return true; + } break; + + case ftxui::Mouse::Button::WheelUp: { + decrement_focus(); + return true; + } break; + + default: + break; + } + } else if (!event.is_character()) { + if (event == ftxui::Event::ArrowDown) { + increment_focus(); + return true; + } else if (event == ftxui::Event::ArrowUp) { + decrement_focus(); + return true; + } + } + + return false; + }), + }); + + Add({main}); + } + }; + + return ftxui::Make(scr, smap, is_ready); +} diff --git a/src/canid_unit.cpp b/src/canid_unit.cpp new file mode 100644 index 0000000..0d34314 --- /dev/null +++ b/src/canid_unit.cpp @@ -0,0 +1,657 @@ +// #include +// #include +#include +#include +#include +#include +#include +#include + +#include "canid_unit.hpp" +#include "process.hpp" +#include "tagsettings.hpp" +#include "json/json.hpp" +#include +#include + +// For sqlite +// #include "sqlite_modern_cpp.h" +#include "src/json/expander.hpp" + +extern ftxui::Component makeSpnSettingsForm(ftxui::ScreenInteractive *, signals_map_t &, const std::string &, + std::string &, ftxui::Component, ftxui::Component, bool &, + std::map> &, + spn_settings_map_t &); + +ftxui::Component makeCanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol, + size_t &spn_count, const std::vector &data, ftxui::ScreenInteractive *screen, + signals_map_t &smap, ftxui::Component content, ftxui::Component canids_container, + ftxui::Component spn_settings_dialog, ftxui::Component cansettings_dialog, + bool is_deployed, bool is_verbose, bool is_brief, bool is_manual, + std::string &canid_active, bool &custom_spn_settings_shown, + bool &canbus_parameters_export_shown, bool &filedialog_shown, + std::map> &spnSettingsFormMap, + spn_settings_map_t &spnSettingsMap) { + return ftxui::Make(iface, canid, protocol, spn_count, data, screen, smap, content, canids_container, + spn_settings_dialog, cansettings_dialog, is_deployed, is_verbose, is_brief, is_manual, + canid_active, custom_spn_settings_shown, canbus_parameters_export_shown, + filedialog_shown, spnSettingsFormMap, spnSettingsMap); +} + +CanIDUnit::CanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol, size_t &spn_count, + const std::vector &data, ftxui::ScreenInteractive *screen, signals_map_t &smap, + ftxui::Component content, ftxui::Component canids_container, ftxui::Component spn_settings_dialog, + ftxui::Component cansettings_dialog, bool is_deployed, bool is_verbose, bool is_brief, + bool is_manual, std::string &canid_active, bool &custom_spn_settings_shown, + bool &canbus_parameters_export_shown, bool &filedialog_shown, + std::map> &spnSettingsFormMap, + spn_settings_map_t &spnSettingsMap) + + : m_canid_(canid), m_iface_(iface), m_data_(data), m_deployed_(is_deployed), m_verbose_(is_verbose), + m_brief_(is_brief), m_manual_mode_(is_manual), + m_spnSettingsForm_(makeSpnSettingsForm(screen, smap, canid, canid_active, canids_container, spn_settings_dialog, + custom_spn_settings_shown, spnSettingsFormMap, spnSettingsMap)) { + + m_cansettings_dialog_ = cansettings_dialog; + m_spnSettingsMap_ = &spnSettingsMap; + + m_brief_content_ = ftxui::Container::Vertical({}); + m_verbose_content_ = ftxui::Container::Vertical({}); + + auto arrow = ftxui::Checkbox({ + .checked = &m_deployed_, + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::text(m_deployed_ ? "▼ " : "▶ "), + }); + }, + + .on_change = [&, this]() { canid_active = m_canid_; }, + }); + + auto contentbox = ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + ftxui::Elements line; + + // Interface + line.push_back(ftxui::text(m_iface_ + " ") | + (m_diff_.is_new_interface ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) + : ftxui::color(ftxui::Color::Aquamarine1) | ftxui::bold)); + + // CAN ID + line.push_back(ftxui::text(fmt::format("{:8} ", m_canid_)) | + (m_diff_.is_new_canid ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) + : ftxui::color(ftxui::Color::GreenLight) | ftxui::bold)); + + // Size + line.push_back(ftxui::text(fmt::format("{} ", m_data_.size()))); + + // Padding for < 8 bytes + for (size_t i = m_data_.size(); i < 8; ++i) + line.push_back(ftxui::text("---- ")); + + // Payload bytes with diff highlighting + bool has_updates = false; + for (size_t idx = 0; idx < m_data_.size(); ++idx) { + bool changed = idx < m_diff_.payload_changed.size() && m_diff_.payload_changed[idx]; + + if (changed) { + has_updates = true; + } + + line.push_back(ftxui::text(fmt::format("0x{:02X} ", m_data_[idx])) | + (changed ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) : ftxui::nothing)); + } + + // Last update time + line.push_back( + ftxui::text(fmt::format("(updated: {})", m_last_update_time_)) | + (has_updates ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) : ftxui::color(ftxui::Color::Cyan))); + + auto row = ftxui::hbox(std::move(line)); + if (m_hovered_) { + row = row | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return row | ftxui::reflect(m_box_); + }, + + .on_change = + [&canid_active, this]() { + canid_active = m_canid_; + m_deployed_ = !m_deployed_; + }, + }); + + auto label = ftxui::Renderer([this, protocol]() -> ftxui::Element { + if (m_data_verbose_->contains("Label")) { + + return ftxui::hbox({ + ftxui::text(fmt::format(" - {}", (*m_data_verbose_)["Label"].get())) | + ftxui::color(ftxui::Color::Magenta) | (m_deployed_ ? ftxui::bold : ftxui::nothing), + ftxui::filler(), + ftxui::text(fmt::format("{}", protocol.empty() ? "Unknown" : protocol)) | ftxui::color(ftxui::Color::Red), + }); + } else { + return ftxui::text(""); + } + }); + + Add({ + ftxui::Container::Vertical({ + ftxui::Container::Horizontal({ + arrow, + contentbox, + label | ftxui::flex, + }) | ftxui::flex, + + ftxui::Maybe( + ftxui::Container::Vertical({ + + // Tab switcher: + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(" "); }), + ftxui::Checkbox({ + .checked = &m_brief_, + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::hbox({ + ftxui::text("<"), + ftxui::text("brief") | (m_brief_ ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(" | "), + }); + + if (state.focused || state.active) + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + return el; + }, + + .on_change = + [this]() { + m_brief_ = true; + m_verbose_ = false; + m_manual_mode_ = false; + }, + }), + + ftxui::Checkbox({ + .checked = &m_verbose_, + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::text("verbose") | (m_verbose_ ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan); + + if (state.focused || state.active) { + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + + .on_change = + [this]() { + m_brief_ = false; + m_verbose_ = true; + m_manual_mode_ = false; + }, + }), + + ftxui::Checkbox({ + .checked = &m_manual_mode_, + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::hbox({ + ftxui::text(" | "), + ftxui::text("manual") | (m_manual_mode_ ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(">"), + }); + + if (state.focused || state.active) { + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + + .on_change = + [this]() { + m_brief_ = false; + m_verbose_ = false; + m_manual_mode_ = true; + }, + }), + + }), + + // Brief content + ftxui::Maybe(m_brief_content_ | ftxui::flex, &m_brief_), + + // Verbose content + ftxui::Maybe(m_verbose_content_ | ftxui::flex, &m_verbose_), + + // Manual mode content + ftxui::Maybe(m_spnSettingsForm_ | ftxui::flex, &m_manual_mode_), + + }) | ftxui::border, + + &m_deployed_), + }), + }); +} + +bool CanIDUnit::OnEvent(ftxui::Event event) { + static auto log = spdlog::systemd_logger_mt("canidunit", "cansniffer-hover"); + log->set_level(spdlog::level::debug); + + if (event.is_mouse()) { + bool prev = m_hovered_; + m_hovered_ = m_box_.Contain(event.mouse().x, event.mouse().y); + if (m_hovered_ != prev) { + log->debug("{}: hover={} mouse=({},{}) box=({},{},{},{})", m_canid_, m_hovered_, event.mouse().x, event.mouse().y, + m_box_.x_min, m_box_.x_max, m_box_.y_min, m_box_.y_max); + } + } + + return ftxui::ComponentBase::OnEvent(event); +} + +void CanIDUnit::update(const can_frame_data_s &data, const can_frame_diff_s &diff, + std::shared_ptr verbose, std::shared_ptr brief) { + m_data_ = data.payload; + m_diff_ = diff; + + // Update timestamp + auto t = std::time(nullptr); + struct tm tm_buf; + localtime_r(&t, &tm_buf); + char buf[32]; + std::strftime(buf, sizeof(buf), "%d-%m-%Y %H-%M-%S", &tm_buf); + m_last_update_time_ = buf; + + bool was_null = m_data_short_->is_null(); + + if (verbose) { + *m_data_verbose_ = std::move(*verbose); + } + + if (brief) { + *m_data_short_ = std::move(*brief); + } + + // Ensure skeleton verbose/brief exist for custom SPN injection + if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) && !(*m_spnSettingsMap_)[m_canid_].empty()) { + if (m_data_verbose_->is_null()) { + *m_data_verbose_ = nlohmann::json{{"SPNs", nlohmann::json::array()}}; + } + + if (m_data_short_->is_null()) { + *m_data_short_ = nlohmann::json{{"SPNs", nlohmann::json::array()}}; + } + + if (!m_data_verbose_->contains("SPNs")) { + (*m_data_verbose_)["SPNs"] = nlohmann::json::array(); + } + + if (!m_data_short_->contains("SPNs")) { + (*m_data_short_)["SPNs"] = nlohmann::json::array(); + } + + for (const auto &[tag_id, settings] : (*m_spnSettingsMap_)[m_canid_]) { + double resolution = 1.0, offset_val = 0.0; + try { + resolution = std::stod(settings.resolution.empty() ? settings.x_coeff : settings.resolution); + } catch (...) { + } + + try { + offset_val = std::stod(settings.offset); + } catch (...) { + } + + int64_t result = 0; + size_t total_bits = 0; + for (const auto &frag : settings.fragments) { + int32_t bo = 0, bi = 0, bc = 0; + + try { + bo = std::stoi(frag.byte_offset); + } catch (...) { + } + + try { + bi = std::stoi(frag.bit_offset); + } catch (...) { + } + + try { + bc = std::stoi(frag.bit_count); + } catch (...) { + } + + int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH; + if (bc > 0 && bo >= 0 && static_cast(bo + byte_cnt) <= m_data_.size()) { + int64_t frag_val = 0; + + for (int32_t i = 0; i < byte_cnt; ++i) { + frag_val |= static_cast(m_data_[bo + i]) << (i * UINT8_WIDTH); + } + + frag_val = (frag_val >> bi) & ((1LL << bc) - 1); + result |= frag_val << total_bits; + total_bits += bc; + } + } + if (settings.big_endian && total_bits > 8) { + int64_t swapped = 0; + size_t total_bytes = (total_bits + 7) / 8; + for (size_t i = 0; i < total_bytes; ++i) + swapped |= ((result >> (i * 8)) & 0xFF) << ((total_bytes - 1 - i) * 8); + result = swapped; + } + double spn_val = static_cast(result) * resolution + offset_val; + + nlohmann::json::array_t frags_json; + for (size_t fi = 0; fi < settings.fragments.size(); ++fi) { + const auto &f = settings.fragments[fi]; + int32_t bo = 0, bi = 0, bc = 0; + + try { + bo = std::stoi(f.byte_offset); + } catch (...) { + } + + try { + bi = std::stoi(f.bit_offset); + } catch (...) { + } + + try { + bc = std::stoi(f.bit_count); + } catch (...) { + } + + frags_json.push_back( + {{fmt::format("Fragment#{}", fi), {{"byte_offset", bo}, {"bit_offset", bi}, {"size_bits", bc}}}}); + } + + nlohmann::json custom_spn = { + {"SPN (integer)", -static_cast(tag_id)}, + {"SPN name", settings.spn_name + " (custom)"}, + {"Value", spn_val}, + {"Unit", settings.unit}, + {"Resolution", resolution}, + {"Offset", offset_val}, + {"Fragments", frags_json}, + }; + + if (m_data_verbose_->contains("SPNs")) { + (*m_data_verbose_)["SPNs"].push_back(custom_spn); + } + + if (!m_data_short_->is_null() && m_data_short_->contains("SPNs")) { + (*m_data_short_)["SPNs"].push_back( + fmt::format("{}: {:.6g} {}", settings.spn_name + " (custom)", spn_val, settings.unit)); + } + } + } + + // Rebuild FromLive when structure changes (first data or SPN count changed) + size_t verbose_spn_count = + (!m_data_verbose_->is_null() && m_data_verbose_->contains("SPNs")) ? (*m_data_verbose_)["SPNs"].size() : 0; + size_t brief_spn_count = + (!m_data_short_->is_null() && m_data_short_->contains("SPNs")) ? (*m_data_short_)["SPNs"].size() : 0; + bool structure_changed = verbose_spn_count != m_last_verbose_spn_count_ || brief_spn_count != m_last_brief_spn_count_; + + if (structure_changed || (was_null && !m_data_short_->is_null())) { + m_brief_content_->DetachAllChildren(); + if (!m_data_short_->is_null()) { + m_brief_content_->Add(FromLive(m_data_short_, nlohmann::json::json_pointer(), true, -100, ExpanderImpl::Root())); + } + + m_verbose_content_->DetachAllChildren(); + if (!m_data_verbose_->is_null()) { + m_verbose_content_->Add( + FromLive(m_data_verbose_, nlohmann::json::json_pointer(), true, -100, ExpanderImpl::Root())); + } + + m_last_verbose_spn_count_ = verbose_spn_count; + m_last_brief_spn_count_ = brief_spn_count; + } + + // Populate export dialog once when verbose data first becomes available + if (!m_data_short_->is_null() && !m_data_verbose_->is_null() && m_cansettings_dialog_) { + if (!s_canbus_parameters_export_map_.contains(m_canid_)) { + s_canbus_parameters_export_map_.insert({m_canid_, {false, false, {}}}); + } + + if (!std::get<1u>(s_canbus_parameters_export_map_[m_canid_])) { + std::get<1u>(s_canbus_parameters_export_map_[m_canid_]) = true; + m_export_selectors_ = ftxui::Container::Vertical({}); + auto &selectors_container = m_export_selectors_; + + for (const auto &[pgn_key, pgn_val] : m_data_verbose_->items()) { + if (pgn_key == "SPNs") { + for (const auto &[spns_arr_k, spns_arr_v] : pgn_val.items()) { + for (const auto &[spn_k, spn_v] : spns_arr_v.items()) { + if (spn_k == "SPN name") { + std::string spn_name = spn_v.get(); + if (spn_name.find("(custom)") != std::string::npos) + continue; + auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]); + + spn_map.insert_or_assign(spn_name, std::make_tuple(false, false, spns_arr_v)); + selectors_container + ->Add( + ftxui::Container::Vertical( + { + ftxui::Container::Horizontal( + { + ftxui::Checkbox({ + .checked = &std::get<0u>(spn_map[spn_name]), + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(), + ftxui::text(state.state ? "▼ " : "▶ ")}); + }, + }), + + ftxui::Checkbox( + { + .checked = &std::get<1u>(spn_map[spn_name]), + .transform = + [spn_name](const ftxui::EntryState state) { + return ftxui::hbox({ + ftxui::text( + fmt::format("[{}] ", state.state ? "X" : "")) | + (state.state ? ftxui::color(ftxui::Color::Red) + : ftxui::color(ftxui::Color::Cyan)), + ftxui::text(spn_name), + }) | + (state.focused ? ftxui::bold : ftxui::nothing) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) + : ftxui::nothing) | + ftxui::flex; + }, + }), + }), + + ftxui::Maybe( + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(), + ftxui::separatorEmpty(), ftxui::separatorEmpty()}); + }), + FromLive(m_data_verbose_, nlohmann::json::json_pointer("/SPNs/" + spns_arr_k), + false, -100, ExpanderImpl::Root()), + }), + &std::get<0u>(spn_map[spn_name])), + })); + } + } + } + } + } + + auto container = ftxui::Container::Vertical({}); + container->Add(ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .checked = &std::get<0u>(s_canbus_parameters_export_map_[m_canid_]), + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox( + {ftxui::text(std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) ? "▼ " : "▶ ")}); + }, + }), + + ftxui::Checkbox({ + .checked = &std::get<0u>(s_canbus_parameters_export_map_[m_canid_]), + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]); + size_t selected_cnt = std::count_if(spn_map.begin(), spn_map.end(), + [](auto &e) -> bool { return std::get<1u>(e.second); }); + std::string label = + m_data_short_->contains("Label") ? (*m_data_short_)["Label"].get() : ""; + + return ftxui::hbox({ + ftxui::text(fmt::format("{:8}", fmt::format("[{}/{}] ", selected_cnt, spn_map.size()))) | + (state.focused ? ftxui::bold : ftxui::nothing) | ftxui::color(ftxui::Color::LightGreen), + ftxui::text(fmt::format("{} ", m_iface_)) | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan), + ftxui::text(fmt::format("{} ", m_canid_)) | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::LightGreen), + ftxui::text(fmt::format("- {} ", label)) | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Magenta), + ftxui::filler(), + ftxui::text("J1939 ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Red), + }) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing) | ftxui::flex; + }, + }), + })); + + // SPN list with border if not empty + container->Add(ftxui::Maybe(selectors_container | ftxui::border, [selectors_container, this]() -> bool { + return std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) && selectors_container->ChildCount() > 0; + })); + + // Add empty separator if PG deployed and empty + container->Add(ftxui::Maybe(ftxui::Renderer([]() -> ftxui::Element { return ftxui::separatorEmpty(); }), + [selectors_container, this]() -> bool { + return std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) && + !selectors_container->ChildCount(); + })); + + m_cansettings_dialog_->Add(container); + } + + // Add custom SPNs to export dialog if not already present (tracked by tag_id) + if (m_export_selectors_ && m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_)) { + auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]); + for (const auto &[tag_id, settings] : (*m_spnSettingsMap_)[m_canid_]) { + std::string key = fmt::format("__custom_{}", tag_id); + if (!spn_map.contains(key)) { + nlohmann::json custom_data = {{"SPN name", key}}; + + // Find matching SPN entry in verbose JSON + nlohmann::json spn_verbose; + if (m_data_verbose_->contains("SPNs")) { + for (const auto &spn_entry : (*m_data_verbose_)["SPNs"]) { + if (spn_entry.contains("SPN name") && + spn_entry["SPN name"].get().find("(custom)") != std::string::npos) { + std::string name_in_verbose = spn_entry["SPN name"].get(); + std::string expected = settings.spn_name.empty() ? " (custom)" : settings.spn_name + " (custom)"; + + if (name_in_verbose == expected) { + spn_verbose = spn_entry; + break; + } + } + } + } + + spn_map.insert_or_assign(key, + std::make_tuple(false, false, spn_verbose.is_null() ? custom_data : spn_verbose)); + m_export_selectors_ + ->Add( + ftxui::Container::Vertical( + { + ftxui::Container::Horizontal( + { + ftxui::Checkbox({ + .checked = &std::get<0u>(spn_map[key]), + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(), + ftxui::text(state.state ? "▼ " : "▶ ")}); + }, + }), + + ftxui::Checkbox( + { + .checked = &std::get<1u>(spn_map[key]), + .transform = + [this, tag_id](const ftxui::EntryState state) { + std::string display_name = "custom"; + if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) && + (*m_spnSettingsMap_)[m_canid_].contains(tag_id)) { + auto &s = (*m_spnSettingsMap_)[m_canid_][tag_id]; + display_name = s.spn_name.empty() ? fmt::format("custom_{}", tag_id) + : s.spn_name + " (custom)"; + } + + return ftxui::hbox({ + ftxui::text(fmt::format("[{}] ", state.state ? "X" : "")) | + (state.state ? ftxui::color(ftxui::Color::Red) + : ftxui::color(ftxui::Color::Cyan)), + ftxui::text(display_name), + }) | + (state.focused ? ftxui::bold : ftxui::nothing) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) + : ftxui::nothing) | + ftxui::flex; + }, + }), + }), + + ftxui::Maybe(ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(), + ftxui::separatorEmpty(), ftxui::separatorEmpty()}); + }), + [this, tag_id]() -> ftxui::Component { + auto wrapper = ftxui::Container::Vertical({}); + m_export_custom_containers_[tag_id] = {wrapper, 0}; + return wrapper; + }(), + }), + + &std::get<0u>(spn_map[key])), + })); + } + } + } + + // Rebuild FromLive for custom SPN export entries when fragment count changes + if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) && !m_data_verbose_->is_null() && m_data_verbose_->contains("SPNs")) { + for (auto &[tag_id, pair] : m_export_custom_containers_) { + auto &[wrapper, prev_frag_count] = pair; + size_t cur_frag_count = 0; + if ((*m_spnSettingsMap_)[m_canid_].contains(tag_id)) { + cur_frag_count = (*m_spnSettingsMap_)[m_canid_][tag_id].fragments.size(); + } + if (cur_frag_count != prev_frag_count) { + wrapper->DetachAllChildren(); + std::string search_name; + if ((*m_spnSettingsMap_)[m_canid_].contains(tag_id)) { + search_name = (*m_spnSettingsMap_)[m_canid_][tag_id].spn_name + " (custom)"; + } + const auto &spns = (*m_data_verbose_)["SPNs"]; + for (size_t i = 0; i < spns.size(); ++i) { + if (spns[i].contains("SPN name") && spns[i]["SPN name"].get() == search_name) { + wrapper->Add(FromLive(m_data_verbose_, nlohmann::json::json_pointer("/SPNs/" + std::to_string(i)), false, -100, ExpanderImpl::Root())); + break; + } + } + prev_frag_count = cur_frag_count; + } + } + } + } +} diff --git a/src/canid_unit.hpp b/src/canid_unit.hpp new file mode 100644 index 0000000..7df4473 --- /dev/null +++ b/src/canid_unit.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#define FMT_HEADER_ONLY +#include + +#include "signals.hpp" +#include "tagsettings.hpp" +#include +#include +#include + +class CanIDUnit : public ftxui::ComponentBase { +public: + CanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol, size_t &spn_count, const std::vector &data, ftxui::ScreenInteractive *screen, + signals_map_t &smap, ftxui::Component content, ftxui::Component container, ftxui::Component spn_settings_dialog, ftxui::Component cansettings_dialog, bool is_deployed, + bool is_verbose, bool is_brief, bool is_manual, std::string &, bool &, bool &canbus_parameters_export_shown, bool &filedialog_shown, + std::map> &spnSettingsFormMap, + spn_settings_map_t &spnSettingsMap); + + inline const std::string &getIfaceName() const { return m_iface_; } + inline const std::string &getCanID() const { return m_canid_; } + inline size_t getDataSize() const { return m_data_.size(); } + inline const std::vector &getData() const { return m_data_; } + + inline bool getDeployed() const { return m_deployed_; } + inline bool getVerbose() const { return m_verbose_; } + inline bool getBrief() const { return m_brief_; } + inline bool getManual() const { return m_manual_mode_; } + inline ftxui::Component getSpnSettingsForm() { return m_spnSettingsForm_; } + inline const auto &getParametersExportMap() const { return s_canbus_parameters_export_map_; } + + void update(const can_frame_data_s &data, const can_frame_diff_s &diff, + std::shared_ptr verbose, std::shared_ptr brief); + + bool OnEvent(ftxui::Event event) override; + +private: + const ftxui::Component m_spnSettingsForm_; + const std::string m_canid_, m_iface_; + std::vector m_data_; + mutable bool m_deployed_ = false, m_verbose_ = false, m_brief_ = true, m_manual_mode_ = false; + can_frame_diff_s m_diff_; + std::string m_last_update_time_; + bool m_hovered_ = false; + ftxui::Box m_box_ = {}; + std::shared_ptr m_data_verbose_ = std::make_shared(nullptr); + std::shared_ptr m_data_short_ = std::make_shared(nullptr); + ftxui::Component m_brief_content_, m_verbose_content_; + ftxui::Component m_cansettings_dialog_; + spn_settings_map_t *m_spnSettingsMap_ = nullptr; + size_t m_last_verbose_spn_count_ = 0; + size_t m_last_brief_spn_count_ = 0; + ftxui::Component m_export_selectors_; + std::map> m_export_custom_containers_; + + static inline std::map< + /* canid */ std::string, + std::tuple>>> + s_canbus_parameters_export_map_ = {}; +}; diff --git a/src/cansettingsexportdialog.cpp b/src/cansettingsexportdialog.cpp new file mode 100644 index 0000000..0b3c924 --- /dev/null +++ b/src/cansettingsexportdialog.cpp @@ -0,0 +1,41 @@ +// #include "src/canid_unit.hpp" +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include + +#include "signals.hpp" +#include +#include + +ftxui::Component makeCanSettingsExportDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, ftxui::Component canids, bool &shown, bool &file_export_shown) { + class Impl : public ftxui::ComponentBase { + public: + Impl(ftxui::ScreenInteractive *scr, signals_map_t &smap, ftxui::Component canids, bool &shown, bool &file_export_shown) { + auto cmps = ftxui::Container::Vertical({}); + + for (uint32_t i = 0; i < canids->ChildCount(); ++i) { + cmps->Add(canids->ChildAt(i)); + } + + Add({ + ftxui::Container::Vertical({ + cmps, + + ftxui::Container::Horizontal({ + ftxui::Button({.label = "Export", .on_click = [&file_export_shown]() { file_export_shown = true; }}), + ftxui::Button({.label = "Close", .on_click = [&shown]() { shown = false; }}), + }), + }), + }); + } + }; + + return ftxui::Make(scr, smap, canids, shown, file_export_shown); +} diff --git a/src/clipp.hpp b/src/clipp.hpp new file mode 100644 index 0000000..38ad7a5 --- /dev/null +++ b/src/clipp.hpp @@ -0,0 +1,7023 @@ +/***************************************************************************** + * ___ _ _ ___ ___ + * | _|| | | | | _ \ _ \ CLIPP - command line interfaces for modern C++ + * | |_ | |_ | | | _/ _/ version 1.2.3 + * |___||___||_| |_| |_| https://github.com/muellan/clipp + * + * Licensed under the MIT License . + * Copyright (c) 2017-2018 André Müller + * + * --------------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef AM_CLIPP_H__ +#define AM_CLIPP_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/*************************************************************************//** + * + * @brief primary namespace + * + *****************************************************************************/ +namespace clipp { + + + +/***************************************************************************** + * + * basic constants and datatype definitions + * + *****************************************************************************/ +using arg_index = int; + +using arg_string = std::string; +using doc_string = std::string; + +using arg_list = std::vector; + + + +/*************************************************************************//** + * + * @brief tristate + * + *****************************************************************************/ +enum class tri : char { no, yes, either }; + +inline constexpr bool operator == (tri t, bool b) noexcept { + return b ? t != tri::no : t != tri::yes; +} +inline constexpr bool operator == (bool b, tri t) noexcept { return (t == b); } +inline constexpr bool operator != (tri t, bool b) noexcept { return !(t == b); } +inline constexpr bool operator != (bool b, tri t) noexcept { return !(t == b); } + + + +/*************************************************************************//** + * + * @brief (start,size) index range + * + *****************************************************************************/ +class subrange { +public: + using size_type = arg_string::size_type; + + /** @brief default: no match */ + explicit constexpr + subrange() noexcept : + at_{arg_string::npos}, length_{0} + {} + + /** @brief match length & position within subject string */ + explicit constexpr + subrange(size_type pos, size_type len) noexcept : + at_{pos}, length_{len} + {} + + /** @brief position of the match within the subject string */ + constexpr size_type at() const noexcept { return at_; } + /** @brief length of the matching subsequence */ + constexpr size_type length() const noexcept { return length_; } + + /** @brief returns true, if query string is a prefix of the subject string */ + constexpr bool prefix() const noexcept { + return at_ == 0; + } + + /** @brief returns true, if query is a substring of the query string */ + constexpr explicit operator bool () const noexcept { + return at_ != arg_string::npos; + } + +private: + size_type at_; + size_type length_; +}; + + + +/*************************************************************************//** + * + * @brief match predicates + * + *****************************************************************************/ +using match_predicate = std::function; +using match_function = std::function; + + + + + + +/*************************************************************************//** + * + * @brief type traits (NOT FOR DIRECT USE IN CLIENT CODE!) + * no interface guarantees; might be changed or removed in the future + * + *****************************************************************************/ +namespace traits { + +/*************************************************************************//** + * + * @brief function (class) signature type trait + * + *****************************************************************************/ +template +constexpr auto +check_is_callable(int) -> decltype( + std::declval()(std::declval()...), + std::integral_constant::type>::value>{} ); + +template +constexpr auto +check_is_callable(long) -> std::false_type; + +template +constexpr auto +check_is_callable_without_arg(int) -> decltype( + std::declval()(), + std::integral_constant::type>::value>{} ); + +template +constexpr auto +check_is_callable_without_arg(long) -> std::false_type; + + + +template +constexpr auto +check_is_void_callable(int) -> decltype( + std::declval()(std::declval()...), std::true_type{}); + +template +constexpr auto +check_is_void_callable(long) -> std::false_type; + +template +constexpr auto +check_is_void_callable_without_arg(int) -> decltype( + std::declval()(), std::true_type{}); + +template +constexpr auto +check_is_void_callable_without_arg(long) -> std::false_type; + + + +template +struct is_callable; + + +template +struct is_callable : + decltype(check_is_callable(0)) +{}; + +template +struct is_callable : + decltype(check_is_callable_without_arg(0)) +{}; + + +template +struct is_callable : + decltype(check_is_void_callable(0)) +{}; + +template +struct is_callable : + decltype(check_is_void_callable_without_arg(0)) +{}; + + + +/*************************************************************************//** + * + * @brief input range type trait + * + *****************************************************************************/ +template +constexpr auto +check_is_input_range(int) -> decltype( + begin(std::declval()), end(std::declval()), + std::true_type{}); + +template +constexpr auto +check_is_input_range(char) -> decltype( + std::begin(std::declval()), std::end(std::declval()), + std::true_type{}); + +template +constexpr auto +check_is_input_range(long) -> std::false_type; + +template +struct is_input_range : + decltype(check_is_input_range(0)) +{}; + + + +/*************************************************************************//** + * + * @brief size() member type trait + * + *****************************************************************************/ +template +constexpr auto +check_has_size_getter(int) -> + decltype(std::declval().size(), std::true_type{}); + +template +constexpr auto +check_has_size_getter(long) -> std::false_type; + +template +struct has_size_getter : + decltype(check_has_size_getter(0)) +{}; + +} // namespace traits + + + + + + +/*************************************************************************//** + * + * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) + * no interface guarantees; might be changed or removed in the future + * + *****************************************************************************/ +namespace detail { + + +/*************************************************************************//** + * @brief forwards string to first non-whitespace char; + * std string -> unsigned conv yields max value, but we want 0; + * also checks for nullptr + *****************************************************************************/ +inline bool +fwd_to_unsigned_int(const char*& s) +{ + if(!s) return false; + for(; std::isspace(*s); ++s); + if(!s[0] || s[0] == '-') return false; + if(s[0] == '-') return false; + return true; +} + + +/*************************************************************************//** + * + * @brief value limits clamping + * + *****************************************************************************/ +template sizeof(T))> +struct limits_clamped { + static T from(const V& v) { + if(v >= V(std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + if(v <= V(std::numeric_limits::lowest())) { + return std::numeric_limits::lowest(); + } + return T(v); + } +}; + +template +struct limits_clamped { + static T from(const V& v) { return T(v); } +}; + + +/*************************************************************************//** + * + * @brief returns value of v as a T, clamped at T's maximum + * + *****************************************************************************/ +template +inline T clamped_on_limits(const V& v) { + return limits_clamped::from(v); +} + + + + +/*************************************************************************//** + * + * @brief type conversion helpers + * + *****************************************************************************/ +template +struct make { + static inline T from(const char* s) { + if(!s) return false; + //a conversion from const char* to / must exist + return static_cast(s); + } +}; + +template<> +struct make { + static inline bool from(const char* s) { + if(!s) return false; + return static_cast(s); + } +}; + +template<> +struct make { + static inline unsigned char from(const char* s) { + if(!fwd_to_unsigned_int(s)) return (0); + return clamped_on_limits(std::strtoull(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline unsigned short int from(const char* s) { + if(!fwd_to_unsigned_int(s)) return (0); + return clamped_on_limits(std::strtoull(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline unsigned int from(const char* s) { + if(!fwd_to_unsigned_int(s)) return (0); + return clamped_on_limits(std::strtoull(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline unsigned long int from(const char* s) { + if(!fwd_to_unsigned_int(s)) return (0); + return clamped_on_limits(std::strtoull(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline unsigned long long int from(const char* s) { + if(!fwd_to_unsigned_int(s)) return (0); + return clamped_on_limits(std::strtoull(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline char from(const char* s) { + //parse as single character? + const auto n = std::strlen(s); + if(n == 1) return s[0]; + //parse as integer + return clamped_on_limits(std::strtoll(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline short int from(const char* s) { + return clamped_on_limits(std::strtoll(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline int from(const char* s) { + return clamped_on_limits(std::strtoll(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline long int from(const char* s) { + return clamped_on_limits(std::strtoll(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline long long int from(const char* s) { + return (std::strtoll(s,nullptr,10)); + } +}; + +template<> +struct make { + static inline float from(const char* s) { + return (std::strtof(s,nullptr)); + } +}; + +template<> +struct make { + static inline double from(const char* s) { + return (std::strtod(s,nullptr)); + } +}; + +template<> +struct make { + static inline long double from(const char* s) { + return (std::strtold(s,nullptr)); + } +}; + +template<> +struct make { + static inline std::string from(const char* s) { + return std::string(s); + } +}; + + + +/*************************************************************************//** + * + * @brief assigns boolean constant to one or multiple target objects + * + *****************************************************************************/ +template +class assign_value +{ +public: + template + explicit constexpr + assign_value(T& target, X&& value) noexcept : + t_{std::addressof(target)}, v_{std::forward(value)} + {} + + void operator () () const { + if(t_) *t_ = v_; + } + +private: + T* t_; + V v_; +}; + + + +/*************************************************************************//** + * + * @brief flips bools + * + *****************************************************************************/ +class flip_bool +{ +public: + explicit constexpr + flip_bool(bool& target) noexcept : + b_{&target} + {} + + void operator () () const { + if(b_) *b_ = !*b_; + } + +private: + bool* b_; +}; + + + +/*************************************************************************//** + * + * @brief increments using operator ++ + * + *****************************************************************************/ +template +class increment +{ +public: + explicit constexpr + increment(T& target) noexcept : t_{std::addressof(target)} {} + + void operator () () const { + if(t_) ++(*t_); + } + +private: + T* t_; +}; + + + +/*************************************************************************//** + * + * @brief decrements using operator -- + * + *****************************************************************************/ +template +class decrement +{ +public: + explicit constexpr + decrement(T& target) noexcept : t_{std::addressof(target)} {} + + void operator () () const { + if(t_) --(*t_); + } + +private: + T* t_; +}; + + + +/*************************************************************************//** + * + * @brief increments by a fixed amount using operator += + * + *****************************************************************************/ +template +class increment_by +{ +public: + explicit constexpr + increment_by(T& target, T by) noexcept : + t_{std::addressof(target)}, by_{std::move(by)} + {} + + void operator () () const { + if(t_) (*t_) += by_; + } + +private: + T* t_; + T by_; +}; + + + + +/*************************************************************************//** + * + * @brief makes a value from a string and assigns it to an object + * + *****************************************************************************/ +template +class map_arg_to +{ +public: + explicit constexpr + map_arg_to(T& target) noexcept : t_{std::addressof(target)} {} + + void operator () (const char* s) const { + if(t_ && s) *t_ = detail::make::from(s); + } + +private: + T* t_; +}; + + +//------------------------------------------------------------------- +/** + * @brief specialization for vectors: append element + */ +template +class map_arg_to> +{ +public: + map_arg_to(std::vector& target): t_{std::addressof(target)} {} + + void operator () (const char* s) const { + if(t_ && s) t_->push_back(detail::make::from(s)); + } + +private: + std::vector* t_; +}; + + +//------------------------------------------------------------------- +/** + * @brief specialization for bools: + * set to true regardless of string content + */ +template<> +class map_arg_to +{ +public: + map_arg_to(bool& target): t_{&target} {} + + void operator () (const char* s) const { + if(t_ && s) *t_ = true; + } + +private: + bool* t_; +}; + + +} // namespace detail + + + + + + +/*************************************************************************//** + * + * @brief string matching and processing tools + * + *****************************************************************************/ + +namespace str { + + +/*************************************************************************//** + * + * @brief converts string to value of target type 'T' + * + *****************************************************************************/ +template +T make(const arg_string& s) +{ + return detail::make::from(s); +} + + + +/*************************************************************************//** + * + * @brief removes trailing whitespace from string + * + *****************************************************************************/ +template +inline void +trimr(std::basic_string& s) +{ + if(s.empty()) return; + + s.erase( + std::find_if_not(s.rbegin(), s.rend(), + [](char c) { return std::isspace(c);} ).base(), + s.end() ); +} + + +/*************************************************************************//** + * + * @brief removes leading whitespace from string + * + *****************************************************************************/ +template +inline void +triml(std::basic_string& s) +{ + if(s.empty()) return; + + s.erase( + s.begin(), + std::find_if_not(s.begin(), s.end(), + [](char c) { return std::isspace(c);}) + ); +} + + +/*************************************************************************//** + * + * @brief removes leading and trailing whitespace from string + * + *****************************************************************************/ +template +inline void +trim(std::basic_string& s) +{ + triml(s); + trimr(s); +} + + +/*************************************************************************//** + * + * @brief removes all whitespaces from string + * + *****************************************************************************/ +template +inline void +remove_ws(std::basic_string& s) +{ + if(s.empty()) return; + + s.erase(std::remove_if(s.begin(), s.end(), + [](char c) { return std::isspace(c); }), + s.end() ); +} + + +/*************************************************************************//** + * + * @brief returns true, if the 'prefix' argument + * is a prefix of the 'subject' argument + * + *****************************************************************************/ +template +inline bool +has_prefix(const std::basic_string& subject, + const std::basic_string& prefix) +{ + if(prefix.size() > subject.size()) return false; + return subject.find(prefix) == 0; +} + + +/*************************************************************************//** + * + * @brief returns true, if the 'postfix' argument + * is a postfix of the 'subject' argument + * + *****************************************************************************/ +template +inline bool +has_postfix(const std::basic_string& subject, + const std::basic_string& postfix) +{ + if(postfix.size() > subject.size()) return false; + return (subject.size() - postfix.size()) == subject.find(postfix); +} + + + +/*************************************************************************//** +* +* @brief returns longest common prefix of several +* sequential random access containers +* +* @details InputRange require begin and end (member functions or overloads) +* the elements of InputRange require a size() member +* +*****************************************************************************/ +template +auto +longest_common_prefix(const InputRange& strs) + -> typename std::decay::type +{ + static_assert(traits::is_input_range(), + "parameter must satisfy the InputRange concept"); + + static_assert(traits::has_size_getter< + typename std::decay::type>(), + "elements of input range must have a ::size() member function"); + + using std::begin; + using std::end; + + using item_t = typename std::decay::type; + using str_size_t = typename std::decaysize())>::type; + + const auto n = size_t(distance(begin(strs), end(strs))); + if(n < 1) return item_t(""); + if(n == 1) return *begin(strs); + + //length of shortest string + auto m = std::min_element(begin(strs), end(strs), + [](const item_t& a, const item_t& b) { + return a.size() < b.size(); })->size(); + + //check each character until we find a mismatch + for(str_size_t i = 0; i < m; ++i) { + for(str_size_t j = 1; j < n; ++j) { + if(strs[j][i] != strs[j-1][i]) + return strs[0].substr(0, i); + } + } + return strs[0].substr(0, m); +} + + + +/*************************************************************************//** + * + * @brief returns longest substring range that could be found in 'arg' + * + * @param arg string to be searched in + * @param substrings range of candidate substrings + * + *****************************************************************************/ +template +subrange +longest_substring_match(const std::basic_string& arg, + const InputRange& substrings) +{ + using string_t = std::basic_string; + + static_assert(traits::is_input_range(), + "parameter must satisfy the InputRange concept"); + + static_assert(std::is_same::type>(), + "substrings must have same type as 'arg'"); + + auto i = string_t::npos; + auto n = string_t::size_type(0); + for(const auto& s : substrings) { + auto j = arg.find(s); + if(j != string_t::npos && s.size() > n) { + i = j; + n = s.size(); + } + } + return subrange{i,n}; +} + + + +/*************************************************************************//** + * + * @brief returns longest prefix range that could be found in 'arg' + * + * @param arg string to be searched in + * @param prefixes range of candidate prefix strings + * + *****************************************************************************/ +template +subrange +longest_prefix_match(const std::basic_string& arg, + const InputRange& prefixes) +{ + using string_t = std::basic_string; + using s_size_t = typename string_t::size_type; + + static_assert(traits::is_input_range(), + "parameter must satisfy the InputRange concept"); + + static_assert(std::is_same::type>(), + "prefixes must have same type as 'arg'"); + + auto i = string_t::npos; + auto n = s_size_t(0); + for(const auto& s : prefixes) { + auto j = arg.find(s); + if(j == 0 && s.size() > n) { + i = 0; + n = s.size(); + } + } + return subrange{i,n}; +} + + + +/*************************************************************************//** + * + * @brief returns the first occurrence of 'query' within 'subject' + * + *****************************************************************************/ +template +inline subrange +substring_match(const std::basic_string& subject, + const std::basic_string& query) +{ + if(subject.empty() && query.empty()) return subrange(0,0); + if(subject.empty() || query.empty()) return subrange{}; + auto i = subject.find(query); + if(i == std::basic_string::npos) return subrange{}; + return subrange{i,query.size()}; +} + + + +/*************************************************************************//** + * + * @brief returns first substring match (pos,len) within the input string + * that represents a number + * (with at maximum one decimal point and digit separators) + * + *****************************************************************************/ +template +subrange +first_number_match(std::basic_string s, + C digitSeparator = C(','), + C decimalPoint = C('.'), + C exponential = C('e')) +{ + using string_t = std::basic_string; + + str::trim(s); + if(s.empty()) return subrange{}; + + auto i = s.find_first_of("0123456789+-"); + if(i == string_t::npos) { + i = s.find(decimalPoint); + if(i == string_t::npos) return subrange{}; + } + + bool point = false; + bool sep = false; + auto exp = string_t::npos; + auto j = i + 1; + for(; j < s.size(); ++j) { + if(s[j] == digitSeparator) { + if(!sep) sep = true; else break; + } + else { + sep = false; + if(s[j] == decimalPoint) { + //only one decimal point before exponent allowed + if(!point && exp == string_t::npos) point = true; else break; + } + else if(std::tolower(s[j]) == std::tolower(exponential)) { + //only one exponent separator allowed + if(exp == string_t::npos) exp = j; else break; + } + else if(exp != string_t::npos && (exp+1) == j) { + //only sign or digit after exponent separator + if(s[j] != '+' && s[j] != '-' && !std::isdigit(s[j])) break; + } + else if(!std::isdigit(s[j])) { + break; + } + } + } + + //if length == 1 then must be a digit + if(j-i == 1 && !std::isdigit(s[i])) return subrange{}; + + return subrange{i,j-i}; +} + + + +/*************************************************************************//** + * + * @brief returns first substring match (pos,len) + * that represents an integer (with optional digit separators) + * + *****************************************************************************/ +template +subrange +first_integer_match(std::basic_string s, + C digitSeparator = C(',')) +{ + using string_t = std::basic_string; + + str::trim(s); + if(s.empty()) return subrange{}; + + auto i = s.find_first_of("0123456789+-"); + if(i == string_t::npos) return subrange{}; + + bool sep = false; + auto j = i + 1; + for(; j < s.size(); ++j) { + if(s[j] == digitSeparator) { + if(!sep) sep = true; else break; + } + else { + sep = false; + if(!std::isdigit(s[j])) break; + } + } + + //if length == 1 then must be a digit + if(j-i == 1 && !std::isdigit(s[i])) return subrange{}; + + return subrange{i,j-i}; +} + + + +/*************************************************************************//** + * + * @brief returns true if candidate string represents a number + * + *****************************************************************************/ +template +bool represents_number(const std::basic_string& candidate, + C digitSeparator = C(','), + C decimalPoint = C('.'), + C exponential = C('e')) +{ + const auto match = str::first_number_match(candidate, digitSeparator, + decimalPoint, exponential); + + return (match && match.length() == candidate.size()); +} + + + +/*************************************************************************//** + * + * @brief returns true if candidate string represents an integer + * + *****************************************************************************/ +template +bool represents_integer(const std::basic_string& candidate, + C digitSeparator = C(',')) +{ + const auto match = str::first_integer_match(candidate, digitSeparator); + return (match && match.length() == candidate.size()); +} + +} // namespace str + + + + + + +/*************************************************************************//** + * + * @brief makes function object with a const char* parameter + * that assigns a value to a ref-captured object + * + *****************************************************************************/ +template +inline detail::assign_value +set(T& target, V value) { + return detail::assign_value{target, std::move(value)}; +} + + + +/*************************************************************************//** + * + * @brief makes parameter-less function object + * that assigns value(s) to a ref-captured object; + * value(s) are obtained by converting the const char* argument to + * the captured object types; + * bools are always set to true if the argument is not nullptr + * + *****************************************************************************/ +template +inline detail::map_arg_to +set(T& target) { + return detail::map_arg_to{target}; +} + + + +/*************************************************************************//** + * + * @brief makes function object that sets a bool to true + * + *****************************************************************************/ +inline detail::assign_value +set(bool& target) { + return detail::assign_value{target,true}; +} + +/*************************************************************************//** + * + * @brief makes function object that sets a bool to false + * + *****************************************************************************/ +inline detail::assign_value +unset(bool& target) { + return detail::assign_value{target,false}; +} + +/*************************************************************************//** + * + * @brief makes function object that flips the value of a ref-captured bool + * + *****************************************************************************/ +inline detail::flip_bool +flip(bool& b) { + return detail::flip_bool(b); +} + + + + + +/*************************************************************************//** + * + * @brief makes function object that increments using operator ++ + * + *****************************************************************************/ +template +inline detail::increment +increment(T& target) { + return detail::increment{target}; +} + +/*************************************************************************//** + * + * @brief makes function object that decrements using operator -- + * + *****************************************************************************/ +template +inline detail::increment_by +increment(T& target, T by) { + return detail::increment_by{target, std::move(by)}; +} + +/*************************************************************************//** + * + * @brief makes function object that increments by a fixed amount using operator += + * + *****************************************************************************/ +template +inline detail::decrement +decrement(T& target) { + return detail::decrement{target}; +} + + + + + + +/*************************************************************************//** + * + * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) + * + *****************************************************************************/ +namespace detail { + + +/*************************************************************************//** + * + * @brief mixin that provides action definition and execution + * + *****************************************************************************/ +template +class action_provider +{ +private: + //--------------------------------------------------------------- + using simple_action = std::function; + using arg_action = std::function; + using index_action = std::function; + + //----------------------------------------------------- + class simple_action_adapter { + public: + simple_action_adapter() = default; + simple_action_adapter(const simple_action& a): action_(a) {} + simple_action_adapter(simple_action&& a): action_(std::move(a)) {} + void operator() (const char*) const { action_(); } + void operator() (int) const { action_(); } + private: + simple_action action_; + }; + + +public: + //--------------------------------------------------------------- + /** @brief adds an action that has an operator() that is callable + * with a 'const char*' argument */ + Derived& + call(arg_action a) { + argActions_.push_back(std::move(a)); + return *static_cast(this); + } + + /** @brief adds an action that has an operator()() */ + Derived& + call(simple_action a) { + argActions_.push_back(simple_action_adapter(std::move(a))); + return *static_cast(this); + } + + /** @brief adds an action that has an operator() that is callable + * with a 'const char*' argument */ + Derived& operator () (arg_action a) { return call(std::move(a)); } + + /** @brief adds an action that has an operator()() */ + Derived& operator () (simple_action a) { return call(std::move(a)); } + + + //--------------------------------------------------------------- + /** @brief adds an action that will set the value of 't' from + * a 'const char*' arg */ + template + Derived& + set(Target& t) { + static_assert(!std::is_pointer::value, + "parameter target type must not be a pointer"); + + return call(clipp::set(t)); + } + + /** @brief adds an action that will set the value of 't' to 'v' */ + template + Derived& + set(Target& t, Value&& v) { + return call(clipp::set(t, std::forward(v))); + } + + + //--------------------------------------------------------------- + /** @brief adds an action that will be called if a parameter + * matches an argument for the 2nd, 3rd, 4th, ... time + */ + Derived& + if_repeated(simple_action a) { + repeatActions_.push_back(simple_action_adapter{std::move(a)}); + return *static_cast(this); + } + /** @brief adds an action that will be called with the argument's + * index if a parameter matches an argument for + * the 2nd, 3rd, 4th, ... time + */ + Derived& + if_repeated(index_action a) { + repeatActions_.push_back(std::move(a)); + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief adds an action that will be called if a required parameter + * is missing + */ + Derived& + if_missing(simple_action a) { + missingActions_.push_back(simple_action_adapter{std::move(a)}); + return *static_cast(this); + } + /** @brief adds an action that will be called if a required parameter + * is missing; the action will get called with the index of + * the command line argument where the missing event occurred first + */ + Derived& + if_missing(index_action a) { + missingActions_.push_back(std::move(a)); + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief adds an action that will be called if a parameter + * was matched, but was unreachable in the current scope + */ + Derived& + if_blocked(simple_action a) { + blockedActions_.push_back(simple_action_adapter{std::move(a)}); + return *static_cast(this); + } + /** @brief adds an action that will be called if a parameter + * was matched, but was unreachable in the current scope; + * the action will be called with the index of + * the command line argument where the problem occurred + */ + Derived& + if_blocked(index_action a) { + blockedActions_.push_back(std::move(a)); + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief adds an action that will be called if a parameter match + * was in conflict with a different alternative parameter + */ + Derived& + if_conflicted(simple_action a) { + conflictActions_.push_back(simple_action_adapter{std::move(a)}); + return *static_cast(this); + } + /** @brief adds an action that will be called if a parameter match + * was in conflict with a different alternative parameter; + * the action will be called with the index of + * the command line argument where the problem occurred + */ + Derived& + if_conflicted(index_action a) { + conflictActions_.push_back(std::move(a)); + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief adds targets = either objects whose values should be + * set by command line arguments or actions that should + * be called in case of a match */ + template + Derived& + target(T&& t, Ts&&... ts) { + target(std::forward(t)); + target(std::forward(ts)...); + return *static_cast(this); + } + + /** @brief adds action that should be called in case of a match */ + template::type>() && + (traits::is_callable() || + traits::is_callable() ) + >::type> + Derived& + target(T&& t) { + call(std::forward(t)); + return *static_cast(this); + } + + /** @brief adds object whose value should be set by command line arguments + */ + template::type>() || + (!traits::is_callable() && + !traits::is_callable() ) + >::type> + Derived& + target(T& t) { + set(t); + return *static_cast(this); + } + + //TODO remove ugly empty param list overload + Derived& + target() { + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief adds target, see member function 'target' */ + template + inline friend Derived& + operator << (Target&& t, Derived& p) { + p.target(std::forward(t)); + return p; + } + /** @brief adds target, see member function 'target' */ + template + inline friend Derived&& + operator << (Target&& t, Derived&& p) { + p.target(std::forward(t)); + return std::move(p); + } + + //----------------------------------------------------- + /** @brief adds target, see member function 'target' */ + template + inline friend Derived& + operator >> (Derived& p, Target&& t) { + p.target(std::forward(t)); + return p; + } + /** @brief adds target, see member function 'target' */ + template + inline friend Derived&& + operator >> (Derived&& p, Target&& t) { + p.target(std::forward(t)); + return std::move(p); + } + + + //--------------------------------------------------------------- + /** @brief executes all argument actions */ + void execute_actions(const arg_string& arg) const { + int i = 0; + for(const auto& a : argActions_) { + ++i; + a(arg.c_str()); + } + } + + /** @brief executes repeat actions */ + void notify_repeated(arg_index idx) const { + for(const auto& a : repeatActions_) a(idx); + } + /** @brief executes missing error actions */ + void notify_missing(arg_index idx) const { + for(const auto& a : missingActions_) a(idx); + } + /** @brief executes blocked error actions */ + void notify_blocked(arg_index idx) const { + for(const auto& a : blockedActions_) a(idx); + } + /** @brief executes conflict error actions */ + void notify_conflict(arg_index idx) const { + for(const auto& a : conflictActions_) a(idx); + } + +private: + //--------------------------------------------------------------- + std::vector argActions_; + std::vector repeatActions_; + std::vector missingActions_; + std::vector blockedActions_; + std::vector conflictActions_; +}; + + + + + + +/*************************************************************************//** + * + * @brief mixin that provides basic common settings of parameters and groups + * + *****************************************************************************/ +template +class token +{ +public: + //--------------------------------------------------------------- + using doc_string = clipp::doc_string; + + + //--------------------------------------------------------------- + /** @brief returns documentation string */ + const doc_string& doc() const noexcept { + return doc_; + } + + /** @brief sets documentations string */ + Derived& doc(const doc_string& txt) { + doc_ = txt; + return *static_cast(this); + } + + /** @brief sets documentations string */ + Derived& doc(doc_string&& txt) { + doc_ = std::move(txt); + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief returns if a group/parameter is repeatable */ + bool repeatable() const noexcept { + return repeatable_; + } + + /** @brief sets repeatability of group/parameter */ + Derived& repeatable(bool yes) noexcept { + repeatable_ = yes; + return *static_cast(this); + } + + + //--------------------------------------------------------------- + /** @brief returns if a group/parameter is blocking/positional */ + bool blocking() const noexcept { + return blocking_; + } + + /** @brief determines, if a group/parameter is blocking/positional */ + Derived& blocking(bool yes) noexcept { + blocking_ = yes; + return *static_cast(this); + } + + +private: + //--------------------------------------------------------------- + doc_string doc_; + bool repeatable_ = false; + bool blocking_ = false; +}; + + + + +/*************************************************************************//** + * + * @brief sets documentation strings on a token + * + *****************************************************************************/ +template +inline T& +operator % (doc_string docstr, token& p) +{ + return p.doc(std::move(docstr)); +} +//--------------------------------------------------------- +template +inline T&& +operator % (doc_string docstr, token&& p) +{ + return std::move(p.doc(std::move(docstr))); +} + +//--------------------------------------------------------- +template +inline T& +operator % (token& p, doc_string docstr) +{ + return p.doc(std::move(docstr)); +} +//--------------------------------------------------------- +template +inline T&& +operator % (token&& p, doc_string docstr) +{ + return std::move(p.doc(std::move(docstr))); +} + + + + +/*************************************************************************//** + * + * @brief sets documentation strings on a token + * + *****************************************************************************/ +template +inline T& +doc(doc_string docstr, token& p) +{ + return p.doc(std::move(docstr)); +} +//--------------------------------------------------------- +template +inline T&& +doc(doc_string docstr, token&& p) +{ + return std::move(p.doc(std::move(docstr))); +} + + + +} // namespace detail + + + +/*************************************************************************//** + * + * @brief contains parameter matching functions and function classes + * + *****************************************************************************/ +namespace match { + + +/*************************************************************************//** + * + * @brief predicate that is always true + * + *****************************************************************************/ +inline bool +any(const arg_string&) { return true; } + +/*************************************************************************//** + * + * @brief predicate that is always false + * + *****************************************************************************/ +inline bool +none(const arg_string&) { return false; } + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the argument string is non-empty string + * + *****************************************************************************/ +inline bool +nonempty(const arg_string& s) { + return !s.empty(); +} + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the argument is a non-empty + * string that consists only of alphanumeric characters + * + *****************************************************************************/ +inline bool +alphanumeric(const arg_string& s) { + if(s.empty()) return false; + return std::all_of(s.begin(), s.end(), [](char c) {return std::isalnum(c); }); +} + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the argument is a non-empty + * string that consists only of alphabetic characters + * + *****************************************************************************/ +inline bool +alphabetic(const arg_string& s) { + return std::all_of(s.begin(), s.end(), [](char c) {return std::isalpha(c); }); +} + + + +/*************************************************************************//** + * + * @brief predicate that returns false if the argument string is + * equal to any string from the exclusion list + * + *****************************************************************************/ +class none_of +{ +public: + none_of(arg_list strs): + excluded_{std::move(strs)} + {} + + template + none_of(arg_string str, Strings&&... strs): + excluded_{std::move(str), std::forward(strs)...} + {} + + template + none_of(const char* str, Strings&&... strs): + excluded_{arg_string(str), std::forward(strs)...} + {} + + bool operator () (const arg_string& arg) const { + return (std::find(begin(excluded_), end(excluded_), arg) + == end(excluded_)); + } + +private: + arg_list excluded_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns the first substring match within the input + * string that rmeepresents a number + * (with at maximum one decimal point and digit separators) + * + *****************************************************************************/ +class numbers +{ +public: + explicit + numbers(char decimalPoint = '.', + char digitSeparator = ' ', + char exponentSeparator = 'e') + : + decpoint_{decimalPoint}, separator_{digitSeparator}, + exp_{exponentSeparator} + {} + + subrange operator () (const arg_string& s) const { + return str::first_number_match(s, separator_, decpoint_, exp_); + } + +private: + char decpoint_; + char separator_; + char exp_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the input string represents an integer + * (with optional digit separators) + * + *****************************************************************************/ +class integers { +public: + explicit + integers(char digitSeparator = ' '): separator_{digitSeparator} {} + + subrange operator () (const arg_string& s) const { + return str::first_integer_match(s, separator_); + } + +private: + char separator_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the input string represents + * a non-negative integer (with optional digit separators) + * + *****************************************************************************/ +class positive_integers { +public: + explicit + positive_integers(char digitSeparator = ' '): separator_{digitSeparator} {} + + subrange operator () (const arg_string& s) const { + auto match = str::first_integer_match(s, separator_); + if(!match) return subrange{}; + if(s[match.at()] == '-') return subrange{}; + return match; + } + +private: + char separator_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the input string + * contains a given substring + * + *****************************************************************************/ +class substring +{ +public: + explicit + substring(arg_string str): str_{std::move(str)} {} + + subrange operator () (const arg_string& s) const { + return str::substring_match(s, str_); + } + +private: + arg_string str_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the input string starts + * with a given prefix + * + *****************************************************************************/ +class prefix { +public: + explicit + prefix(arg_string p): prefix_{std::move(p)} {} + + bool operator () (const arg_string& s) const { + return s.find(prefix_) == 0; + } + +private: + arg_string prefix_; +}; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the input string does not start + * with a given prefix + * + *****************************************************************************/ +class prefix_not { +public: + explicit + prefix_not(arg_string p): prefix_{std::move(p)} {} + + bool operator () (const arg_string& s) const { + return s.find(prefix_) != 0; + } + +private: + arg_string prefix_; +}; + + +/** @brief alias for prefix_not */ +using noprefix = prefix_not; + + + +/*************************************************************************//** + * + * @brief predicate that returns true if the length of the input string + * is wihtin a given interval + * + *****************************************************************************/ +class length { +public: + explicit + length(std::size_t exact): + min_{exact}, max_{exact} + {} + + explicit + length(std::size_t min, std::size_t max): + min_{min}, max_{max} + {} + + bool operator () (const arg_string& s) const { + return s.size() >= min_ && s.size() <= max_; + } + +private: + std::size_t min_; + std::size_t max_; +}; + + +/*************************************************************************//** + * + * @brief makes function object that returns true if the input string has a + * given minimum length + * + *****************************************************************************/ +inline length min_length(std::size_t min) +{ + return length{min, arg_string::npos-1}; +} + +/*************************************************************************//** + * + * @brief makes function object that returns true if the input string is + * not longer than a given maximum length + * + *****************************************************************************/ +inline length max_length(std::size_t max) +{ + return length{0, max}; +} + + +} // namespace match + + + + + +/*************************************************************************//** + * + * @brief command line parameter that can match one or many arguments. + * + *****************************************************************************/ +class parameter : + public detail::token, + public detail::action_provider +{ + /** @brief adapts a 'match_predicate' to the 'match_function' interface */ + class predicate_adapter { + public: + explicit + predicate_adapter(match_predicate pred): match_{std::move(pred)} {} + + subrange operator () (const arg_string& arg) const { + return match_(arg) ? subrange{0,arg.size()} : subrange{}; + } + + private: + match_predicate match_; + }; + +public: + //--------------------------------------------------------------- + /** @brief makes default parameter, that will match nothing */ + parameter(): + flags_{}, + matcher_{predicate_adapter{match::none}}, + label_{}, required_{false}, greedy_{false} + {} + + /** @brief makes "flag" parameter */ + template + explicit + parameter(arg_string str, Strings&&... strs): + flags_{}, + matcher_{predicate_adapter{match::none}}, + label_{}, required_{false}, greedy_{false} + { + add_flags(std::move(str), std::forward(strs)...); + } + + /** @brief makes "flag" parameter from range of strings */ + explicit + parameter(const arg_list& flaglist): + flags_{}, + matcher_{predicate_adapter{match::none}}, + label_{}, required_{false}, greedy_{false} + { + add_flags(flaglist); + } + + //----------------------------------------------------- + /** @brief makes "value" parameter with custom match predicate + * (= yes/no matcher) + */ + explicit + parameter(match_predicate filter): + flags_{}, + matcher_{predicate_adapter{std::move(filter)}}, + label_{}, required_{false}, greedy_{false} + {} + + /** @brief makes "value" parameter with custom match function + * (= partial matcher) + */ + explicit + parameter(match_function filter): + flags_{}, + matcher_{std::move(filter)}, + label_{}, required_{false}, greedy_{false} + {} + + + //--------------------------------------------------------------- + /** @brief returns if a parameter is required */ + bool + required() const noexcept { + return required_; + } + + /** @brief determines if a parameter is required */ + parameter& + required(bool yes) noexcept { + required_ = yes; + return *this; + } + + + //--------------------------------------------------------------- + /** @brief returns if a parameter should match greedily */ + bool + greedy() const noexcept { + return greedy_; + } + + /** @brief determines if a parameter should match greedily */ + parameter& + greedy(bool yes) noexcept { + greedy_ = yes; + return *this; + } + + + //--------------------------------------------------------------- + /** @brief returns parameter label; + * will be used for documentation, if flags are empty + */ + const doc_string& + label() const { + return label_; + } + + /** @brief sets parameter label; + * will be used for documentation, if flags are empty + */ + parameter& + label(const doc_string& lbl) { + label_ = lbl; + return *this; + } + + /** @brief sets parameter label; + * will be used for documentation, if flags are empty + */ + parameter& + label(doc_string&& lbl) { + label_ = lbl; + return *this; + } + + + //--------------------------------------------------------------- + /** @brief returns either longest matching prefix of 'arg' in any + * of the flags or the result of the custom match operation + */ + subrange + match(const arg_string& arg) const + { + if(flags_.empty()) { + return matcher_(arg); + } + else { + //empty flags are not allowed + if(arg.empty()) return subrange{}; + + if(std::find(flags_.begin(), flags_.end(), arg) != flags_.end()) { + return subrange{0,arg.size()}; + } + return str::longest_prefix_match(arg, flags_); + } + } + + + //--------------------------------------------------------------- + /** @brief access range of flag strings */ + const arg_list& + flags() const noexcept { + return flags_; + } + + /** @brief access custom match operation */ + const match_function& + matcher() const noexcept { + return matcher_; + } + + + //--------------------------------------------------------------- + /** @brief prepend prefix to each flag */ + inline friend parameter& + with_prefix(const arg_string& prefix, parameter& p) + { + if(prefix.empty() || p.flags().empty()) return p; + + for(auto& f : p.flags_) { + if(f.find(prefix) != 0) f.insert(0, prefix); + } + return p; + } + + + /** @brief prepend prefix to each flag + */ + inline friend parameter& + with_prefixes_short_long( + const arg_string& shortpfx, const arg_string& longpfx, + parameter& p) + { + if(shortpfx.empty() && longpfx.empty()) return p; + if(p.flags().empty()) return p; + + for(auto& f : p.flags_) { + if(f.size() == 1) { + if(f.find(shortpfx) != 0) f.insert(0, shortpfx); + } else { + if(f.find(longpfx) != 0) f.insert(0, longpfx); + } + } + return p; + } + + + //--------------------------------------------------------------- + /** @brief prepend suffix to each flag */ + inline friend parameter& + with_suffix(const arg_string& suffix, parameter& p) + { + if(suffix.empty() || p.flags().empty()) return p; + + for(auto& f : p.flags_) { + if(f.find(suffix) + suffix.size() != f.size()) { + f.insert(f.end(), suffix.begin(), suffix.end()); + } + } + return p; + } + + + /** @brief prepend suffix to each flag + */ + inline friend parameter& + with_suffixes_short_long( + const arg_string& shortsfx, const arg_string& longsfx, + parameter& p) + { + if(shortsfx.empty() && longsfx.empty()) return p; + if(p.flags().empty()) return p; + + for(auto& f : p.flags_) { + if(f.size() == 1) { + if(f.find(shortsfx) + shortsfx.size() != f.size()) { + f.insert(f.end(), shortsfx.begin(), shortsfx.end()); + } + } else { + if(f.find(longsfx) + longsfx.size() != f.size()) { + f.insert(f.end(), longsfx.begin(), longsfx.end()); + } + } + } + return p; + } + +private: + //--------------------------------------------------------------- + void add_flags(arg_string str) { + //empty flags are not allowed + str::remove_ws(str); + if(!str.empty()) flags_.push_back(std::move(str)); + } + + //--------------------------------------------------------------- + void add_flags(const arg_list& strs) { + if(strs.empty()) return; + flags_.reserve(flags_.size() + strs.size()); + for(const auto& s : strs) add_flags(s); + } + + template + void + add_flags(String1&& s1, String2&& s2, Strings&&... ss) { + flags_.reserve(2 + sizeof...(ss)); + add_flags(std::forward(s1)); + add_flags(std::forward(s2), std::forward(ss)...); + } + + arg_list flags_; + match_function matcher_; + doc_string label_; + bool required_ = false; + bool greedy_ = false; +}; + + + + +/*************************************************************************//** + * + * @brief makes required non-blocking exact match parameter + * + *****************************************************************************/ +template +inline parameter +command(String&& flag, Strings&&... flags) +{ + return parameter{std::forward(flag), std::forward(flags)...} + .required(true).blocking(true).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required non-blocking exact match parameter + * + *****************************************************************************/ +template +inline parameter +required(String&& flag, Strings&&... flags) +{ + return parameter{std::forward(flag), std::forward(flags)...} + .required(true).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes optional, non-blocking exact match parameter + * + *****************************************************************************/ +template +inline parameter +option(String&& flag, Strings&&... flags) +{ + return parameter{std::forward(flag), std::forward(flags)...} + .required(false).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking, repeatable value parameter; + * matches any non-empty string + * + *****************************************************************************/ +template +inline parameter +value(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::nonempty} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(false); +} + +template::value || + traits::is_callable::value>::type> +inline parameter +value(Filter&& filter, doc_string label, Targets&&... tgts) +{ + return parameter{std::forward(filter)} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking, repeatable value parameter; + * matches any non-empty string + * + *****************************************************************************/ +template +inline parameter +values(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::nonempty} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(true); +} + +template::value || + traits::is_callable::value>::type> +inline parameter +values(Filter&& filter, doc_string label, Targets&&... tgts) +{ + return parameter{std::forward(filter)} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking value parameter; + * matches any non-empty string + * + *****************************************************************************/ +template +inline parameter +opt_value(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::nonempty} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(false); +} + +template::value || + traits::is_callable::value>::type> +inline parameter +opt_value(Filter&& filter, doc_string label, Targets&&... tgts) +{ + return parameter{std::forward(filter)} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking, repeatable value parameter; + * matches any non-empty string + * + *****************************************************************************/ +template +inline parameter +opt_values(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::nonempty} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + +template::value || + traits::is_callable::value>::type> +inline parameter +opt_values(Filter&& filter, doc_string label, Targets&&... tgts) +{ + return parameter{std::forward(filter)} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking value parameter; + * matches any string consisting of alphanumeric characters + * + *****************************************************************************/ +template +inline parameter +word(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::alphanumeric} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking, repeatable value parameter; + * matches any string consisting of alphanumeric characters + * + *****************************************************************************/ +template +inline parameter +words(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::alphanumeric} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking value parameter; + * matches any string consisting of alphanumeric characters + * + *****************************************************************************/ +template +inline parameter +opt_word(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::alphanumeric} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking, repeatable value parameter; + * matches any string consisting of alphanumeric characters + * + *****************************************************************************/ +template +inline parameter +opt_words(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::alphanumeric} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking value parameter; + * matches any string that represents a number + * + *****************************************************************************/ +template +inline parameter +number(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::numbers{}} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking, repeatable value parameter; + * matches any string that represents a number + * + *****************************************************************************/ +template +inline parameter +numbers(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::numbers{}} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking value parameter; + * matches any string that represents a number + * + *****************************************************************************/ +template +inline parameter +opt_number(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::numbers{}} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking, repeatable value parameter; + * matches any string that represents a number + * + *****************************************************************************/ +template +inline parameter +opt_numbers(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::numbers{}} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking value parameter; + * matches any string that represents an integer + * + *****************************************************************************/ +template +inline parameter +integer(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::integers{}} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes required, blocking, repeatable value parameter; + * matches any string that represents an integer + * + *****************************************************************************/ +template +inline parameter +integers(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::integers{}} + .label(label) + .target(std::forward(tgts)...) + .required(true).blocking(true).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking value parameter; + * matches any string that represents an integer + * + *****************************************************************************/ +template +inline parameter +opt_integer(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::integers{}} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(false); +} + + + +/*************************************************************************//** + * + * @brief makes optional, blocking, repeatable value parameter; + * matches any string that represents an integer + * + *****************************************************************************/ +template +inline parameter +opt_integers(const doc_string& label, Targets&&... tgts) +{ + return parameter{match::integers{}} + .label(label) + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes catch-all value parameter + * + *****************************************************************************/ +template +inline parameter +any_other(Targets&&... tgts) +{ + return parameter{match::any} + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes catch-all value parameter with custom filter + * + *****************************************************************************/ +template::value || + traits::is_callable::value>::type> +inline parameter +any(Filter&& filter, Targets&&... tgts) +{ + return parameter{std::forward(filter)} + .target(std::forward(tgts)...) + .required(false).blocking(false).repeatable(true); +} + + + + +/*************************************************************************//** + * + * @brief group of parameters and/or other groups; + * can be configured to act as a group of alternatives (exclusive match) + * + *****************************************************************************/ +class group : + public detail::token +{ + //--------------------------------------------------------------- + /** + * @brief tagged union type that either stores a parameter or a group + * and provides a common interface to them + * could be replaced by std::variant in the future + * + * Note to future self: do NOT try again to do this with + * dynamic polymorphism; there are a couple of + * nasty problems associated with it and the implementation + * becomes bloated and needlessly complicated. + */ + template + struct child_t { + enum class type : char {param, group}; + public: + + explicit + child_t(const Param& v) : m_{v}, type_{type::param} {} + child_t( Param&& v) noexcept : m_{std::move(v)}, type_{type::param} {} + + explicit + child_t(const Group& g) : m_{g}, type_{type::group} {} + child_t( Group&& g) noexcept : m_{std::move(g)}, type_{type::group} {} + + child_t(const child_t& src): type_{src.type_} { + switch(type_) { + default: + case type::param: new(&m_)data{src.m_.param}; break; + case type::group: new(&m_)data{src.m_.group}; break; + } + } + + child_t(child_t&& src) noexcept : type_{src.type_} { + switch(type_) { + default: + case type::param: new(&m_)data{std::move(src.m_.param)}; break; + case type::group: new(&m_)data{std::move(src.m_.group)}; break; + } + } + + child_t& operator = (const child_t& src) { + destroy_content(); + type_ = src.type_; + switch(type_) { + default: + case type::param: new(&m_)data{src.m_.param}; break; + case type::group: new(&m_)data{src.m_.group}; break; + } + return *this; + } + + child_t& operator = (child_t&& src) noexcept { + destroy_content(); + type_ = src.type_; + switch(type_) { + default: + case type::param: new(&m_)data{std::move(src.m_.param)}; break; + case type::group: new(&m_)data{std::move(src.m_.group)}; break; + } + return *this; + } + + ~child_t() { + destroy_content(); + } + + const doc_string& + doc() const noexcept { + switch(type_) { + default: + case type::param: return m_.param.doc(); + case type::group: return m_.group.doc(); + } + } + + bool blocking() const noexcept { + switch(type_) { + case type::param: return m_.param.blocking(); + case type::group: return m_.group.blocking(); + default: return false; + } + } + bool repeatable() const noexcept { + switch(type_) { + case type::param: return m_.param.repeatable(); + case type::group: return m_.group.repeatable(); + default: return false; + } + } + bool required() const noexcept { + switch(type_) { + case type::param: return m_.param.required(); + case type::group: + return (m_.group.exclusive() && m_.group.all_required() ) || + (!m_.group.exclusive() && m_.group.any_required() ); + default: return false; + } + } + bool exclusive() const noexcept { + switch(type_) { + case type::group: return m_.group.exclusive(); + case type::param: + default: return false; + } + } + std::size_t param_count() const noexcept { + switch(type_) { + case type::group: return m_.group.param_count(); + case type::param: + default: return std::size_t(1); + } + } + std::size_t depth() const noexcept { + switch(type_) { + case type::group: return m_.group.depth(); + case type::param: + default: return std::size_t(0); + } + } + + void execute_actions(const arg_string& arg) const { + switch(type_) { + default: + case type::group: return; + case type::param: m_.param.execute_actions(arg); break; + } + + } + + void notify_repeated(arg_index idx) const { + switch(type_) { + default: + case type::group: return; + case type::param: m_.param.notify_repeated(idx); break; + } + } + void notify_missing(arg_index idx) const { + switch(type_) { + default: + case type::group: return; + case type::param: m_.param.notify_missing(idx); break; + } + } + void notify_blocked(arg_index idx) const { + switch(type_) { + default: + case type::group: return; + case type::param: m_.param.notify_blocked(idx); break; + } + } + void notify_conflict(arg_index idx) const { + switch(type_) { + default: + case type::group: return; + case type::param: m_.param.notify_conflict(idx); break; + } + } + + bool is_param() const noexcept { return type_ == type::param; } + bool is_group() const noexcept { return type_ == type::group; } + + Param& as_param() noexcept { return m_.param; } + Group& as_group() noexcept { return m_.group; } + + const Param& as_param() const noexcept { return m_.param; } + const Group& as_group() const noexcept { return m_.group; } + + private: + void destroy_content() { + switch(type_) { + default: + case type::param: m_.param.~Param(); break; + case type::group: m_.group.~Group(); break; + } + } + + union data { + data() {} + + data(const Param& v) : param{v} {} + data( Param&& v) noexcept : param{std::move(v)} {} + + data(const Group& g) : group{g} {} + data( Group&& g) noexcept : group{std::move(g)} {} + ~data() {} + + Param param; + Group group; + }; + + data m_; + type type_; + }; + + +public: + //--------------------------------------------------------------- + using child = child_t; + using value_type = child; + +private: + using children_store = std::vector; + +public: + using const_iterator = children_store::const_iterator; + using iterator = children_store::iterator; + using size_type = children_store::size_type; + + + //--------------------------------------------------------------- + /** + * @brief recursively iterates over all nodes + */ + class depth_first_traverser + { + public: + //----------------------------------------------------- + struct context { + context() = default; + context(const group& p): + parent{&p}, cur{p.begin()}, end{p.end()} + {} + const group* parent = nullptr; + const_iterator cur; + const_iterator end; + }; + using context_list = std::vector; + + //----------------------------------------------------- + class memento { + friend class depth_first_traverser; + int level_; + context context_; + public: + int level() const noexcept { return level_; } + const child* param() const noexcept { return &(*context_.cur); } + }; + + depth_first_traverser() = default; + + explicit + depth_first_traverser(const group& cur): stack_{} { + if(!cur.empty()) stack_.emplace_back(cur); + } + + explicit operator bool() const noexcept { + return !stack_.empty(); + } + + int level() const noexcept { + return int(stack_.size()); + } + + bool is_first_in_parent() const noexcept { + if(stack_.empty()) return false; + return (stack_.back().cur == stack_.back().parent->begin()); + } + + bool is_last_in_parent() const noexcept { + if(stack_.empty()) return false; + return (stack_.back().cur+1 == stack_.back().end); + } + + bool is_last_in_path() const noexcept { + if(stack_.empty()) return false; + for(const auto& t : stack_) { + if(t.cur+1 != t.end) return false; + } + const auto& top = stack_.back(); + //if we have to descend into group on next ++ => not last in path + if(top.cur->is_group()) return false; + return true; + } + + /** @brief inside a group of alternatives >= minlevel */ + bool is_alternative(int minlevel = 0) const noexcept { + if(stack_.empty()) return false; + if(minlevel > 0) minlevel -= 1; + if(minlevel >= int(stack_.size())) return false; + return std::any_of(stack_.begin() + minlevel, stack_.end(), + [](const context& c) { return c.parent->exclusive(); }); + } + + /** @brief repeatable or inside a repeatable group >= minlevel */ + bool is_repeatable(int minlevel = 0) const noexcept { + if(stack_.empty()) return false; + if(stack_.back().cur->repeatable()) return true; + if(minlevel > 0) minlevel -= 1; + if(minlevel >= int(stack_.size())) return false; + return std::any_of(stack_.begin() + minlevel, stack_.end(), + [](const context& c) { return c.parent->repeatable(); }); + } + + /** @brief inside a particular group */ + bool is_inside(const group* g) const noexcept { + if(!g) return false; + return std::any_of(stack_.begin(), stack_.end(), + [g](const context& c) { return c.parent == g; }); + } + + /** @brief inside group with joinable flags */ + bool joinable() const noexcept { + if(stack_.empty()) return false; + return std::any_of(stack_.begin(), stack_.end(), + [](const context& c) { return c.parent->joinable(); }); + } + + const context_list& + stack() const { + return stack_; + } + + /** @brief innermost repeat group */ + const group* + innermost_repeat_group() const noexcept { + auto i = std::find_if(stack_.rbegin(), stack_.rend(), + [](const context& c) { return c.parent->repeatable(); }); + return i != stack_.rend() ? i->parent : nullptr; + } + + /** @brief innermost exclusive (alternatives) group */ + const group* + innermost_exclusive_group() const noexcept { + auto i = std::find_if(stack_.rbegin(), stack_.rend(), + [](const context& c) { return c.parent->exclusive(); }); + return i != stack_.rend() ? i->parent : nullptr; + } + + /** @brief innermost blocking group */ + const group* + innermost_blocking_group() const noexcept { + auto i = std::find_if(stack_.rbegin(), stack_.rend(), + [](const context& c) { return c.parent->blocking(); }); + return i != stack_.rend() ? i->parent : nullptr; + } + + /** @brief returns the outermost group that will be left on next ++*/ + const group* + outermost_blocking_group_fully_explored() const noexcept { + if(stack_.empty()) return nullptr; + + const group* g = nullptr; + for(auto i = stack_.rbegin(); i != stack_.rend(); ++i) { + if(i->cur+1 == i->end) { + if(i->parent->blocking()) g = i->parent; + } else { + return g; + } + } + return g; + } + + /** @brief outermost join group */ + const group* + outermost_join_group() const noexcept { + auto i = std::find_if(stack_.begin(), stack_.end(), + [](const context& c) { return c.parent->joinable(); }); + return i != stack_.end() ? i->parent : nullptr; + } + + const group* root() const noexcept { + return stack_.empty() ? nullptr : stack_.front().parent; + } + + /** @brief common flag prefix of all flags in current group */ + arg_string common_flag_prefix() const noexcept { + if(stack_.empty()) return ""; + auto g = outermost_join_group(); + return g ? g->common_flag_prefix() : arg_string(""); + } + + const child& + operator * () const noexcept { + return *stack_.back().cur; + } + + const child* + operator -> () const noexcept { + return &(*stack_.back().cur); + } + + const group& + parent() const noexcept { + return *(stack_.back().parent); + } + + + /** @brief go to next element of depth first search */ + depth_first_traverser& + operator ++ () { + if(stack_.empty()) return *this; + //at group -> decend into group + if(stack_.back().cur->is_group()) { + stack_.emplace_back(stack_.back().cur->as_group()); + } + else { + next_sibling(); + } + return *this; + } + + /** @brief go to next sibling of current */ + depth_first_traverser& + next_sibling() { + if(stack_.empty()) return *this; + ++stack_.back().cur; + //at the end of current group? + while(stack_.back().cur == stack_.back().end) { + //go to parent + stack_.pop_back(); + if(stack_.empty()) return *this; + //go to next sibling in parent + ++stack_.back().cur; + } + return *this; + } + + /** @brief go to next position after siblings of current */ + depth_first_traverser& + next_after_siblings() { + if(stack_.empty()) return *this; + stack_.back().cur = stack_.back().end-1; + next_sibling(); + return *this; + } + + /** + * @brief + */ + depth_first_traverser& + back_to_ancestor(const group* g) { + if(!g) return *this; + while(!stack_.empty()) { + const auto& top = stack_.back().cur; + if(top->is_group() && &(top->as_group()) == g) return *this; + stack_.pop_back(); + } + return *this; + } + + /** @brief don't visit next siblings, go back to parent on next ++ + * note: renders siblings unreachable for *this + **/ + depth_first_traverser& + skip_siblings() { + if(stack_.empty()) return *this; + //future increments won't visit subsequent siblings: + stack_.back().end = stack_.back().cur+1; + return *this; + } + + /** @brief skips all other alternatives in surrounding exclusive groups + * on next ++ + * note: renders alternatives unreachable for *this + */ + depth_first_traverser& + skip_alternatives() { + if(stack_.empty()) return *this; + + //exclude all other alternatives in surrounding groups + //by making their current position the last one + for(auto& c : stack_) { + if(c.parent && c.parent->exclusive() && c.cur < c.end) + c.end = c.cur+1; + } + + return *this; + } + + void invalidate() { + stack_.clear(); + } + + inline friend bool operator == (const depth_first_traverser& a, + const depth_first_traverser& b) + { + if(a.stack_.empty() || b.stack_.empty()) return false; + + //parents not the same -> different position + if(a.stack_.back().parent != b.stack_.back().parent) return false; + + bool aEnd = a.stack_.back().cur == a.stack_.back().end; + bool bEnd = b.stack_.back().cur == b.stack_.back().end; + //either both at the end of the same parent => same position + if(aEnd && bEnd) return true; + //or only one at the end => not at the same position + if(aEnd || bEnd) return false; + return std::addressof(*a.stack_.back().cur) == + std::addressof(*b.stack_.back().cur); + } + inline friend bool operator != (const depth_first_traverser& a, + const depth_first_traverser& b) + { + return !(a == b); + } + + memento + undo_point() const { + memento m; + m.level_ = int(stack_.size()); + if(!stack_.empty()) m.context_ = stack_.back(); + return m; + } + + void undo(const memento& m) { + if(m.level_ < 1) return; + if(m.level_ <= int(stack_.size())) { + stack_.erase(stack_.begin() + m.level_, stack_.end()); + stack_.back() = m.context_; + } + else if(stack_.empty() && m.level_ == 1) { + stack_.push_back(m.context_); + } + } + + private: + context_list stack_; + }; + + + //--------------------------------------------------------------- + group() = default; + + template + explicit + group(doc_string docstr, Param param, Params... params): + children_{}, exclusive_{false}, joinable_{false}, scoped_{true} + { + doc(std::move(docstr)); + push_back(std::move(param), std::move(params)...); + } + + template + explicit + group(parameter param, Params... params): + children_{}, exclusive_{false}, joinable_{false}, scoped_{true} + { + push_back(std::move(param), std::move(params)...); + } + + template + explicit + group(group p1, P2 p2, Ps... ps): + children_{}, exclusive_{false}, joinable_{false}, scoped_{true} + { + push_back(std::move(p1), std::move(p2), std::move(ps)...); + } + + + //----------------------------------------------------- + group(const group&) = default; + group(group&&) = default; + + + //--------------------------------------------------------------- + group& operator = (const group&) = default; + group& operator = (group&&) = default; + + + //--------------------------------------------------------------- + /** @brief determines if a command line argument can be matched by a + * combination of (partial) matches through any number of children + */ + group& joinable(bool yes) { + joinable_ = yes; + return *this; + } + + /** @brief returns if a command line argument can be matched by a + * combination of (partial) matches through any number of children + */ + bool joinable() const noexcept { + return joinable_; + } + + + //--------------------------------------------------------------- + /** @brief turns explicit scoping on or off + * operators , & | and other combinating functions will + * not merge groups that are marked as scoped + */ + group& scoped(bool yes) { + scoped_ = yes; + return *this; + } + + /** @brief returns true if operators , & | and other combinating functions + * will merge groups and false otherwise + */ + bool scoped() const noexcept + { + return scoped_; + } + + + //--------------------------------------------------------------- + /** @brief determines if children are mutually exclusive alternatives */ + group& exclusive(bool yes) { + exclusive_ = yes; + return *this; + } + /** @brief returns if children are mutually exclusive alternatives */ + bool exclusive() const noexcept { + return exclusive_; + } + + + //--------------------------------------------------------------- + /** @brief returns true, if any child is required to match */ + bool any_required() const + { + return std::any_of(children_.begin(), children_.end(), + [](const child& n){ return n.required(); }); + } + /** @brief returns true, if all children are required to match */ + bool all_required() const + { + return std::all_of(children_.begin(), children_.end(), + [](const child& n){ return n.required(); }); + } + + + //--------------------------------------------------------------- + /** @brief returns true if any child is optional (=non-required) */ + bool any_optional() const { + return !all_required(); + } + /** @brief returns true if all children are optional (=non-required) */ + bool all_optional() const { + return !any_required(); + } + + + //--------------------------------------------------------------- + /** @brief returns if the entire group is blocking / positional */ + bool blocking() const noexcept { + return token::blocking() || (exclusive() && all_blocking()); + } + //----------------------------------------------------- + /** @brief determines if the entire group is blocking / positional */ + group& blocking(bool yes) { + return token::blocking(yes); + } + + //--------------------------------------------------------------- + /** @brief returns true if any child is blocking */ + bool any_blocking() const + { + return std::any_of(children_.begin(), children_.end(), + [](const child& n){ return n.blocking(); }); + } + //--------------------------------------------------------------- + /** @brief returns true if all children is blocking */ + bool all_blocking() const + { + return std::all_of(children_.begin(), children_.end(), + [](const child& n){ return n.blocking(); }); + } + + + //--------------------------------------------------------------- + /** @brief returns if any child is a value parameter (recursive) */ + bool any_flagless() const + { + return std::any_of(children_.begin(), children_.end(), + [](const child& p){ + return p.is_param() && p.as_param().flags().empty(); + }); + } + /** @brief returns if all children are value parameters (recursive) */ + bool all_flagless() const + { + return std::all_of(children_.begin(), children_.end(), + [](const child& p){ + return p.is_param() && p.as_param().flags().empty(); + }); + } + + + //--------------------------------------------------------------- + /** @brief adds child parameter at the end */ + group& + push_back(const parameter& v) { + children_.emplace_back(v); + return *this; + } + //----------------------------------------------------- + /** @brief adds child parameter at the end */ + group& + push_back(parameter&& v) { + children_.emplace_back(std::move(v)); + return *this; + } + //----------------------------------------------------- + /** @brief adds child group at the end */ + group& + push_back(const group& g) { + children_.emplace_back(g); + return *this; + } + //----------------------------------------------------- + /** @brief adds child group at the end */ + group& + push_back(group&& g) { + children_.emplace_back(std::move(g)); + return *this; + } + + + //----------------------------------------------------- + /** @brief adds children (groups and/or parameters) */ + template + group& + push_back(Param1&& param1, Param2&& param2, Params&&... params) + { + children_.reserve(children_.size() + 2 + sizeof...(params)); + push_back(std::forward(param1)); + push_back(std::forward(param2), std::forward(params)...); + return *this; + } + + + //--------------------------------------------------------------- + /** @brief adds child parameter at the beginning */ + group& + push_front(const parameter& v) { + children_.emplace(children_.begin(), v); + return *this; + } + //----------------------------------------------------- + /** @brief adds child parameter at the beginning */ + group& + push_front(parameter&& v) { + children_.emplace(children_.begin(), std::move(v)); + return *this; + } + //----------------------------------------------------- + /** @brief adds child group at the beginning */ + group& + push_front(const group& g) { + children_.emplace(children_.begin(), g); + return *this; + } + //----------------------------------------------------- + /** @brief adds child group at the beginning */ + group& + push_front(group&& g) { + children_.emplace(children_.begin(), std::move(g)); + return *this; + } + + + //--------------------------------------------------------------- + /** @brief adds all children of other group at the end */ + group& + merge(group&& g) + { + children_.insert(children_.end(), + std::make_move_iterator(g.begin()), + std::make_move_iterator(g.end())); + return *this; + } + //----------------------------------------------------- + /** @brief adds all children of several other groups at the end */ + template + group& + merge(group&& g1, group&& g2, Groups&&... gs) + { + merge(std::move(g1)); + merge(std::move(g2), std::forward(gs)...); + return *this; + } + + + //--------------------------------------------------------------- + /** @brief indexed, nutable access to child */ + child& operator [] (size_type index) noexcept { + return children_[index]; + } + /** @brief indexed, non-nutable access to child */ + const child& operator [] (size_type index) const noexcept { + return children_[index]; + } + + //--------------------------------------------------------------- + /** @brief mutable access to first child */ + child& front() noexcept { return children_.front(); } + /** @brief non-mutable access to first child */ + const child& front() const noexcept { return children_.front(); } + //----------------------------------------------------- + /** @brief mutable access to last child */ + child& back() noexcept { return children_.back(); } + /** @brief non-mutable access to last child */ + const child& back() const noexcept { return children_.back(); } + + + //--------------------------------------------------------------- + /** @brief returns true, if group has no children, false otherwise */ + bool empty() const noexcept { return children_.empty(); } + + /** @brief returns number of children */ + size_type size() const noexcept { return children_.size(); } + + /** @brief returns number of nested levels; 1 for a flat group */ + size_type depth() const { + size_type n = 0; + for(const auto& c : children_) { + auto l = 1 + c.depth(); + if(l > n) n = l; + } + return n; + } + + + //--------------------------------------------------------------- + /** @brief returns mutating iterator to position of first element */ + iterator begin() noexcept { return children_.begin(); } + /** @brief returns non-mutating iterator to position of first element */ + const_iterator begin() const noexcept { return children_.begin(); } + /** @brief returns non-mutating iterator to position of first element */ + const_iterator cbegin() const noexcept { return children_.begin(); } + + /** @brief returns mutating iterator to position one past the last element */ + iterator end() noexcept { return children_.end(); } + /** @brief returns non-mutating iterator to position one past the last element */ + const_iterator end() const noexcept { return children_.end(); } + /** @brief returns non-mutating iterator to position one past the last element */ + const_iterator cend() const noexcept { return children_.end(); } + + + //--------------------------------------------------------------- + /** @brief returns augmented iterator for depth first searches + * @details traverser knows end of iteration and can skip over children + */ + depth_first_traverser + begin_dfs() const noexcept { + return depth_first_traverser{*this}; + } + + + //--------------------------------------------------------------- + /** @brief returns recursive parameter count */ + size_type param_count() const { + size_type c = 0; + for(const auto& n : children_) { + c += n.param_count(); + } + return c; + } + + + //--------------------------------------------------------------- + /** @brief returns range of all flags (recursive) */ + arg_list all_flags() const + { + std::vector all; + gather_flags(children_, all); + return all; + } + + /** @brief returns true, if no flag occurs as true + * prefix of any other flag (identical flags will be ignored) */ + bool flags_are_prefix_free() const + { + const auto fs = all_flags(); + + using std::begin; using std::end; + for(auto i = begin(fs), e = end(fs); i != e; ++i) { + if(!i->empty()) { + for(auto j = i+1; j != e; ++j) { + if(!j->empty() && *i != *j) { + if(i->find(*j) == 0) return false; + if(j->find(*i) == 0) return false; + } + } + } + } + + return true; + } + + + //--------------------------------------------------------------- + /** @brief returns longest common prefix of all flags */ + arg_string common_flag_prefix() const + { + arg_list prefixes; + gather_prefixes(children_, prefixes); + return str::longest_common_prefix(prefixes); + } + + +private: + //--------------------------------------------------------------- + static void + gather_flags(const children_store& nodes, arg_list& all) + { + for(const auto& p : nodes) { + if(p.is_group()) { + gather_flags(p.as_group().children_, all); + } + else { + const auto& pf = p.as_param().flags(); + using std::begin; + using std::end; + if(!pf.empty()) all.insert(end(all), begin(pf), end(pf)); + } + } + } + //--------------------------------------------------------------- + static void + gather_prefixes(const children_store& nodes, arg_list& all) + { + for(const auto& p : nodes) { + if(p.is_group()) { + gather_prefixes(p.as_group().children_, all); + } + else if(!p.as_param().flags().empty()) { + auto pfx = str::longest_common_prefix(p.as_param().flags()); + if(!pfx.empty()) all.push_back(std::move(pfx)); + } + } + } + + //--------------------------------------------------------------- + children_store children_; + bool exclusive_ = false; + bool joinable_ = false; + bool scoped_ = false; +}; + + + +/*************************************************************************//** + * + * @brief group or parameter + * + *****************************************************************************/ +using pattern = group::child; + + + +/*************************************************************************//** + * + * @brief apply an action to all parameters in a group + * + *****************************************************************************/ +template +void for_all_params(group& g, Action&& action) +{ + for(auto& p : g) { + if(p.is_group()) { + for_all_params(p.as_group(), action); + } + else { + action(p.as_param()); + } + } +} + +template +void for_all_params(const group& g, Action&& action) +{ + for(auto& p : g) { + if(p.is_group()) { + for_all_params(p.as_group(), action); + } + else { + action(p.as_param()); + } + } +} + + + +/*************************************************************************//** + * + * @brief makes a group of parameters and/or groups + * + *****************************************************************************/ +inline group +operator , (parameter a, parameter b) +{ + return group{std::move(a), std::move(b)}.scoped(false); +} + +//--------------------------------------------------------- +inline group +operator , (parameter a, group b) +{ + return !b.scoped() && !b.blocking() && !b.exclusive() && !b.repeatable() + && !b.joinable() && (b.doc().empty() || b.doc() == a.doc()) + ? b.push_front(std::move(a)) + : group{std::move(a), std::move(b)}.scoped(false); +} + +//--------------------------------------------------------- +inline group +operator , (group a, parameter b) +{ + return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable() + && !a.joinable() && (a.doc().empty() || a.doc() == b.doc()) + ? a.push_back(std::move(b)) + : group{std::move(a), std::move(b)}.scoped(false); +} + +//--------------------------------------------------------- +inline group +operator , (group a, group b) +{ + return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable() + && !a.joinable() && (a.doc().empty() || a.doc() == b.doc()) + ? a.push_back(std::move(b)) + : group{std::move(a), std::move(b)}.scoped(false); +} + + + +/*************************************************************************//** + * + * @brief makes a group of alternative parameters or groups + * + *****************************************************************************/ +template +inline group +one_of(Param param, Params... params) +{ + return group{std::move(param), std::move(params)...}.exclusive(true); +} + + +/*************************************************************************//** + * + * @brief makes a group of alternative parameters or groups + * + *****************************************************************************/ +inline group +operator | (parameter a, parameter b) +{ + return group{std::move(a), std::move(b)}.scoped(false).exclusive(true); +} + +//------------------------------------------------------------------- +inline group +operator | (parameter a, group b) +{ + return !b.scoped() && !b.blocking() && b.exclusive() && !b.repeatable() + && !b.joinable() + && (b.doc().empty() || b.doc() == a.doc()) + ? b.push_front(std::move(a)) + : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); +} + +//------------------------------------------------------------------- +inline group +operator | (group a, parameter b) +{ + return !a.scoped() && a.exclusive() && !a.repeatable() && !a.joinable() + && a.blocking() == b.blocking() + && (a.doc().empty() || a.doc() == b.doc()) + ? a.push_back(std::move(b)) + : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); +} + +inline group +operator | (group a, group b) +{ + return !a.scoped() && a.exclusive() &&!a.repeatable() && !a.joinable() + && a.blocking() == b.blocking() + && (a.doc().empty() || a.doc() == b.doc()) + ? a.push_back(std::move(b)) + : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); +} + + + +/*************************************************************************//** + * + * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) + * no interface guarantees; might be changed or removed in the future + * + *****************************************************************************/ +namespace detail { + +inline void set_blocking(bool) {} + +template +void set_blocking(bool yes, P& p, Ps&... ps) { + p.blocking(yes); + set_blocking(yes, ps...); +} + +} // namespace detail + + +/*************************************************************************//** + * + * @brief makes a parameter/group sequence by making all input objects blocking + * + *****************************************************************************/ +template +inline group +in_sequence(Param param, Params... params) +{ + detail::set_blocking(true, param, params...); + return group{std::move(param), std::move(params)...}.scoped(true); +} + + +/*************************************************************************//** + * + * @brief makes a parameter/group sequence by making all input objects blocking + * + *****************************************************************************/ +inline group +operator & (parameter a, parameter b) +{ + a.blocking(true); + b.blocking(true); + return group{std::move(a), std::move(b)}.scoped(true); +} + +//--------------------------------------------------------- +inline group +operator & (parameter a, group b) +{ + a.blocking(true); + return group{std::move(a), std::move(b)}.scoped(true); +} + +//--------------------------------------------------------- +inline group +operator & (group a, parameter b) +{ + b.blocking(true); + if(a.all_blocking() && !a.exclusive() && !a.repeatable() && !a.joinable() + && (a.doc().empty() || a.doc() == b.doc())) + { + return a.push_back(std::move(b)); + } + else { + if(!a.all_blocking()) a.blocking(true); + return group{std::move(a), std::move(b)}.scoped(true); + } +} + +inline group +operator & (group a, group b) +{ + if(!b.all_blocking()) b.blocking(true); + if(a.all_blocking() && !a.exclusive() && !a.repeatable() + && !a.joinable() && (a.doc().empty() || a.doc() == b.doc())) + { + return a.push_back(std::move(b)); + } + else { + if(!a.all_blocking()) a.blocking(true); + return group{std::move(a), std::move(b)}.scoped(true); + } +} + + + +/*************************************************************************//** + * + * @brief makes a group of parameters and/or groups + * where all single char flag params ("-a", "b", ...) are joinable + * + *****************************************************************************/ +inline group +joinable(group g) { + return g.joinable(true); +} + +//------------------------------------------------------------------- +template +inline group +joinable(parameter param, Params... params) +{ + return group{std::move(param), std::move(params)...}.joinable(true); +} + +template +inline group +joinable(group p1, P2 p2, Ps... ps) +{ + return group{std::move(p1), std::move(p2), std::move(ps)...}.joinable(true); +} + +template +inline group +joinable(doc_string docstr, Param param, Params... params) +{ + return group{std::move(param), std::move(params)...} + .joinable(true).doc(std::move(docstr)); +} + + + +/*************************************************************************//** + * + * @brief makes a repeatable copy of a parameter + * + *****************************************************************************/ +inline parameter +repeatable(parameter p) { + return p.repeatable(true); +} + +/*************************************************************************//** + * + * @brief makes a repeatable copy of a group + * + *****************************************************************************/ +inline group +repeatable(group g) { + return g.repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes a group of parameters and/or groups + * that is repeatable as a whole + * Note that a repeatable group consisting entirely of non-blocking + * children is equivalent to a non-repeatable group of + * repeatable children. + * + *****************************************************************************/ +template +inline group +repeatable(parameter p1, P2 p2, Ps... ps) +{ + return group{std::move(p1), std::move(p2), + std::move(ps)...}.repeatable(true); +} + +template +inline group +repeatable(group p1, P2 p2, Ps... ps) +{ + return group{std::move(p1), std::move(p2), + std::move(ps)...}.repeatable(true); +} + + + +/*************************************************************************//** + * + * @brief makes a parameter greedy (match with top priority) + * + *****************************************************************************/ +inline parameter +greedy(parameter p) { + return p.greedy(true); +} + +inline parameter +operator ! (parameter p) { + return greedy(p); +} + + + +/*************************************************************************//** + * + * @brief recursively prepends a prefix to all flags + * + *****************************************************************************/ +inline parameter&& +with_prefix(const arg_string& prefix, parameter&& p) { + return std::move(with_prefix(prefix, p)); +} + + +//------------------------------------------------------------------- +inline group& +with_prefix(const arg_string& prefix, group& g) +{ + for(auto& p : g) { + if(p.is_group()) { + with_prefix(prefix, p.as_group()); + } else { + with_prefix(prefix, p.as_param()); + } + } + return g; +} + + +inline group&& +with_prefix(const arg_string& prefix, group&& params) +{ + return std::move(with_prefix(prefix, params)); +} + + +template +inline group +with_prefix(arg_string prefix, Param&& param, Params&&... params) +{ + return with_prefix(prefix, group{std::forward(param), + std::forward(params)...}); +} + + + +/*************************************************************************//** + * + * @brief recursively prepends a prefix to all flags + * + * @param shortpfx : used for single-letter flags + * @param longpfx : used for flags with length > 1 + * + *****************************************************************************/ +inline parameter&& +with_prefixes_short_long(const arg_string& shortpfx, const arg_string& longpfx, + parameter&& p) +{ + return std::move(with_prefixes_short_long(shortpfx, longpfx, p)); +} + + +//------------------------------------------------------------------- +inline group& +with_prefixes_short_long(const arg_string& shortFlagPrefix, + const arg_string& longFlagPrefix, + group& g) +{ + for(auto& p : g) { + if(p.is_group()) { + with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_group()); + } else { + with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_param()); + } + } + return g; +} + + +inline group&& +with_prefixes_short_long(const arg_string& shortFlagPrefix, + const arg_string& longFlagPrefix, + group&& params) +{ + return std::move(with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, + params)); +} + + +template +inline group +with_prefixes_short_long(const arg_string& shortFlagPrefix, + const arg_string& longFlagPrefix, + Param&& param, Params&&... params) +{ + return with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, + group{std::forward(param), + std::forward(params)...}); +} + + + +/*************************************************************************//** + * + * @brief recursively prepends a suffix to all flags + * + *****************************************************************************/ +inline parameter&& +with_suffix(const arg_string& suffix, parameter&& p) { + return std::move(with_suffix(suffix, p)); +} + + +//------------------------------------------------------------------- +inline group& +with_suffix(const arg_string& suffix, group& g) +{ + for(auto& p : g) { + if(p.is_group()) { + with_suffix(suffix, p.as_group()); + } else { + with_suffix(suffix, p.as_param()); + } + } + return g; +} + + +inline group&& +with_suffix(const arg_string& suffix, group&& params) +{ + return std::move(with_suffix(suffix, params)); +} + + +template +inline group +with_suffix(arg_string suffix, Param&& param, Params&&... params) +{ + return with_suffix(suffix, group{std::forward(param), + std::forward(params)...}); +} + + + +/*************************************************************************//** + * + * @brief recursively prepends a suffix to all flags + * + * @param shortsfx : used for single-letter flags + * @param longsfx : used for flags with length > 1 + * + *****************************************************************************/ +inline parameter&& +with_suffixes_short_long(const arg_string& shortsfx, const arg_string& longsfx, + parameter&& p) +{ + return std::move(with_suffixes_short_long(shortsfx, longsfx, p)); +} + + +//------------------------------------------------------------------- +inline group& +with_suffixes_short_long(const arg_string& shortFlagSuffix, + const arg_string& longFlagSuffix, + group& g) +{ + for(auto& p : g) { + if(p.is_group()) { + with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, p.as_group()); + } else { + with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, p.as_param()); + } + } + return g; +} + + +inline group&& +with_suffixes_short_long(const arg_string& shortFlagSuffix, + const arg_string& longFlagSuffix, + group&& params) +{ + return std::move(with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, + params)); +} + + +template +inline group +with_suffixes_short_long(const arg_string& shortFlagSuffix, + const arg_string& longFlagSuffix, + Param&& param, Params&&... params) +{ + return with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, + group{std::forward(param), + std::forward(params)...}); +} + + + + + + + + +/*************************************************************************//** + * + * @brief parsing implementation details + * + *****************************************************************************/ + +namespace detail { + + +/*************************************************************************//** + * + * @brief DFS traverser that keeps track of 'scopes' + * scope = all parameters that are either bounded by + * two blocking parameters on the same depth level + * or the beginning/end of the outermost group + * + *****************************************************************************/ +class scoped_dfs_traverser +{ +public: + using dfs_traverser = group::depth_first_traverser; + + scoped_dfs_traverser() = default; + + explicit + scoped_dfs_traverser(const group& g): + pos_{g}, lastMatch_{}, posAfterLastMatch_{}, scopes_{}, + ignoreBlocks_{false}, + repeatGroupStarted_{false}, repeatGroupContinues_{false} + {} + + const dfs_traverser& base() const noexcept { return pos_; } + const dfs_traverser& last_match() const noexcept { return lastMatch_; } + + const group& parent() const noexcept { return pos_.parent(); } + + const group* innermost_repeat_group() const noexcept { + return pos_.innermost_repeat_group(); + } + const group* outermost_join_group() const noexcept { + return pos_.outermost_join_group(); + } + const group* innermost_blocking_group() const noexcept { + return pos_.innermost_blocking_group(); + } + const group* innermost_exclusive_group() const noexcept { + return pos_.innermost_exclusive_group(); + } + + const pattern* operator ->() const noexcept { return pos_.operator->(); } + const pattern& operator *() const noexcept { return *pos_; } + + const pattern* ptr() const noexcept { return pos_.operator->(); } + + explicit operator bool() const noexcept { return bool(pos_); } + + bool joinable() const noexcept { return pos_.joinable(); } + arg_string common_flag_prefix() const { return pos_.common_flag_prefix(); } + + void ignore_blocking(bool yes) { ignoreBlocks_ = yes; } + + void invalidate() { + pos_.invalidate(); + } + + bool matched() const noexcept { + return (pos_ == lastMatch_); + } + + bool start_of_repeat_group() const noexcept { return repeatGroupStarted_; } + + //----------------------------------------------------- + scoped_dfs_traverser& + next_sibling() { pos_.next_sibling(); return *this; } + + scoped_dfs_traverser& + next_after_siblings() { pos_.next_after_siblings(); return *this; } + + + //----------------------------------------------------- + scoped_dfs_traverser& + operator ++ () + { + if(!pos_) return *this; + + if(pos_.is_last_in_path()) { + return_to_outermost_scope(); + return *this; + } + + //current pattern can block if it didn't match already + if(ignoreBlocks_ || matched()) { + ++pos_; + } + else if(!pos_->is_group()) { + //current group can block if we didn't have any match in it + const group* g = pos_.outermost_blocking_group_fully_explored(); + //no match in 'g' before -> skip to after its siblings + if(g && !lastMatch_.is_inside(g)) { + pos_.back_to_ancestor(g).next_after_siblings(); + if(!pos_) return_to_outermost_scope(); + } + else if(pos_->blocking()) { + if(pos_.parent().exclusive()) { + pos_.next_sibling(); + } else { + //no match => skip siblings of blocking param + pos_.next_after_siblings(); + } + if(!pos_) return_to_outermost_scope(); + } else { + ++pos_; + } + } else { + ++pos_; + } + check_if_left_scope(); + return *this; + } + + //----------------------------------------------------- + void next_after_match(scoped_dfs_traverser match) + { + if(!match || ignoreBlocks_) return; + + check_repeat_group_start(match); + + lastMatch_ = match.base(); + + // if there is a blocking ancestor -> go back to it + if(!match->blocking()) { + match.pos_.back_to_ancestor(match.innermost_blocking_group()); + } + + //if match is not in current position & current position is blocking + //=> current position has to be advanced by one so that it is + //no longer reachable within current scope + //(can happen for repeatable, blocking parameters) + if(match.base() != pos_ && pos_->blocking()) pos_.next_sibling(); + + if(match->blocking()) { + if(match.pos_.is_alternative()) { + //discard other alternatives + match.pos_.skip_alternatives(); + } + + if(is_last_in_current_scope(match.pos_)) { + //if current param is not repeatable -> back to previous scope + if(!match->repeatable() && !match->is_group()) { + pos_ = std::move(match.pos_); + if(!scopes_.empty()) pos_.undo(scopes_.top()); + } + else { //stay at match position + pos_ = std::move(match.pos_); + } + } + else { //not last in current group + //if current param is not repeatable, go directly to next + if(!match->repeatable() && !match->is_group()) { + ++match.pos_; + } + + if(match.pos_.level() > pos_.level()) { + scopes_.push(pos_.undo_point()); + pos_ = std::move(match.pos_); + } + else if(match.pos_.level() < pos_.level()) { + return_to_level(match.pos_.level()); + } + else { + pos_ = std::move(match.pos_); + } + } + posAfterLastMatch_ = pos_; + } + else { + if(match.pos_.level() < pos_.level()) { + return_to_level(match.pos_.level()); + } + posAfterLastMatch_ = pos_; + } + repeatGroupContinues_ = repeat_group_continues(); + } + +private: + //----------------------------------------------------- + bool is_last_in_current_scope(const dfs_traverser& pos) const + { + if(scopes_.empty()) return pos.is_last_in_path(); + //check if we would leave the current scope on ++ + auto p = pos; + ++p; + return p.level() < scopes_.top().level(); + } + + //----------------------------------------------------- + void check_repeat_group_start(const scoped_dfs_traverser& newMatch) + { + const auto newrg = newMatch.innermost_repeat_group(); + if(!newrg) { + repeatGroupStarted_ = false; + } + else if(lastMatch_.innermost_repeat_group() != newrg) { + repeatGroupStarted_ = true; + } + else if(!repeatGroupContinues_ || !newMatch.repeatGroupContinues_) { + repeatGroupStarted_ = true; + } + else { + //special case: repeat group is outermost group + //=> we can never really 'leave' and 'reenter' it + //but if the current scope is the first element, then we are + //conceptually at a position 'before' the group + repeatGroupStarted_ = scopes_.empty() || ( + newrg == pos_.root() && + scopes_.top().param() == &(*pos_.root()->begin()) ); + } + repeatGroupContinues_ = repeatGroupStarted_; + } + + //----------------------------------------------------- + bool repeat_group_continues() const + { + if(!repeatGroupContinues_) return false; + const auto curRepGroup = pos_.innermost_repeat_group(); + if(!curRepGroup) return false; + if(curRepGroup != lastMatch_.innermost_repeat_group()) return false; + if(!posAfterLastMatch_) return false; + return true; + } + + //----------------------------------------------------- + void check_if_left_scope() + { + if(posAfterLastMatch_) { + if(pos_.level() < posAfterLastMatch_.level()) { + while(!scopes_.empty() && scopes_.top().level() >= pos_.level()) { + pos_.undo(scopes_.top()); + scopes_.pop(); + } + posAfterLastMatch_.invalidate(); + } + } + while(!scopes_.empty() && scopes_.top().level() > pos_.level()) { + pos_.undo(scopes_.top()); + scopes_.pop(); + } + repeatGroupContinues_ = repeat_group_continues(); + } + + //----------------------------------------------------- + void return_to_outermost_scope() + { + posAfterLastMatch_.invalidate(); + + if(scopes_.empty()) { + pos_.invalidate(); + repeatGroupContinues_ = false; + return; + } + + while(!scopes_.empty() && (!pos_ || pos_.level() >= 1)) { + pos_.undo(scopes_.top()); + scopes_.pop(); + } + while(!scopes_.empty()) scopes_.pop(); + + repeatGroupContinues_ = repeat_group_continues(); + } + + //----------------------------------------------------- + void return_to_level(int level) + { + if(pos_.level() <= level) return; + while(!scopes_.empty() && pos_.level() > level) { + pos_.undo(scopes_.top()); + scopes_.pop(); + } + }; + + dfs_traverser pos_; + dfs_traverser lastMatch_; + dfs_traverser posAfterLastMatch_; + std::stack scopes_; + bool ignoreBlocks_ = false; + bool repeatGroupStarted_ = false; + bool repeatGroupContinues_ = false; +}; + + + + +/***************************************************************************** + * + * some parameter property predicates + * + *****************************************************************************/ +struct select_all { + bool operator () (const parameter&) const noexcept { return true; } +}; + +struct select_flags { + bool operator () (const parameter& p) const noexcept { + return !p.flags().empty(); + } +}; + +struct select_values { + bool operator () (const parameter& p) const noexcept { + return p.flags().empty(); + } +}; + + + +/*************************************************************************//** + * + * @brief result of a matching operation + * + *****************************************************************************/ +class match_t { +public: + using size_type = arg_string::size_type; + + match_t() = default; + + match_t(arg_string s, scoped_dfs_traverser p): + str_{std::move(s)}, pos_{std::move(p)} + {} + + size_type length() const noexcept { return str_.size(); } + + const arg_string& str() const noexcept { return str_; } + const scoped_dfs_traverser& pos() const noexcept { return pos_; } + + explicit operator bool() const noexcept { return bool(pos_); } + +private: + arg_string str_; + scoped_dfs_traverser pos_; +}; + + + +/*************************************************************************//** + * + * @brief finds the first parameter that matches a given string; + * candidate parameters are traversed using a scoped DFS traverser + * + *****************************************************************************/ +template +match_t +full_match(scoped_dfs_traverser pos, const arg_string& arg, + const ParamSelector& select) +{ + while(pos) { + if(pos->is_param()) { + const auto& param = pos->as_param(); + if(select(param)) { + const auto match = param.match(arg); + if(match && match.length() == arg.size()) { + return match_t{arg, std::move(pos)}; + } + } + } + ++pos; + } + return match_t{}; +} + + + +/*************************************************************************//** + * + * @brief finds the first parameter that matches any (non-empty) prefix + * of a given string; + * candidate parameters are traversed using a scoped DFS traverser + * + *****************************************************************************/ +template +match_t +longest_prefix_match(scoped_dfs_traverser pos, const arg_string& arg, + const ParamSelector& select) +{ + match_t longest; + + while(pos) { + if(pos->is_param()) { + const auto& param = pos->as_param(); + if(select(param)) { + auto match = param.match(arg); + if(match.prefix()) { + if(match.length() == arg.size()) { + return match_t{arg, std::move(pos)}; + } + else if(match.length() > longest.length()) { + longest = match_t{arg.substr(match.at(), match.length()), + pos}; + } + } + } + } + ++pos; + } + return longest; +} + + + +/*************************************************************************//** + * + * @brief finds the first parameter that partially matches a given string; + * candidate parameters are traversed using a scoped DFS traverser + * + *****************************************************************************/ +template +match_t +partial_match(scoped_dfs_traverser pos, const arg_string& arg, + const ParamSelector& select) +{ + while(pos) { + if(pos->is_param()) { + const auto& param = pos->as_param(); + if(select(param)) { + const auto match = param.match(arg); + if(match) { + return match_t{arg.substr(match.at(), match.length()), + std::move(pos)}; + } + } + } + ++pos; + } + return match_t{}; +} + +} //namespace detail + + + + + + +/***************************************************************//** + * + * @brief default command line arguments parser + * + *******************************************************************/ +class parser +{ +public: + using dfs_traverser = group::depth_first_traverser; + using scoped_dfs_traverser = detail::scoped_dfs_traverser; + + + /*****************************************************//** + * @brief arg -> parameter mapping + *********************************************************/ + class arg_mapping { + public: + friend class parser; + + explicit + arg_mapping(arg_index idx, arg_string s, + const dfs_traverser& match) + : + index_{idx}, arg_{std::move(s)}, match_{match}, + repeat_{0}, startsRepeatGroup_{false}, + blocked_{false}, conflict_{false} + {} + + explicit + arg_mapping(arg_index idx, arg_string s) : + index_{idx}, arg_{std::move(s)}, match_{}, + repeat_{0}, startsRepeatGroup_{false}, + blocked_{false}, conflict_{false} + {} + + arg_index index() const noexcept { return index_; } + const arg_string& arg() const noexcept { return arg_; } + + const parameter* param() const noexcept { + return match_ && match_->is_param() + ? &(match_->as_param()) : nullptr; + } + + std::size_t repeat() const noexcept { return repeat_; } + + bool blocked() const noexcept { return blocked_; } + bool conflict() const noexcept { return conflict_; } + + bool bad_repeat() const noexcept { + if(!param()) return false; + return repeat_ > 0 && !param()->repeatable() + && !match_.innermost_repeat_group(); + } + + bool any_error() const noexcept { + return !match_ || blocked() || conflict() || bad_repeat(); + } + + private: + arg_index index_; + arg_string arg_; + dfs_traverser match_; + std::size_t repeat_; + bool startsRepeatGroup_; + bool blocked_; + bool conflict_; + }; + + /*****************************************************//** + * @brief references a non-matched, required parameter + *********************************************************/ + class missing_event { + public: + explicit + missing_event(const parameter* p, arg_index after): + param_{p}, aftIndex_{after} + {} + + const parameter* param() const noexcept { return param_; } + + arg_index after_index() const noexcept { return aftIndex_; } + + private: + const parameter* param_; + arg_index aftIndex_; + }; + + //----------------------------------------------------- + using missing_events = std::vector; + using arg_mappings = std::vector; + + +private: + struct miss_candidate { + miss_candidate(dfs_traverser p, arg_index idx, + bool firstInRepeatGroup = false): + pos{std::move(p)}, index{idx}, + startsRepeatGroup{firstInRepeatGroup} + {} + + dfs_traverser pos; + arg_index index; + bool startsRepeatGroup; + }; + using miss_candidates = std::vector; + + +public: + //--------------------------------------------------------------- + /** @brief initializes parser with a command line interface + * @param offset = argument index offset used for reports + * */ + explicit + parser(const group& root, arg_index offset = 0): + root_{&root}, pos_{root}, + index_{offset-1}, eaten_{0}, + args_{}, missCand_{}, blocked_{false} + { + for_each_potential_miss(dfs_traverser{root}, + [this](const dfs_traverser& p){ + missCand_.emplace_back(p, index_); + }); + } + + + //--------------------------------------------------------------- + /** @brief processes one command line argument */ + bool operator() (const arg_string& arg) + { + ++eaten_; + ++index_; + + if(!valid()) return false; + + if(!blocked_ && try_match(arg)) return true; + + if(try_match_blocked(arg)) return false; + + //skipping of blocking & required patterns is not allowed + if(!blocked_ && !pos_.matched() && pos_->required() && pos_->blocking()) { + blocked_ = true; + } + + add_nomatch(arg); + return false; + } + + + //--------------------------------------------------------------- + /** @brief returns range of argument -> parameter mappings */ + const arg_mappings& args() const { + return args_; + } + + /** @brief returns list of missing events */ + missing_events missed() const { + missing_events misses; + misses.reserve(missCand_.size()); + for(auto i = missCand_.begin(); i != missCand_.end(); ++i) { + misses.emplace_back(&(i->pos->as_param()), i->index); + } + return misses; + } + + /** @brief returns number of processed command line arguments */ + arg_index parse_count() const noexcept { return eaten_; } + + /** @brief returns false if previously processed command line arguments + * lead to an invalid / inconsistent parsing result + */ + bool valid() const noexcept { return bool(pos_); } + + /** @brief returns false if previously processed command line arguments + * lead to an invalid / inconsistent parsing result + */ + explicit operator bool() const noexcept { return valid(); } + + +private: + //--------------------------------------------------------------- + using match_t = detail::match_t; + + + //--------------------------------------------------------------- + /** @brief try to match argument with unreachable parameter */ + bool try_match_blocked(const arg_string& arg) + { + //try to match ahead (using temporary parser) + if(pos_) { + auto ahead = *this; + if(try_match_blocked(std::move(ahead), arg)) return true; + } + + //try to match from the beginning (using temporary parser) + if(root_) { + parser all{*root_, index_+1}; + if(try_match_blocked(std::move(all), arg)) return true; + } + + return false; + } + + //--------------------------------------------------------------- + bool try_match_blocked(parser&& parse, const arg_string& arg) + { + const auto nold = int(parse.args_.size()); + + parse.pos_.ignore_blocking(true); + + if(!parse.try_match(arg)) return false; + + for(auto i = parse.args_.begin() + nold; i != parse.args_.end(); ++i) { + args_.push_back(*i); + args_.back().blocked_ = true; + } + return true; + } + + //--------------------------------------------------------------- + /** @brief try to find a parameter/pattern that matches 'arg' */ + bool try_match(const arg_string& arg) + { + //match greedy parameters before everything else + if(pos_->is_param() && pos_->blocking() && pos_->as_param().greedy()) { + const auto match = pos_->as_param().match(arg); + if(match && match.length() == arg.size()) { + add_match(detail::match_t{arg,pos_}); + return true; + } + } + + //try flags first (alone, joinable or strict sequence) + if(try_match_full(arg, detail::select_flags{})) return true; + if(try_match_joined_flags(arg)) return true; + if(try_match_joined_sequence(arg, detail::select_flags{})) return true; + //try value params (alone or strict sequence) + if(try_match_full(arg, detail::select_values{})) return true; + if(try_match_joined_sequence(arg, detail::select_all{})) return true; + //try joinable params + values in any order + if(try_match_joined_params(arg)) return true; + return false; + } + + //--------------------------------------------------------------- + /** + * @brief try to match full argument + * @param select : predicate that candidate parameters must satisfy + */ + template + bool try_match_full(const arg_string& arg, const ParamSelector& select) + { + auto match = detail::full_match(pos_, arg, select); + if(!match) return false; + add_match(match); + return true; + } + + //--------------------------------------------------------------- + /** + * @brief try to match argument as blocking sequence of parameters + * @param select : predicate that a parameter matching the prefix of + * 'arg' must satisfy + */ + template + bool try_match_joined_sequence(arg_string arg, + const ParamSelector& acceptFirst) + { + auto fstMatch = detail::longest_prefix_match(pos_, arg, acceptFirst); + + if(!fstMatch) return false; + + if(fstMatch.str().size() == arg.size()) { + add_match(fstMatch); + return true; + } + + if(!fstMatch.pos()->blocking()) return false; + + auto pos = fstMatch.pos(); + pos.ignore_blocking(true); + const auto parent = &pos.parent(); + if(!pos->repeatable()) ++pos; + + arg.erase(0, fstMatch.str().size()); + std::vector matches { std::move(fstMatch) }; + + while(!arg.empty() && pos && + pos->blocking() && pos->is_param() && + (&pos.parent() == parent)) + { + auto match = pos->as_param().match(arg); + + if(match.prefix()) { + matches.emplace_back(arg.substr(0,match.length()), pos); + arg.erase(0, match.length()); + if(!pos->repeatable()) ++pos; + } + else { + if(!pos->repeatable()) return false; + ++pos; + } + + } + //if arg not fully covered => discard temporary matches + if(!arg.empty() || matches.empty()) return false; + + for(const auto& m : matches) add_match(m); + return true; + } + + //----------------------------------------------------- + /** @brief try to match 'arg' as a concatenation of joinable flags */ + bool try_match_joined_flags(const arg_string& arg) + { + return find_join_group(pos_, [&](const group& g) { + return try_match_joined(g, arg, detail::select_flags{}, + g.common_flag_prefix()); + }); + } + + //--------------------------------------------------------------- + /** @brief try to match 'arg' as a concatenation of joinable parameters */ + bool try_match_joined_params(const arg_string& arg) + { + return find_join_group(pos_, [&](const group& g) { + return try_match_joined(g, arg, detail::select_all{}); + }); + } + + //----------------------------------------------------- + /** @brief try to match 'arg' as concatenation of joinable parameters + * that are all contained within one group + */ + template + bool try_match_joined(const group& joinGroup, arg_string arg, + const ParamSelector& select, + const arg_string& prefix = "") + { + //temporary parser with 'joinGroup' as top-level group + parser parse {joinGroup}; + //records temporary matches + std::vector matches; + + while(!arg.empty()) { + auto match = detail::longest_prefix_match(parse.pos_, arg, select); + + if(!match) return false; + + arg.erase(0, match.str().size()); + //make sure prefix is always present after the first match + //so that, e.g., flags "-a" and "-b" will be found in "-ab" + if(!arg.empty() && !prefix.empty() && arg.find(prefix) != 0 && + prefix != match.str()) + { + arg.insert(0,prefix); + } + + parse.add_match(match); + matches.push_back(std::move(match)); + } + + if(!arg.empty() || matches.empty()) return false; + + if(!parse.missCand_.empty()) return false; + for(const auto& a : parse.args_) if(a.any_error()) return false; + + //replay matches onto *this + for(const auto& m : matches) add_match(m); + return true; + } + + //----------------------------------------------------- + template + bool find_join_group(const scoped_dfs_traverser& start, + const GroupSelector& accept) const + { + if(start && start.parent().joinable()) { + const auto& g = start.parent(); + if(accept(g)) return true; + return false; + } + + auto pos = start; + while(pos) { + if(pos->is_group() && pos->as_group().joinable()) { + const auto& g = pos->as_group(); + if(accept(g)) return true; + pos.next_sibling(); + } + else { + ++pos; + } + } + return false; + } + + + //--------------------------------------------------------------- + void add_nomatch(const arg_string& arg) { + args_.emplace_back(index_, arg); + } + + + //--------------------------------------------------------------- + void add_match(const match_t& match) + { + const auto& pos = match.pos(); + if(!pos || !pos->is_param()) return; + + pos_.next_after_match(pos); + + arg_mapping newArg{index_, match.str(), pos.base()}; + newArg.repeat_ = occurrences_of(&pos->as_param()); + newArg.conflict_ = check_conflicts(pos.base()); + newArg.startsRepeatGroup_ = pos_.start_of_repeat_group(); + args_.push_back(std::move(newArg)); + + add_miss_candidates_after(pos); + clean_miss_candidates_for(pos.base()); + discard_alternative_miss_candidates(pos.base()); + + } + + //----------------------------------------------------- + bool check_conflicts(const dfs_traverser& match) + { + if(pos_.start_of_repeat_group()) return false; + bool conflict = false; + for(const auto& m : match.stack()) { + if(m.parent->exclusive()) { + for(auto i = args_.rbegin(); i != args_.rend(); ++i) { + if(!i->blocked()) { + for(const auto& c : i->match_.stack()) { + //sibling within same exclusive group => conflict + if(c.parent == m.parent && c.cur != m.cur) { + conflict = true; + i->conflict_ = true; + } + } + } + //check for conflicts only within current repeat cycle + if(i->startsRepeatGroup_) break; + } + } + } + return conflict; + } + + //----------------------------------------------------- + void clean_miss_candidates_for(const dfs_traverser& match) + { + auto i = std::find_if(missCand_.rbegin(), missCand_.rend(), + [&](const miss_candidate& m) { + return &(*m.pos) == &(*match); + }); + + if(i != missCand_.rend()) { + missCand_.erase(prev(i.base())); + } + } + + //----------------------------------------------------- + void discard_alternative_miss_candidates(const dfs_traverser& match) + { + if(missCand_.empty()) return; + //find out, if miss candidate is sibling of one of the same + //alternative groups that the current match is a member of + //if so, we can discard the miss + + //go through all exclusive groups of matching pattern + for(const auto& m : match.stack()) { + if(m.parent->exclusive()) { + for(auto i = int(missCand_.size())-1; i >= 0; --i) { + bool removed = false; + for(const auto& c : missCand_[i].pos.stack()) { + //sibling within same exclusive group => discard + if(c.parent == m.parent && c.cur != m.cur) { + missCand_.erase(missCand_.begin() + i); + if(missCand_.empty()) return; + removed = true; + break; + } + } + //remove miss candidates only within current repeat cycle + if(i > 0 && removed) { + if(missCand_[i-1].startsRepeatGroup) break; + } else { + if(missCand_[i].startsRepeatGroup) break; + } + } + } + } + } + + //----------------------------------------------------- + void add_miss_candidates_after(const scoped_dfs_traverser& match) + { + auto npos = match.base(); + if(npos.is_alternative()) npos.skip_alternatives(); + ++npos; + //need to add potential misses if: + //either new repeat group was started + const auto newRepGroup = match.innermost_repeat_group(); + if(newRepGroup) { + if(pos_.start_of_repeat_group()) { + for_each_potential_miss(std::move(npos), + [&,this](const dfs_traverser& pos) { + //only add candidates within repeat group + if(newRepGroup == pos.innermost_repeat_group()) { + missCand_.emplace_back(pos, index_, true); + } + }); + } + } + //... or an optional blocking param was hit + else if(match->blocking() && !match->required() && + npos.level() >= match.base().level()) + { + for_each_potential_miss(std::move(npos), + [&,this](const dfs_traverser& pos) { + //only add new candidates + if(std::find_if(missCand_.begin(), missCand_.end(), + [&](const miss_candidate& c){ + return &(*c.pos) == &(*pos); + }) == missCand_.end()) + { + missCand_.emplace_back(pos, index_); + } + }); + } + + } + + //----------------------------------------------------- + template + static void + for_each_potential_miss(dfs_traverser pos, Action&& action) + { + const auto level = pos.level(); + while(pos && pos.level() >= level) { + if(pos->is_group() ) { + const auto& g = pos->as_group(); + if(g.all_optional() || (g.exclusive() && g.any_optional())) { + pos.next_sibling(); + } else { + ++pos; + } + } else { //param + if(pos->required()) { + action(pos); + ++pos; + } else if(pos->blocking()) { //optional + blocking + pos.next_after_siblings(); + } else { + ++pos; + } + } + } + } + + + //--------------------------------------------------------------- + std::size_t occurrences_of(const parameter* p) const + { + if(!p) return 0; + + auto i = std::find_if(args_.rbegin(), args_.rend(), + [p](const arg_mapping& a){ return a.param() == p; }); + + if(i != args_.rend()) return i->repeat() + 1; + return 0; + } + + + //--------------------------------------------------------------- + const group* root_; + scoped_dfs_traverser pos_; + arg_index index_; + arg_index eaten_; + arg_mappings args_; + miss_candidates missCand_; + bool blocked_; +}; + + + + +/*************************************************************************//** + * + * @brief contains argument -> parameter mappings + * and missing parameters + * + *****************************************************************************/ +class parsing_result +{ +public: + using arg_mapping = parser::arg_mapping; + using arg_mappings = parser::arg_mappings; + using missing_event = parser::missing_event; + using missing_events = parser::missing_events; + using iterator = arg_mappings::const_iterator; + + //----------------------------------------------------- + /** @brief default: empty result */ + parsing_result() = default; + + parsing_result(arg_mappings arg2param, missing_events misses): + arg2param_{std::move(arg2param)}, missing_{std::move(misses)} + {} + + //----------------------------------------------------- + /** @brief returns number of arguments that could not be mapped to + * a parameter + */ + arg_mappings::size_type + unmapped_args_count() const noexcept { + return std::count_if(arg2param_.begin(), arg2param_.end(), + [](const arg_mapping& a){ return !a.param(); }); + } + + /** @brief returns if any argument could only be matched by an + * unreachable parameter + */ + bool any_blocked() const noexcept { + return std::any_of(arg2param_.begin(), arg2param_.end(), + [](const arg_mapping& a){ return a.blocked(); }); + } + + /** @brief returns if any argument matched more than one parameter + * that were mutually exclusive */ + bool any_conflict() const noexcept { + return std::any_of(arg2param_.begin(), arg2param_.end(), + [](const arg_mapping& a){ return a.conflict(); }); + } + + /** @brief returns if any parameter matched repeatedly although + * it was not allowed to */ + bool any_bad_repeat() const noexcept { + return std::any_of(arg2param_.begin(), arg2param_.end(), + [](const arg_mapping& a){ return a.bad_repeat(); }); + } + + /** @brief returns true if any parsing error / violation of the + * command line interface definition occurred */ + bool any_error() const noexcept { + return unmapped_args_count() > 0 || !missing().empty() || + any_blocked() || any_conflict() || any_bad_repeat(); + } + + /** @brief returns true if no parsing error / violation of the + * command line interface definition occurred */ + explicit operator bool() const noexcept { return !any_error(); } + + /** @brief access to range of missing parameter match events */ + const missing_events& missing() const noexcept { return missing_; } + + /** @brief returns non-mutating iterator to position of + * first argument -> parameter mapping */ + iterator begin() const noexcept { return arg2param_.begin(); } + /** @brief returns non-mutating iterator to position one past the + * last argument -> parameter mapping */ + iterator end() const noexcept { return arg2param_.end(); } + +private: + //----------------------------------------------------- + arg_mappings arg2param_; + missing_events missing_; +}; + + + + +namespace detail { +namespace { + +/*************************************************************************//** + * + * @brief correct some common problems + * does not - and MUST NOT - change the number of arguments + * (no insertions or deletions allowed) + * + *****************************************************************************/ +void sanitize_args(arg_list& args) +{ + //e.g. {"-o12", ".34"} -> {"-o", "12.34"} + + if(args.empty()) return; + + for(auto i = begin(args)+1; i != end(args); ++i) { + if(i != begin(args) && i->size() > 1 && + i->find('.') == 0 && std::isdigit((*i)[1]) ) + { + //find trailing digits in previous arg + using std::prev; + auto& prv = *prev(i); + auto fstDigit = std::find_if_not(prv.rbegin(), prv.rend(), + [](arg_string::value_type c){ + return std::isdigit(c); + }).base(); + + //handle leading sign + if(fstDigit > prv.begin() && + (*prev(fstDigit) == '+' || *prev(fstDigit) == '-')) + { + --fstDigit; + } + + //prepend digits from previous arg + i->insert(begin(*i), fstDigit, end(prv)); + + //erase digits in previous arg + prv.erase(fstDigit, end(prv)); + } + } +} + + + +/*************************************************************************//** + * + * @brief executes actions based on a parsing result + * + *****************************************************************************/ +void execute_actions(const parsing_result& res) +{ + for(const auto& m : res) { + if(m.param()) { + const auto& param = *(m.param()); + + if(m.repeat() > 0) param.notify_repeated(m.index()); + if(m.blocked()) param.notify_blocked(m.index()); + if(m.conflict()) param.notify_conflict(m.index()); + //main action + if(!m.any_error()) param.execute_actions(m.arg()); + } + } + + for(auto m : res.missing()) { + if(m.param()) m.param()->notify_missing(m.after_index()); + } +} + + + +/*************************************************************************//** + * + * @brief parses input args + * + *****************************************************************************/ +static parsing_result +parse_args(const arg_list& args, const group& cli, + arg_index offset = 0) +{ + //parse args and store unrecognized arg indices + parser parse{cli, offset}; + for(const auto& arg : args) { + parse(arg); + if(!parse.valid()) break; + } + + return parsing_result{parse.args(), parse.missed()}; +} + +/*************************************************************************//** + * + * @brief parses input args & executes actions + * + *****************************************************************************/ +static parsing_result +parse_and_execute(const arg_list& args, const group& cli, + arg_index offset = 0) +{ + auto result = parse_args(args, cli, offset); + + execute_actions(result); + + return result; +} + +} //anonymous namespace +} // namespace detail + + + + +/*************************************************************************//** + * + * @brief parses vector of arg strings and executes actions + * + *****************************************************************************/ +inline parsing_result +parse(arg_list args, const group& cli) +{ + detail::sanitize_args(args); + return detail::parse_and_execute(args, cli); +} + + +/*************************************************************************//** + * + * @brief parses initializer_list of C-style arg strings and executes actions + * + *****************************************************************************/ +inline parsing_result +parse(std::initializer_list arglist, const group& cli) +{ + arg_list args; + args.reserve(arglist.size()); + for(auto a : arglist) { + args.push_back(a); + } + + return parse(std::move(args), cli); +} + + +/*************************************************************************//** + * + * @brief parses range of arg strings and executes actions + * + *****************************************************************************/ +template +inline parsing_result +parse(InputIterator first, InputIterator last, const group& cli) +{ + return parse(arg_list(first,last), cli); +} + + +/*************************************************************************//** + * + * @brief parses the standard array of command line arguments; omits argv[0] + * + *****************************************************************************/ +inline parsing_result +parse(const int argc, char* argv[], const group& cli, arg_index offset = 1) +{ + arg_list args; + if(offset < argc) args.assign(argv+offset, argv+argc); + detail::sanitize_args(args); + return detail::parse_and_execute(args, cli, offset); +} + + + + + + +/*************************************************************************//** + * + * @brief filter predicate for parameters and groups; + * Can be used to limit documentation generation to parameter subsets. + * + *****************************************************************************/ +class param_filter +{ +public: + /** @brief only allow parameters with given prefix */ + param_filter& prefix(const arg_string& p) noexcept { + prefix_ = p; return *this; + } + /** @brief only allow parameters with given prefix */ + param_filter& prefix(arg_string&& p) noexcept { + prefix_ = std::move(p); return *this; + } + const arg_string& prefix() const noexcept { return prefix_; } + + /** @brief only allow parameters with given requirement status */ + param_filter& required(tri t) noexcept { required_ = t; return *this; } + tri required() const noexcept { return required_; } + + /** @brief only allow parameters with given blocking status */ + param_filter& blocking(tri t) noexcept { blocking_ = t; return *this; } + tri blocking() const noexcept { return blocking_; } + + /** @brief only allow parameters with given repeatable status */ + param_filter& repeatable(tri t) noexcept { repeatable_ = t; return *this; } + tri repeatable() const noexcept { return repeatable_; } + + /** @brief only allow parameters with given docstring status */ + param_filter& has_doc(tri t) noexcept { hasDoc_ = t; return *this; } + tri has_doc() const noexcept { return hasDoc_; } + + + /** @brief returns true, if parameter satisfies all filters */ + bool operator() (const parameter& p) const noexcept { + if(!prefix_.empty()) { + if(!std::any_of(p.flags().begin(), p.flags().end(), + [&](const arg_string& flag){ + return str::has_prefix(flag, prefix_); + })) return false; + } + if(required() != p.required()) return false; + if(blocking() != p.blocking()) return false; + if(repeatable() != p.repeatable()) return false; + if(has_doc() != !p.doc().empty()) return false; + return true; + } + +private: + arg_string prefix_; + tri required_ = tri::either; + tri blocking_ = tri::either; + tri repeatable_ = tri::either; + tri hasDoc_ = tri::yes; +}; + + + + + + +/*************************************************************************//** + * + * @brief documentation formatting options + * + *****************************************************************************/ +class doc_formatting +{ +public: + using string = doc_string; + + /** @brief same as 'first_column' */ +#if __cplusplus >= 201402L + [[deprecated]] +#endif + doc_formatting& start_column(int col) { return first_column(col); } +#if __cplusplus >= 201402L + [[deprecated]] +#endif + int start_column() const noexcept { return first_column(); } + + /** @brief determines column where documentation printing starts */ + doc_formatting& + first_column(int col) { + //limit to [0,last_column] but push doc_column to the right if necessary + if(col < 0) col = 0; + else if(col > last_column()) col = last_column(); + if(col > doc_column()) doc_column(first_column()); + firstCol_ = col; + return *this; + } + int first_column() const noexcept { + return firstCol_; + } + + /** @brief determines column where docstrings start */ + doc_formatting& + doc_column(int col) { + //limit to [first_column,last_column] + if(col < 0) col = 0; + else if(col < first_column()) col = first_column(); + else if(col > last_column()) col = last_column(); + docCol_ = col; + return *this; + } + int doc_column() const noexcept { + return docCol_; + } + + /** @brief determines column that no documentation text must exceed; + * (text should be wrapped appropriately after this column) + */ + doc_formatting& + last_column(int col) { + //limit to [first_column,oo] but push doc_column to the left if necessary + if(col < first_column()) col = first_column(); + if(col < doc_column()) doc_column(col); + lastCol_ = col; + return *this; + } + + int last_column() const noexcept { + return lastCol_; + } + + /** @brief determines indent of documentation lines + * for children of a documented group */ + doc_formatting& indent_size(int indent) { indentSize_ = indent; return *this; } + int indent_size() const noexcept { return indentSize_; } + + /** @brief determines string to be used + * if a parameter has no flags and no label */ + doc_formatting& empty_label(const string& label) { + emptyLabel_ = label; + return *this; + } + const string& empty_label() const noexcept { return emptyLabel_; } + + /** @brief determines string for separating parameters */ + doc_formatting& param_separator(const string& sep) { + paramSep_ = sep; + return *this; + } + const string& param_separator() const noexcept { return paramSep_; } + + /** @brief determines string for separating groups (in usage lines) */ + doc_formatting& group_separator(const string& sep) { + groupSep_ = sep; + return *this; + } + const string& group_separator() const noexcept { return groupSep_; } + + /** @brief determines string for separating alternative parameters */ + doc_formatting& alternative_param_separator(const string& sep) { + altParamSep_ = sep; + return *this; + } + const string& alternative_param_separator() const noexcept { return altParamSep_; } + + /** @brief determines string for separating alternative groups */ + doc_formatting& alternative_group_separator(const string& sep) { + altGroupSep_ = sep; + return *this; + } + const string& alternative_group_separator() const noexcept { return altGroupSep_; } + + /** @brief determines string for separating flags of the same parameter */ + doc_formatting& flag_separator(const string& sep) { + flagSep_ = sep; + return *this; + } + const string& flag_separator() const noexcept { return flagSep_; } + + /** @brief determines strings surrounding parameter labels */ + doc_formatting& + surround_labels(const string& prefix, const string& postfix) { + labelPre_ = prefix; + labelPst_ = postfix; + return *this; + } + const string& label_prefix() const noexcept { return labelPre_; } + const string& label_postfix() const noexcept { return labelPst_; } + + /** @brief determines strings surrounding optional parameters/groups */ + doc_formatting& + surround_optional(const string& prefix, const string& postfix) { + optionPre_ = prefix; + optionPst_ = postfix; + return *this; + } + const string& optional_prefix() const noexcept { return optionPre_; } + const string& optional_postfix() const noexcept { return optionPst_; } + + /** @brief determines strings surrounding repeatable parameters/groups */ + doc_formatting& + surround_repeat(const string& prefix, const string& postfix) { + repeatPre_ = prefix; + repeatPst_ = postfix; + return *this; + } + const string& repeat_prefix() const noexcept { return repeatPre_; } + const string& repeat_postfix() const noexcept { return repeatPst_; } + + /** @brief determines strings surrounding exclusive groups */ + doc_formatting& + surround_alternatives(const string& prefix, const string& postfix) { + alternPre_ = prefix; + alternPst_ = postfix; + return *this; + } + const string& alternatives_prefix() const noexcept { return alternPre_; } + const string& alternatives_postfix() const noexcept { return alternPst_; } + + /** @brief determines strings surrounding alternative flags */ + doc_formatting& + surround_alternative_flags(const string& prefix, const string& postfix) { + alternFlagPre_ = prefix; + alternFlagPst_ = postfix; + return *this; + } + const string& alternative_flags_prefix() const noexcept { return alternFlagPre_; } + const string& alternative_flags_postfix() const noexcept { return alternFlagPst_; } + + /** @brief determines strings surrounding non-exclusive groups */ + doc_formatting& + surround_group(const string& prefix, const string& postfix) { + groupPre_ = prefix; + groupPst_ = postfix; + return *this; + } + const string& group_prefix() const noexcept { return groupPre_; } + const string& group_postfix() const noexcept { return groupPst_; } + + /** @brief determines strings surrounding joinable groups */ + doc_formatting& + surround_joinable(const string& prefix, const string& postfix) { + joinablePre_ = prefix; + joinablePst_ = postfix; + return *this; + } + const string& joinable_prefix() const noexcept { return joinablePre_; } + const string& joinable_postfix() const noexcept { return joinablePst_; } + + /** @brief determines maximum number of flags per parameter to be printed + * in detailed parameter documentation lines */ + doc_formatting& max_flags_per_param_in_doc(int max) { + maxAltInDocs_ = max > 0 ? max : 0; + return *this; + } + int max_flags_per_param_in_doc() const noexcept { return maxAltInDocs_; } + + /** @brief determines maximum number of flags per parameter to be printed + * in usage lines */ + doc_formatting& max_flags_per_param_in_usage(int max) { + maxAltInUsage_ = max > 0 ? max : 0; + return *this; + } + int max_flags_per_param_in_usage() const noexcept { return maxAltInUsage_; } + + /** @brief determines number of empty rows after one single-line + * documentation entry */ + doc_formatting& line_spacing(int lines) { + lineSpc_ = lines > 0 ? lines : 0; + return *this; + } + int line_spacing() const noexcept { return lineSpc_; } + + /** @brief determines number of empty rows before and after a paragraph; + * a paragraph is defined by a documented group or if + * a parameter documentation entry used more than one line */ + doc_formatting& paragraph_spacing(int lines) { + paragraphSpc_ = lines > 0 ? lines : 0; + return *this; + } + int paragraph_spacing() const noexcept { return paragraphSpc_; } + + /** @brief determines if alternative flags with a common prefix should + * be printed in a merged fashion */ + doc_formatting& merge_alternative_flags_with_common_prefix(bool yes = true) { + mergeAltCommonPfx_ = yes; + return *this; + } + bool merge_alternative_flags_with_common_prefix() const noexcept { + return mergeAltCommonPfx_; + } + + /** @brief determines if joinable flags with a common prefix should + * be printed in a merged fashion */ + doc_formatting& merge_joinable_with_common_prefix(bool yes = true) { + mergeJoinableCommonPfx_ = yes; + return *this; + } + bool merge_joinable_with_common_prefix() const noexcept { + return mergeJoinableCommonPfx_; + } + + /** @brief determines if children of exclusive groups should be printed + * on individual lines if the exceed 'alternatives_min_split_size' + */ + doc_formatting& split_alternatives(bool yes = true) { + splitTopAlt_ = yes; + return *this; + } + bool split_alternatives() const noexcept { + return splitTopAlt_; + } + + /** @brief determines how many children exclusive groups can have before + * their children are printed on individual usage lines */ + doc_formatting& alternatives_min_split_size(int size) { + groupSplitSize_ = size > 0 ? size : 0; + return *this; + } + int alternatives_min_split_size() const noexcept { return groupSplitSize_; } + + /** @brief determines whether to ignore new line characters in docstrings + */ + doc_formatting& ignore_newline_chars(bool yes = true) { + ignoreNewlines_ = yes; + return *this; + } + bool ignore_newline_chars() const noexcept { + return ignoreNewlines_; + } + +private: + string paramSep_ = string(" "); + string groupSep_ = string(" "); + string altParamSep_ = string("|"); + string altGroupSep_ = string(" | "); + string flagSep_ = string(", "); + string labelPre_ = string("<"); + string labelPst_ = string(">"); + string optionPre_ = string("["); + string optionPst_ = string("]"); + string repeatPre_ = string(""); + string repeatPst_ = string("..."); + string groupPre_ = string("("); + string groupPst_ = string(")"); + string alternPre_ = string("("); + string alternPst_ = string(")"); + string alternFlagPre_ = string(""); + string alternFlagPst_ = string(""); + string joinablePre_ = string("("); + string joinablePst_ = string(")"); + string emptyLabel_ = string(""); + int firstCol_ = 8; + int docCol_ = 20; + int lastCol_ = 100; + int indentSize_ = 4; + int maxAltInUsage_ = 1; + int maxAltInDocs_ = 32; + int lineSpc_ = 0; + int paragraphSpc_ = 1; + int groupSplitSize_ = 3; + bool splitTopAlt_ = true; + bool mergeAltCommonPfx_ = false; + bool mergeJoinableCommonPfx_ = true; + bool ignoreNewlines_ = false; +}; + + + +namespace detail { + +/*************************************************************************//** + * + * @brief stream decorator + * that applies formatting like line wrapping + * + *****************************************************************************/ +template +class formatting_ostream +{ +public: + using string_type = StringT; + using size_type = typename string_type::size_type; + using char_type = typename string_type::value_type; + + formatting_ostream(OStream& os): + os_(os), + curCol_{0}, firstCol_{0}, lastCol_{100}, + hangingIndent_{0}, paragraphSpacing_{0}, paragraphSpacingThreshold_{2}, + curBlankLines_{0}, curParagraphLines_{1}, + totalNonBlankLines_{0}, + ignoreInputNls_{false} + {} + + + //--------------------------------------------------------------- + const OStream& base() const noexcept { return os_; } + OStream& base() noexcept { return os_; } + + bool good() const { return os_.good(); } + + + //--------------------------------------------------------------- + /** @brief determines the leftmost border of the text body */ + formatting_ostream& first_column(int c) { + firstCol_ = c < 0 ? 0 : c; + return *this; + } + int first_column() const noexcept { return firstCol_; } + + /** @brief determines the rightmost border of the text body */ + formatting_ostream& last_column(int c) { + lastCol_ = c < 0 ? 0 : c; + return *this; + } + + int last_column() const noexcept { return lastCol_; } + + int text_width() const noexcept { + return lastCol_ - firstCol_; + } + + /** @brief additional indentation for the 2nd, 3rd, ... line of + a paragraph (sequence of soft-wrapped lines) */ + formatting_ostream& hanging_indent(int amount) { + hangingIndent_ = amount; + return *this; + } + int hanging_indent() const noexcept { + return hangingIndent_; + } + + /** @brief amount of blank lines between paragraphs */ + formatting_ostream& paragraph_spacing(int lines) { + paragraphSpacing_ = lines; + return *this; + } + int paragraph_spacing() const noexcept { + return paragraphSpacing_; + } + + /** @brief insert paragraph spacing + if paragraph is at least 'lines' lines long */ + formatting_ostream& min_paragraph_lines_for_spacing(int lines) { + paragraphSpacingThreshold_ = lines; + return *this; + } + int min_paragraph_lines_for_spacing() const noexcept { + return paragraphSpacingThreshold_; + } + + /** @brief if set to true, newline characters will be ignored */ + formatting_ostream& ignore_newline_chars(bool yes) { + ignoreInputNls_ = yes; + return *this; + } + + bool ignore_newline_chars() const noexcept { + return ignoreInputNls_; + } + + + //--------------------------------------------------------------- + /* @brief insert 'n' spaces */ + void write_spaces(int n) { + if(n < 1) return; + os_ << string_type(size_type(n), ' '); + curCol_ += n; + } + + /* @brief go to new line, but continue current paragraph */ + void wrap_soft(int times = 1) { + if(times < 1) return; + if(times > 1) { + os_ << string_type(size_type(times), '\n'); + } else { + os_ << '\n'; + } + curCol_ = 0; + ++curParagraphLines_; + } + + /* @brief go to new line, and start a new paragraph */ + void wrap_hard(int times = 1) { + if(times < 1) return; + + if(paragraph_spacing() > 0 && + paragraph_lines() >= min_paragraph_lines_for_spacing()) + { + times = paragraph_spacing() + 1; + } + + if(times > 1) { + os_ << string_type(size_type(times), '\n'); + curBlankLines_ += times - 1; + } else { + os_ << '\n'; + } + if(at_begin_of_line()) { + ++curBlankLines_; + } + curCol_ = 0; + curParagraphLines_ = 1; + } + + + //--------------------------------------------------------------- + bool at_begin_of_line() const noexcept { + return curCol_ <= current_line_begin(); + } + int current_line_begin() const noexcept { + return in_hanging_part_of_paragraph() + ? firstCol_ + hangingIndent_ + : firstCol_; + } + + int current_column() const noexcept { + return curCol_; + } + + int total_non_blank_lines() const noexcept { + return totalNonBlankLines_; + } + int paragraph_lines() const noexcept { + return curParagraphLines_; + } + int blank_lines_before_paragraph() const noexcept { + return curBlankLines_; + } + + + //--------------------------------------------------------------- + template + friend formatting_ostream& + operator << (formatting_ostream& os, const T& x) { + os.write(x); + return os; + } + + void flush() { + os_.flush(); + } + + +private: + bool in_hanging_part_of_paragraph() const noexcept { + return hanging_indent() > 0 && paragraph_lines() > 1; + } + bool current_line_empty() const noexcept { + return curCol_ < 1; + } + bool left_of_text_area() const noexcept { + return curCol_ < current_line_begin(); + } + bool right_of_text_area() const noexcept { + return curCol_ > lastCol_; + } + int columns_left_in_line() const noexcept { + return lastCol_ - std::max(current_line_begin(), curCol_); + } + + void fix_indent() { + if(left_of_text_area()) { + const auto fst = current_line_begin(); + write_spaces(fst - curCol_); + curCol_ = fst; + } + } + + template + bool only_whitespace(Iter first, Iter last) const { + return last == std::find_if_not(first, last, + [](char_type c) { return std::isspace(c); }); + } + + /** @brief write any object */ + template + void write(const T& x) { + std::ostringstream ss; + ss << x; + write(std::move(ss).str()); + } + + /** @brief write a stringstream */ + void write(const std::ostringstream& s) { + write(s.str()); + } + + /** @brief write a string */ + void write(const string_type& s) { + write(s.begin(), s.end()); + } + + /** @brief partition output into lines */ + template + void write(Iter first, Iter last) + { + if(first == last) return; + if(*first == '\n') { + if(!ignore_newline_chars()) wrap_hard(); + ++first; + if(first == last) return; + } + auto i = std::find(first, last, '\n'); + if(i != last) { + if(ignore_newline_chars()) ++i; + if(i != last) { + write_line(first, i); + write(i, last); + } + } + else { + write_line(first, last); + } + } + + /** @brief handle line wrapping due to column constraints */ + template + void write_line(Iter first, Iter last) + { + if(first == last) return; + if(only_whitespace(first, last)) return; + + if(right_of_text_area()) wrap_soft(); + + if(at_begin_of_line()) { + //discard whitespace, it we start a new line + first = std::find_if(first, last, + [](char_type c) { return !std::isspace(c); }); + if(first == last) return; + } + + const auto n = int(std::distance(first,last)); + const auto m = columns_left_in_line(); + //if text to be printed is too long for one line -> wrap + if(n > m) { + //break before word, if break is mid-word + auto breakat = first + m; + while(breakat > first && !std::isspace(*breakat)) --breakat; + //could not find whitespace before word -> try after the word + if(!std::isspace(*breakat) && breakat == first) { + breakat = std::find_if(first+m, last, + [](char_type c) { return std::isspace(c); }); + } + if(breakat > first) { + if(curCol_ < 1) ++totalNonBlankLines_; + fix_indent(); + std::copy(first, breakat, std::ostream_iterator(os_)); + curBlankLines_ = 0; + } + if(breakat < last) { + wrap_soft(); + write_line(breakat, last); + } + } + else { + if(curCol_ < 1) ++totalNonBlankLines_; + fix_indent(); + std::copy(first, last, std::ostream_iterator(os_)); + curCol_ += n; + curBlankLines_ = 0; + } + } + + /** @brief write a single character */ + void write(char_type c) + { + if(c == '\n') { + if(!ignore_newline_chars()) wrap_hard(); + } + else { + if(at_begin_of_line()) ++totalNonBlankLines_; + fix_indent(); + os_ << c; + ++curCol_; + } + } + + OStream& os_; + int curCol_; + int firstCol_; + int lastCol_; + int hangingIndent_; + int paragraphSpacing_; + int paragraphSpacingThreshold_; + int curBlankLines_; + int curParagraphLines_; + int totalNonBlankLines_; + bool ignoreInputNls_; +}; + + +} + + + + +/*************************************************************************//** + * + * @brief generates usage lines + * + * @details lazily evaluated + * + *****************************************************************************/ +class usage_lines +{ +public: + using string = doc_string; + + usage_lines(const group& cli, string prefix = "", + const doc_formatting& fmt = doc_formatting{}) + : + cli_(cli), fmt_(fmt), prefix_(std::move(prefix)) + { + if(!prefix_.empty()) prefix_ += ' '; + } + + usage_lines(const group& cli, const doc_formatting& fmt): + usage_lines(cli, "", fmt) + {} + + usage_lines& ommit_outermost_group_surrounders(bool yes) { + ommitOutermostSurrounders_ = yes; + return *this; + } + bool ommit_outermost_group_surrounders() const { + return ommitOutermostSurrounders_; + } + + template + inline friend OStream& operator << (OStream& os, const usage_lines& p) { + p.write(os); + return os; + } + + string str() const { + std::ostringstream os; os << *this; return os.str(); + } + + +private: + using stream_t = detail::formatting_ostream<>; + const group& cli_; + doc_formatting fmt_; + string prefix_; + bool ommitOutermostSurrounders_ = false; + + + //----------------------------------------------------- + struct context { + group::depth_first_traverser pos; + std::stack separators; + std::stack postfixes; + int level = 0; + const group* outermost = nullptr; + bool linestart = false; + bool useOutermost = true; + int line = 0; + + bool is_singleton() const noexcept { + return linestart && pos.is_last_in_path(); + } + bool is_alternative() const noexcept { + return pos.parent().exclusive(); + } + }; + + + /***************************************************************//** + * + * @brief writes usage text for command line parameters + * + *******************************************************************/ + template + void write(OStream& os) const + { + detail::formatting_ostream fos(os); + fos.first_column(fmt_.first_column()); + fos.last_column(fmt_.last_column()); + + auto hindent = int(prefix_.size()); + if(fos.first_column() + hindent >= int(0.4 * fos.text_width())) { + hindent = fmt_.indent_size(); + } + fos.hanging_indent(hindent); + + fos.paragraph_spacing(fmt_.paragraph_spacing()); + fos.min_paragraph_lines_for_spacing(2); + fos.ignore_newline_chars(fmt_.ignore_newline_chars()); + + context cur; + cur.pos = cli_.begin_dfs(); + cur.linestart = true; + cur.level = cur.pos.level(); + cur.outermost = &cli_; + + write(fos, cur, prefix_); + } + + + /***************************************************************//** + * + * @brief writes usage text for command line parameters + * + * @param prefix all that goes in front of current things to print + * + *******************************************************************/ + template + void write(OStream& os, context cur, string prefix) const + { + if(!cur.pos) return; + + std::ostringstream buf; + if(cur.linestart) buf << prefix; + const auto initPos = buf.tellp(); + + cur.level = cur.pos.level(); + + if(cur.useOutermost) { + //we cannot start outside of the outermost group + //so we have to treat it separately + start_group(buf, cur.pos.parent(), cur); + if(!cur.pos) { + os << buf.str(); + return; + } + } + else { + //don't visit siblings of starter node + cur.pos.skip_siblings(); + } + check_end_group(buf, cur); + + do { + if(buf.tellp() > initPos) cur.linestart = false; + if(!cur.linestart && !cur.pos.is_first_in_parent()) { + buf << cur.separators.top(); + } + if(cur.pos->is_group()) { + start_group(buf, cur.pos->as_group(), cur); + if(!cur.pos) { + os << buf.str(); + return; + } + } + else { + buf << param_label(cur.pos->as_param(), cur); + ++cur.pos; + } + check_end_group(buf, cur); + } while(cur.pos); + + os << buf.str(); + } + + + /***************************************************************//** + * + * @brief handles pattern group surrounders and separators + * and alternative splitting + * + *******************************************************************/ + void start_group(std::ostringstream& os, + const group& group, context& cur) const + { + //does cur.pos already point to a member or to group itself? + //needed for special treatment of outermost group + const bool alreadyInside = &(cur.pos.parent()) == &group; + + auto lbl = joined_label(group, cur); + if(!lbl.empty()) { + os << lbl; + cur.linestart = false; + //skip over entire group as its label has already been created + if(alreadyInside) { + cur.pos.next_after_siblings(); + } else { + cur.pos.next_sibling(); + } + } + else { + const bool splitAlternatives = group.exclusive() && + fmt_.split_alternatives() && + std::any_of(group.begin(), group.end(), + [this](const pattern& p) { + return int(p.param_count()) >= fmt_.alternatives_min_split_size(); + }); + + if(splitAlternatives) { + cur.postfixes.push(""); + cur.separators.push(""); + //recursively print alternative paths in decision-DAG + //enter group? + if(!alreadyInside) ++cur.pos; + cur.linestart = true; + cur.useOutermost = false; + auto pfx = os.str(); + os.str(""); + //print paths in DAG starting at each group member + for(std::size_t i = 0; i < group.size(); ++i) { + std::stringstream buf; + cur.outermost = cur.pos->is_group() ? &(cur.pos->as_group()) : nullptr; + write(buf, cur, pfx); + if(buf.tellp() > int(pfx.size())) { + os << buf.str(); + if(i < group.size()-1) { + if(cur.line > 0) { + os << string(fmt_.line_spacing(), '\n'); + } + ++cur.line; + os << '\n'; + } + } + cur.pos.next_sibling(); //do not descend into members + } + cur.pos.invalidate(); //signal end-of-path + return; + } + else { + //pre & postfixes, separators + auto surround = group_surrounders(group, cur); + os << surround.first; + cur.postfixes.push(std::move(surround.second)); + cur.separators.push(group_separator(group, fmt_)); + //descend into group? + if(!alreadyInside) ++cur.pos; + } + } + cur.level = cur.pos.level(); + } + + + /***************************************************************//** + * + *******************************************************************/ + void check_end_group(std::ostringstream& os, context& cur) const + { + for(; cur.level > cur.pos.level(); --cur.level) { + os << cur.postfixes.top(); + cur.postfixes.pop(); + cur.separators.pop(); + } + cur.level = cur.pos.level(); + } + + + /***************************************************************//** + * + * @brief makes usage label for one command line parameter + * + *******************************************************************/ + string param_label(const parameter& p, const context& cur) const + { + const auto& parent = cur.pos.parent(); + + const bool startsOptionalSequence = + parent.size() > 1 && p.blocking() && cur.pos.is_first_in_parent(); + + const bool outermost = + ommitOutermostSurrounders_ && cur.outermost == &parent; + + const bool showopt = !cur.is_alternative() && !p.required() + && !startsOptionalSequence && !outermost; + + const bool showrep = p.repeatable() && !outermost; + + string lbl; + + if(showrep) lbl += fmt_.repeat_prefix(); + if(showopt) lbl += fmt_.optional_prefix(); + + const auto& flags = p.flags(); + if(!flags.empty()) { + const int n = std::min(fmt_.max_flags_per_param_in_usage(), + int(flags.size())); + + const bool surrAlt = n > 1 && !showopt && !cur.is_singleton(); + + if(surrAlt) lbl += fmt_.alternative_flags_prefix(); + bool sep = false; + for(int i = 0; i < n; ++i) { + if(sep) { + if(cur.is_singleton()) + lbl += fmt_.alternative_group_separator(); + else + lbl += fmt_.flag_separator(); + } + lbl += flags[i]; + sep = true; + } + if(surrAlt) lbl += fmt_.alternative_flags_postfix(); + } + else { + if(!p.label().empty()) { + lbl += fmt_.label_prefix() + + p.label() + + fmt_.label_postfix(); + } else if(!fmt_.empty_label().empty()) { + lbl += fmt_.label_prefix() + + fmt_.empty_label() + + fmt_.label_postfix(); + } else { + return ""; + } + } + + if(showopt) lbl += fmt_.optional_postfix(); + if(showrep) lbl += fmt_.repeat_postfix(); + + return lbl; + } + + + /***************************************************************//** + * + * @brief prints flags in one group in a merged fashion + * + *******************************************************************/ + string joined_label(const group& g, const context& cur) const + { + if(!fmt_.merge_alternative_flags_with_common_prefix() && + !fmt_.merge_joinable_with_common_prefix()) return ""; + + const bool flagsonly = std::all_of(g.begin(), g.end(), + [](const pattern& p){ + return p.is_param() && !p.as_param().flags().empty(); + }); + + if(!flagsonly) return ""; + + const bool showOpt = g.all_optional() && + !(ommitOutermostSurrounders_ && cur.outermost == &g); + + auto pfx = g.common_flag_prefix(); + if(pfx.empty()) return ""; + + const auto n = pfx.size(); + if(g.exclusive() && + fmt_.merge_alternative_flags_with_common_prefix()) + { + string lbl; + if(showOpt) lbl += fmt_.optional_prefix(); + lbl += pfx + fmt_.alternatives_prefix(); + bool first = true; + for(const auto& p : g) { + if(p.is_param()) { + if(first) + first = false; + else + lbl += fmt_.alternative_param_separator(); + lbl += p.as_param().flags().front().substr(n); + } + } + lbl += fmt_.alternatives_postfix(); + if(showOpt) lbl += fmt_.optional_postfix(); + return lbl; + } + //no alternatives, but joinable flags + else if(g.joinable() && + fmt_.merge_joinable_with_common_prefix()) + { + const bool allSingleChar = std::all_of(g.begin(), g.end(), + [&](const pattern& p){ + return p.is_param() && + p.as_param().flags().front().substr(n).size() == 1; + }); + + if(allSingleChar) { + string lbl; + if(showOpt) lbl += fmt_.optional_prefix(); + lbl += pfx; + for(const auto& p : g) { + if(p.is_param()) + lbl += p.as_param().flags().front().substr(n); + } + if(showOpt) lbl += fmt_.optional_postfix(); + return lbl; + } + } + + return ""; + } + + + /***************************************************************//** + * + * @return symbols with which to surround a group + * + *******************************************************************/ + std::pair + group_surrounders(const group& group, const context& cur) const + { + string prefix; + string postfix; + + const bool isOutermost = &group == cur.outermost; + if(isOutermost && ommitOutermostSurrounders_) + return {string{}, string{}}; + + if(group.exclusive()) { + if(group.all_optional()) { + prefix = fmt_.optional_prefix(); + postfix = fmt_.optional_postfix(); + if(group.all_flagless()) { + prefix += fmt_.label_prefix(); + postfix = fmt_.label_prefix() + postfix; + } + } else if(group.all_flagless()) { + prefix = fmt_.label_prefix(); + postfix = fmt_.label_postfix(); + } else if(!cur.is_singleton() || !isOutermost) { + prefix = fmt_.alternatives_prefix(); + postfix = fmt_.alternatives_postfix(); + } + } + else if(group.size() > 1 && + group.front().blocking() && !group.front().required()) + { + prefix = fmt_.optional_prefix(); + postfix = fmt_.optional_postfix(); + } + else if(group.size() > 1 && cur.is_alternative() && + &group != cur.outermost) + { + prefix = fmt_.group_prefix(); + postfix = fmt_.group_postfix(); + } + else if(!group.exclusive() && + group.joinable() && !cur.linestart) + { + prefix = fmt_.joinable_prefix(); + postfix = fmt_.joinable_postfix(); + } + + if(group.repeatable()) { + if(prefix.empty()) prefix = fmt_.group_prefix(); + prefix = fmt_.repeat_prefix() + prefix; + if(postfix.empty()) postfix = fmt_.group_postfix(); + postfix += fmt_.repeat_postfix(); + } + + return {std::move(prefix), std::move(postfix)}; + } + + + /***************************************************************//** + * + * @return symbol that separates members of a group + * + *******************************************************************/ + static string + group_separator(const group& group, const doc_formatting& fmt) + { + const bool only1ParamPerMember = std::all_of(group.begin(), group.end(), + [](const pattern& p) { return p.param_count() < 2; }); + + if(only1ParamPerMember) { + if(group.exclusive()) { + return fmt.alternative_param_separator(); + } else { + return fmt.param_separator(); + } + } + else { //there is at least one large group inside + if(group.exclusive()) { + return fmt.alternative_group_separator(); + } else { + return fmt.group_separator(); + } + } + } +}; + + + + +/*************************************************************************//** + * + * @brief generates parameter and group documentation from docstrings + * + * @details lazily evaluated + * + *****************************************************************************/ +class documentation +{ +public: + using string = doc_string; + using filter_function = std::function; + + documentation(const group& cli, + const doc_formatting& fmt = doc_formatting{}, + filter_function filter = param_filter{}) + : + cli_(cli), fmt_{fmt}, usgFmt_{fmt}, filter_{std::move(filter)} + { + //necessary, because we re-use "usage_lines" to generate + //labels for documented groups + usgFmt_.max_flags_per_param_in_usage( + usgFmt_.max_flags_per_param_in_doc()); + } + + documentation(const group& cli, filter_function filter) : + documentation{cli, doc_formatting{}, std::move(filter)} + {} + + documentation(const group& cli, const param_filter& filter) : + documentation{cli, doc_formatting{}, + [filter](const parameter& p) { return filter(p); }} + {} + + template + inline friend OStream& operator << (OStream& os, const documentation& p) { + p.write(os); + return os; + } + + string str() const { + std::ostringstream os; + write(os); + return os.str(); + } + + +private: + using dfs_traverser = group::depth_first_traverser; + + const group& cli_; + doc_formatting fmt_; + doc_formatting usgFmt_; + filter_function filter_; + enum class paragraph { param, group }; + + + /***************************************************************//** + * + * @brief writes documentation to output stream + * + *******************************************************************/ + template + void write(OStream& os) const { + detail::formatting_ostream fos(os); + fos.first_column(fmt_.first_column()); + fos.last_column(fmt_.last_column()); + fos.hanging_indent(0); + fos.paragraph_spacing(0); + fos.ignore_newline_chars(fmt_.ignore_newline_chars()); + print_doc(fos, cli_); + } + + + /***************************************************************//** + * + * @brief writes full documentation text for command line parameters + * + *******************************************************************/ + template + void print_doc(detail::formatting_ostream& os, + const group& cli, int indentLvl = 0) const + { + if(cli.empty()) return; + + //if group itself doesn't have docstring + if(cli.doc().empty()) { + for(const auto& p : cli) { + print_doc(os, p, indentLvl); + } + } + else { //group itself does have docstring + bool anyDocInside = std::any_of(cli.begin(), cli.end(), + [](const pattern& p){ return !p.doc().empty(); }); + + if(anyDocInside) { //group docstring as title, then child entries + handle_spacing(os, paragraph::group, indentLvl); + os << cli.doc(); + for(const auto& p : cli) { + print_doc(os, p, indentLvl + 1); + } + } + else { //group label first then group docstring + auto lbl = usage_lines(cli, usgFmt_) + .ommit_outermost_group_surrounders(true).str(); + + str::trim(lbl); + handle_spacing(os, paragraph::param, indentLvl); + print_entry(os, lbl, cli.doc()); + } + } + } + + + /***************************************************************//** + * + * @brief writes documentation text for one group or parameter + * + *******************************************************************/ + template + void print_doc(detail::formatting_ostream& os, + const pattern& ptrn, int indentLvl) const + { + if(ptrn.is_group()) { + print_doc(os, ptrn.as_group(), indentLvl); + } + else { + const auto& p = ptrn.as_param(); + if(!filter_(p)) return; + + handle_spacing(os, paragraph::param, indentLvl); + print_entry(os, param_label(p, fmt_), p.doc()); + } + } + + /***************************************************************//** + * + * @brief handles line and paragraph spacings + * + *******************************************************************/ + template + void handle_spacing(detail::formatting_ostream& os, + paragraph p, int indentLvl) const + { + const auto oldIndent = os.first_column(); + const auto indent = fmt_.first_column() + indentLvl * fmt_.indent_size(); + + if(os.total_non_blank_lines() < 1) { + os.first_column(indent); + return; + } + + if(os.paragraph_lines() > 1 || indent < oldIndent) { + os.wrap_hard(fmt_.paragraph_spacing() + 1); + } else { + os.wrap_hard(); + } + + if(p == paragraph::group) { + if(os.blank_lines_before_paragraph() < fmt_.paragraph_spacing()) { + os.wrap_hard(fmt_.paragraph_spacing() - os.blank_lines_before_paragraph()); + } + } + else if(os.blank_lines_before_paragraph() < fmt_.line_spacing()) { + os.wrap_hard(fmt_.line_spacing() - os.blank_lines_before_paragraph()); + } + os.first_column(indent); + } + + /*********************************************************************//** + * + * @brief prints one entry = label + docstring + * + ************************************************************************/ + template + void print_entry(detail::formatting_ostream& os, + const string& label, const string& docstr) const + { + if(label.empty()) return; + + os << label; + + if(!docstr.empty()) { + if(os.current_column() >= fmt_.doc_column()) os.wrap_soft(); + const auto oldcol = os.first_column(); + os.first_column(fmt_.doc_column()); + os << docstr; + os.first_column(oldcol); + } + } + + + /*********************************************************************//** + * + * @brief makes label for one parameter + * + ************************************************************************/ + static doc_string + param_label(const parameter& param, const doc_formatting& fmt) + { + doc_string lbl; + + if(param.repeatable()) lbl += fmt.repeat_prefix(); + + const auto& flags = param.flags(); + if(!flags.empty()) { + lbl += flags[0]; + const int n = std::min(fmt.max_flags_per_param_in_doc(), + int(flags.size())); + for(int i = 1; i < n; ++i) { + lbl += fmt.flag_separator() + flags[i]; + } + } + else if(!param.label().empty() || !fmt.empty_label().empty()) { + lbl += fmt.label_prefix(); + if(!param.label().empty()) { + lbl += param.label(); + } else { + lbl += fmt.empty_label(); + } + lbl += fmt.label_postfix(); + } + + if(param.repeatable()) lbl += fmt.repeat_postfix(); + + return lbl; + } + +}; + + + + +/*************************************************************************//** + * + * @brief stores strings for man page sections + * + *****************************************************************************/ +class man_page +{ +public: + //--------------------------------------------------------------- + using string = doc_string; + + //--------------------------------------------------------------- + /** @brief man page section */ + class section { + public: + using string = doc_string; + + section(string stitle, string scontent): + title_{std::move(stitle)}, content_{std::move(scontent)} + {} + + const string& title() const noexcept { return title_; } + const string& content() const noexcept { return content_; } + + private: + string title_; + string content_; + }; + +private: + using section_store = std::vector
; + +public: + //--------------------------------------------------------------- + using value_type = section; + using const_iterator = section_store::const_iterator; + using size_type = section_store::size_type; + + + //--------------------------------------------------------------- + man_page& + append_section(string title, string content) + { + sections_.emplace_back(std::move(title), std::move(content)); + return *this; + } + //----------------------------------------------------- + man_page& + prepend_section(string title, string content) + { + sections_.emplace(sections_.begin(), + std::move(title), std::move(content)); + return *this; + } + + + //--------------------------------------------------------------- + const section& operator [] (size_type index) const noexcept { + return sections_[index]; + } + + //--------------------------------------------------------------- + size_type size() const noexcept { return sections_.size(); } + + bool empty() const noexcept { return sections_.empty(); } + + + //--------------------------------------------------------------- + const_iterator begin() const noexcept { return sections_.begin(); } + const_iterator end() const noexcept { return sections_.end(); } + + + //--------------------------------------------------------------- + man_page& program_name(const string& n) { + progName_ = n; + return *this; + } + man_page& program_name(string&& n) { + progName_ = std::move(n); + return *this; + } + const string& program_name() const noexcept { + return progName_; + } + + + //--------------------------------------------------------------- + man_page& section_row_spacing(int rows) { + sectionSpc_ = rows > 0 ? rows : 0; + return *this; + } + int section_row_spacing() const noexcept { return sectionSpc_; } + + +private: + int sectionSpc_ = 1; + section_store sections_; + string progName_; +}; + + + +/*************************************************************************//** + * + * @brief generates man sections from command line parameters + * with sections "synopsis" and "options" + * + *****************************************************************************/ +inline man_page +make_man_page(const group& cli, + doc_string progname = "", + const doc_formatting& fmt = doc_formatting{}) +{ + man_page man; + man.append_section("SYNOPSIS", usage_lines(cli,progname,fmt).str()); + man.append_section("OPTIONS", documentation(cli,fmt).str()); + return man; +} + + + +/*************************************************************************//** + * + * @brief generates man page based on command line parameters + * + *****************************************************************************/ +template +OStream& +operator << (OStream& os, const man_page& man) +{ + bool first = true; + const auto secSpc = doc_string(man.section_row_spacing() + 1, '\n'); + for(const auto& section : man) { + if(!section.content().empty()) { + if(first) first = false; else os << secSpc; + if(!section.title().empty()) os << section.title() << '\n'; + os << section.content(); + } + } + os << '\n'; + return os; +} + + + + + +/*************************************************************************//** + * + * @brief printing methods for debugging command line interfaces + * + *****************************************************************************/ +namespace debug { + + +/*************************************************************************//** + * + * @brief prints first flag or value label of a parameter + * + *****************************************************************************/ +inline doc_string doc_label(const parameter& p) +{ + if(!p.flags().empty()) return p.flags().front(); + if(!p.label().empty()) return p.label(); + return doc_string{""}; +} + +inline doc_string doc_label(const group&) +{ + return ""; +} + +inline doc_string doc_label(const pattern& p) +{ + return p.is_group() ? doc_label(p.as_group()) : doc_label(p.as_param()); +} + + +/*************************************************************************//** + * + * @brief prints parsing result + * + *****************************************************************************/ +template +void print(OStream& os, const parsing_result& result) +{ + for(const auto& m : result) { + os << "#" << m.index() << " " << m.arg() << " -> "; + auto p = m.param(); + if(p) { + os << doc_label(*p) << " \t"; + if(m.repeat() > 0) { + os << (m.bad_repeat() ? "[bad repeat " : "[repeat ") + << m.repeat() << "]"; + } + if(m.blocked()) os << " [blocked]"; + if(m.conflict()) os << " [conflict]"; + os << '\n'; + } + else { + os << " [unmapped]\n"; + } + } + + for(const auto& m : result.missing()) { + auto p = m.param(); + if(p) { + os << doc_label(*p) << " \t"; + os << " [missing after " << m.after_index() << "]\n"; + } + } +} + + +/*************************************************************************//** + * + * @brief prints parameter label and some properties + * + *****************************************************************************/ +template +void print(OStream& os, const parameter& p) +{ + if(p.greedy()) os << '!'; + if(p.blocking()) os << '~'; + if(!p.required()) os << '['; + os << doc_label(p); + if(p.repeatable()) os << "..."; + if(!p.required()) os << "]"; +} + + +//------------------------------------------------------------------- +template +void print(OStream& os, const group& g, int level = 0); + + +/*************************************************************************//** + * + * @brief prints group or parameter; uses indentation + * + *****************************************************************************/ +template +void print(OStream& os, const pattern& param, int level = 0) +{ + if(param.is_group()) { + print(os, param.as_group(), level); + } + else { + os << doc_string(4*level, ' '); + print(os, param.as_param()); + } +} + + +/*************************************************************************//** + * + * @brief prints group and its contents; uses indentation + * + *****************************************************************************/ +template +void print(OStream& os, const group& g, int level) +{ + auto indent = doc_string(4*level, ' '); + os << indent; + if(g.blocking()) os << '~'; + if(g.joinable()) os << 'J'; + os << (g.exclusive() ? "(|\n" : "(\n"); + for(const auto& p : g) { + print(os, p, level+1); + } + os << '\n' << indent << (g.exclusive() ? "|)" : ")"); + if(g.repeatable()) os << "..."; + os << '\n'; +} + + +} // namespace debug +} //namespace clipp + +#endif diff --git a/src/filedialog.cpp b/src/filedialog.cpp new file mode 100644 index 0000000..5855784 --- /dev/null +++ b/src/filedialog.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include + +#include "signals.hpp" + +static std::filesystem::path currentPath = std::filesystem::current_path(), + pathToExport = std::filesystem::current_path(); +static std::string fileName = ""; + +static void updateFileDialog(ftxui::Component entryList, signals_map_t &smap) { + entryList->DetachAllChildren(); + + for (const auto &entry : {".", ".."}) { + entryList->Add({ + ftxui::Button({ + .label = entry, + .on_click = + [entryList, entry, &smap]() { + if (std::string(entry) == "..") { + currentPath = currentPath.parent_path(); + updateFileDialog(entryList, smap); + } + }, + + .transform = [entry](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(entry) | ftxui::color(ftxui::Color::Blue) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey7) : ftxui::nothing); + }, + }), + }); + } + + for (const auto &entry : std::filesystem::directory_iterator(currentPath)) { + entryList->Add({ + ftxui::Button({ + .label = entry.path().filename().c_str(), + .on_click = + [entry, entryList, &smap]() { + if (entry.is_directory()) { + currentPath = entry; + } else { + fileName = entry.path().filename().c_str(); + pathToExport = entry.path(); + } + + updateFileDialog(entryList, smap); + }, + + .transform = [entry](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(entry.path().filename().c_str()) | + (entry.is_directory() ? ftxui::color(ftxui::Color::Blue) : ftxui::nothing) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey7) : ftxui::nothing); + }, + }), + }); + } +}; + +ftxui::Component makeFileDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &shown) { + class FileDialog : public ftxui::ComponentBase { + public: + explicit FileDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &shown) { + static bool checked = false; + auto entryList = ftxui::Container::Vertical({}); + + Add({ + ftxui::Container::Vertical({ + ftxui::Renderer([&]() -> ftxui::Element { return ftxui::text("Export file"); }) | + ftxui::color(ftxui::Color::Red) | ftxui::hcenter, + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() -> ftxui::Element { + return ftxui::text(fmt::format("{}: ", currentPath.c_str())) | ftxui::bold; + }), + + ftxui::Input({ + .content = &fileName, + .multiline = false, + .on_change = []() { (pathToExport = currentPath) /= fileName; }, + }), + }), + + ftxui::Renderer([]() { return ftxui::separatorEmpty(); }), + + entryList | ftxui::vscroll_indicator | ftxui::frame | ftxui::flex, + + ftxui::Container::Horizontal({ + ftxui::Button({ + .on_click = + [&]() { + smap.get("export_file_request") + ->operator()(pathToExport.c_str()); + shown = false; + }, + + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[Export]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + }), + + ftxui::Button({ + .on_click = [&]() { shown = false; }, + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[Cancel]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + }), + }) | ftxui::hcenter, + + }) | ftxui::border | + ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 96) | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 48), + }); + + updateFileDialog(entryList, smap); + } + }; + + return ftxui::Make(scr, smap, shown); +} diff --git a/src/graph.cpp b/src/graph.cpp new file mode 100644 index 0000000..2e7911e --- /dev/null +++ b/src/graph.cpp @@ -0,0 +1,193 @@ +#include "tuple.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +#include "signals.hpp" +#include "tagsettings.hpp" + +class GraphId_ { +public: + GraphId_(const std::string &name, const std::string &uuid = generateUUID_()) : m_hash_(std::hash()(uuid)), m_name_(name) {} + ~GraphId_() = default; + + bool operator==(const GraphId_ &other) const { return m_hash_ == other.hash(); } + + size_t hash() const { return m_hash_; } + const std::string &name() const { return m_name_; } + +private: + static std::string generateUUID_() { return boost::lexical_cast(boost::uuids::random_generator()()); }; + const size_t m_hash_; + const std::string m_name_; +}; + +template <> struct std::hash { + std::size_t operator()(const GraphId_ &g) const noexcept { return g.hash(); } +}; + +ftxui::Component makeGraph(const std::string &canid, const struct spn_settings_s &settings, std::function()> data_fn, ftxui::ScreenInteractive *screen, + signals_map_t &smap) { + static const auto json_array_to_payload = [](const nlohmann::json &settings, const std::vector &strvec_bytes) { + std::vector res; + + if (settings.contains("size") && settings.contains("pos") && settings.contains("le")) { + boost::cnv::cstream converter; + size_t size_setting = settings["size"].get(); + res.reserve(size_setting); + converter(std::hex)(std::skipws); + auto f = apply(std::ref(converter)).value_or(-1); + + if (auto [pos, size, data_size] = + std::tuple{ + settings["pos"].get(), + size_setting, + strvec_bytes.size(), + }; + pos < data_size && (pos + size) < data_size) { + + { + namespace rv = std::ranges::views; + const auto push = [&](const auto &i) { res.push_back(boost::lexical_cast(f(strvec_bytes[i]))); }; + + auto seq = rv::iota(pos, pos + size); + if (settings["le"].get()) { + for (const auto &i : seq) { + + push(i); + } + } else { + for (const auto &i : seq | rv::reverse) { + + push(i); + } + } + } + } + } + + return res; + }; + + static const auto create_json = [](const std::vector &data, const struct spn_settings_s &settings) { + nlohmann::json array = nlohmann::json::array(), settings_json, json; + uint32_t crc = std::accumulate(data.begin(), data.end(), uint32_t{}); + double spn_val = 0.0f; + + for (auto &d : data) { + array.push_back(fmt::format("{:2x}", d)); + } + + json = {{"crc", crc}, {"data", array}}; + + { + std::string current_setting; + try { + + tp::for_each( + std::tuple{ + std::make_tuple("name", &settings.spn_name, static_cast(nullptr)), + std::make_tuple("le", &settings.le, static_cast(nullptr)), + std::make_tuple("size", &settings.size, static_cast(nullptr)), + std::make_tuple("offset", &settings.offset, static_cast(nullptr)), + std::make_tuple("pos", &settings.pos, static_cast(nullptr)), + std::make_tuple("x", &settings.x_coeff, static_cast(nullptr)), + std::make_tuple("y", &settings.y_coeff, static_cast(nullptr)), + std::make_tuple("discrete", &settings.discrete, static_cast(nullptr)), + std::make_tuple("bit_offset", &settings.bit_offset, static_cast(nullptr)), + std::make_tuple("bit_count", &settings.bit_count, static_cast(nullptr)), + std::make_tuple("uuid", &settings.uuid, static_cast(nullptr)), + }, + + [&](const auto &e) { + current_setting = std::get<0u>(e); + settings_json[std::get<0u>(e)] = boost::lexical_cast(e))>>(*std::get<1u>(e)); + }); + + current_setting.clear(); + + std::vector v = json["data"].is_array() ? json["data"].template get>() : std::vector{}; + std::vector bytes = json_array_to_payload(settings_json, v); + + json["payload_bytes"] = [&]() { + nlohmann::json::array_t array; + for (const auto &byte : bytes) { + array.push_back(fmt::format("{:2x}", byte)); + } + + return array; + }(); + + int32_t integer = 0; + + for (auto iter = int32_t{0}; const auto &b : bytes) { + integer |= b << ((iter++) * UINT8_WIDTH); + } + + if (settings_json["x"].get() / settings_json["y"].get() != NAN) { + spn_val = static_cast(integer) * settings_json["x"].get() / settings_json["y"].get() + settings_json["offset"].get(); + } + + json["spn_value"] = spn_val; + } catch (const boost::bad_lexical_cast &e) { + + settings_json["warning"] = fmt::format("Bad '{}' setting", current_setting); + } + } + + return json; + }; + + class Impl : public ftxui::ComponentBase { + public: + explicit Impl(const std::string &canid, const struct spn_settings_s &settings, std::function()> data_fn, ftxui::ScreenInteractive *screen, signals_map_t &smap) + : m_settings_(settings), m_data_fn_(std::move(data_fn)) { + + auto renderer = ftxui::Renderer([this]() { + auto data = m_data_fn_(); + auto json = create_json(data, m_settings_); + return ftxui::vbox({ + ftxui::text(fmt::format("Raw data: {}", json["data"].dump())) | ftxui::color(ftxui::Color::Cyan) | ftxui::hcenter, + ftxui::text(fmt::format("CRC: {}", json["crc"].dump())) | ftxui::color(ftxui::Color::Cyan) | ftxui::hcenter, + ftxui::text(fmt::format("Payload: {}", json.contains("payload_bytes") ? json["payload_bytes"].dump() : "[]")) | ftxui::color(ftxui::Color::Cyan) | + ftxui::hcenter, + + ftxui::hbox({ + ftxui::text("Value: ") | ftxui::bold, + ftxui::text(fmt::format("{:.6g}", json.contains("tag_value") ? json["tag_value"].template get() : 0.0f)) | + ftxui::color(ftxui::Color::IndianRed), + }) | ftxui::hcenter, + }); + }); + + Add(renderer); + } + + private: + spn_settings_s m_settings_; + std::function()> m_data_fn_; + }; + + return ftxui::Make(canid, settings, std::move(data_fn), screen, smap); +} diff --git a/src/headless.cpp b/src/headless.cpp new file mode 100644 index 0000000..b3d0b02 --- /dev/null +++ b/src/headless.cpp @@ -0,0 +1,68 @@ +#include "headless.hpp" + +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +HeadlessHandler::HeadlessHandler(const std::string &output_file) + : output_file_(output_file) {} + +void HeadlessHandler::onDatabaseReady(sqlite::database &db) { + database_ = &db; +} + +void HeadlessHandler::onBatch(const std::vector &batch) { + if (!database_) return; + + extern std::pair processFrame(sqlite::database &db, const std::string &iface, const std::string &canid, const std::vector &data); + + for (const auto &entry : batch) { + const auto &iface = entry.iface; + const auto &canid = entry.canid; + const auto &frame_data = entry.data; + + if (configuration_map_.contains(canid)) continue; + + auto [verbose, brief] = processFrame(*database_, iface, canid, frame_data.payload); + configuration_map_.insert({ + canid, + {std::move(verbose), std::move(brief)}, + }); + + nlohmann::json j; + j[canid] = verboseToExportJson(configuration_map_.at(canid).first); + + if (!output_file_.empty()) { + nlohmann::json file_j; + + { + std::ifstream fin(output_file_); + if (fin.is_open() && fin.peek() != std::ifstream::traits_type::eof()) { + try { + fin >> file_j; + } catch (const std::exception &e) { + fmt::print("{}\r\n", e.what()); + return; + } + } + } + + for (const auto &[k, v] : j.items()) { + file_j[k] = v; + } + + { + std::ofstream fout(output_file_, std::ofstream::out | std::ofstream::trunc); + if (fout.is_open()) { + fout << file_j; + } + } + } else { + fmt::print("{}\r\n\r\n", j.dump()); + } + } +} diff --git a/src/headless.hpp b/src/headless.hpp new file mode 100644 index 0000000..97736ab --- /dev/null +++ b/src/headless.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "can_data.hpp" +#include "sqlite_modern_cpp.h" + +#include +#include +#include + +#include + +class HeadlessHandler { +public: + explicit HeadlessHandler(const std::string &output_file); + + void onDatabaseReady(sqlite::database &db); + void onBatch(const std::vector &batch); + +private: + std::string output_file_; + sqlite::database *database_ = nullptr; + std::map> configuration_map_; +}; diff --git a/src/json/button.cpp b/src/json/button.cpp new file mode 100644 index 0000000..fcdabee --- /dev/null +++ b/src/json/button.cpp @@ -0,0 +1,54 @@ +#include "button.hpp" +#include +#include +#include +#include + +ftxui::Component MyButton(const char *prefix, const char *title, std::function on_click) { + class Impl : public ftxui::ComponentBase { + public: + Impl(const char *prefix, const char *title, std::function on_click) : on_click_(std::move(on_click)), prefix_(prefix), title_(title) {} + + // Component implementation: + ftxui::Element OnRender() override { + auto style = Focused() ? (ftxui::Decorator(ftxui::inverted) | ftxui::focus) : ftxui::nothing; + return ftxui::hbox({ + ftxui::text(prefix_), + ftxui::text(title_) | style | ftxui::color(ftxui::Color::GrayDark) | ftxui::reflect(box_), + }); + } + + bool OnEvent(ftxui::Event event) override { + if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) { + if (!CaptureMouse(event)) + return false; + + TakeFocus(); + + if (event.mouse().button == ftxui::Mouse::Left && event.mouse().motion == ftxui::Mouse::Pressed) { + on_click_(); + return true; + } + + return false; + } + + if (event == ftxui::Event::Return) { + on_click_(); + return true; + } + + return false; + } + + bool Focusable() const final { return true; } + + private: + std::function on_click_; + const char *prefix_; + const char *title_; + ftxui::Box box_; + }; + + return ftxui::Make(prefix, title, std::move(on_click)); +} diff --git a/src/json/button.hpp b/src/json/button.hpp new file mode 100644 index 0000000..2757a67 --- /dev/null +++ b/src/json/button.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +ftxui::Component MyButton(const char *prefix, const char *title, std::function); diff --git a/src/json/expander.cpp b/src/json/expander.cpp new file mode 100644 index 0000000..e2f4c67 --- /dev/null +++ b/src/json/expander.cpp @@ -0,0 +1,83 @@ +#include "expander.hpp" +#include +#include + +ExpanderImpl::~ExpanderImpl() { + // Remove this from parent: + if (parent_) { + parent_->children_.erase(std::remove(parent_->children_.begin(), parent_->children_.end(), this), parent_->children_.end()); + parent_ = nullptr; + } + + // Remove this from children: + for (auto &child : children_) { + child->parent_ = nullptr; + } +} + +// static +Expander ExpanderImpl::Root() { return std::make_unique(); } + +Expander ExpanderImpl::Child() { + auto expander = Root(); + expander->parent_ = this; + children_.push_back(expander.get()); + return expander; +} + +bool ExpanderImpl::Expand() { + int min_level = MinLevel(); + Expand(min_level + 1); + return MinLevel() != min_level; +} + +bool ExpanderImpl::Collapse() { + int max_level = MaxLevel(); + Collapse(max_level - 1); + return MaxLevel() != max_level; +} + +int ExpanderImpl::MinLevel() const { + if (!expanded) { + return 0; + } + if (children_.empty()) { + return 1; + } + int min_level = INT_MAX; + for (auto &child : children_) { + min_level = std::min(min_level, 1 + child->MinLevel()); + } + return min_level; +} + +int ExpanderImpl::MaxLevel() const { + if (!expanded) { + return 0; + } + int max_level = 1; + for (auto &child : children_) { + max_level = std::max(max_level, 1 + child->MaxLevel()); + } + return max_level; +} + +void ExpanderImpl::Expand(int minLevel) { + if (minLevel <= 0) + return; + expanded = true; + minLevel--; + for (auto &child : children_) { + child->Expand(minLevel); + } +} + +void ExpanderImpl::Collapse(int maxLevel) { + if (maxLevel <= 0) { + expanded = false; + } + maxLevel--; + for (auto &child : children_) { + child->Collapse(maxLevel); + } +} diff --git a/src/json/expander.hpp b/src/json/expander.hpp new file mode 100644 index 0000000..8ce644c --- /dev/null +++ b/src/json/expander.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class ExpanderImpl; +using Expander = std::unique_ptr; + +class ExpanderImpl { +public: + ~ExpanderImpl(); + static Expander Root(); + Expander Child(); + + bool Expand(); + bool Collapse(); + + int MinLevel() const; + int MaxLevel() const; + + bool expanded = false; + +private: + void Expand(int minLevel); + void Collapse(int maxLevel); + + ExpanderImpl *parent_ = nullptr; + std::vector children_; +}; diff --git a/src/json/json.cpp b/src/json/json.cpp new file mode 100644 index 0000000..9a43e51 --- /dev/null +++ b/src/json/json.cpp @@ -0,0 +1,477 @@ +#include "json.hpp" +#include +#include + +#define FMT_HEADER_ONLY +#include + +bool ParseJSON(const std::string &input, nlohmann::json &out) { + class JsonParser : public nlohmann::detail::json_sax_dom_parser { + public: + explicit JsonParser(nlohmann::json &j) : nlohmann::detail::json_sax_dom_parser(j, false) {} + static bool parse_error(std::size_t /*position*/, const std::string & /*last_token*/, const nlohmann::json::exception &ex) { + std::cerr << std::endl; + std::cerr << ex.what() << std::endl; + return false; + } + }; + + JsonParser parser(out); + return nlohmann::json::sax_parse(input, &parser); +} + +ftxui::Component From(const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { + if (json.is_object()) + return FromObject(Empty(), json, is_last, depth, expander); + if (json.is_array()) + return FromArrayAny(Empty(), json, is_last, depth, expander); + if (json.is_string()) + return FromString(json, is_last); + if (json.is_number()) + return FromNumber(json, is_last); + if (json.is_boolean()) + return FromBoolean(json, is_last); + if (json.is_null()) + return FromNull(is_last); + return Unimplemented(); +} + +ftxui::Component FromString(const nlohmann::json &json, bool is_last) { + std::string value = json; + std::string str = "\"" + value + "\""; + return Basic(str, ftxui::Color::GreenLight, is_last); +} + +ftxui::Component FromNumber(const nlohmann::json &json, bool is_last) { + if (json.is_number_float()) + return Basic(fmt::format("{:.6g}", json.get()), ftxui::Color::CyanLight, is_last); + return Basic(json.dump(), ftxui::Color::CyanLight, is_last); +} + +ftxui::Component FromBoolean(const nlohmann::json &json, bool is_last) { + bool value = json; + std::string str = value ? "true" : "false"; + return Basic(str, ftxui::Color::YellowLight, is_last); +} + +ftxui::Component FromNull(bool is_last) { return Basic("null", ftxui::Color::RedLight, is_last); } + +ftxui::Component Unimplemented() { + return ftxui::Renderer([] { return ftxui::text("Unimplemented"); }); +} + +ftxui::Component Empty() { + return ftxui::Renderer([] { return ftxui::text(""); }); +} + +ftxui::Component Basic(const std::string &value, ftxui::Color c, bool is_last) { + return ftxui::Renderer([value, c, is_last](bool) { + auto element = ftxui::paragraph(value) | color(c); + if (!is_last) + element = ftxui::hbox({element, ftxui::text(",")}); + return element; + }); +} + +bool IsSuitableForTableView(const nlohmann::json &) { + return false; +} + +ftxui::Component Indentation(const ftxui::Component &child) { + return ftxui::Renderer(child, [child] { + return ftxui::hbox({ + ftxui::text(" "), + child->Render(), + }); + }); +} + +ftxui::Component FakeHorizontal(const ftxui::Component &a, const ftxui::Component &b) { + auto c = ftxui::Container::Vertical({a, b}); + c->SetActiveChild(b); + + return ftxui::Renderer(c, [a, b] { + return ftxui::hbox({ + a->Render(), + b->Render(), + }); + }); +} + +class ComponentExpandable : public ftxui::ComponentBase { +public: + explicit ComponentExpandable(const Expander &expander) : expander_(expander->Child()) {} + + bool &Expanded() const { return expander_->expanded; } + + bool OnEvent(ftxui::Event event) override { + if (event.is_mouse()) + return ftxui::ComponentBase::OnEvent(event); + return false; + } + + Expander expander_; +}; + +ftxui::Component FromObject(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { + class Impl : public ComponentExpandable { + public: + Impl(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) { + Expanded() = (depth <= 1); + + auto children = ftxui::Container::Vertical({}); + int size = static_cast(json.size()); + + for (auto &it : json.items()) { + bool is_children_last = --size == 0; + children->Add(Indentation(FromKeyValue(it.key(), it.value(), is_children_last, depth + 1, expander_))); + } + + if (is_last) + children->Add(ftxui::Renderer([] { return ftxui::text("}"); })); + else + children->Add(ftxui::Renderer([] { return ftxui::text("},"); })); + + auto toggle = MyToggle("{", is_last ? "{...}" : "{...},", &Expanded()); + Add(ftxui::Container::Vertical({ + FakeHorizontal(prefix, toggle), + Maybe(children, &Expanded()), + })); + } + }; + + return ftxui::Make(prefix, json, is_last, depth, expander); +} + +ftxui::Component FromKeyValue(const std::string &key, const nlohmann::json &value, bool is_last, int depth, const Expander &expander) { + std::string str = "\"" + key + "\""; + if (value.is_object() || value.is_array()) { + auto prefix = ftxui::Renderer([str] { + return ftxui::hbox({ + ftxui::text(str) | color(ftxui::Color::BlueLight), + ftxui::text(": "), + }); + }); + + if (value.is_object()) + return FromObject(prefix, value, is_last, depth, expander); + else + return FromArrayAny(prefix, value, is_last, depth, expander); + } + + auto child = From(value, is_last, depth, expander); + return ftxui::Renderer(child, [str, child] { + return ftxui::hbox({ + ftxui::text(str) | color(ftxui::Color::BlueLight), + ftxui::text(": "), + child->Render(), + }); + }); +} + +ftxui::Component FromArrayAny(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { + class Impl : public ftxui::ComponentBase { + public: + Impl(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { Add(FromArray(prefix, json, is_last, depth, expander)); } + }; + + return ftxui::Make(prefix, json, is_last, depth, expander); +} + +ftxui::Component FromArray(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { + class Impl : public ComponentExpandable { + public: + Impl(ftxui::Component prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) + : ComponentExpandable(expander), prefix_(std::move(prefix)), json_(json), is_last_(is_last), depth_(depth) { + Expanded() = (depth <= 0); + auto children = ftxui::Container::Vertical({}); + int size = static_cast(json_.size()); + for (auto &it : json_.items()) { + bool is_children_last = --size == 0; + children->Add(Indentation(From(it.value(), is_children_last, depth + 1, expander_))); + } + + if (is_last) + children->Add(ftxui::Renderer([] { return ftxui::text("]"); })); + else + children->Add(ftxui::Renderer([] { return ftxui::text("],"); })); + + auto toggle = MyToggle("[", is_last ? "[...]" : "[...],", &Expanded()); + + auto upper = ftxui::Container::Horizontal({ + FakeHorizontal(prefix_, toggle), + }); + + // Turn this array into a table. + if (IsSuitableForTableView(json)) { + auto expand_button = MyButton(" ", "(table view)", [this, &expander] { + auto *parent = Parent(); + auto replacement = FromTable(prefix_, json_, is_last_, depth_, expander); + parent->DetachAllChildren(); // Detach this. + parent->Add(replacement); + }); + + upper = ftxui::Container::Horizontal({upper, expand_button}); + } + + Add(ftxui::Container::Vertical({ + upper, + Maybe(children, &Expanded()), + })); + } + + ftxui::Component prefix_; + const nlohmann::json &json_; + bool is_last_; + int depth_; + }; + + return ftxui::Make(prefix, json, is_last, depth, expander); +} + +ftxui::Component FromTable(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { + class Impl : public ftxui::ComponentBase { + public: + Impl(ftxui::Component prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) + : prefix_(std::move(prefix)), json_(json), is_last_(is_last), depth_(depth) { + std::vector components; + + // Turn this array into a table. + expand_button_ = MyButton("", "(array view)", [this, &expander] { + auto *parent = Parent(); + auto replacement = FromArray(prefix_, json_, is_last_, depth_, expander); + replacement->OnEvent(ftxui::Event::ArrowRight); + parent->DetachAllChildren(); // Detach this. + parent->Add(replacement); + }); + + components.push_back(expand_button_); + + std::map columns_index; + for (auto &row : json_.items()) { + children_.emplace_back(); + auto &children_row = children_.back(); + for (auto &cell : row.value().items()) { + // Does it require a new column? + if (!columns_index.count(cell.key())) { + columns_index[cell.key()] = columns_.size(); + columns_.push_back(cell.key()); + } + + // Does the current row fits in the current column? + if ((int)children_row.size() <= columns_index[cell.key()]) { + children_row.resize(columns_index[cell.key()] + 1); + } + + // Fill in the data + auto child = From(cell.value(), /*is_last=*/true, depth_ + 1, expander); + children_row[columns_index[cell.key()]] = child; + } + } + // Layout + for (auto &rows : children_) { + auto row = ftxui::Container::Horizontal({}); + for (auto &cell : rows) { + if (cell) + row->Add(cell); + } + + components.push_back(row); + } + + Add(ftxui::Container::Vertical(std::move(components))); + } + + bool OnEvent(ftxui::Event event) override final { return false; } + + private: + ftxui::Element OnRender() override { + std::vector> data; + data.push_back({ftxui::text("") | ftxui::color(ftxui::Color::GrayDark)}); + + for (auto &title : columns_) { + data.back().push_back(ftxui::text(title)); + } + + int i = 0; + + for (auto &row_children : children_) { + std::vector data_row; + data_row.push_back(ftxui::text(std::to_string(i++)) | ftxui::color(ftxui::Color::GrayDark)); + + for (auto &child : row_children) { + if (child) { + + data_row.push_back(child->Render()); + } else { + + data_row.push_back(ftxui::text("")); + } + } + + data.push_back(std::move(data_row)); + } + + auto table = ftxui::Table(std::move(data)); + table.SelectColumns(1, -1).SeparatorVertical(ftxui::LIGHT); + table.SelectColumns(1, -1).Border(ftxui::LIGHT); + table.SelectRectangle(1, -1, 0, 0).SeparatorVertical(ftxui::HEAVY); + table.SelectRectangle(1, -1, 0, 0).Border(ftxui::HEAVY); + + return ftxui::vbox({ + ftxui::hbox({ + prefix_->Render(), + expand_button_->Render(), + }), + + table.Render(), + }); + } + + std::vector columns_; + std::vector> children_; + + ftxui::Component prefix_; + ftxui::Component expand_button_; + const nlohmann::json &json_; + bool is_last_; + int depth_; + }; + + return ftxui::Make(prefix, json, is_last, depth, expander); +} + +// --- Live JSON viewer: reads leaf values from shared_ptr on each render --- + +static ftxui::Component LiveLeaf(std::shared_ptr root, nlohmann::json::json_pointer ptr, bool is_last) { + return ftxui::Renderer([root, ptr = std::move(ptr), is_last](bool focused) -> ftxui::Element { + const auto &val = root->at(ptr); + std::string text_str; + ftxui::Color c; + + if (val.is_string()) { + text_str = "\"" + val.get() + "\""; + c = ftxui::Color::GreenLight; + } else if (val.is_number()) { + text_str = val.is_number_float() ? fmt::format("{:.6g}", val.get()) : val.dump(); + c = ftxui::Color::CyanLight; + } else if (val.is_boolean()) { + text_str = val.get() ? "true" : "false"; + c = ftxui::Color::YellowLight; + } else if (val.is_null()) { + text_str = "null"; + c = ftxui::Color::RedLight; + } else { + text_str = val.dump(); + c = ftxui::Color::White; + } + + auto element = ftxui::paragraph(text_str) | color(c); + if (focused) + element = element | ftxui::inverted | ftxui::focus; + if (!is_last) + element = ftxui::hbox({element, ftxui::text(",")}); + return element; + }); +} + +static ftxui::Component FromLiveKeyValue(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const std::string &key, bool is_last, int depth, const Expander &expander); + +static ftxui::Component FromLiveObject(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) { + class Impl : public ComponentExpandable { + public: + Impl(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) { + Expanded() = (depth <= 1); + + const auto &json = root->at(ptr); + auto children = ftxui::Container::Vertical({}); + int size = static_cast(json.size()); + + for (auto &it : json.items()) { + bool is_children_last = --size == 0; + children->Add(Indentation(FromLiveKeyValue(root, ptr / it.key(), it.key(), is_children_last, depth + 1, expander_))); + } + + children->Add(ftxui::Renderer([is_last] { return ftxui::text(is_last ? "}" : "},"); })); + + auto toggle = MyToggle("{", is_last ? "{...}" : "{...},", &Expanded()); + Add(ftxui::Container::Vertical({ + FakeHorizontal(prefix, toggle), + Maybe(children, &Expanded()), + })); + } + }; + + return ftxui::Make(root, ptr, prefix, is_last, depth, expander); +} + +static ftxui::Component FromLiveArray(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) { + class Impl : public ComponentExpandable { + public: + Impl(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) { + Expanded() = (depth <= 0); + + const auto &json = root->at(ptr); + auto children = ftxui::Container::Vertical({}); + int size = static_cast(json.size()); + + for (int i = 0; i < static_cast(json.size()); ++i) { + bool is_children_last = --size == 0; + children->Add(Indentation(FromLive(root, ptr / i, is_children_last, depth + 1, expander_))); + } + + children->Add(ftxui::Renderer([is_last] { return ftxui::text(is_last ? "]" : "],"); })); + + auto toggle = MyToggle("[", is_last ? "[...]" : "[...],", &Expanded()); + Add(ftxui::Container::Vertical({ + FakeHorizontal(prefix, toggle), + Maybe(children, &Expanded()), + })); + } + }; + + return ftxui::Make(root, ptr, prefix, is_last, depth, expander); +} + +static ftxui::Component FromLiveKeyValue(std::shared_ptr root, nlohmann::json::json_pointer ptr, + const std::string &key, bool is_last, int depth, const Expander &expander) { + const auto &value = root->at(ptr); + std::string str = "\"" + key + "\""; + + if (value.is_object()) { + auto prefix = ftxui::Renderer([str] { + return ftxui::hbox({ftxui::text(str) | color(ftxui::Color::BlueLight), ftxui::text(": ")}); + }); + return FromLiveObject(root, ptr, prefix, is_last, depth, expander); + } + + if (value.is_array()) { + auto prefix = ftxui::Renderer([str] { + return ftxui::hbox({ftxui::text(str) | color(ftxui::Color::BlueLight), ftxui::text(": ")}); + }); + return FromLiveArray(root, ptr, prefix, is_last, depth, expander); + } + + auto child = LiveLeaf(root, ptr, is_last); + return ftxui::Renderer(child, [str, child] { + return ftxui::hbox({ + ftxui::text(str) | color(ftxui::Color::BlueLight), + ftxui::text(": "), + child->Render(), + }); + }); +} + +ftxui::Component FromLive(std::shared_ptr root, const nlohmann::json::json_pointer &ptr, bool is_last, int depth, const Expander &expander) { + const auto &json = root->at(ptr); + if (json.is_object()) + return FromLiveObject(root, ptr, Empty(), is_last, depth, expander); + if (json.is_array()) + return FromLiveArray(root, ptr, Empty(), is_last, depth, expander); + return LiveLeaf(root, ptr, is_last); +} diff --git a/src/json/json.hpp b/src/json/json.hpp new file mode 100644 index 0000000..4806780 --- /dev/null +++ b/src/json/json.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include "button.hpp" +#include "expander.hpp" +#include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border +#include "mytoggle.hpp" + +bool ParseJSON(const std::string &input, nlohmann::json &out); + +ftxui::Component From(const nlohmann::json &json, bool is_last, int depth, const Expander &expander); +ftxui::Component FromLive(std::shared_ptr root, const nlohmann::json::json_pointer &ptr, bool is_last, int depth, const Expander &expander); +ftxui::Component FromString(const nlohmann::json &json, bool is_last); +ftxui::Component FromNumber(const nlohmann::json &json, bool is_last); +ftxui::Component FromBoolean(const nlohmann::json &json, bool is_last); +ftxui::Component FromNull(bool is_last); +ftxui::Component FromObject(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander); +ftxui::Component FromArrayAny(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander); +ftxui::Component FromArray(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander); +ftxui::Component FromTable(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander); +ftxui::Component FromKeyValue(const std::string &key, const nlohmann::json &value, bool is_last, int depth, const Expander &expander); +ftxui::Component Empty(); +ftxui::Component Unimplemented(); +ftxui::Component Basic(const std::string &value, ftxui::Color c, bool is_last); diff --git a/src/json/mytoggle.cpp b/src/json/mytoggle.cpp new file mode 100644 index 0000000..de022c4 --- /dev/null +++ b/src/json/mytoggle.cpp @@ -0,0 +1,46 @@ +#include "mytoggle.hpp" +#include "ftxui/component/event.hpp" + +namespace { +class MyToggleImpl : public ftxui::ComponentBase { +public: + MyToggleImpl(const char *label_on, const char *label_off, bool *state) : label_on_(label_on), label_off_(label_off), state_(state) {} + +private: + // Component implementation. + ftxui::Element OnRender() override { + auto style = hovered_ ? ftxui::bold : ftxui::nothing; + ftxui::Element my_text = *state_ ? ftxui::text(label_on_) : ftxui::text(label_off_); + return my_text | style | reflect(box_); + } + + bool OnEvent(ftxui::Event event) override { + if (!event.is_mouse()) + return false; + + return OnMouseEvent(event); + } + + bool OnMouseEvent(ftxui::Event event) { + bool was_hovered = hovered_; + hovered_ = box_.Contain(event.mouse().x, event.mouse().y); + + if (hovered_ && event.mouse().button == ftxui::Mouse::Left && event.mouse().motion == ftxui::Mouse::Pressed) { + *state_ = !*state_; + return true; + } + + return hovered_ || was_hovered; + } + + bool Focusable() const final { return false; } + + const char *label_on_; + const char *label_off_; + bool *const state_; + bool hovered_ = false; + ftxui::Box box_; +}; +} // namespace + +ftxui::Component MyToggle(const char *label_on, const char *label_off, bool *state) { return ftxui::Make(label_on, label_off, state); } diff --git a/src/json/mytoggle.hpp b/src/json/mytoggle.hpp new file mode 100644 index 0000000..16d17f3 --- /dev/null +++ b/src/json/mytoggle.hpp @@ -0,0 +1,4 @@ +#pragma once +#include "ftxui/component/component.hpp" + +ftxui::Component MyToggle(const char *label_on, const char *label_off, bool *state); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..110c11d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +// for XLSX files +#include "xlnt/xlnt.hpp" + +// For sqlite +#include "sqlite_modern_cpp.h" + +// For parsers +#include +#include + +#include "clipp.hpp" +#include "ftxui/component/component.hpp" +#include "ftxui/component/screen_interactive.hpp" +#include "ftxui/dom/elements.hpp" +#include "headless.hpp" +#include "process.hpp" +#include "recorder.hpp" +#include "signals.hpp" + +std::mutex g_j1939_db_mtx; + +int32_t main(int32_t argc, char *argv[]) { + static auto screen = ftxui::ScreenInteractive::Fullscreen(); + static std::mutex rw_mtx; + static std::map>> aggregated; + static signals_s signals; + static std::atomic j1939_db{nullptr}; + static std::stop_source aggregator_task_stop, refresh_task_stop, headless_task_stop; + static TinyProcessLib::Process *p = nullptr; + std::future xlsx_parser_task, headless_task; + extern std::unique_ptr parseXlsx(const std::string &file); + static std::unique_ptr j1939_db_owner; + static std::unique_ptr recorder; + static std::unique_ptr headless_handler; + + static struct { + std::string docfile, command = "", output_file = "", record_db_path = ""; + bool show_help = false, headless_mode = false, sync_to_server = false, record_mode = false, tui_mode = false; + } cli_opts; + + // Parse cli options + { + static const auto print_usage = [](const Man &man) { + fmt::print("{}\r\n", (std::stringstream{} << man).str()); + }; + + auto cli = ( + + clipp::option("-hl", "--headless") + .doc("Headless mode, write configuration to stdout if output file is not specified") + .set(cli_opts.headless_mode) + .call([]() { fmt::println("Headless mode"); }), + + clipp::option("-of", "--output-file") & + clipp::value("Output file to write configuration", cli_opts.output_file) + .call([&]() { fmt::println("Output file is: {}", cli_opts.output_file); }) + .doc("Specify output file to write configuration"), + + clipp::option("-rec", "--record") + .doc("Record mode: collect decoded J1939 SPN values to SQLite DB") + .set(cli_opts.record_mode) + .call([]() { fmt::println("Record mode"); }), + + clipp::option("-db", "--database") & + clipp::value("SQLite output database path", cli_opts.record_db_path) + .call([&]() { fmt::println("Record DB: {}", cli_opts.record_db_path); }) + .doc("Path for the output SQLite database (used with -rec)"), + + clipp::option("-tui").doc("Enable TUI mode (use with -rec to show UI while recording)").set(cli_opts.tui_mode), + + clipp::option("-e", "--execute-command") & + clipp::value("command", cli_opts.command).call([&]() {}).doc("execute cli command to read its output"), + + clipp::required("-j1939", "--j1939-document") & + clipp::value("J1939 Document file", cli_opts.docfile) + .call([&]() { + xlsx_parser_task = std::async(std::launch::async, [&]() { + j1939_db_owner = parseXlsx(cli_opts.docfile); + j1939_db.store(j1939_db_owner.get()); + signals.map.get("j1939_database_ready")->operator()(*j1939_db_owner); + }); + }) + .doc("provide .xlsx document file for J1939 processing")); + + auto man = clipp::make_man_page(cli, argv[0]); + auto cli_with_help = (cli | clipp::option("-h", "--help").set(cli_opts.show_help).doc("show this help").call([&]() { + print_usage(man); + })); + + if (!clipp::parse(argc, argv, cli_with_help)) { + print_usage(man); + return -1; + } + + if (cli_opts.record_mode && cli_opts.record_db_path.empty()) { + fmt::println(stderr, "Error: -rec requires -db "); + return -1; + } + } + + // Parse a single candump line and aggregate it + static const auto parse_candump_line = [](const std::string &line) { + if (line.empty()) + return; + + std::vector words; + std::istringstream iss(line); + std::string s; + + while (std::getline(iss, s, ' ')) { + if (!s.empty()) { + words.push_back(s); + } + } + + if (words.size() >= 4u) { // 0 - interface, 1 - canid, 2 - size, 3 - payload (1 byte minimum) + if (words[0].starts_with("can") || words[0].starts_with("vcan")) { + can_frame_data_s entry; + + // Parse DLC + { + auto sv = std::string_view(words[2]).substr(1, words[2].size() - 2); + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), entry.size); + if (ec != std::errc{}) { + entry.size = 0; + } + } + + // Parse payload bytes directly + entry.payload.reserve(words.size() - 3); + for (size_t i = 3; i < words.size(); ++i) { + uint8_t byte = 0; + std::from_chars(words[i].data(), words[i].data() + words[i].size(), byte, 16); + entry.payload.push_back(byte); + } + + { + std::lock_guard lock(rw_mtx); + aggregated[words[0]][words[1]] = std::make_shared(std::move(entry)); + } + } + } + }; + + // If reading from stdin pipe, save the pipe fd and reopen stdin as /dev/tty for FTXUI + static int candump_fd = -1; + if (cli_opts.command.empty()) { + candump_fd = ::dup(STDIN_FILENO); + std::freopen("/dev/tty", "r", stdin); + } + + // Reads candump data from stdin pipe or subprocess and aggregates it + auto aggregator_task = std::async( + std::launch::async, + [command = cli_opts.command](std::stop_token stop_token) { + fmt::println(stderr, "[task] aggregator started"); + if (command.empty()) { + + // Read from the saved pipe fd using epoll to avoid blocking on stop + int epfd = ::epoll_create1(0); + if (epfd < 0) + return; + + struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = candump_fd}}; + ::epoll_ctl(epfd, EPOLL_CTL_ADD, candump_fd, &ev); + + FILE *pipe_stream = ::fdopen(candump_fd, "r"); + if (!pipe_stream) { + ::close(epfd); + return; + } + + struct epoll_event events[1]; + char buf[4096]; + + while (!stop_token.stop_requested()) { + int32_t nfds = ::epoll_wait(epfd, events, 1, 50); + if (nfds > 0 && !stop_token.stop_requested()) { + if (events[0].events & EPOLLIN) { + if (!std::fgets(buf, sizeof(buf), pipe_stream)) { + break; + } + + std::string line(buf); + + if (!line.empty() && line.back() == '\n') { + line.pop_back(); + } + + parse_candump_line(line); + } + + if (events[0].events & (EPOLLHUP | EPOLLERR)) { + break; + } + } + } + + std::fclose(pipe_stream); + ::close(epfd); + } else { + // Launch subprocess + TinyProcessLib::Config cfg = { + .buffer_size = PIPE_BUF, .inherit_file_descriptors = true, .on_stdout_close = []() {}}; + p = new TinyProcessLib::Process( + command, "", + [stop_token](const char *bytes, size_t n) { + if (n > PIPE_BUF || stop_token.stop_requested()) + return; + + std::string buf(bytes, n), line; + std::istringstream ss(buf); + while (std::getline(ss, line)) { + parse_candump_line(line); + } + }, + [](const char *, size_t) {}, false, cfg); + + while (true) { + if (stop_token.stop_requested()) { + if (p) { + p->kill(); + ::kill(-p->get_id(), SIGKILL); + ::kill(p->get_id(), SIGKILL); + p->get_exit_status(); + delete p; + p = nullptr; + } + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + fmt::println(stderr, "[task] aggregator finished"); + }, + + aggregator_task_stop.get_token()); + + // UI refresh task: compares snapshots at ~30fps and emits signals for changed entries + auto refresh_task = std::async( + std::launch::async, + [](std::stop_token stop_token) { + fmt::println(stderr, "[task] refresh started"); + using aggregated_t = std::map>>; + aggregated_t old_data; + + while (!stop_token.stop_requested()) { + std::this_thread::sleep_for(std::chrono::milliseconds(33u)); + + aggregated_t current; + { + std::lock_guard lock(rw_mtx); + current = aggregated; + } + + std::vector batch; + + for (const auto &[iface, canids] : current) { + bool new_iface = !old_data.contains(iface); + + for (const auto &[canid, ptr] : canids) { + if (!ptr) + continue; + + can_frame_diff_s diff; + diff.is_new_interface = new_iface; + diff.is_new_canid = new_iface || !old_data[iface].contains(canid); + + if (!diff.is_new_canid) { + const auto &old_ptr = old_data[iface][canid]; + if (old_ptr == ptr) + continue; + + const auto &old_entry = *old_ptr; + diff.payload_changed.resize(ptr->payload.size(), false); + + for (size_t i = 0; i < ptr->payload.size(); ++i) { + diff.payload_changed[i] = (i >= old_entry.payload.size() || ptr->payload[i] != old_entry.payload[i]); + } + } else { + diff.payload_changed.assign(ptr->payload.size(), true); + } + + std::shared_ptr verbose, brief; + auto *db = j1939_db.load(); + if (db) { + std::lock_guard db_lock(g_j1939_db_mtx); + extern std::pair processFrame( + sqlite::database & db, const std::string &iface, const std::string &canid, + const std::vector &data); + auto [v, b] = processFrame(*db, iface, canid, ptr->payload); + + verbose = std::make_shared(std::move(v)); + brief = std::make_shared(std::move(b)); + } + + batch.push_back({iface, canid, *ptr, std::move(diff), std::move(verbose), std::move(brief)}); + } + } + + if (!batch.empty()) { + signals.map.get &)>("new_entries_batch")->operator()(batch); + } + + old_data.swap(current); + } + + fmt::println(stderr, "[task] refresh finished"); + }, + + refresh_task_stop.get_token()); + + // Stop all tasks on SIGINT + { + static auto signal_handler = [](int sig) { + fmt::println(stderr, "[signal] SIGINT received, stopping tasks..."); + for (auto *source : {&aggregator_task_stop, &refresh_task_stop, &headless_task_stop}) { + if (!source->stop_requested()) { + source->request_stop(); + } + } + + if (candump_fd >= 0) { + ::close(candump_fd); + candump_fd = -1; + } + }; + + ::signal(SIGINT, signal_handler); + } + + if (cli_opts.record_mode) { + bool rec_console = !cli_opts.tui_mode && !cli_opts.headless_mode; + recorder = std::make_unique(cli_opts.record_db_path, rec_console); + signals.map.get &)>("new_entries_batch") + ->connect([](const std::vector &batch) { recorder->onBatch(batch); }); + } + + if (cli_opts.headless_mode) { + headless_handler = std::make_unique(cli_opts.output_file); + + signals.map.get("j1939_database_ready")->connect([](sqlite::database &db) { + headless_handler->onDatabaseReady(db); + }); + + signals.map.get &)>("new_entries_batch") + ->connect([](const std::vector &batch) { headless_handler->onBatch(batch); }); + } + + // Determine if TUI should run: default on, off if headless, off if rec-only (no -tui) + bool run_tui = !cli_opts.headless_mode && (!cli_opts.record_mode || cli_opts.tui_mode); + + if (run_tui) { + extern ftxui::Component makeMainForm(ftxui::ScreenInteractive * screen, signals_map_t & smap); + screen.Loop(makeMainForm(&screen, signals.map) | ftxui::Renderer([](ftxui::Element inner) -> ftxui::Element { + return ftxui::Window( + { + .inner = ftxui::Renderer([inner]() -> ftxui::Element { return inner | ftxui::flex; }), + .title = "canscope", + .width = ftxui::Terminal::Size().dimx, + .height = ftxui::Terminal::Size().dimy, + .resize_left = false, + .resize_right = false, + .resize_top = false, + .resize_down = false, + .render = [&](ftxui::WindowRenderState state) -> ftxui::Element { + return ftxui::window(ftxui::Renderer([state]() { + return ftxui::text(fmt::format(" {{ {} }} ", state.title)); + })->Render(), + state.inner); + }, + }) + ->Render(); + })); + + signals.map.get("canplayer_stopped")->operator()(); + fmt::println(stderr, "[exit] TUI exited, stopping tasks..."); + for (auto *source : {&aggregator_task_stop, &refresh_task_stop, &headless_task_stop}) { + if (!source->stop_requested()) { + source->request_stop(); + } + } + if (candump_fd >= 0) { + ::close(candump_fd); + candump_fd = -1; + } + } else { + + // Headless or rec-only: wait for SIGINT + headless_task = std::async( + std::launch::async, + [](std::stop_token st) { + while (!st.stop_requested()) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }, + + headless_task_stop.get_token()); + } + + // Wait for all tasks to finish (with timeout) + { + const char *names[] = {"xlsx_parser", "aggregator", "refresh", "headless"}; + int idx = 0; + for (auto *task : {&xlsx_parser_task, &aggregator_task, &refresh_task, &headless_task}) { + if (task && task->valid()) { + fmt::println(stderr, "[exit] waiting for {}...", names[idx]); + if (task->wait_for(std::chrono::seconds(3)) == std::future_status::timeout) { + fmt::println(stderr, "[exit] {} timed out!", names[idx]); + } else { + fmt::println(stderr, "[exit] {} done", names[idx]); + } + } + + idx++; + } + } + + if (recorder) { + fmt::println("Flushing recorded data to database, please wait..."); + recorder->flushAndClose(); + fmt::println("Done. Database saved to: {}", cli_opts.record_db_path); + recorder.reset(); + } + + return 0; +} diff --git a/src/mainform.cpp b/src/mainform.cpp new file mode 100644 index 0000000..35ed8f2 --- /dev/null +++ b/src/mainform.cpp @@ -0,0 +1,418 @@ +#include "canid_unit.hpp" +#include "signals.hpp" +#include "tagsettingrow.hpp" +#include "tagsettings.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// For sqlite +#include "process.hpp" +#include "signals.hpp" +#include "sqlite_modern_cpp.h" + +ftxui::Component makeMainForm(ftxui::ScreenInteractive *screen, signals_map_t &smap) { + class Impl : public ftxui::ComponentBase { + public: + explicit Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap) { + static auto logger = spdlog::systemd_logger_mt("mainform", "cansniffer"); + + static bool canbus_params_export_dialog_shown = false, file_dialog_shown = false, + canbus_player_dialog_shown = false, canplayer_is_ready = false; + ; + static float focus_relative = 0.15f; + static float canbus_params_focus_relative = 0; + static std::string canid_active; + static size_t tags_count = 0u; + static std::unordered_map> canid_lookup; + static std::atomic database_atomic{nullptr}; + + extern ftxui::Component makeCanIDUnit( + const std::string &, const std::string &, const std::string &, size_t &, const std::vector &, + ftxui::ScreenInteractive *, signals_map_t &, ftxui::Component, ftxui::Component, ftxui::Component, + ftxui::Component, bool, bool, bool, bool, std::string &, bool &, bool &, bool &, + std::map> &, spn_settings_map_t &); + extern ftxui::Component makeFileDialog(ftxui::ScreenInteractive * scr, signals_map_t & smap, bool &shown); + extern ftxui::Component makeCanPlayerDialog(ftxui::ScreenInteractive * scr, signals_map_t & smap, bool &is_ready); + + static auto canidsCont = ftxui::Container::Vertical({}); + static spn_settings_map_t tagSettingsMap; + static std::map> spnSettingsFormMap; + static auto spn_export_dialog = ftxui::Container::Vertical({}); + static auto canbus_params_export_dialog = ftxui::Container::Vertical({}); + static auto canbus_player_dialog = ftxui::Container::Vertical({}); + + static const auto convertParametersMapToJson = []() { + nlohmann::json ret; + + if (canidsCont->ChildCount()) { + auto &map = std::static_pointer_cast(canidsCont->ChildAt(0))->getParametersExportMap(); + + for (const auto &[canid_k, canid_v] : map) { + nlohmann::json::array_t spns_selected; + for (const auto &[selected_spn_k, selected_spn_v] : std::get<2u>(canid_v)) { + if (std::get<1u>(selected_spn_v)) { + const auto &spn_data = std::get<2u>(selected_spn_v); + nlohmann::json spn = { + {"name", spn_data.value("SPN name", "")}, + {"offset", spn_data.value("Offset", 0)}, + {"resolution", spn_data.value("Resolution", 0.0)}, + {"value", spn_data.value("Value", 0.0)}, + {"unit", spn_data.value("Unit", "")}, + }; + if (spn_data.contains("Fragments")) { + nlohmann::json::array_t frags; + for (const auto &[k, frag] : spn_data["Fragments"].items()) { + auto frag_key = fmt::format("Fragment#{}", k); + if (frag.contains(frag_key)) { + frags.push_back({{fmt::format("fragment#{}", k), + { + {"byte_pos", frag[frag_key].value("byte_offset", 0)}, + {"bit_pos", frag[frag_key].value("bit_offset", 0)}, + {"bit_size", frag[frag_key].value("size_bits", 0)}, + }}}); + } + } + spn["fragments"] = std::move(frags); + } + spns_selected.push_back(std::move(spn)); + } + } + + if (!spns_selected.empty()) { + ret[canid_k] = spns_selected; + } + } + } + + return ret; + }; + + // Connections + { + static struct export_file_request_connection_s { + export_file_request_connection_s(signals_map_t &smap) { + smap.get("export_file_request")->connect([](const std::string &path) { + std::ofstream ofs(path); + ofs << convertParametersMapToJson().dump(); + ofs.close(); + }); + } + } export_file_request_connection(smap); + + static struct database_ready_connection_s { + database_ready_connection_s(signals_map_t &smap) { + smap.get("j1939_database_ready")->connect([](sqlite::database &db) { + database_atomic.store(&db); + }); + } + } database_ready_connection(smap); + + static struct new_entries_batch_connection_s { + new_entries_batch_connection_s(signals_map_t &smap, ftxui::ScreenInteractive *screen) { + smap.get &)>("new_entries_batch") + ->connect([screen, &smap](const std::vector &batch) { + screen->Post([screen, &smap, batch]() { + for (const auto &entry : batch) { + auto it = canid_lookup.find(entry.canid); + if (it != canid_lookup.end()) { + it->second->update(entry.data, entry.diff, entry.verbose, entry.brief); + } else { + auto component = ftxui::Renderer([]() { return ftxui::text(""); }); + auto new_cmp = makeCanIDUnit( + entry.iface, entry.canid, "J1939", tags_count, entry.data.payload, screen, smap, component, + canidsCont, ftxui::Container::Vertical({}), canbus_params_export_dialog, false, false, true, + false, canid_active, file_dialog_shown, canbus_params_export_dialog_shown, + file_dialog_shown, spnSettingsFormMap, tagSettingsMap); + auto unit = std::static_pointer_cast(new_cmp); + unit->update(entry.data, entry.diff, entry.verbose, entry.brief); + canid_lookup[entry.canid] = unit; + canidsCont->Add(new_cmp); + } + } + }); + + screen->Post(ftxui::Event::Custom); + }); + } + } new_entries_batch_connection(smap, screen); + } + + Add({ + ftxui::Container::Vertical({ + + // Tab bar + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + return ftxui::hbox({ + ftxui::text(" J1939 database is: "), + ftxui::text(fmt::format("{} ", database_atomic.load() ? "ready" : "not ready")) | + ftxui::color(database_atomic.load() ? ftxui::Color::Green : ftxui::Color::Red), + }); + }), + + ftxui::Renderer([]() { + static auto start = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + std::chrono::duration elapsed = now - start; + + // Convert elapsed time to hours, minutes, and seconds + int32_t hours = static_cast(elapsed.count()) / 3600u; + int32_t minutes = (static_cast(elapsed.count()) % 3600u) / 60u; + int32_t seconds = static_cast(elapsed.count()) % 60u; + + return ftxui::hbox({ftxui::text( + fmt::format(" Uptime: {} ", fmt::format("{:02}:{:02}:{:02}", hours, minutes, seconds)))}); + }), + + ftxui::Renderer([]() { + return ftxui::hbox({ + ftxui::separator(), + ftxui::filler(), + ftxui::separator(), + }) | + ftxui::xflex; + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[can_player]< ") | + (canbus_player_dialog_shown || state.focused ? ftxui::bold : ftxui::nothing) | + (canplayer_is_ready ? ftxui::color(ftxui::Color::Cyan) + : ftxui::color(ftxui::Color::Grey23)) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = [screen, &smap]() { canbus_player_dialog_shown = canplayer_is_ready; }, + }), + + ftxui::Checkbox({ + .checked = &canbus_params_export_dialog_shown, + + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[export_parameters]< ") | + (canbus_params_export_dialog_shown || state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = [screen, &smap]() { canbus_params_export_dialog_shown = true; }, + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[exit]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + .on_change = [screen]() { screen->Exit(); }, + }), + }), + + ftxui::Renderer([]() { return ftxui::separator(); }), + + // Body + (canidsCont | ftxui::Renderer([](ftxui::Element inner) { + return inner | ftxui::focusPositionRelative(0, focus_relative) | ftxui::vscroll_indicator | + ftxui::frame | ftxui::flex; + })) | + ftxui::CatchEvent([](ftxui::Event event) { + static constexpr float scroll_step = 0.03f; + static const auto increment_focus = []() { + focus_relative = std::clamp(focus_relative + scroll_step, 0.0f, 1.0f); + }; + + static const auto decrement_focus = []() { + focus_relative = std::clamp(focus_relative - scroll_step, 0.0f, 1.0f); + }; + + if (event.is_mouse()) { + switch (static_cast(event.mouse().button)) { + case ftxui::Mouse::Button::WheelDown: { + increment_focus(); + goto done; + } break; + + case ftxui::Mouse::Button::WheelUp: { + decrement_focus(); + goto done; + } break; + + default: + break; + } + } else if (!event.is_character()) { + if (event == ftxui::Event::ArrowDown) { + + increment_focus(); + goto done; + } else if (event == ftxui::Event::ArrowUp) { + + decrement_focus(); + goto done; + } + } + + forward: + return false; + + done: + return true; + }) | + + ftxui::Modal( + ftxui::Container::Vertical({ + ftxui::Renderer([]() { + return ftxui::text("Export parameters") | ftxui::color(ftxui::Color::Red); + }) | ftxui::hcenter, + ftxui::Renderer([]() { return ftxui::separator(); }), + + (canbus_params_export_dialog | ftxui::Renderer([](ftxui::Element inner) { + return inner | ftxui::focusPositionRelative(0, canbus_params_focus_relative) | + ftxui::vscroll_indicator | ftxui::frame | ftxui::flex; + })), + + ftxui::Renderer([]() { return ftxui::separatorEmpty(); }), + + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[export_to_file]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = []() { file_dialog_shown = true; }, + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[copy_to_clipboard]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = + [this]() { + TinyProcessLib::Process( + fmt::format("echo '{}' | xsel -bi", convertParametersMapToJson().dump())) + .get_exit_status(); + }, + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[close]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = []() { canbus_params_export_dialog_shown = false; }, + }), + }) | ftxui::hcenter, + }) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 96) | + ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 48) | ftxui::border | + ftxui::CatchEvent([](ftxui::Event event) { + static constexpr float scroll_step = 0.03f; + static const auto increment_focus = []() { + canbus_params_focus_relative = + std::clamp(canbus_params_focus_relative + scroll_step, 0.0f, 1.0f); + }; + static const auto decrement_focus = []() { + canbus_params_focus_relative = + std::clamp(canbus_params_focus_relative - scroll_step, 0.0f, 1.0f); + }; + + if (event.is_mouse()) { + switch (static_cast(event.mouse().button)) { + case ftxui::Mouse::Button::WheelDown: { + increment_focus(); + goto done; + } break; + + case ftxui::Mouse::Button::WheelUp: { + decrement_focus(); + goto done; + } break; + + default: + break; + } + } else if (!event.is_character()) { + if (event == ftxui::Event::ArrowDown) { + + increment_focus(); + goto done; + } else if (event == ftxui::Event::ArrowUp) { + + decrement_focus(); + goto done; + } + } + + forward: + return false; + + done: + return true; + }), + &canbus_params_export_dialog_shown) | + ftxui::Modal(makeFileDialog(screen, smap, file_dialog_shown), &file_dialog_shown) | + + ftxui::Modal( + ftxui::Container::Vertical({ + ftxui::Renderer([]() { + return ftxui::text("CAN player") | ftxui::color(ftxui::Color::Red); + }) | ftxui::hcenter, + ftxui::Renderer([]() { return ftxui::separator(); }), + + // Render window here + makeCanPlayerDialog(screen, smap, canplayer_is_ready) | ftxui::flex, + + ftxui::Renderer([]() { return ftxui::separator(); }), + + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[stop_all]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = [&smap]() { smap.get("canplayer_stopped")->operator()(); }, + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(" >[close]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing); + }, + + .on_change = []() { canbus_player_dialog_shown = false; }, + }), + }) | ftxui::hcenter, + }) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, ftxui::Terminal::Size().dimx) | + ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, ftxui::Terminal::Size().dimy) | ftxui::border, + &canbus_player_dialog_shown), + }), + }); + } + + ~Impl() override = default; + }; + + return ftxui::Make(screen, smap); +} diff --git a/src/parsers.cpp b/src/parsers.cpp new file mode 100644 index 0000000..86efa84 --- /dev/null +++ b/src/parsers.cpp @@ -0,0 +1,33 @@ +#include "parsers.hpp" + +namespace parsers { +std::optional parseSpnDataRange(const std::string &str) { + struct range_s result; + auto begin = str.begin(), end = str.end(); + return phrase_parse(begin, end, range::range_parser_s{}, ascii::space, result) ? std::optional(result) : std::nullopt; +} + +std::optional parseSpnSize(const std::string &str) { + struct size_s result; + auto begin = str.begin(), end = str.end(); + return phrase_parse(begin, end, size::size_parser_s{}, ascii::space, result) ? std::optional(result) : std::nullopt; +} + +std::optional parseSpnOffset(const std::string &str) { + struct offset_s result; + auto begin = str.begin(), end = str.end(); + return phrase_parse(begin, end, offset::offset_parser_s{}, ascii::space, result) ? std::optional(result) : std::nullopt; +} + +std::optional parseSpnResolution(const std::string &str) { + struct resolution_s result; + auto begin = str.begin(), end = str.end(); + return phrase_parse(begin, end, resolution::resolution_parser_s{}, ascii::space, result) ? std::optional(result) : std::nullopt; +} + +std::optional parseSpnPosition(size_t spn_size_bits, const std::string &str) { + struct spn_fragments_s result; + auto begin = str.begin(), end = str.end(); + return phrase_parse(begin, end, position::position_parser_s(spn_size_bits), ascii::space, result) ? std::optional(result) : std::nullopt; +} +}; // namespace parsers diff --git a/src/parsers.hpp b/src/parsers.hpp new file mode 100644 index 0000000..bf333e4 --- /dev/null +++ b/src/parsers.hpp @@ -0,0 +1,336 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +namespace parsers { +using namespace boost::spirit; + +// Parse data range string to this struct +struct range_s { + double min = .0f, max = .0f; + std::string other; +}; + +// Parse data range string to this struct +struct size_s { + size_t size_bytes = 0u, size_bits = 0u; +}; + +// Parse data range string to this struct +struct offset_s { + double offset = .0f; +}; + +// SPN position here +struct spn_fragments_s { + struct spn_part_s { + size_t byte_offset, bit_offset, size; + }; + + std::vector spn_fragments; +}; + +// Parse resolution string to this struct +struct resolution_s { + double resolution; +}; + +namespace _detail { +template struct parser_s : qi::grammar { + parser_s() : parser_s::base_type(rule) {} + qi::rule rule; // Main rule + qi::rule strnum; + qi::rule num; +}; + +// String to double converter +struct string_to_double_s { + double operator()(const std::string &str) const { + std::string num_str = str; + num_str.erase(std::remove(num_str.begin(), num_str.end(), ','), num_str.end()); + return std::stod(num_str); + } +}; + +// String to int32 converter +struct string_to_int_s { + int32_t operator()(const std::string &str) const { + std::string num_str = str; + num_str.erase(std::remove(num_str.begin(), num_str.end(), ','), num_str.end()); + return std::stoi(num_str); + } +}; +}; // namespace _detail + +// Data range parsers +namespace range { +template struct range_parser_s : _detail::parser_s { + range_parser_s() : _detail::parser_s() { + + this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))]; + this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)]; + this->rule = (this->num >> "to" >> this->num) | (-qi::digit >> -qi::digit >> +qi::char_("a-zA-Z-")); + } +}; +}; // namespace range + +namespace size { +template struct size_parser_s : _detail::parser_s { + size_parser_s() : _detail::parser_s() { + + struct bytes_to_bits_s { + size_s operator()(size_t bytes) const { + return { + .size_bytes = bytes, + .size_bits = bytes * UINT8_WIDTH, + }; + } + }; + + struct as_bits_s { + size_s operator()(size_t bits) const { + return { + .size_bytes = 0u, + .size_bits = bits, + }; + } + }; + + this->rule = ((qi::uint_ >> "byte") | (qi::uint_ >> "bytes"))[_val = boost::phoenix::function{}(qi::_1)] | + ((qi::uint_ >> "bit") | (qi::uint_ >> "bits"))[_val = boost::phoenix::function{}(qi::_1)]; + } +}; +}; // namespace size + +namespace position { +template struct position_parser_s : _detail::parser_s { + position_parser_s(size_t size_bits) : _detail::parser_s() { + + // just start byte + struct rule_v0_handler_s { + spn_fragments_s operator()(uint32_t start_byte) const { + return { + .spn_fragments = + { + { + .byte_offset = (start_byte - 1u), // From zero + .bit_offset = 0, + .size = UINT8_WIDTH, + }, + }, + }; + } + }; + + // start byte and bit offset (1 part of size - up to 1 byte) + struct rule_v1_handler_s { + spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t bit_offset) const { + return { + .spn_fragments = + { + { + .byte_offset = (start_byte - 1u), // From zero + .bit_offset = (bit_offset - 1u), + .size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH, + }, + }, + }; + } + }; + + // start byte and last_byte (1 part multiple of 1 byte) + struct rule_v2_handler_s { + spn_fragments_s operator()(uint32_t start_byte, uint32_t last_byte) const { + return { + .spn_fragments = + { + { + .byte_offset = (start_byte - 1u), + .bit_offset = 0u, + .size = ((last_byte - start_byte) + 1u) * UINT8_WIDTH, + }, + }, + }; + } + }; + + // start byte and last byte with bit offset (2 parts - first is integer bytes and second with bit offset) + struct rule_v3_handler_s { + spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t last_byte, uint32_t bit_offset) const { + return { + .spn_fragments = + { + // First - integer bytes (from start_byte to last_byte-1) + { + .byte_offset = (start_byte - 1u), + .bit_offset = 0u, + .size = ((last_byte - start_byte)) * UINT8_WIDTH, + }, + + // Second - with bit mask + { + .byte_offset = (last_byte - 1u), + .bit_offset = (bit_offset - 1u), + .size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH, + }, + }, + }; + } + }; + + // start with bit offset, last byte (2 parts - first with bit offset and second integer byte) + struct rule_v4_handler_s { + spn_fragments_s operator()(uint32_t start_byte, uint32_t bit_offset, uint32_t last_byte) const { + return { + .spn_fragments = + { + // First - with bit mask + { + .byte_offset = (start_byte - 1u), + .bit_offset = (bit_offset - 1u), + .size = UINT8_WIDTH - (bit_offset - 1u), + }, + + // second - integer byte + { + .byte_offset = (last_byte - 1u), + .bit_offset = 0u, + .size = UINT8_WIDTH, // Size is equal to 1 byte + }, + }, + }; + } + }; + + // start byte, last integer byte and last byte with bit offset + struct rule_v5_handler_s { + spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t last_integer_byte, uint32_t last_byte, uint32_t bit_offset) const { + return { + .spn_fragments = + { + { + .byte_offset = (start_byte - 1u), + .bit_offset = 0u, + .size = ((last_integer_byte - start_byte) + 1u) * UINT8_WIDTH, + }, + + // Second - with bit mask + { + .byte_offset = (last_byte - 1u), + .bit_offset = (bit_offset - 1u), + .size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH, + }, + }, + }; + } + }; + + // start byte with bit offset, first integer byte and last integer byte + struct rule_v6_handler_s { + spn_fragments_s operator()(uint32_t start_byte, uint32_t bit_offset, uint32_t first_integer_byte, uint32_t last_byte) const { + return { + .spn_fragments = + { + // First - with bit mask + { + .byte_offset = (start_byte - 1u), + .bit_offset = (bit_offset - 1u), + .size = UINT8_WIDTH - (bit_offset - 1u), + }, + + // Second - integer bytes + { + .byte_offset = (first_integer_byte - 1u), + .bit_offset = 0u, + .size = ((last_byte - first_integer_byte) + 1u) * UINT8_WIDTH, + }, + }, + }; + } + }; + + position_rule_v0 = (qi::uint_)[qi::_val = boost::phoenix::function{}(qi::_1)]; + position_rule_v1 = (qi::uint_ >> '.' >> qi::uint_)[qi::_val = boost::phoenix::function{}(size_bits, qi::_1, qi::_2)]; + position_rule_v2 = (qi::uint_ >> '-' >> qi::uint_)[qi::_val = boost::phoenix::function{}(qi::_1, qi::_2)]; + position_rule_v3 = (qi::uint_ >> ',' >> qi::uint_ >> '.' >> qi::uint_)[qi::_val = boost::phoenix::function{}(size_bits, qi::_1, qi::_2, qi::_3)]; + position_rule_v4 = (qi::uint_ >> '.' >> qi::uint_ >> ',' >> qi::uint_)[qi::_val = boost::phoenix::function{}(qi::_1, qi::_2, qi::_3)]; + position_rule_v5 = (qi::uint_ >> '-' >> qi::uint_ >> ',' >> qi::uint_ >> '.' >> + qi::uint_)[qi::_val = boost::phoenix::function{}(size_bits, qi::_1, qi::_2, qi::_3, qi::_4)]; + position_rule_v6 = + (qi::uint_ >> '.' >> qi::uint_ >> ',' >> qi::uint_ >> '-' >> qi::uint_)[qi::_val = boost::phoenix::function{}(qi::_1, qi::_2, qi::_3, qi::_4)]; + + // If one of rules works + this->rule = position_rule_v6 | position_rule_v5 | position_rule_v4 | position_rule_v3 | position_rule_v2 | position_rule_v1 | position_rule_v0; + } + + qi::rule position_rule_v0, position_rule_v1, position_rule_v2, position_rule_v3, position_rule_v4, position_rule_v5, position_rule_v6; +}; +}; // namespace position + +namespace offset { +template struct offset_parser_s : _detail::parser_s { + offset_parser_s() : _detail::parser_s() { + this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))]; + this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)]; + this->rule = this->num; + } +}; +}; // namespace offset + +// Resolution parsers +namespace resolution { +template struct resolution_parser_s : _detail::parser_s { + resolution_parser_s() : _detail::parser_s() { + struct resolution_rule_v0_handler_s { + resolution_s operator()(float number) const { + return { + .resolution = number, + }; + } + }; + + struct resolution_rule_v1_handler_s { + resolution_s operator()(double first, double second) const { + return { + .resolution = first / second, + }; + } + }; + + this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))]; + this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)]; + + resolution_rule_v0 = (this->num >> *qi::char_)[qi::_val = boost::phoenix::function{}(qi::_1)]; + resolution_rule_v1 = (qi::uint_ >> '/' >> qi::uint_ >> *qi::char_)[qi::_val = boost::phoenix::function{}(qi::_1, qi::_2)]; + + this->rule = resolution_rule_v1 | resolution_rule_v0; + } + + qi::rule resolution_rule_v0, resolution_rule_v1; +}; +}; // namespace resolution + +std::optional parseSpnDataRange(const std::string &str); +std::optional parseSpnSize(const std::string &str); +std::optional parseSpnOffset(const std::string &str); +std::optional parseSpnPosition(size_t spn_size_bits, const std::string &str); +std::optional parseSpnResolution(const std::string &str); +}; // namespace parsers + +BOOST_FUSION_ADAPT_STRUCT(parsers::range_s, (double, min)(double, max)(std::string, other)); +BOOST_FUSION_ADAPT_STRUCT(parsers::size_s, (size_t, size_bytes)(size_t, size_bits)); +BOOST_FUSION_ADAPT_STRUCT(parsers::spn_fragments_s::spn_part_s, (size_t, byte_offset)(size_t, bit_offset)(size_t, size)); +BOOST_FUSION_ADAPT_STRUCT(parsers::spn_fragments_s, (std::vector, spn_fragments)); +BOOST_FUSION_ADAPT_STRUCT(parsers::offset_s, (double, offset)); +BOOST_FUSION_ADAPT_STRUCT(parsers::resolution_s, (double, resolution)); diff --git a/src/recorder.cpp b/src/recorder.cpp new file mode 100644 index 0000000..eac82bf --- /dev/null +++ b/src/recorder.cpp @@ -0,0 +1,164 @@ +#include "recorder.hpp" +#include "sqlite_modern_cpp.h" + +#include +#include + +#define FMT_HEADER_ONLY +#include + +#include + +Recorder::Recorder(const std::string &db_path, bool console_output) + : disk_db_path_(db_path), console_output_(console_output) { + log_ = spdlog::systemd_logger_mt("recorder", "cansniffer-rec"); + log_->set_level(spdlog::level::info); + + flush_task_ = std::async(std::launch::async, [this](std::stop_token st) { background_flush_task(st); }, flush_stop_.get_token()); + log_->info("recorder initialized, db_path={}", db_path); + if (console_output_) fmt::println("Recording to: {}", db_path); +} + +Recorder::~Recorder() { + flushAndClose(); +} + +void Recorder::onBatch(const std::vector &batch) { + int64_t now = epoch_ms(); + std::lock_guard lock(batch_mtx_); + if (batch_ts_start_ == 0) batch_ts_start_ = now; + for (const auto &u : batch) { + if (u.verbose && !u.verbose->is_null() && u.verbose->contains("SPNs")) { + pending_.push_back({now, u.iface, u.canid, u.verbose}); + } + } +} + +void Recorder::flushAndClose() { + flush_stop_.request_stop(); + if (flush_task_.valid()) flush_task_.wait(); + + std::lock_guard lock(batch_mtx_); + if (!pending_.empty()) { + log_->info("flushing remaining {} frames on exit", pending_.size()); + compress_batch(); + } +} + +int64_t Recorder::epoch_ms() { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +size_t Recorder::get_rss_mb() { + struct rusage ru {}; + getrusage(RUSAGE_SELF, &ru); + return static_cast(ru.ru_maxrss) / 1024; +} + +std::vector Recorder::gzip_compress(const std::string &src) { + z_stream strm{}; + deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY); + + strm.next_in = reinterpret_cast(const_cast(src.data())); + strm.avail_in = static_cast(src.size()); + + std::vector out; + out.resize(deflateBound(&strm, static_cast(src.size()))); + + strm.next_out = out.data(); + strm.avail_out = static_cast(out.size()); + + deflate(&strm, Z_FINISH); + out.resize(strm.total_out); + deflateEnd(&strm); + + return out; +} + +void Recorder::compress_batch() { + if (pending_.empty()) return; + + nlohmann::json::array_t arr; + arr.reserve(pending_.size()); + + int64_t ts_start = pending_.front().ts_ms; + int64_t ts_end = pending_.back().ts_ms; + + for (const auto &rec : pending_) { + nlohmann::json entry; + entry["ts"] = rec.ts_ms; + entry["canid"] = rec.canid; + + if (rec.verbose && !rec.verbose->is_null()) { + const auto &v = *rec.verbose; + if (v.contains("PGN")) entry["pgn"] = v["PGN"]; + + if (v.contains("SPNs")) { + nlohmann::json::array_t spns; + for (const auto &spn : v["SPNs"]) { + nlohmann::json s; + if (spn.contains("SPN (integer)")) s["spn"] = spn["SPN (integer)"]; + if (spn.contains("SPN name")) s["name"] = spn["SPN name"]; + if (spn.contains("Value")) s["value"] = spn["Value"]; + if (spn.contains("Unit")) s["unit"] = spn["Unit"]; + spns.push_back(std::move(s)); + } + entry["spns"] = std::move(spns); + } + } + + arr.push_back(std::move(entry)); + } + + std::string json_str = nlohmann::json(arr).dump(); + auto compressed = gzip_compress(json_str); + + try { + sqlite::database disk_db(disk_db_path_); + disk_db << "PRAGMA journal_mode = WAL;"; + disk_db << R"( + CREATE TABLE IF NOT EXISTS batches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts_start INTEGER NOT NULL, + ts_end INTEGER NOT NULL, + frame_count INTEGER NOT NULL, + data BLOB NOT NULL + ); + )"; + disk_db << R"(CREATE INDEX IF NOT EXISTS idx_batches_ts ON batches(ts_start);)"; + + disk_db << "INSERT INTO batches (ts_start, ts_end, frame_count, data) VALUES (?, ?, ?, ?);" + << ts_start << ts_end << static_cast(pending_.size()) << compressed; + + auto msg = fmt::format("Flushed batch: {} frames, {:.1f}KB -> {:.1f}KB gzip ({:.0f}% compression)", + pending_.size(), + static_cast(json_str.size()) / 1024.0, + static_cast(compressed.size()) / 1024.0, + (1.0 - static_cast(compressed.size()) / static_cast(json_str.size())) * 100.0); + log_->info("{}", msg); + if (console_output_) fmt::println("{}", msg); + } catch (const sqlite::sqlite_exception &e) { + log_->error("disk DB write failed: {}", e.what()); + } + + pending_.clear(); + batch_ts_start_ = 0; +} + +void Recorder::background_flush_task(std::stop_token st) { + while (!st.stop_requested()) { + std::this_thread::sleep_for(std::chrono::seconds(10)); + + std::lock_guard lock(batch_mtx_); + if (pending_.empty()) continue; + + int64_t now = epoch_ms(); + bool time_trigger = (now - batch_ts_start_) >= 60'000; + bool mem_trigger = get_rss_mb() >= 500; + + if (time_trigger || mem_trigger) { + if (mem_trigger) log_->warn("RSS >= 500MB, forcing flush"); + compress_batch(); + } + } +} diff --git a/src/recorder.hpp b/src/recorder.hpp new file mode 100644 index 0000000..477ad8c --- /dev/null +++ b/src/recorder.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "can_data.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +struct FrameRecord { + int64_t ts_ms; + std::string iface; + std::string canid; + std::shared_ptr verbose; +}; + +class Recorder { +public: + Recorder(const std::string &db_path, bool console_output); + ~Recorder(); + + void onBatch(const std::vector &batch); + void flushAndClose(); + +private: + static int64_t epoch_ms(); + static size_t get_rss_mb(); + static std::vector gzip_compress(const std::string &src); + + void compress_batch(); + void background_flush_task(std::stop_token st); + + std::string disk_db_path_; + std::mutex batch_mtx_; + std::vector pending_; + int64_t batch_ts_start_ = 0; + std::stop_source flush_stop_; + std::future flush_task_; + bool console_output_ = false; + std::shared_ptr log_; +}; diff --git a/src/signals.hpp b/src/signals.hpp new file mode 100644 index 0000000..6858fcb --- /dev/null +++ b/src/signals.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "can_data.hpp" + +#define FMT_HEADER_ONLY +#include + +#include "sqlite_modern_cpp.h" + +class signals_map_s { + struct signal_holder_base { + virtual ~signal_holder_base() = default; + }; + + template + struct signal_holder : signal_holder_base { + boost::signals2::signal signal; + }; + +public: + template + void register_signal(const std::string &name) { + signals_.emplace(name, std::make_unique>()); + } + + template + auto *get(const std::string &name) { + auto it = signals_.find(name); + if (it == signals_.end()) { + throw std::runtime_error(fmt::format("Signal '{}' not found", name)); + } + auto *holder = dynamic_cast *>(it->second.get()); + if (!holder) { + throw std::runtime_error(fmt::format("Signal '{}' type mismatch", name)); + } + return &holder->signal; + } + + template + auto *get(const char *name) { + return get(std::string(name)); + } + +private: + std::unordered_map> signals_; +}; + +struct signals_s { + static inline signals_map_s map = []() { + signals_map_s m; + m.register_signal("new_data_recvd"); + m.register_signal("new_entry"); + m.register_signal &)>("new_entries_batch"); + m.register_signal("show_settings"); + m.register_signal("show_file_dialog_request"); + m.register_signal("export_file_request"); + m.register_signal("new_tag_request"); + m.register_signal("j1939_database_ready"); + m.register_signal("canplayer_started"); + m.register_signal("canplayer_stopped"); + return m; + }(); +}; + +using signals_map_t = signals_map_s; diff --git a/src/spnselector.cpp b/src/spnselector.cpp new file mode 100644 index 0000000..1dd1729 --- /dev/null +++ b/src/spnselector.cpp @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define FMT_HEADER_ONLY +#include + +#include "signals.hpp" +#include +#include +#include "json/json.hpp" + +ftxui::Component makeSpnSelector(ftxui::ScreenInteractive *screen, signals_map_t &smap, nlohmann::json data) { + class Impl : public ftxui::ComponentBase { + public: + Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap, nlohmann::json data) {} + }; + + return ftxui::Make(screen, smap, data); + +} diff --git a/src/tagsettingrow.cpp b/src/tagsettingrow.cpp new file mode 100644 index 0000000..ff8ce3a --- /dev/null +++ b/src/tagsettingrow.cpp @@ -0,0 +1,6 @@ +#include "tagsettingrow.hpp" + +ftxui::Component makeSpnSettingsRow(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, ftxui::Component cmpCont, size_t spn_id, + spn_settings_map_t &tagSettingsMap) { + return ftxui::Make(screen, smap, canid, cmpCont, spn_id, tagSettingsMap); +} diff --git a/src/tagsettingrow.hpp b/src/tagsettingrow.hpp new file mode 100644 index 0000000..769a657 --- /dev/null +++ b/src/tagsettingrow.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +#include "signals.hpp" +#include "tagsettings.hpp" + +class SpnSettingRow : public ftxui::ComponentBase { +public: + using spn_settings_map_t = std::map>; + + explicit SpnSettingRow(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, ftxui::Component cmpCont, size_t tag_id, + spn_settings_map_t &tagSettingsMap) { + Add(ftxui::Container::Vertical({ + ftxui::Container::Vertical({ + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Name: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.spn_name, .placeholder = "SPN name"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "X coefficient: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.x_coeff, .placeholder = "X coefficient"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Y coefficient: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.y_coeff, .placeholder = "Y coefficient"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Offset: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.offset, .placeholder = "Offset"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Size: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.size, .placeholder = "Size"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Pos: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.pos, .placeholder = "Pos"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Bit offset: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.bit_offset, .placeholder = "Bit offset"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Bit count: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }), + ftxui::Input({.content = &m_current_settings_.bit_count, .placeholder = "Bit count"}) | ftxui::vcenter | ftxui::xflex, + }), + + ftxui::Checkbox({ + .label = fmt::format("{:32}", "Little endian"), + .checked = &m_current_settings_.le, + }) | ftxui::vcenter | + ftxui::xflex, + + ftxui::Checkbox({ + .label = fmt::format("{:32}", "Discrete"), + .checked = &m_current_settings_.discrete, + }) | ftxui::vcenter | + ftxui::xflex, + }) | ftxui::xflex, + })); + } + + const spn_settings_s &exportSettings() const { return m_current_settings_; } + +private: + spn_settings_s m_current_settings_; +}; diff --git a/src/tagsettings.hpp b/src/tagsettings.hpp new file mode 100644 index 0000000..654353d --- /dev/null +++ b/src/tagsettings.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +struct spn_fragment_s { + std::string byte_offset; + std::string bit_offset; + std::string bit_count; +}; + +struct spn_settings_s { + bool selected = false; + std::string spn_name; + std::string resolution; + std::string offset; + bool big_endian = false; + std::string unit; + std::string uuid; + std::vector fragments = {{}}; + int32_t active_fragment = 0; + + // Legacy compat + std::string x_coeff; + std::string y_coeff; + std::string size; + std::string pos; + std::string bit_offset; + std::string bit_count; + bool le = false; + bool discrete = false; +}; + +using spn_settings_map_t = std::map>; diff --git a/src/tagsettingsform.cpp b/src/tagsettingsform.cpp new file mode 100644 index 0000000..7cb36a0 --- /dev/null +++ b/src/tagsettingsform.cpp @@ -0,0 +1,548 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +#include "canid_unit.hpp" +#include "signals.hpp" +#include "tagsettings.hpp" + +ftxui::Component makeSpnSettingsForm(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, + std::string &canid_active, ftxui::Component cmpCont, ftxui::Component dialog, + bool &modal_shown, + std::map> &spnSettingsFormMap, + spn_settings_map_t &spnSettingsMap) { + class Impl : public ftxui::ComponentBase { + public: + explicit Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, + std::string &canid_active, ftxui::Component cmpCont, ftxui::Component dialog, bool &modal_shown, + std::map> &spnSettingsFormMap, + spn_settings_map_t &spnSettingsMap) + : m_canid_(canid), m_screen_(screen), m_smap_(smap), m_cmpCont_(cmpCont), + m_spnSettingsFormMap_(spnSettingsFormMap), m_spnSettingsMap_(spnSettingsMap) { + static size_t spns_count = 0u; + + for (auto &[k, v] : m_spnSettingsFormMap_[m_canid_]) { + addSpnComponent(k); + } + + smap.get("new_tag_request") + ->connect([this](const std::string &canid, size_t tag_id) { + if (canid == m_canid_) { + addSpnComponent(static_cast(tag_id)); + } + }); + + Add(ftxui::Container::Vertical({ + ftxui::Container::Vertical({ + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::text(" >[add_parameter]< ") | (state.focused ? ftxui::bold : ftxui::nothing) | + ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing), + ftxui::filler(), + }); + }, + + .on_change = + [&, this]() { + canid_active = m_canid_; + m_spnSettingsMap_[m_canid_].insert_or_assign( + static_cast(spns_count), + spn_settings_s{ + .uuid = boost::lexical_cast(boost::uuids::random_generator()()), + .le = true, + }); + addSpnComponent(static_cast(spns_count)); + spns_count++; + }, + }), + + ftxui::Checkbox({ + .transform = [this](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::hbox({ + ftxui::text(" >[remove_selected_params]< ") | + (state.focused ? ftxui::bold : ftxui::nothing) | ftxui::color(ftxui::Color::Cyan) | + (state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing), + ftxui::filler(), + }); + }, + + .on_change = + [&, this]() { + std::vector keys; + + for (const auto &[k, v] : m_spnSettingsMap_[m_canid_]) { + if (v.selected) { + keys.push_back(k); + } + } + + for (auto i : keys) { + m_spnSettingsFormMap_[m_canid_].erase(i); + m_spnSettingsMap_[m_canid_].erase(i); + } + + m_current_spn_settings_->DetachAllChildren(); + for (auto &[k, v] : m_spnSettingsFormMap_[m_canid_]) { + m_current_spn_settings_->Add(v); + } + }, + }), + }), + + m_current_spn_settings_ | ftxui::xflex, + }) | + ftxui::flex); + } + + private: + void addSpnComponent(int32_t k) { + auto &s = m_spnSettingsMap_[m_canid_][k]; + + auto make_field = [](const char *label, const char *placeholder, std::string *content) { + return ftxui::Container::Horizontal({ + ftxui::Renderer([label]() { + return ftxui::text(fmt::format("{:24}", label)) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); + }), + + ftxui::Renderer([]() { return ftxui::text("[ "); }), + ftxui::Input({ + .content = content, + .placeholder = placeholder, + .transform = [](ftxui::InputState state) -> ftxui::Element { + auto el = state.element; + + if (state.focused) { + el = el | ftxui::bgcolor(ftxui::Color::Grey27) | ftxui::focusCursorBarBlinking; + } else if (state.hovered) { + el = el | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + + .multiline = false, + }) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 32), + ftxui::Renderer([]() { return ftxui::text(" ]"); }), + }); + }; + + auto get_data = [this]() -> std::vector { + for (uint32_t i = 0; i < m_cmpCont_->ChildCount(); i++) { + if (std::static_pointer_cast(m_cmpCont_->ChildAt(i))->getCanID() == m_canid_) { + return std::static_pointer_cast(m_cmpCont_->ChildAt(i))->getData(); + } + } + + return {}; + }; + + // Fragment tab container — shows fields for active fragment + auto frag_fields_container = ftxui::Container::Vertical({}); + + auto rebuild_frag_fields = [&s, frag_fields_container, make_field]() { + frag_fields_container->DetachAllChildren(); + if (s.active_fragment >= 0 && s.active_fragment < static_cast(s.fragments.size())) { + + auto &f = s.fragments[s.active_fragment]; + frag_fields_container->Add(make_field("Byte offset:", "enter byte offset here ...", &f.byte_offset)); + frag_fields_container->Add(make_field("Bit offset:", "enter bit offset here ...", &f.bit_offset)); + frag_fields_container->Add(make_field("Bit count:", "enter bit count here ...", &f.bit_count)); + } + }; + + rebuild_frag_fields(); + + // Fragment tab switcher + auto frag_tabs = ftxui::Renderer([&s]() { + ftxui::Elements tabs; + tabs.push_back(ftxui::text("<")); + + for (size_t i = 0; i < s.fragments.size(); ++i) { + if (i > 0) { + tabs.push_back(ftxui::text(" | ")); + } + + auto label = ftxui::text(fmt::format("fragment#{}", i)); + if (static_cast(i) == s.active_fragment) { + label = label | ftxui::bold | ftxui::color(ftxui::Color::Red); + } else { + label = label | ftxui::color(ftxui::Color::Cyan); + } + + tabs.push_back(label); + } + + tabs.push_back(ftxui::text(">")); + return ftxui::hbox(std::move(tabs)); + }); + + // Payload view — highlights ALL fragments simultaneously + auto payload_view = ftxui::Renderer([&s, get_data]() { + auto data = get_data(); + + double resolution = 1.0, offset_val = 0.0; + try { + resolution = std::stod(s.resolution); + } catch (...) { + } + + try { + offset_val = std::stod(s.offset); + } catch (...) { + } + + // Collect all bit ranges from all fragments + struct bit_range { + int32_t global_start, global_end; + int32_t byte_off, byte_cnt; + }; + + std::vector ranges; + for (const auto &f : s.fragments) { + int32_t bo = 0, bi = 0, bc = 0; + + try { + bo = std::stoi(f.byte_offset); + } catch (...) { + } + + try { + bi = std::stoi(f.bit_offset); + } catch (...) { + } + + try { + bc = std::stoi(f.bit_count); + } catch (...) { + } + + if (bc > 0) { + int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH; + ranges.push_back({bo * UINT8_WIDTH + bi, bo * UINT8_WIDTH + bi + bc, bo, byte_cnt}); + } + } + + auto byte_in_range = [&ranges](int32_t idx) { + for (const auto &r : ranges) { + if (idx >= r.byte_off && idx < r.byte_off + r.byte_cnt) { + return true; + } + } + + return false; + }; + + auto bit_in_range = [&ranges](int32_t global_bit) { + for (const auto &r : ranges) { + if (global_bit >= r.global_start && global_bit < r.global_end) { + return true; + } + } + + return false; + }; + + // Hex + ftxui::Elements hex_els; + hex_els.push_back(ftxui::text("{ ")); + for (size_t i = 0; i < data.size(); ++i) { + auto val = fmt::format("0x{:02X}", data[i]); + auto t = ftxui::text(i + 1 < data.size() ? fmt::format("{:<15s}", val + ",") : fmt::format("{:<15s}", val)); + hex_els.push_back(byte_in_range(static_cast(i)) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold) + : t); + } + + hex_els.push_back(ftxui::text(" }")); + + // Dec + ftxui::Elements dec_els; + dec_els.push_back(ftxui::text("{ ")); + + for (size_t i = 0; i < data.size(); ++i) { + auto val = fmt::format("{}", data[i]); + auto t = ftxui::text(i + 1 < data.size() ? fmt::format("{:<15s}", val + ",") : fmt::format("{:<15s}", val)); + dec_els.push_back(byte_in_range(static_cast(i)) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold) + : t); + } + + dec_els.push_back(ftxui::text(" }")); + + // Bin — per-bit highlighting across all fragments + ftxui::Elements bin_els; + bin_els.push_back(ftxui::text("{ ")); + + for (size_t i = 0; i < data.size(); ++i) { + ftxui::Elements bits; + bits.push_back(ftxui::text("0b")); + + for (int32_t b = 7; b >= 0; --b) { + int32_t global_bit = static_cast(i) * UINT8_WIDTH + b; + char ch = (data[i] >> b) & 1 ? '1' : '0'; + auto t = ftxui::text(std::string(1, ch)); + bits.push_back(bit_in_range(global_bit) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold) : t); + } + + if (i + 1 < data.size()) { + bits.push_back(ftxui::text(",")); + } + + bin_els.push_back(ftxui::hbox(std::move(bits))); + bin_els.push_back(ftxui::text(" ")); + } + + bin_els.push_back(ftxui::text(" }")); + + // Extract value from all fragments (always LE within each fragment) + int64_t result = 0; + size_t total_bits = 0; + for (const auto &f : s.fragments) { + int32_t bo = 0, bi = 0, bc = 0; + + try { + bo = std::stoi(f.byte_offset); + bi = std::stoi(f.bit_offset); + bc = std::stoi(f.bit_count); + } catch (...) { + } + + int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH; + + if (bc > 0 && bo >= 0 && static_cast(bo + byte_cnt) <= data.size()) { + int64_t frag_val = 0; + + for (int32_t i = 0; i < byte_cnt; ++i) { + frag_val |= static_cast(data[bo + i]) << (i * UINT8_WIDTH); + } + + frag_val = (frag_val >> bi) & ((1LL << bc) - 1); + result |= frag_val << total_bits; + total_bits += bc; + } + } + // Byteswap the assembled result if big endian + if (s.big_endian && total_bits > 8u) { + int64_t swapped = 0; + size_t total_bytes = (total_bits + 7u) / 8u; + + for (size_t i = 0; i < total_bytes; ++i) { + swapped |= ((result >> (i * 8u)) & 0xFFu) << ((total_bytes - 1 - i) * 8u); + } + + result = swapped; + } + + double value = static_cast(result) * resolution + offset_val; + + return ftxui::vbox({ + ftxui::separatorEmpty(), + ftxui::separatorEmpty(), + ftxui::vbox({ + ftxui::hbox(hex_els), + ftxui::hbox(dec_els), + ftxui::hbox(bin_els), + }) | ftxui::xframe, + ftxui::hbox({ + ftxui::text("Value: ") | ftxui::bold, + ftxui::text(fmt::format("({:0{}} | 0x{:0{}X} | 0b{:0{}b}) * {} + {} = ", result, + static_cast(fmt::format("{}", (1LL << total_bits) - 1).size()), result, + static_cast((total_bits + 3) / 4), result, + static_cast(total_bits), resolution, offset_val)), + + ftxui::text(fmt::format("{:.6g} {}", value, s.unit)) | ftxui::color(ftxui::Color::IndianRed) | + ftxui::bold, + }), + }); + }); + + // Fragment buttons + switcher (full width, above the main horizontal layout) + auto frag_switcher = ftxui::Container::Vertical({ + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::text(">[add_fragment]<") | ftxui::color(ftxui::Color::Cyan); + if (state.focused || state.active) { + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + + .on_change = + [&s, rebuild_frag_fields]() { + s.fragments.push_back({}); + s.active_fragment = static_cast(s.fragments.size()) - 1; + rebuild_frag_fields(); + }, + }), + ftxui::Renderer([]() { return ftxui::text(" "); }), + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::text(">[remove_fragment]<") | ftxui::color(ftxui::Color::Cyan); + if (state.focused || state.active) + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + return el; + }, + .on_change = + [&s, rebuild_frag_fields]() { + if (s.fragments.size() > 1) { + s.fragments.erase(s.fragments.begin() + s.active_fragment); + if (s.active_fragment >= static_cast(s.fragments.size())) + s.active_fragment = static_cast(s.fragments.size()) - 1; + rebuild_frag_fields(); + } + }, + }), + }), + + ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::text("[<]") | ftxui::color(ftxui::Color::Cyan); + + if (state.focused || state.active) { + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + + .on_change = + [&s, rebuild_frag_fields]() { + if (s.active_fragment > 0) { + s.active_fragment--; + rebuild_frag_fields(); + } + }, + }), + + ftxui::Renderer([&s]() { + ftxui::Elements tabs; + tabs.push_back(ftxui::text(" ")); + for (size_t i = 0; i < s.fragments.size(); ++i) { + if (i > 0) { + tabs.push_back(ftxui::text(" | ")); + } + + auto label = ftxui::text(fmt::format("fragment#{}", i)); + + if (static_cast(i) == s.active_fragment) { + label = label | ftxui::bold | ftxui::color(ftxui::Color::Red); + + } else { + label = label | ftxui::color(ftxui::Color::Cyan); + } + + tabs.push_back(label); + } + + tabs.push_back(ftxui::text(" ")); + return ftxui::hbox(std::move(tabs)); + }), + + ftxui::Checkbox({ + .transform = [](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::text("[>]") | ftxui::color(ftxui::Color::Cyan); + if (state.focused || state.active) + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + return el; + }, + .on_change = + [&s, rebuild_frag_fields]() { + if (s.active_fragment < static_cast(s.fragments.size()) - 1) { + s.active_fragment++; + rebuild_frag_fields(); + } + }, + }), + }), + }); + + // Main horizontal: checkbox | settings | payload + auto main_row = ftxui::Container::Horizontal({ + ftxui::Checkbox({ + .checked = &s.selected, + .transform = [&s](const ftxui::EntryState &state) -> ftxui::Element { + return ftxui::text(s.selected ? "[X]" : "[ ]") | + (s.selected ? ftxui::color(ftxui::Color::Red) : ftxui::color(ftxui::Color::Cyan)); + }, + }) | ftxui::vcenter | + ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 5), + + ftxui::Container::Vertical({ + make_field("SPN name:", "enter name here ...", &s.spn_name), + make_field("SPN resolution:", "enter resolution here ...", &s.resolution), + make_field("SPN offset:", "enter offset here ...", &s.offset), + make_field("SPN unit:", "enter unit here ...", &s.unit), + frag_fields_container, + ftxui::Container::Horizontal({ + ftxui::Renderer([]() { + return ftxui::text(fmt::format("{:24}", "Endianness:")) | ftxui::bold | + ftxui::color(ftxui::Color::Yellow); + }), + + ftxui::Checkbox({ + .checked = &s.big_endian, + .transform = [&s](const ftxui::EntryState &state) -> ftxui::Element { + auto el = ftxui::hbox({ + ftxui::text("< "), + ftxui::text("little") | + (!s.big_endian ? (ftxui::bold | ftxui::color(ftxui::Color::Red)) : ftxui::nothing), + ftxui::text(" | "), + ftxui::text("big") | + (s.big_endian ? (ftxui::bold | ftxui::color(ftxui::Color::Red)) : ftxui::nothing), + ftxui::text(" >"), + }); + + if (state.focused || state.active) { + el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11); + } + + return el; + }, + }), + }), + }) | ftxui::flex, + + ftxui::Renderer([]() { return ftxui::text(" "); }), + payload_view | ftxui::flex, + }); + + auto component = ftxui::Container::Vertical({frag_switcher, main_row}) | ftxui::border | ftxui::flex; + + m_current_spn_settings_->Add({component}); + m_spnSettingsFormMap_[m_canid_].insert_or_assign(k, component); + } + + private: + ftxui::Component m_current_spn_settings_ = ftxui::Container::Vertical({}); + std::string m_canid_; + ftxui::ScreenInteractive *m_screen_; + signals_map_t &m_smap_; + ftxui::Component m_cmpCont_; + std::map> &m_spnSettingsFormMap_; + spn_settings_map_t &m_spnSettingsMap_; + }; + + return ftxui::Make(screen, smap, canid, canid_active, cmpCont, dialog, modal_shown, spnSettingsFormMap, + spnSettingsMap); +} diff --git a/src/tuple.hpp b/src/tuple.hpp new file mode 100644 index 0000000..f1909d1 --- /dev/null +++ b/src/tuple.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tp { +namespace detail { + +template constexpr Func for_each_arg(Func f, Args &&...args) { + std::initializer_list{f(std::forward(args), 0)...}; + return f; +} + +template constexpr Func for_each_impl(Tuple &&t, Func &&f, std::index_sequence is) { + return std::initializer_list{(std::forward(f)(std::get(std::forward(t))), 0)...}, f; +} + +template auto transform_impl(std::tuple const &inputs, Function function, std::index_sequence is) { + return std::tuple...>{function(std::get(inputs))...}; +} + +template auto subtuple(const std::tuple &t, std::index_sequence) { return std::make_tuple(std::get(t)...); } + +// ZIP utilities +template using zip_tuple_at_index_t = std::tuple>...>; +template zip_tuple_at_index_t zip_tuple_at_index(Tuples &&...tuples) { return {std::get(std::forward(tuples))...}; } +template std::tuple...> tuple_zip_impl(Tuples &&...tuples, std::index_sequence) { + return {zip_tuple_at_index(std::forward(tuples)...)...}; +} +}; // namespace detail + +template constexpr decltype(auto) for_each(Tuple &&tuple, F &&f) { + return [](Tuple &&tuple, F &&f, std::index_sequence) { + (f(std::get(tuple)), ...); + return f; + }(std::forward(tuple), std::forward(f), std::make_index_sequence>::value>{}); +} + +template auto transform(std::tuple const &inputs, Function function) { + return detail::transform_impl(inputs, function, std::make_index_sequence{}); +} + +template constexpr size_t find_if(Tuple &&tuple, Predicate pred) { + size_t index = std::tuple_size>::value; + size_t currentIndex = 0; + bool found = false; + + for_each(tuple, [&](auto &&value) { + if (!found && pred(value)) { + index = currentIndex; + found = true; + } + + ++currentIndex; + }); + + return index; +} + +template void perform(Tuple &&tuple, size_t index, Action action) { + size_t currentIndex = 0; + for_each(tuple, [&action, index, ¤tIndex](auto &&value) { + if (currentIndex == index) { + + action(std::forward(value)); + } + + ++currentIndex; + }); +} + +template bool all_of(Tuple &&tuple, Predicate pred) { + return find_if(tuple, std::not_fn(pred)) == std::tuple_size>::value; +} + +template bool none_of(Tuple &&tuple, Predicate pred) { return find_if(tuple, pred) == std::tuple_size>::value; } +template bool any_of(Tuple &&tuple, Predicate pred) { return !none_of(tuple, pred); } + +template Tuple &operator|(Tuple &&tuple, Function func) { + for_each(tuple, func); + return tuple; +} + +template auto subtuple(const std::tuple &t) { return detail::subtuple(t, std::make_index_sequence()); } + +template ())> struct sub_range; + +template struct sub_range, std::index_sequence> { + static_assert(elems <= sizeof...(Args) - starting, "sub range is out of bounds!"); + using tuple = std::tuple>...>; +}; + +template auto select_tuple(Tuple &&tuple, std::index_sequence) { + return std::tuple...>(std::get(std::forward(tuple))...); +} + +template struct tuple_index; + +template struct tuple_index> { + static const std::size_t value = 0; +}; + +template struct tuple_index> { + static const std::size_t value = 1 + tuple_index>::value; +}; + +// ZIP +template + requires((std::tuple_size_v> == std::tuple_size_v>) && ...) +auto tuple_zip(Head &&head, Tail &&...tail) { + return detail::tuple_zip_impl(std::forward(head), std::forward(tail)..., std::make_index_sequence>>()); +} +}; // namespace tp diff --git a/src/types.hpp b/src/types.hpp new file mode 100644 index 0000000..f4be461 --- /dev/null +++ b/src/types.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +/** + * @brief Returns pretty-print name of the type + */ +template constexpr std::string_view type_name() { + using namespace std; +#if defined(__clang__) + std::string_view p = __PRETTY_FUNCTION__; + return std::string_view(p.data() + 34, p.size() - 34 - 1); +#elif defined(__GNUC__) + std::string_view p = __PRETTY_FUNCTION__; + return std::string_view(p.data() + 49, p.find(';', 49) - 49); +#endif +} diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..eb3d8ff --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,17 @@ +#include "utils.hpp" + +namespace utils { +void backup_db(const sqlite::database &db, const std::string &backup_path) { + // sqlite::database bkp(backup_path); + // auto con = db.connection(); + // auto state = std::unique_ptr(sqlite3_backup_init(bkp.connection().get(), "main", con.get(), "main"), sqlite3_backup_finish); + + // if (state) { + // int rc; + + // do { + // rc = sqlite3_backup_step(state.get(), 100); + // } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); + // } +} +}; // namespace utils diff --git a/src/utils.hpp b/src/utils.hpp new file mode 100644 index 0000000..b61958f --- /dev/null +++ b/src/utils.hpp @@ -0,0 +1,5 @@ +#include "sqlite_modern_cpp.h" + +namespace utils { + void backup_db(const sqlite::database &db, const std::string &backup_path); +}; diff --git a/src/xlsx.cpp b/src/xlsx.cpp new file mode 100644 index 0000000..830e95e --- /dev/null +++ b/src/xlsx.cpp @@ -0,0 +1,371 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FMT_HEADER_ONLY +#include +#include + +// for XLSX files +#include + +// For sqlite +#include "parsers.hpp" +#include "sqlite_modern_cpp.h" + +static void init_db(sqlite::database &db) { + try { + db << R"( + CREATE TABLE IF NOT EXISTS pgns ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pgn INTEGER UNIQUE, + pg_label TEXT, + pg_acronym TEXT, + pg_descr TEXT, + edp INTEGER, + dp INTEGER, + pf INTEGER, + ps INTEGER, + pg_datalen INTEGER, + pg_priority INTEGER + ); + )"; + + db << R"( + CREATE TABLE IF NOT EXISTS spns ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pgn INTEGER, + spn INTEGER UNIQUE, + spn_name TEXT, + spn_pos TEXT, + spn_length INTEGER, + resolution REAL, + offset REAL, + data_range TEXT, + min_value REAL, + max_value REAL, + units TEXT, + slot_id TEXT, + slot_name TEXT, + spn_type TEXT + ); + )"; + + db << R"( + CREATE TABLE IF NOT EXISTS spn_fragments ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + spn INTEGER, + pgn INTEGER, + byte_offset INTEGER, + bit_offset INTEGER, + size INTEGER + ); + )"; + + db << "PRAGMA journal_mode = OFF"; + db << "PRAGMA synchronous = OFF"; + db << "PRAGMA foreign_keys = ON;"; + } catch (const sqlite::sqlite_exception &e) { + throw; + } +} + +std::unique_ptr parseXlsx(const std::string &file) { + auto db_ptr = std::make_unique(":memory:"); + auto &db = *db_ptr; + + init_db(db); + + static const std::map> + pgn_mapping_table = + { + {"PGN", {5u, "pgn"}}, + {"Parameter Group Label", {6u, "pg_label"}}, + {"PG Acronym", {7u, "pg_acronym"}}, + {"PG Description", {8u, "pg_descr"}}, + {"EDP", {9u, "edp"}}, + {"DP", {10u, "dp"}}, + {"PF", {11u, "pf"}}, + {"PS", {12u, "ps"}}, + {"PG Data Length", {15u, "pg_datalen"}}, + {"Default Priority", {16u, "pg_priority"}}, + }, + + spn_mapping_table = { + {"SPN", {19u, "spn"}}, + {"SPN Name", {20u, "spn_name"}}, + {"SPN Position in PG", {18u, "spn_pos"}}, + {"SPN Length", {22u, "spn_length"}}, + {"Offset", {24u, "offset"}}, + {"Data Range", {25u, "data_range"}}, + {"Resolution", {23u, "resolution"}}, + {"Units", {27u, "units"}}, + {"SLOT Identifier", {28u, "slot_id"}}, + {"SLOT Name", {29u, "slot_name"}}, + {"SPN Type", {30u, "spn_type"}}, + }; + + std::map pgn_headers, spn_headers; + xlnt::workbook wb; + + if (xlnt::path(file).exists()) { + wb = xlnt::workbook(xlnt::path(file)); + + if (wb.sheet_count() == 0u) { + throw std::runtime_error(fmt::format("Workbook {} is empty", file)); + } + + wb.active_sheet(0); + } else { + + throw std::runtime_error(fmt::format("File {} does not exists", file)); + } + + xlnt::worksheet ws = wb.active_sheet(); + for (const auto &col : ws.columns()) { + if (auto [cell, value] = std::pair{col[0], col[0].to_string()}; !value.empty()) { + if (pgn_mapping_table.contains(value)) { + + auto it = pgn_headers.insert_or_assign(cell.column_index(), value); + } else if (spn_mapping_table.contains(value)) { + + auto it = spn_headers.insert_or_assign(cell.column_index(), value); + } + } + } + + for (const auto &row : ws.rows()) { + std::map pgn_row_map, spn_row_map; + for (const auto &cell : row) { + if (cell.row() != 1u) { + + if (pgn_headers.contains(cell.column_index())) { + if (!cell.to_string().empty()) { + pgn_row_map.insert_or_assign( + [&]() { + std::string ret; + + for (const auto &[k, v] : pgn_mapping_table) { + if (k == pgn_headers[cell.column_index()] && cell.column_index() == std::get<0u>(v)) { + + ret = std::get<1u>(v); + } + } + + return ret; + }(), + + cell.to_string()); + } + } else if (spn_headers.contains(cell.column_index())) { + if (!cell.to_string().empty()) { + spn_row_map.insert_or_assign( + [&]() { + std::string ret; + + for (const auto &[k, v] : spn_mapping_table) { + if (k == spn_headers[cell.column_index()] && cell.column_index() == std::get<0u>(v)) { + + ret = std::get<1u>(v); + } + } + + return ret; + }(), + + cell.to_string()); + } + } + } + } + + // Add rows to database + if (!pgn_row_map.empty() && !spn_row_map.empty()) { + for (const auto &[k, v] : pgn_row_map) { + try { + auto ps = db << fmt::format( + "INSERT OR REPLACE INTO pgns ({}) VALUES ({})", + [&]() { + std::string ret; + + for (const auto &[k, v] : pgn_mapping_table) { + auto end = pgn_mapping_table.end(); + bool is_last = k == (--end)->first; + ret += fmt::format("{}{}", std::get<1u>(v), !is_last ? ", " : ""); + } + + return ret; + }(), + + [&]() { + std::string ret; + for (const auto &[k, _] : pgn_mapping_table) { + auto end = pgn_mapping_table.end(); + bool is_last = k == (--end)->first; + ret += !is_last ? "?, " : "?"; + } + + return ret; + }()); + + for (const auto &[k, v] : pgn_mapping_table) { + ps << pgn_row_map[std::get<1u>(v).data()]; + } + + ps.execute(); + } catch (const sqlite::sqlite_exception &e) { + if (e.get_extended_code() == SQLITE_CONSTRAINT_UNIQUE) { + continue; + } + + throw; + } + } + + struct { + bool parts_inserted_flag = false; + double min = 0.0, max = 0.0; + size_t size_bits = 0u; + } spn_settings_calculated; + + // Pre-compute size_bits before the main loop (resolution calculation depends on it) + { + auto it = spn_mapping_table.find("SPN Length"); + if (it != spn_mapping_table.end()) { + auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(it->second).data()]); + spn_settings_calculated.size_bits = size.has_value() ? size.value().size_bits : 0u; + } + } + + for (const auto &[k, v] : spn_row_map) { + try { + auto ps = db << fmt::format( + "INSERT OR REPLACE INTO spns ({}) VALUES ({})", + [&]() { + std::string ret; + + ret += "pgn, "; + for (const auto &[k, v] : spn_mapping_table) { + auto end = spn_mapping_table.end(); + bool is_last = k == (--end)->first; + + // Split 'data range' to two columns 'min' and 'max' + if (std::get<1u>(v) == "data_range") { + + ret += is_last ? ", min_value, max_value" : "min_value, max_value, "; + } else { + + ret += fmt::format("{}{}", std::get<1u>(v), !is_last ? ", " : ""); + } + } + + return ret; + }(), + + [&]() { + std::string ret; + + ret += "?, "; + for (const auto &[k, v] : spn_mapping_table) { + auto end = spn_mapping_table.end(); + bool is_last = k == (--end)->first; + + // Split 'data range' to two columns 'min' and 'max' + if (std::get<1u>(v) == "data_range") { + + ret += is_last ? ", ?, ?" : "?, ?, "; + } else { + + ret += !is_last ? "?, " : "?"; + } + } + + return ret; + }()); + + ps << pgn_row_map["pgn"]; + for (const auto &[k, v] : spn_mapping_table) { + if (std::get<1u>(v) == "data_range") { // Parse 'data range' string and split it to two columns 'min' and 'max' + + auto range = parsers::parseSpnDataRange(spn_row_map[std::get<1u>(v).data()]); + if (range.has_value()) { + (spn_settings_calculated.min = range.value().min, spn_settings_calculated.max = range.value().max); + + for (const auto &val : {range.value().min, range.value().max}) { + ps << val; + } + } else { + ps << nullptr << nullptr; + } + } else if (std::get<1u>(v) == "offset") { + + auto offset = parsers::parseSpnOffset(spn_row_map[std::get<1u>(v).data()]); + if (offset.has_value()) { + ps << offset.value().offset; + } else { + ps << nullptr; + } + } else if (std::get<1u>(v) == "spn_length") { + + auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(v).data()]); + spn_settings_calculated.size_bits = size.has_value() ? size.value().size_bits : 0u; + ps << (size.has_value() ? size.value().size_bits : 0u); + } else if (std::get<1u>(v) == "resolution") { + double calculated = (spn_settings_calculated.max - spn_settings_calculated.min) / (std::pow(2.0f, spn_settings_calculated.size_bits) - 1u); + + // If is discrete -- use calculated resolution + if (std::fabs(calculated - 1.0) < 1e-9) { + + ps << calculated; + } else { + + auto resolution = parsers::parseSpnResolution(spn_row_map[std::get<1u>(v).data()]); + ps << (resolution.has_value() ? resolution.value().resolution : 1.0f); + } + } else if (std::get<1u>(v) == "spn_pos") { + ps << spn_row_map[std::get<1u>(v).data()]; + } else { + + ps << spn_row_map[std::get<1u>(v).data()]; + } + } + + ps.execute(); + + // Insert SPN fragments after successful spns INSERT + if (!spn_settings_calculated.parts_inserted_flag) { + auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(spn_mapping_table.at("SPN Length")).data()]); + + if (size.has_value()) { + auto spn_fragments = parsers::parseSpnPosition(size.value().size_bits, spn_row_map[std::get<1u>(spn_mapping_table.at("SPN Position in PG")).data()]); + + if (spn_fragments.has_value()) { + auto spn = std::stoll(spn_row_map["spn"]); + for (const auto &part : spn_fragments.value().spn_fragments) { + db << R"(INSERT OR REPLACE INTO spn_fragments (spn, pgn, byte_offset, bit_offset, size) VALUES (?, ?, ?, ?, ?);)" << spn << std::stoll(pgn_row_map["pgn"]) + << part.byte_offset << part.bit_offset << part.size; + } + + spn_settings_calculated.parts_inserted_flag = true; + } + } + } + } catch (const sqlite::sqlite_exception &e) { + if (e.get_extended_code() == SQLITE_CONSTRAINT_UNIQUE) { + continue; + } + + throw; + } + } + } + } + + return db_ptr; +} diff --git a/thirdparty/ftxui-empty-container.patch b/thirdparty/ftxui-empty-container.patch new file mode 100644 index 0000000..7216569 --- /dev/null +++ b/thirdparty/ftxui-empty-container.patch @@ -0,0 +1,27 @@ +--- a/src/ftxui/component/container.cpp ++++ b/src/ftxui/component/container.cpp +@@ -105,7 +105,7 @@ + } + if (elements.empty()) { +- return text("Empty container") | reflect(box_); ++ return text("") | reflect(box_); + } + return vbox(std::move(elements)) | reflect(box_); + } +@@ -189,7 +189,7 @@ + } + if (elements.empty()) { +- return text("Empty container"); ++ return text(""); + } + return hbox(std::move(elements)); + } +@@ -223,7 +223,7 @@ + if (active_child) { + return active_child->Render(); + } +- return text("Empty container"); ++ return text(""); + } + + bool Focusable() const override { diff --git a/thirdparty/ftxui-window.patch b/thirdparty/ftxui-window.patch new file mode 100644 index 0000000..7d93304 --- /dev/null +++ b/thirdparty/ftxui-window.patch @@ -0,0 +1,77 @@ +diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp +index d387fad..dd9173c 100644 +--- a/include/ftxui/component/component_options.hpp ++++ b/include/ftxui/component/component_options.hpp +@@ -261,6 +261,7 @@ struct WindowOptions { + Ref resize_right = true; ///< Can the right side be resized? + Ref resize_top = true; ///< Can the top side be resized? + Ref resize_down = true; ///< Can the down side be resized? ++ Ref draggable = false; ///< Can do window dragging? + + /// An optional function to customize how the window looks like: + std::function render; +diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp +index 63d5bf0..c8e5445 100644 +--- a/include/ftxui/dom/elements.hpp ++++ b/include/ftxui/dom/elements.hpp +@@ -87,7 +87,7 @@ Decorator borderStyled(BorderStyle); + Decorator borderStyled(BorderStyle, Color); + Decorator borderStyled(Color); + Decorator borderWith(const Pixel&); +-Element window(Element title, Element content, BorderStyle border = ROUNDED); ++Element window(Element title, Element content, BorderStyle border = ROUNDED, std::optional color = std::nullopt); + Element spinner(int charset_index, size_t image_index); + Element paragraph(const std::string& text); + Element paragraphAlignLeft(const std::string& text); +diff --git a/src/ftxui/component/window.cpp b/src/ftxui/component/window.cpp +index 7690781..7095e91 100644 +--- a/src/ftxui/component/window.cpp ++++ b/src/ftxui/component/window.cpp +@@ -256,7 +256,7 @@ class WindowImpl : public ComponentBase, public WindowOptions { + drag_start_y = event.mouse().y - top() - box_.y_min; + + // Drag only if we are not resizeing a border yet: +- drag_ = !resize_right_ && !resize_down_ && !resize_top_ && !resize_left_; ++ drag_ = (!resize_right_ && !resize_down_ && !resize_top_ && !resize_left_) && *draggable; + return true; + } + +diff --git a/src/ftxui/dom/border.cpp b/src/ftxui/dom/border.cpp +index eb793b2..595b6e4 100644 +--- a/src/ftxui/dom/border.cpp ++++ b/src/ftxui/dom/border.cpp +@@ -109,11 +109,6 @@ class Border : public Node { + p4.automerge = true; + } + +- // Draw title. +- if (children_.size() == 2) { +- children_[1]->Render(screen); +- } +- + // Draw the border color. + if (foreground_color_) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { +@@ -125,6 +120,11 @@ class Border : public Node { + screen.PixelAt(box_.x_max, y).foreground_color = *foreground_color_; + } + } ++ ++ // Draw title. ++ if (children_.size() == 2) { ++ children_[1]->Render(screen); ++ } + } + }; + +@@ -504,8 +504,8 @@ Element borderEmpty(Element child) { + /// │content│ + /// └───────┘ + /// ``` +-Element window(Element title, Element content, BorderStyle border) { ++ Element window(Element title, Element content, BorderStyle border, std::optional color) { + return std::make_shared(unpack(std::move(content), std::move(title)), +- border); ++ border, color); + } + } // namespace ftxui diff --git a/thirdparty/j1939da_2018.xlsx b/thirdparty/j1939da_2018.xlsx new file mode 100644 index 0000000..0ae40f8 Binary files /dev/null and b/thirdparty/j1939da_2018.xlsx differ diff --git a/thirdparty/j1939da_2018_hitachi.xlsx b/thirdparty/j1939da_2018_hitachi.xlsx new file mode 100644 index 0000000..1438642 Binary files /dev/null and b/thirdparty/j1939da_2018_hitachi.xlsx differ