From 164c4b5fde2f9dc6c74aa83464fed6cb0aac1cce Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sat, 26 Jul 2025 14:20:23 -0400 Subject: [PATCH 01/50] cli scafolding --- CMakeLists.txt | 2 +- cli/CMakeLists.txt | 10 ++++++++++ cli/main.cpp | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 cli/CMakeLists.txt create mode 100644 cli/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d5accf..3146929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,7 +56,7 @@ add_definitions(-D"BLOT_GIT_SHA1=${GIT_SHA1}") INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include) add_subdirectory(lib) -#add_subdirectory(cli) +add_subdirectory(cli) add_subdirectory(examples/c) add_subdirectory(examples/cpp) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt new file mode 100644 index 0000000..626bf06 --- /dev/null +++ b/cli/CMakeLists.txt @@ -0,0 +1,10 @@ +enable_testing() + +ADD_EXECUTABLE(blot + main.cpp +) + +TARGET_LINK_LIBRARIES(blot + blot_a + -lm +) diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 0000000..704061c --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,4 @@ + +int main(int argc, char *argv[]) +{ +} From 90248fc8f4f9bc2e3c0f5985e2ec89e95ccb7aa7 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sat, 26 Jul 2025 14:24:48 -0400 Subject: [PATCH 02/50] cli will use spdlog and clipp --- cli/CMakeLists.txt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 626bf06..ef41b55 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -1,10 +1,27 @@ -enable_testing() +INCLUDE(FetchContent) +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog.git + GIT_TAG v1.14.1 +) +FetchContent_Declare( + clipp + GIT_REPOSITORY https://github.com/muellan/clipp.git + GIT_TAG v1.2.3 +) +FetchContent_MakeAvailable(spdlog clipp) + ADD_EXECUTABLE(blot main.cpp ) +TARGET_INCLUDE_DIRECTORIES(blot PRIVATE + ${clipp_SOURCE_DIR}/include +) + TARGET_LINK_LIBRARIES(blot blot_a -lm + spdlog::spdlog ) From 673e526279cb74cb5e77e47d03f5acb4a7af1ce9 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sat, 26 Jul 2025 14:29:26 -0400 Subject: [PATCH 03/50] move glib dependency to library --- CMakeLists.txt | 9 ------- lib/CMakeLists.txt | 58 +++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3146929..afdff55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,18 +9,9 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS YES) SET(CMAKE_INCLUDE_CURRENT_DIR ON) SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) -#FIND_PACKAGE(libev REQUIRED) FIND_PACKAGE(PkgConfig) PKG_CHECK_MODULES(GLIB2 REQUIRED glib-2.0>=2.44) -#PKG_CHECK_MODULES(GIO REQUIRED gio-2.0>=2.44) -#PKG_CHECK_MODULES(GIOUNIX REQUIRED gio-unix-2.0>=2.44) - -LINK_DIRECTORIES( - ${GLIB2_LIBRARY_DIRS} - ${GIO_LIBRARY_DIRS} - ${GIOUNIX_LIBRARY_DIRS} -) if (NOT DEFINED CMAKE_C_STANDARD) SET(CMAKE_C_STANDARD 99) # use C99 if one is not set by user diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 7e712be..62499b1 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,13 +1,13 @@ SET(LIBBLOT_SRCS - blot_axis.c - blot_braille.c - blot_canvas.c - blot_color.c - blot_figure.c - blot_layer.c - blot_screen.c - blot_utils.c - blot_terminal.c + blot_axis.c + blot_braille.c + blot_canvas.c + blot_color.c + blot_figure.c + blot_layer.c + blot_screen.c + blot_utils.c + blot_terminal.c ) # build a libblot.so and a libblot.a @@ -19,33 +19,29 @@ ADD_LIBRARY(blot_so SHARED ${LIBBLOT_SRCS}) foreach(blot blot_a blot_so) - SET_TARGET_PROPERTIES(${blot} PROPERTIES OUTPUT_NAME blot) + SET_TARGET_PROPERTIES(${blot} PROPERTIES OUTPUT_NAME blot) - SET_PROPERTY(TARGET ${blot} PROPERTY POSITION_INDEPENDENT_CODE ON) + SET_PROPERTY(TARGET ${blot} PROPERTY POSITION_INDEPENDENT_CODE ON) - #TARGET_COMPILE_DEFINITIONS(${blot} - # PUBLIC var="val" - #) + TARGET_INCLUDE_DIRECTORIES(${blot} INTERFACE + ${CMAKE_SOURCE_DIR}/blot/include + ) - TARGET_INCLUDE_DIRECTORIES(${blot} INTERFACE - ${CMAKE_SOURCE_DIR}/blot/include - ) + TARGET_INCLUDE_DIRECTORIES(${blot} PUBLIC + ${GLIB2_INCLUDE_DIRS} + ) - TARGET_INCLUDE_DIRECTORIES(${blot} PUBLIC - ${GLIB2_INCLUDE_DIRS} - ) + TARGET_INCLUDE_DIRECTORIES(${blot} PRIVATE + ${CMAKE_SOURCE_DIR}/blot/include + ${CMAKE_CURRENT_SOURCE_DIR}/include + ) - TARGET_INCLUDE_DIRECTORIES(${blot} PRIVATE - ${CMAKE_SOURCE_DIR}/blot/include - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${GIO_INCLUDE_DIRS} - ${GIOUNIX_INCLUDE_DIRS} - ) + TARGET_LINK_DIRECTORIES(${blot} PUBLIC + ${GLIB2_LIBRARY_DIRS} + ) - TARGET_LINK_LIBRARIES(${blot} - ${GLIB2_LIBRARIES} - ${GIO_LIBRARIES} - ${GIOUNIX_LIBRARIES} - ) + TARGET_LINK_LIBRARIES(${blot} + ${GLIB2_LIBRARIES} + ) endforeach(blot) From f939066b29f4d40f9f3fce167a3efb5c5fb894b3 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sat, 26 Jul 2025 14:34:49 -0400 Subject: [PATCH 04/50] cleanup build options --- CMakeLists.txt | 18 +++++++----------- lib/CMakeLists.txt | 5 +++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index afdff55..b9b00ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,29 +2,25 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.10) PROJECT(blot VERSION 0.0.7 LANGUAGES C CXX) set(CMAKE_EXPORT_COMPILE_COMMANDS YES) -#set(CMAKE_BUILD_TYPE Release) -#set(CMAKE_BUILD_TYPE Debug) -#set(CMAKE_BUILD_TYPE RelWithDebInfo) SET(CMAKE_INCLUDE_CURRENT_DIR ON) SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) -FIND_PACKAGE(PkgConfig) - -PKG_CHECK_MODULES(GLIB2 REQUIRED glib-2.0>=2.44) if (NOT DEFINED CMAKE_C_STANDARD) - SET(CMAKE_C_STANDARD 99) # use C99 if one is not set by user + SET(CMAKE_C_STANDARD 99) endif() if (NOT DEFINED CMAKE_CXX_STANDARD) - SET(CMAKE_CXX_STANDARD 20) # used only in test harness + SET(CMAKE_CXX_STANDARD 20) endif() add_definitions(-D_GNU_SOURCE) -if(CMAKE_COMPILER_IS_GNUCC) - add_definitions(-Wall -Werror -Wno-variadic-macros -Wno-dangling-pointer) -endif(CMAKE_COMPILER_IS_GNUCC) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wall -Werror -Wno-variadic-macros -Wno-dangling-pointer) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options(-Wall -Werror -Wno-variadic-macros) +endif() if(CMAKE_BUILD_TYPE MATCHES Debug) message(STATUS "ASAN enabled for debug build") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 62499b1..2378e5f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -10,6 +10,11 @@ SET(LIBBLOT_SRCS blot_terminal.c ) +# we need glib + +FIND_PACKAGE(PkgConfig) +PKG_CHECK_MODULES(GLIB2 REQUIRED glib-2.0>=2.44) + # build a libblot.so and a libblot.a ADD_LIBRARY(blot_a STATIC ${LIBBLOT_SRCS}) From 79e06a6697092f6b9d55fab20c9cad9e0d58b355 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 10:04:26 -0400 Subject: [PATCH 05/50] working out the cli --- CMakeLists.txt | 6 +-- cli/CMakeLists.txt | 3 +- cli/main.cpp | 119 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b9b00ea..43d9a39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,9 +36,9 @@ include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC GIT_SHA1) git_describe(GIT_REVISION) -add_definitions(-D"BLOT_GIT_REFSPEC=${GIT_REFSPEC}") -add_definitions(-D"BLOT_GIT_REVISION=${GIT_REVISION}") -add_definitions(-D"BLOT_GIT_SHA1=${GIT_SHA1}") +add_definitions(-DBLOT_GIT_REFSPEC="${GIT_REFSPEC}") +add_definitions(-DBLOT_GIT_REVISION="${GIT_REVISION}") +add_definitions(-DBLOT_GIT_SHA1="${GIT_SHA1}") INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index ef41b55..bd30647 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -11,7 +11,6 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(spdlog clipp) - ADD_EXECUTABLE(blot main.cpp ) @@ -20,7 +19,7 @@ TARGET_INCLUDE_DIRECTORIES(blot PRIVATE ${clipp_SOURCE_DIR}/include ) -TARGET_LINK_LIBRARIES(blot +TARGET_LINK_LIBRARIES(blot PRIVATE blot_a -lm spdlog::spdlog diff --git a/cli/main.cpp b/cli/main.cpp index 704061c..8668ebc 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1,4 +1,123 @@ +#include +#include + +#include "blot.hpp" +#include "spdlog/spdlog.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#include "clipp.h" +#pragma GCC diagnostic pop int main(int argc, char *argv[]) { + const char *self = basename(argv[0]); + + bool show_help{}, be_verbose{}, show_version{}; + enum output_type { ASCII, UNICODE, BRAILLE } output_type; + blot_plot_type plot_type = BLOT_LINE; + + auto head_cli = ( + clipp::option("-h", "--help").set(show_help).doc("This help"), + clipp::option("-v", "--verbose").set(be_verbose).doc("Enable debug"), + clipp::option("-V", "--version").set(show_version).doc("Version") + ); + + auto output_cli = ( + clipp::option("-A", "--ascii").set(output_type,ASCII).doc("ASCII output") | + clipp::option("-U", "--unicode").set(output_type,UNICODE).doc("Unicode output") | + clipp::option("-B", "--braille").set(output_type,BRAILLE).doc("Braille output") + ); + + blot_color plot_color = 9; + auto modifier_cli = ( + clipp::option("-s", "--scatter").set(plot_type, BLOT_SCATTER).doc("Scatter plot").repeatable(true), + clipp::option("-l", "--line").set(plot_type, BLOT_LINE).doc("Line plot").repeatable(true), + clipp::option("-b", "--bar").set(plot_type, BLOT_BAR).doc("Bar plot").repeatable(true), + + clipp::repeatable( + clipp::option("-c", "--color").doc("Color") + & clipp::value("color").call([&](std::string c) { + plot_color = std::strtol(c.c_str(), NULL, 10); + }) + ) + ); + + struct Input { + enum {FILE,EXEC} type; + std::string details; + blot_plot_type plot_type; + blot_color plot_color; + }; + std::vector inputs; + + auto input_cli = ( + clipp::repeatable( + clipp::option("-f", "--file") + & clipp::value("file").call([&](std::string f) { inputs.push_back({Input::FILE, f, plot_type, plot_color}); }) + ).doc("read from a file"), + clipp::repeatable( + clipp::option("-x", "--exec") + & clipp::value("command").call([&](std::string x) { inputs.push_back({Input::EXEC, x, plot_type, plot_color}); }) + ).doc("execute command") + ); + + std::vector wrong; + auto cli = ( + head_cli , + "Output" % output_cli, + "Modifier" % modifier_cli, + "Input" % input_cli, + clipp::any_other(wrong) + ); + + if(!parse(argc, argv, cli) || wrong.size()) { + spdlog::error("Failed to parse options."); + for (const auto &w : wrong) { + spdlog::error(" {}", w); + } + spdlog::error("Usage: {}", clipp::usage_lines(cli, self).str()); + return 1; + } + + if (show_version) { + std::cout << "refspec: " << BLOT_GIT_REFSPEC << '\n' + << "revision: " << BLOT_GIT_REVISION << '\n' + << "sha1: " << BLOT_GIT_SHA1 << std::endl; + return 0; + } + + if (show_help) { + Blot::Dimensions term; + unsigned doc_start = std::min(30u, term.cols/2); + auto fmt = clipp::doc_formatting{} + .indent_size(4) + .first_column(4) + .doc_column(doc_start) + .last_column(term.cols); + std::cout << clipp::make_man_page(cli, self, fmt) + .append_section("EXAMPE", + "\n" + " blot --braille \\\n" + " line --color 10 --file x_y1_values \\\n" + " scatter --color 11 --file x_y2_values\n" + "\n" + " blot --braille \\\n" + " line --color 10 --exec 'seq 1 100' \\\n" + " scatter --color 11 --file x_y_values\n" + ) + << std::endl; + return 0; + } + + std::cout << std::format("output_type = {}", (int)output_type) << std::endl; + + for (auto &input : inputs) { + std::cout << std::format("-> {} {} {} {}", + (int)input.type, + input.details, + (int)input.plot_type, + (int)input.plot_color) + << std::endl; + } } From 1f5eff99f6c6603eaa3f77a85f16be95be86c01f Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 10:48:00 -0400 Subject: [PATCH 06/50] refactor cli interface --- cli/main.cpp | 92 ++++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index 8668ebc..796de33 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -9,74 +9,80 @@ #include "clipp.h" #pragma GCC diagnostic pop +struct Input { + enum Type {NONE,FILE,EXEC} m_type; + std::string m_details; + blot_plot_type m_plot_type; + blot_color m_plot_color; +}; + int main(int argc, char *argv[]) { const char *self = basename(argv[0]); bool show_help{}, be_verbose{}, show_version{}; - enum output_type { ASCII, UNICODE, BRAILLE } output_type; - blot_plot_type plot_type = BLOT_LINE; - auto head_cli = ( clipp::option("-h", "--help").set(show_help).doc("This help"), clipp::option("-v", "--verbose").set(be_verbose).doc("Enable debug"), clipp::option("-V", "--version").set(show_version).doc("Version") ); + enum output_type { ASCII, UNICODE, BRAILLE } output_type; + auto output_cli = ( clipp::option("-A", "--ascii").set(output_type,ASCII).doc("ASCII output") | clipp::option("-U", "--unicode").set(output_type,UNICODE).doc("Unicode output") | clipp::option("-B", "--braille").set(output_type,BRAILLE).doc("Braille output") ); - blot_color plot_color = 9; - auto modifier_cli = ( - clipp::option("-s", "--scatter").set(plot_type, BLOT_SCATTER).doc("Scatter plot").repeatable(true), - clipp::option("-l", "--line").set(plot_type, BLOT_LINE).doc("Line plot").repeatable(true), - clipp::option("-b", "--bar").set(plot_type, BLOT_BAR).doc("Bar plot").repeatable(true), + std::vector wrong; + auto wrong_cli = clipp::any_other(wrong); - clipp::repeatable( - clipp::option("-c", "--color").doc("Color") - & clipp::value("color").call([&](std::string c) { - plot_color = std::strtol(c.c_str(), NULL, 10); - }) - ) - ); + std::vector inputs; - struct Input { - enum {FILE,EXEC} type; - std::string details; - blot_plot_type plot_type; - blot_color plot_color; + auto start_input = [&](blot_plot_type new_plot_type) { + inputs.push_back({}); + }; + auto set_source = [&](Input::Type type, const std::string &details) { + inputs.back().m_type = type; + inputs.back().m_details = details; + }; + auto set_color = [&](const std::string &c) { + inputs.back().m_plot_color = std::strtol(c.c_str(), NULL, 10); }; - std::vector inputs; - auto input_cli = ( - clipp::repeatable( - clipp::option("-f", "--file") - & clipp::value("file").call([&](std::string f) { inputs.push_back({Input::FILE, f, plot_type, plot_color}); }) - ).doc("read from a file"), + auto cli = ( + head_cli | clipp::repeatable( - clipp::option("-x", "--exec") - & clipp::value("command").call([&](std::string x) { inputs.push_back({Input::EXEC, x, plot_type, plot_color}); }) - ).doc("execute command") - ); + /* select a plot type */ - std::vector wrong; - auto cli = ( - head_cli , - "Output" % output_cli, - "Modifier" % modifier_cli, - "Input" % input_cli, - clipp::any_other(wrong) + clipp::command("scatter").call([&]() { start_input(BLOT_SCATTER); }).doc("Add a scatter plot") | + clipp::command("line") .call([&]() { start_input(BLOT_LINE); }).doc("Add a line/curve plot") | + clipp::command("bar") .call([&]() { start_input(BLOT_BAR); }).doc("Add a bar plot"), + + /* augment this plots chracteristics */ + + clipp::option("-c", "--color").doc("Set plot color") + & clipp::value("color").call([&](const char *c) { set_color(c); }), + + /* source data from file or command */ + + clipp::option("-f", "--file").doc("Read from a file") + & clipp::value("file") .call([&](const char *f) { set_source(Input::FILE, f); }) + | + clipp::option("-x", "--exec").doc("Run command") + & clipp::value("command").call([&](const char *x) { set_source(Input::EXEC, x); }) + + ), + wrong_cli ); if(!parse(argc, argv, cli) || wrong.size()) { spdlog::error("Failed to parse options."); for (const auto &w : wrong) { - spdlog::error(" {}", w); + spdlog::error("Unexpected: {}", w); } - spdlog::error("Usage: {}", clipp::usage_lines(cli, self).str()); + spdlog::error("Usage:\n{}", clipp::usage_lines(cli, self).str()); return 1; } @@ -114,10 +120,10 @@ int main(int argc, char *argv[]) for (auto &input : inputs) { std::cout << std::format("-> {} {} {} {}", - (int)input.type, - input.details, - (int)input.plot_type, - (int)input.plot_color) + (int)input.m_type, + input.m_details, + (int)input.m_plot_type, + (int)input.m_plot_color) << std::endl; } } From e8c742304abd8dfb3a1c4824a8abe0cb046685e8 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 11:37:19 -0400 Subject: [PATCH 07/50] split command line parsing into config --- cli/CMakeLists.txt | 1 + cli/config.cpp | 132 +++++++++++++++++++++++++++++++++++++++++++ cli/config.hpp | 77 +++++++++++++++++++++++++ cli/main.cpp | 121 +++------------------------------------ include/blot.h | 1 + include/blot.hpp | 11 +--- include/blot_names.h | 16 ++++++ 7 files changed, 235 insertions(+), 124 deletions(-) create mode 100644 cli/config.cpp create mode 100644 cli/config.hpp create mode 100644 include/blot_names.h diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index bd30647..471153f 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -13,6 +13,7 @@ FetchContent_MakeAvailable(spdlog clipp) ADD_EXECUTABLE(blot main.cpp + config.cpp ) TARGET_INCLUDE_DIRECTORIES(blot PRIVATE diff --git a/cli/config.cpp b/cli/config.cpp new file mode 100644 index 0000000..5f26c74 --- /dev/null +++ b/cli/config.cpp @@ -0,0 +1,132 @@ +#include "config.hpp" + +#include +#include + +#include "blot.hpp" +#include "spdlog/spdlog.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#include "clipp.h" +#pragma GCC diagnostic pop + + +Config::Config(int argc, char *argv[]) +: m_self(basename(argv[0])) +{ + + bool show_help{}, be_verbose{}, show_version{}; + auto head_cli = ( + clipp::option("-h", "--help").set(show_help).doc("This help"), + clipp::option("-v", "--verbose").set(be_verbose).doc("Enable debug"), + clipp::option("-V", "--version").set(show_version).doc("Version") + ); + + + auto output_cli = ( + clipp::option("-A", "--ascii").set(m_output_type,ASCII).doc("ASCII output") | + clipp::option("-U", "--unicode").set(m_output_type,UNICODE).doc("Unicode output") | + clipp::option("-B", "--braille").set(m_output_type,BRAILLE).doc("Braille output") + ); + + std::vector wrong; + auto wrong_cli = clipp::any_other(wrong); + + auto start_input = [&](blot_plot_type plot_type) { + m_inputs.push_back(Input{plot_type}); + }; + + auto cli = ( + head_cli | + clipp::repeatable( + /* select a plot type */ + + clipp::command("scatter") + .call([&]() { start_input(BLOT_SCATTER); }) + .doc("Add a scatter plot") | + clipp::command("line") + .call([&]() { start_input(BLOT_LINE); }) + .doc("Add a line/curve plot") | + clipp::command("bar") + .call([&]() { start_input(BLOT_BAR); }) + .doc("Add a bar plot"), + + /* augment this plots characteristics */ + + clipp::option("-c", "--color").doc("Set plot color") + & clipp::value("color") + .call([&](const char *c) { m_inputs.back().set_color(c); }), + + /* source data from file or command */ + + clipp::option("-r", "--read").doc("Read file to the end") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }) + | + clipp::option("-f", "--follow").doc("Read file waiting for more") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }) + | + clipp::option("-x", "--exec").doc("Run command, one value per line") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) + | + clipp::option("-w", "--watch").doc("Run command, one value per call") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) + + ), + wrong_cli + ); + + if(!parse(argc, argv, cli) || wrong.size()) { + spdlog::error("Failed to parse options."); + for (const auto &w : wrong) { + spdlog::error("Unexpected: {}", w); + } + spdlog::error("Usage:\n{}", clipp::usage_lines(cli, m_self).str()); + std::exit(1); + } + + if (show_version) { + std::cout << "refspec: " << BLOT_GIT_REFSPEC << '\n' + << "revision: " << BLOT_GIT_REVISION << '\n' + << "sha1: " << BLOT_GIT_SHA1 << std::endl; + std::exit(0); + } + + if (show_help) { + Blot::Dimensions term; + unsigned doc_start = std::min(30u, term.cols/2); + auto fmt = clipp::doc_formatting{} + .indent_size(4) + .first_column(4) + .doc_column(doc_start) + .last_column(term.cols); + std::cout << clipp::make_man_page(cli, m_self, fmt) + .append_section("EXAMPE", + "\n" + " blot --braille \\\n" + " line --color 10 --file x_y1_values \\\n" + " scatter --color 11 --file x_y2_values\n" + "\n" + " blot --braille \\\n" + " line --color 10 --exec 'seq 1 100' \\\n" + " scatter --color 11 --file x_y_values\n" + ) + << std::endl; + std::exit(0); + } + + if (m_inputs.empty()) { + spdlog::error("no plots defined"); + std::exit(1); + } + for (auto &input : m_inputs) { + if (!input) { + spdlog::error("incomplete plot definition"); + std::exit(1); + } + } +} diff --git a/cli/config.hpp b/cli/config.hpp new file mode 100644 index 0000000..c26de95 --- /dev/null +++ b/cli/config.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include "blot.hpp" +#include "spdlog/spdlog.h" + +struct Input final { + enum Source {NONE,READ,FOLLOW,EXEC,WATCH}; + + blot_plot_type m_plot_type; + Source m_source; + std::string m_details; + blot_color m_plot_color{9}; + + explicit Input(blot_plot_type plot_type) : m_plot_type(plot_type) {} + ~Input() {} + + std::string plot_name() const { + return blot_plot_type_to_string(m_plot_type); + } + + std::string source_name() const { + switch (m_source) { + case READ: return "read"; + case FOLLOW: return "follow"; + case EXEC: return "exec"; + case WATCH: return "watch"; + default: return {}; + } + } + + operator bool() const { + switch (m_plot_type) { + case BLOT_SCATTER: + case BLOT_LINE: + case BLOT_BAR: + break; + default: + spdlog::warn("invalid plot type: {}", (int)m_plot_type); + return false; + } + if (m_source == NONE) { + spdlog::warn("{} plot does not defined an input (file or command)", + blot_plot_type_to_string(m_plot_type)); + return false; + } + if (m_details.empty()) { + spdlog::warn("{} plot has empty {} argument", + blot_plot_type_to_string(m_plot_type), source_name()); + return false; + } + + return true; + } + + void set_source (Input::Source source, const std::string &details) { + m_source = source; + m_details = details; + }; + void set_color (const std::string &c) { + m_plot_color = std::strtol(c.c_str(), NULL, 10); + }; +}; + +struct Config { + const char *m_self{}; + std::vector m_inputs; + enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; + + Config(int argc, char *argv[]); + ~Config() {} +}; + + diff --git a/cli/main.cpp b/cli/main.cpp index 796de33..a0fcf54 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1,129 +1,22 @@ #include #include +#include "config.hpp" #include "blot.hpp" #include "spdlog/spdlog.h" -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-but-set-variable" -#include "clipp.h" -#pragma GCC diagnostic pop - -struct Input { - enum Type {NONE,FILE,EXEC} m_type; - std::string m_details; - blot_plot_type m_plot_type; - blot_color m_plot_color; -}; - int main(int argc, char *argv[]) { - const char *self = basename(argv[0]); - - bool show_help{}, be_verbose{}, show_version{}; - auto head_cli = ( - clipp::option("-h", "--help").set(show_help).doc("This help"), - clipp::option("-v", "--verbose").set(be_verbose).doc("Enable debug"), - clipp::option("-V", "--version").set(show_version).doc("Version") - ); - - enum output_type { ASCII, UNICODE, BRAILLE } output_type; - - auto output_cli = ( - clipp::option("-A", "--ascii").set(output_type,ASCII).doc("ASCII output") | - clipp::option("-U", "--unicode").set(output_type,UNICODE).doc("Unicode output") | - clipp::option("-B", "--braille").set(output_type,BRAILLE).doc("Braille output") - ); - - std::vector wrong; - auto wrong_cli = clipp::any_other(wrong); - - std::vector inputs; - - auto start_input = [&](blot_plot_type new_plot_type) { - inputs.push_back({}); - }; - auto set_source = [&](Input::Type type, const std::string &details) { - inputs.back().m_type = type; - inputs.back().m_details = details; - }; - auto set_color = [&](const std::string &c) { - inputs.back().m_plot_color = std::strtol(c.c_str(), NULL, 10); - }; - - auto cli = ( - head_cli | - clipp::repeatable( - /* select a plot type */ - - clipp::command("scatter").call([&]() { start_input(BLOT_SCATTER); }).doc("Add a scatter plot") | - clipp::command("line") .call([&]() { start_input(BLOT_LINE); }).doc("Add a line/curve plot") | - clipp::command("bar") .call([&]() { start_input(BLOT_BAR); }).doc("Add a bar plot"), - - /* augment this plots chracteristics */ - - clipp::option("-c", "--color").doc("Set plot color") - & clipp::value("color").call([&](const char *c) { set_color(c); }), - - /* source data from file or command */ - - clipp::option("-f", "--file").doc("Read from a file") - & clipp::value("file") .call([&](const char *f) { set_source(Input::FILE, f); }) - | - clipp::option("-x", "--exec").doc("Run command") - & clipp::value("command").call([&](const char *x) { set_source(Input::EXEC, x); }) - - ), - wrong_cli - ); - - if(!parse(argc, argv, cli) || wrong.size()) { - spdlog::error("Failed to parse options."); - for (const auto &w : wrong) { - spdlog::error("Unexpected: {}", w); - } - spdlog::error("Usage:\n{}", clipp::usage_lines(cli, self).str()); - return 1; - } - - if (show_version) { - std::cout << "refspec: " << BLOT_GIT_REFSPEC << '\n' - << "revision: " << BLOT_GIT_REVISION << '\n' - << "sha1: " << BLOT_GIT_SHA1 << std::endl; - return 0; - } - - if (show_help) { - Blot::Dimensions term; - unsigned doc_start = std::min(30u, term.cols/2); - auto fmt = clipp::doc_formatting{} - .indent_size(4) - .first_column(4) - .doc_column(doc_start) - .last_column(term.cols); - std::cout << clipp::make_man_page(cli, self, fmt) - .append_section("EXAMPE", - "\n" - " blot --braille \\\n" - " line --color 10 --file x_y1_values \\\n" - " scatter --color 11 --file x_y2_values\n" - "\n" - " blot --braille \\\n" - " line --color 10 --exec 'seq 1 100' \\\n" - " scatter --color 11 --file x_y_values\n" - ) - << std::endl; - return 0; - } + Config config(argc, argv); - std::cout << std::format("output_type = {}", (int)output_type) << std::endl; + std::cout << std::format("output_type = {}", (int)config.m_output_type) << std::endl; - for (auto &input : inputs) { + for (auto &input : config.m_inputs) { std::cout << std::format("-> {} {} {} {}", - (int)input.m_type, + input.plot_name(), + input.source_name(), input.m_details, - (int)input.m_plot_type, - (int)input.m_plot_color) + input.m_plot_color) << std::endl; } } diff --git a/include/blot.h b/include/blot.h index ff29ea7..c5c35e4 100644 --- a/include/blot.h +++ b/include/blot.h @@ -6,6 +6,7 @@ #include "blot_error.h" #include "blot_figure.h" #include "blot_layer.h" +#include "blot_names.h" #include "blot_screen.h" #include "blot_terminal.h" #include "blot_time.h" diff --git a/include/blot.hpp b/include/blot.hpp index 54f8889..8c38ad2 100644 --- a/include/blot.hpp +++ b/include/blot.hpp @@ -1,16 +1,7 @@ /* blot C++ wrapper */ /* vim: set noet sw=8 ts=8 tw=120: */ #pragma once -#include "blot_canvas.h" -#include "blot_color.h" -#include "blot_error.h" -#include "blot_figure.h" -#include "blot_layer.h" -#include "blot_screen.h" -#include "blot_terminal.h" -#include "blot_time.h" -#include "blot_types.h" -#include "blot_utils.h" +#include "blot.h" #include #include diff --git a/include/blot_names.h b/include/blot_names.h new file mode 100644 index 0000000..9346cd1 --- /dev/null +++ b/include/blot_names.h @@ -0,0 +1,16 @@ +/* vim: set noet sw=8 ts=8 tw=120: */ +#pragma once + +#include + +#include "blot_types.h" + +static inline const char * blot_plot_type_to_string(blot_plot_type plot_type) { + switch (plot_type) { + case BLOT_SCATTER: return "scatter"; + case BLOT_LINE: return "line"; + case BLOT_BAR: return "bar"; + default: return NULL; + } +} + From 1d56681ce998c697441d6b2fc29b27dcc7ac7aa5 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 12:14:57 -0400 Subject: [PATCH 08/50] handle verbose, output type, interval --- cli/config.cpp | 33 +++++++++++++++++++++++---------- cli/config.hpp | 12 +++++++++++- cli/main.cpp | 8 ++++---- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 5f26c74..41416ad 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "blot.hpp" #include "spdlog/spdlog.h" @@ -15,30 +17,41 @@ Config::Config(int argc, char *argv[]) : m_self(basename(argv[0])) { - - bool show_help{}, be_verbose{}, show_version{}; - auto head_cli = ( + bool show_help{}, show_version{}; + auto cli_head = ( clipp::option("-h", "--help").set(show_help).doc("This help"), - clipp::option("-v", "--verbose").set(be_verbose).doc("Enable debug"), - clipp::option("-V", "--version").set(show_version).doc("Version") + clipp::option("-V", "--version").set(show_version).doc("Version"), + clipp::option("-v", "--verbose").call([]{ + spdlog::set_level(spdlog::level::debug); + }).doc("Enable debug"), + clipp::option("-i", "--interval").doc("Interval in seconds") + & clipp::value("seconds").call([&](const char *txt){ + auto len = strlen(txt); + auto [_,ec] = std::from_chars(txt, txt+len, m_interval); + if (ec != std::errc{}) { + spdlog::error("failed to parse '{}': {}", + txt, std::make_error_code(ec).message()); + std::exit(1); + } + }) ); - - auto output_cli = ( + auto cli_output = ( clipp::option("-A", "--ascii").set(m_output_type,ASCII).doc("ASCII output") | clipp::option("-U", "--unicode").set(m_output_type,UNICODE).doc("Unicode output") | clipp::option("-B", "--braille").set(m_output_type,BRAILLE).doc("Braille output") ); std::vector wrong; - auto wrong_cli = clipp::any_other(wrong); + auto cli_wrong = clipp::any_other(wrong); auto start_input = [&](blot_plot_type plot_type) { m_inputs.push_back(Input{plot_type}); }; auto cli = ( - head_cli | + cli_head | + cli_output, clipp::repeatable( /* select a plot type */ @@ -77,7 +90,7 @@ Config::Config(int argc, char *argv[]) .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) ), - wrong_cli + cli_wrong ); if(!parse(argc, argv, cli) || wrong.size()) { diff --git a/cli/config.hpp b/cli/config.hpp index c26de95..29859af 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -67,11 +67,21 @@ struct Input final { struct Config { const char *m_self{}; - std::vector m_inputs; enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; + std::vector m_inputs; + double m_interval{0}; Config(int argc, char *argv[]); ~Config() {} + + std::string output_type_name() const { + switch (m_output_type) { + case ASCII: return "ASCII"; + case UNICODE: return "Unicode"; + case BRAILLE: return "Braille"; + default: return {}; + } + } }; diff --git a/cli/main.cpp b/cli/main.cpp index a0fcf54..949b8f0 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -9,14 +9,14 @@ int main(int argc, char *argv[]) { Config config(argc, argv); - std::cout << std::format("output_type = {}", (int)config.m_output_type) << std::endl; + std::cout << std::format("output_type = {}\n", config.output_type_name()); + std::cout << std::format("interval = {}\n", config.m_interval); for (auto &input : config.m_inputs) { - std::cout << std::format("-> {} {} {} {}", + std::cout << std::format("-> {} {} {} {}\n", input.plot_name(), input.source_name(), input.m_details, - input.m_plot_color) - << std::endl; + input.m_plot_color); } } From 31271fb22f6b437ada4f9c4b8fc1c11552cfd6a6 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 13:18:14 -0400 Subject: [PATCH 09/50] simple file reader --- cli/CMakeLists.txt | 1 + cli/main.cpp | 10 +++++ cli/reader.cpp | 99 ++++++++++++++++++++++++++++++++++++++++++++++ cli/reader.hpp | 19 +++++++++ 4 files changed, 129 insertions(+) create mode 100644 cli/reader.cpp create mode 100644 cli/reader.hpp diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 471153f..2dda128 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -14,6 +14,7 @@ FetchContent_MakeAvailable(spdlog clipp) ADD_EXECUTABLE(blot main.cpp config.cpp + reader.cpp ) TARGET_INCLUDE_DIRECTORIES(blot PRIVATE diff --git a/cli/main.cpp b/cli/main.cpp index 949b8f0..8ed1b69 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -2,6 +2,7 @@ #include #include "config.hpp" +#include "reader.hpp" #include "blot.hpp" #include "spdlog/spdlog.h" @@ -13,10 +14,19 @@ int main(int argc, char *argv[]) std::cout << std::format("interval = {}\n", config.m_interval); for (auto &input : config.m_inputs) { + std::cout << "---------------------------------------------\n"; std::cout << std::format("-> {} {} {} {}\n", input.plot_name(), input.source_name(), input.m_details, input.m_plot_color); + + + auto reader = Reader::from(input); + while (*reader) { + auto line = reader->line(); + if (line.has_value()) + std::cout << *line << std::endl; + } } } diff --git a/cli/reader.cpp b/cli/reader.cpp new file mode 100644 index 0000000..7851f6d --- /dev/null +++ b/cli/reader.cpp @@ -0,0 +1,99 @@ +#include "reader.hpp" +#include "config.hpp" + +#include +#include + +#include "spdlog/spdlog.h" + +namespace fs = std::filesystem; + +struct FileReader : public Reader { + + fs::path m_path; + bool m_follow{}; + std::ifstream m_stream; + size_t m_line_number{}; + + FileReader(const std::string &details, bool follow = false) + : m_path(details), m_follow(follow) + { + if (!fs::exists(m_path)) { + spdlog::error("{}: does not exist", m_path.string()); + std::exit(1); + } + if (!fs::is_regular_file(m_path)) { + spdlog::error("{}: is not a file", m_path.string()); + std::exit(1); + } + m_stream = std::ifstream(m_path); + if (!m_stream.is_open()) { + spdlog::error("{}: failed to open", m_path.string()); + std::exit(1); + } + } + ~FileReader() override {} + + bool fail() const override { return m_stream.fail(); } + bool eof() const override { return m_stream.eof(); } + operator bool() const override { + return !fail() && (!eof() || m_follow); + } + + std::optional line() override + { + if (m_stream.fail()) + // reached failed state + return {}; + + // TODO: may need to implement --line-buffered behaviour + std::string line; + if (std::getline(m_stream, line)) { + m_line_number ++; + // successfully read next line + spdlog::debug("{}:{}: {}", m_path.string(), m_line_number, line); + return line; + } + + spdlog::trace("{}:{}: follow={} fail={} eof={}", m_path.string(), m_line_number, + m_follow, m_stream.fail(), m_stream.eof()); + + if (m_stream.eof()) { + // reached the end of file + if (m_follow) + m_stream.clear(); + spdlog::trace("{}:{}: reached end of file", m_path.string(), m_line_number); + return {}; + } + + if (!m_stream.fail()) { + // this will confuse the eof()/fail() logic -- should not happen + spdlog::error("{}: no output, fail={} eof={}", + m_path.string(), m_stream.fail(), m_stream.eof()); + std::terminate(); + } + + // some reading failure + spdlog::warn("{}: failed reading", m_path.string()); + return {}; + } +}; + +std::unique_ptr Reader::from(const Input &input) +{ + switch (input.m_source) { + case Input::READ: + return std::make_unique(input.m_details); + case Input::FOLLOW: + return std::make_unique(input.m_details, true); + case Input::EXEC: + case Input::WATCH: + spdlog::error("input source value {} not yet supported", + (int)input.m_source); + std::terminate(); + + default: + spdlog::error("invalid input source value {}", (int)input.m_source); + std::terminate(); + } +} diff --git a/cli/reader.hpp b/cli/reader.hpp new file mode 100644 index 0000000..b003fee --- /dev/null +++ b/cli/reader.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "config.hpp" + +#include +#include +#include + +struct Reader { + virtual ~Reader() {} + + virtual bool fail() const = 0; + virtual bool eof() const = 0; + virtual operator bool() const = 0; + virtual std::optional line() = 0; + + static std::unique_ptr from(const Input &input); +}; + From 120921edeed2d11d6f82a28c8c813c835d376066 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 13:29:14 -0400 Subject: [PATCH 10/50] --debug --- cli/config.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/config.cpp b/cli/config.cpp index 41416ad..a9a7b61 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -23,7 +23,10 @@ Config::Config(int argc, char *argv[]) clipp::option("-V", "--version").set(show_version).doc("Version"), clipp::option("-v", "--verbose").call([]{ spdlog::set_level(spdlog::level::debug); - }).doc("Enable debug"), + }).doc("Enable verbose output"), + clipp::option("--debug").call([]{ + spdlog::set_level(spdlog::level::trace); + }).doc("Enable debug output"), clipp::option("-i", "--interval").doc("Interval in seconds") & clipp::value("seconds").call([&](const char *txt){ auto len = strlen(txt); From a82b1b75bf4abc8162a5951acc16782d70552448 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 13:29:31 -0400 Subject: [PATCH 11/50] following a pipe or chardev does not make sense --- cli/reader.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cli/reader.cpp b/cli/reader.cpp index 7851f6d..941e8ee 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -22,8 +22,13 @@ struct FileReader : public Reader { spdlog::error("{}: does not exist", m_path.string()); std::exit(1); } - if (!fs::is_regular_file(m_path)) { - spdlog::error("{}: is not a file", m_path.string()); + if (fs::is_character_file(m_path) || fs::is_fifo(m_path)) { + if (follow) { + spdlog::error("{}: fifo/chardev cannot be used with follow", m_path.string()); + std::exit(1); + } + } else if (!fs::is_regular_file(m_path)) { + spdlog::error("{}: is not a file/fifo/chardev", m_path.string()); std::exit(1); } m_stream = std::ifstream(m_path); From 492b324e8c86f7883ae1db848265cd24adf6772e Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 13:35:51 -0400 Subject: [PATCH 12/50] copy-n-paste fix --- cli/config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/config.cpp b/cli/config.cpp index a9a7b61..5e3e0ed 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -90,7 +90,7 @@ Config::Config(int argc, char *argv[]) | clipp::option("-w", "--watch").doc("Run command, one value per call") & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) + .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) ), cli_wrong From ca5b18afb592ab51db3e15d9cd5c21a1fecd61bf Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Sun, 27 Jul 2025 13:44:29 -0400 Subject: [PATCH 13/50] read from command --- cli/reader.cpp | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/cli/reader.cpp b/cli/reader.cpp index 941e8ee..99c791a 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -4,6 +4,12 @@ #include #include +#include // for popen, pclose +#include // for read, fileno +#include // for fcntl, O_NONBLOCK +#include // for errno +#include // for strerror + #include "spdlog/spdlog.h" namespace fs = std::filesystem; @@ -84,6 +90,95 @@ struct FileReader : public Reader { } }; +struct ExecStreamReader : public Reader { + std::string m_command; + FILE* m_pipe = nullptr; + int m_fd = -1; + std::string m_buffer; + bool m_eof = false; + bool m_fail = false; + + ExecStreamReader(const std::string& command) : m_command(command) { + m_pipe = popen(m_command.c_str(), "r"); + if (!m_pipe) { + spdlog::error("failed to execute command '{}': {}", m_command, std::strerror(errno)); + std::exit(1); + } + + m_fd = fileno(m_pipe); + int flags = fcntl(m_fd, F_GETFL, 0); + if (flags == -1 || fcntl(m_fd, F_SETFL, flags | O_NONBLOCK) == -1) { + spdlog::error("failed to set non-blocking mode for command '{}': {}", m_command, std::strerror(errno)); + std::exit(1); + } + } + + ~ExecStreamReader() override { + if (m_pipe) { + pclose(m_pipe); + } + } + + bool fail() const override { + return m_fail; + } + + bool eof() const override { + return m_eof; + } + + operator bool() const override { + return !m_fail && !m_eof; + } + + std::optional line() override { + if (m_fail || m_eof) { + return {}; + } + + // Extract a complete line from the buffer if available + while (true) { + size_t pos = m_buffer.find('\n'); + if (pos != std::string::npos) { + std::string l = m_buffer.substr(0, pos); + m_buffer.erase(0, pos + 1); + spdlog::debug("{}: {}", m_command, l); + return l; + } + + // Read more data non-blockingly + char buf[4096]; + ssize_t bytes_read = read(m_fd, buf, sizeof(buf)); + if (bytes_read > 0) { + m_buffer.append(buf, bytes_read); + continue; // Check for a line again + } else if (bytes_read == 0) { + // EOF: subprocess exited + m_eof = true; + spdlog::trace("command '{}' reached EOF", m_command); + if (!m_buffer.empty()) { + // Return any remaining data as the last (unterminated) line + std::string l = std::move(m_buffer); + m_buffer.clear(); + spdlog::debug("{}: {}", m_command, l); + return l; + } + return {}; + } else { + // Read error + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // No data available yet (async/non-blocking behavior) + return {}; + } else { + spdlog::warn("read error for command '{}': {}", m_command, std::strerror(errno)); + m_fail = true; + return {}; + } + } + } + } +}; + std::unique_ptr Reader::from(const Input &input) { switch (input.m_source) { @@ -92,6 +187,7 @@ std::unique_ptr Reader::from(const Input &input) case Input::FOLLOW: return std::make_unique(input.m_details, true); case Input::EXEC: + return std::make_unique(input.m_details); case Input::WATCH: spdlog::error("input source value {} not yet supported", (int)input.m_source); From 042f6d821c010a6587bdae2dd59159b939eb07d9 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:03:12 -0400 Subject: [PATCH 14/50] refactor interval, parsing, add poll --- cli/config.cpp | 188 +++++++++++++++++++++++++++++++++++-------------- cli/config.hpp | 45 ++++-------- cli/main.cpp | 13 ++-- 3 files changed, 156 insertions(+), 90 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 5e3e0ed..b9c6e7c 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -13,6 +13,7 @@ #include "clipp.h" #pragma GCC diagnostic pop +#pragma GCC diagnostic ignored "-Wunused-variable" Config::Config(int argc, char *argv[]) : m_self(basename(argv[0])) @@ -26,17 +27,7 @@ Config::Config(int argc, char *argv[]) }).doc("Enable verbose output"), clipp::option("--debug").call([]{ spdlog::set_level(spdlog::level::trace); - }).doc("Enable debug output"), - clipp::option("-i", "--interval").doc("Interval in seconds") - & clipp::value("seconds").call([&](const char *txt){ - auto len = strlen(txt); - auto [_,ec] = std::from_chars(txt, txt+len, m_interval); - if (ec != std::errc{}) { - spdlog::error("failed to parse '{}': {}", - txt, std::make_error_code(ec).message()); - std::exit(1); - } - }) + }).doc("Enable debug output") ); auto cli_output = ( @@ -53,47 +44,55 @@ Config::Config(int argc, char *argv[]) }; auto cli = ( - cli_head | - cli_output, - clipp::repeatable( - /* select a plot type */ - - clipp::command("scatter") - .call([&]() { start_input(BLOT_SCATTER); }) - .doc("Add a scatter plot") | - clipp::command("line") - .call([&]() { start_input(BLOT_LINE); }) - .doc("Add a line/curve plot") | - clipp::command("bar") - .call([&]() { start_input(BLOT_BAR); }) - .doc("Add a bar plot"), - - /* augment this plots characteristics */ - - clipp::option("-c", "--color").doc("Set plot color") - & clipp::value("color") - .call([&](const char *c) { m_inputs.back().set_color(c); }), - - /* source data from file or command */ - - clipp::option("-r", "--read").doc("Read file to the end") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }) - | - clipp::option("-f", "--follow").doc("Read file waiting for more") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }) - | - clipp::option("-x", "--exec").doc("Run command, one value per line") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) - | - clipp::option("-w", "--watch").doc("Run command, one value per call") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) - - ), - cli_wrong + cli_head | ( + cli_output, + clipp::repeatable( + /* select a plot type */ + + clipp::command("scatter") + .call([&]() { start_input(BLOT_SCATTER); }) + .doc("Add a scatter plot") | + clipp::command("line") + .call([&]() { start_input(BLOT_LINE); }) + .doc("Add a line/curve plot") | + clipp::command("bar") + .call([&]() { start_input(BLOT_BAR); }) + .doc("Add a bar plot"), + + /* augment this plots characteristics */ + + clipp::option("-c", "--color").doc("Set plot color (1..255)") + & clipp::value("color") + .call([&](const char *txt) { m_inputs.back().set_color(txt); }), + clipp::option("-i", "--interval").doc("Set interval in seconds") + & clipp::value("seconds") + .call([&](const char *txt) { m_inputs.back().set_interval(txt); }), + + /* source data from file or command */ + + clipp::option("-r", "--read").doc("Read file to the end, each line is a record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }) + | + clipp::option("-f", "--follow").doc("Read file waiting for more, each line is a record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }) + | + clipp::option("-p", "--poll").doc("Read file at interval, each read is one record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); }) + | + clipp::option("-x", "--exec").doc("Run command, each line is a record") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) + | + clipp::option("-w", "--watch").doc("Run command at interval, each read is one record") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) + + ), + cli_wrong + ) ); if(!parse(argc, argv, cli) || wrong.size()) { @@ -146,3 +145,88 @@ Config::Config(int argc, char *argv[]) } } } + +void Input::set_source (Input::Source source, const std::string &details) +{ + if (m_source != NONE || m_details.size()) { + spdlog::error("Input source being set twice"); + std::exit(1); + } + m_source = source; + m_details = details; +} + +void Input::set_color (const std::string &txt) +{ + unsigned number; + auto [_,ec] = std::from_chars(txt.data(), txt.data()+txt.size(), number); + if (ec != std::errc{}) { + spdlog::error("failed to parse color from '{}': {}", + txt, std::make_error_code(ec).message()); + std::exit(1); + } + if (number < 1 || number > 255) { + spdlog::error("color {} is not valid", txt); + std::exit(1); + } + m_plot_color = number; +} + +void Input::set_interval (const std::string &txt) +{ + auto [_,ec] = std::from_chars(txt.data(), txt.data()+txt.size(), m_interval); + if (ec != std::errc{}) { + spdlog::error("failed to parse interval from '{}': {}", + txt, std::make_error_code(ec).message()); + std::exit(1); + } +} + +Input::operator bool() const +{ + switch (m_plot_type) { + case BLOT_SCATTER: + case BLOT_LINE: + case BLOT_BAR: + break; + default: + spdlog::warn("invalid plot type: {}", (int)m_plot_type); + return false; + } + bool need_interval = false; + switch (m_source) { + case NONE: + spdlog::warn("{} plot does not defined an input (file or command)", + blot_plot_type_to_string(m_plot_type)); + return false; + case READ: + case FOLLOW: + case EXEC: + break; + case POLL: + case WATCH: + need_interval = true; + break; + default: + spdlog::warn("{} plot has invalid type: {}", + blot_plot_type_to_string(m_plot_type), (int)m_source); + return false; + } + if (need_interval && !m_interval) { + spdlog::warn("{} plot has zero {} interval", + blot_plot_type_to_string(m_plot_type), source_name()); + return false; + } else if (!need_interval && m_interval) { + spdlog::warn("{} plot has non-zero {} interval (%f)", + blot_plot_type_to_string(m_plot_type), source_name(), m_interval); + return false; + } + if (!std::any_of(m_details.begin(), m_details.end(), [](auto c){ return !std::isspace(c); })) { + spdlog::warn("{} plot has empty {} argument '{}'", + blot_plot_type_to_string(m_plot_type), source_name(), m_details); + return false; + } + + return true; +} + diff --git a/cli/config.hpp b/cli/config.hpp index 29859af..5e68bb5 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -8,12 +8,20 @@ #include "spdlog/spdlog.h" struct Input final { - enum Source {NONE,READ,FOLLOW,EXEC,WATCH}; + enum Source { + NONE, // not yet initialized + READ, // read from a file, each line is an entry + FOLLOW, // like READ, but wait for more data + POLL, // read a file repetitively at interval, each read is an entry + EXEC, // run program, read from stdout, each line is an entry + WATCH // run program repetitively at interval, each run is one entry + }; blot_plot_type m_plot_type; Source m_source; std::string m_details; blot_color m_plot_color{9}; + double m_interval{0}; explicit Input(blot_plot_type plot_type) : m_plot_type(plot_type) {} ~Input() {} @@ -32,44 +40,19 @@ struct Input final { } } - operator bool() const { - switch (m_plot_type) { - case BLOT_SCATTER: - case BLOT_LINE: - case BLOT_BAR: - break; - default: - spdlog::warn("invalid plot type: {}", (int)m_plot_type); - return false; - } - if (m_source == NONE) { - spdlog::warn("{} plot does not defined an input (file or command)", - blot_plot_type_to_string(m_plot_type)); - return false; - } - if (m_details.empty()) { - spdlog::warn("{} plot has empty {} argument", - blot_plot_type_to_string(m_plot_type), source_name()); - return false; - } + /* validate if the configuration looks sane */ + operator bool() const; - return true; - } + void set_source (Input::Source source, const std::string &details); + void set_color (const std::string &txt); + void set_interval (const std::string &txt); - void set_source (Input::Source source, const std::string &details) { - m_source = source; - m_details = details; - }; - void set_color (const std::string &c) { - m_plot_color = std::strtol(c.c_str(), NULL, 10); - }; }; struct Config { const char *m_self{}; enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; std::vector m_inputs; - double m_interval{0}; Config(int argc, char *argv[]); ~Config() {} diff --git a/cli/main.cpp b/cli/main.cpp index 8ed1b69..2077f39 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -11,16 +11,15 @@ int main(int argc, char *argv[]) Config config(argc, argv); std::cout << std::format("output_type = {}\n", config.output_type_name()); - std::cout << std::format("interval = {}\n", config.m_interval); for (auto &input : config.m_inputs) { std::cout << "---------------------------------------------\n"; - std::cout << std::format("-> {} {} {} {}\n", - input.plot_name(), - input.source_name(), - input.m_details, - input.m_plot_color); - + std::cout << std::format("-> {} {} {} {} {}\n", + input.plot_name(), + input.source_name(), + input.m_details, + input.m_plot_color, + input.m_interval); auto reader = Reader::from(input); while (*reader) { From 24917d781511179de5cc1e4d3b75b887161694d0 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:16:51 -0400 Subject: [PATCH 15/50] improve --help rendering --- cli/config.cpp | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index b9c6e7c..ebf7c84 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -30,7 +30,7 @@ Config::Config(int argc, char *argv[]) }).doc("Enable debug output") ); - auto cli_output = ( + auto cli_output = "Output:" % ( clipp::option("-A", "--ascii").set(m_output_type,ASCII).doc("ASCII output") | clipp::option("-U", "--unicode").set(m_output_type,UNICODE).doc("Unicode output") | clipp::option("-B", "--braille").set(m_output_type,BRAILLE).doc("Braille output") @@ -49,6 +49,8 @@ Config::Config(int argc, char *argv[]) clipp::repeatable( /* select a plot type */ + "Plot type:" % ( + clipp::command("scatter") .call([&]() { start_input(BLOT_SCATTER); }) .doc("Add a scatter plot") | @@ -57,16 +59,12 @@ Config::Config(int argc, char *argv[]) .doc("Add a line/curve plot") | clipp::command("bar") .call([&]() { start_input(BLOT_BAR); }) - .doc("Add a bar plot"), + .doc("Add a bar plot") + ), - /* augment this plots characteristics */ + /* modifiers - no heading because there is a --help bug in clipp */ - clipp::option("-c", "--color").doc("Set plot color (1..255)") - & clipp::value("color") - .call([&](const char *txt) { m_inputs.back().set_color(txt); }), - clipp::option("-i", "--interval").doc("Set interval in seconds") - & clipp::value("seconds") - .call([&](const char *txt) { m_inputs.back().set_interval(txt); }), + ( /* source data from file or command */ @@ -88,7 +86,18 @@ Config::Config(int argc, char *argv[]) | clipp::option("-w", "--watch").doc("Run command at interval, each read is one record") & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) + .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }), + + /* augment this plots characteristics */ + + clipp::option("-c", "--color").doc("Set plot color (1..255)") + & clipp::value("color") + .call([&](const char *txt) { m_inputs.back().set_color(txt); }), + clipp::option("-i", "--interval").doc("Set interval in seconds") + & clipp::value("seconds") + .call([&](const char *txt) { m_inputs.back().set_interval(txt); }) + + ) ), cli_wrong From 94c38e6cef110c2003531734ce5c3251993a2f28 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:26:22 -0400 Subject: [PATCH 16/50] comments on two missing readers --- cli/reader.cpp | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cli/reader.cpp b/cli/reader.cpp index 99c791a..bfbba03 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -184,12 +184,44 @@ std::unique_ptr Reader::from(const Input &input) switch (input.m_source) { case Input::READ: return std::make_unique(input.m_details); + case Input::FOLLOW: return std::make_unique(input.m_details, true); + + case Input::POLL: + /* TODO: + * + * return std::make_unique(input.m_details, input.m_interval); + * + * Implement FilePoller + * FilePoller::FilePoller(path, interval); + * + * FilePoller::eof() is always false + * FilePoller::fail() can be true, if there is a read error + * + * FilePoller::line() will open the file, read one line, close file, return the line read + */ + spdlog::error("poll source value {} not yet supported", + (int)input.m_source); + std::terminate(); + case Input::EXEC: return std::make_unique(input.m_details); + case Input::WATCH: - spdlog::error("input source value {} not yet supported", + /* TODO: + * + * return std::make_unique(input.m_details, input.m_interval); + * + * Implement ExecWatcher + * ExecWatcher::ExecWatcher(path, interval); + * + * ExecWatcher::eof() is always false + * ExecWatcher::fail() can be true, if there is a exec/read error + * + * ExecWatcher::line() will execute a program, read one line, close pipe, return the line read + */ + spdlog::error("watch source value {} not yet supported", (int)input.m_source); std::terminate(); From 3e9dc7af47357f93e82946b54734eccb73c9ff83 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:42:37 -0400 Subject: [PATCH 17/50] missing initializer for Input members --- cli/config.cpp | 3 ++- cli/config.hpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index ebf7c84..7f9a166 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -158,7 +158,8 @@ Config::Config(int argc, char *argv[]) void Input::set_source (Input::Source source, const std::string &details) { if (m_source != NONE || m_details.size()) { - spdlog::error("Input source being set twice"); + spdlog::error("Input source being set twice, m_source={}/{}, m_details='{}'", + int(m_source), source_name(), m_details); std::exit(1); } m_source = source; diff --git a/cli/config.hpp b/cli/config.hpp index 5e68bb5..0a55d87 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -17,9 +17,9 @@ struct Input final { WATCH // run program repetitively at interval, each run is one entry }; - blot_plot_type m_plot_type; - Source m_source; - std::string m_details; + blot_plot_type m_plot_type{BLOT_LINE}; + Source m_source{NONE}; + std::string m_details{}; blot_color m_plot_color{9}; double m_interval{0}; From 3dd927bbef87fbcab2bab9a2ad6a4d7817f3829e Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:42:53 -0400 Subject: [PATCH 18/50] FilePoller --- cli/reader.cpp | 88 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/cli/reader.cpp b/cli/reader.cpp index bfbba03..5c3cf03 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -3,6 +3,7 @@ #include #include +#include #include // for popen, pclose #include // for read, fileno @@ -14,6 +15,7 @@ namespace fs = std::filesystem; +// used by Input::READ and Input::FOLLOW struct FileReader : public Reader { fs::path m_path; @@ -90,6 +92,74 @@ struct FileReader : public Reader { } }; +// used by Input::POLL +struct FilePoller : public Reader { + fs::path m_path; + double m_interval; + bool m_fail = false; + std::chrono::steady_clock::time_point m_last_read{}; + + FilePoller(const std::string &details, double interval) + : m_path(details), m_interval(interval) + { + if (m_interval <= 0.0) { + spdlog::error("invalid polling interval: {}", m_interval); + std::exit(1); + } + if (!fs::exists(m_path)) { + spdlog::error("{}: does not exist", m_path.string()); + std::exit(1); + } + if (!fs::is_regular_file(m_path) && !fs::is_character_file(m_path) && !fs::is_other(m_path)) { + spdlog::error("{}: unsupported file type for polling", m_path.string()); + std::exit(1); + } + } + + ~FilePoller() override {} + + bool fail() const override { return m_fail; } + bool eof() const override { return false; } + operator bool() const override { return !m_fail; } + + std::optional line() override + { + if (m_fail) { + return {}; + } + + auto now = std::chrono::steady_clock::now(); + if (m_last_read != std::chrono::steady_clock::time_point{} && + std::chrono::duration(now - m_last_read).count() < m_interval) { + return {}; + } + + std::ifstream stream(m_path); + if (!stream.is_open()) { + spdlog::error("{}: failed to open", m_path.string()); + m_fail = true; + return {}; + } + + std::string line; + if (std::getline(stream, line)) { + spdlog::debug("{}: {}", m_path.string(), line); + m_last_read = now; + return line; + } else { + if (stream.fail() && !stream.eof()) { + spdlog::warn("{}: failed reading", m_path.string()); + m_fail = true; + } else { + spdlog::trace("{}: no data available", m_path.string()); + } + m_last_read = now; // Update timestamp even on empty read to maintain polling interval + return {}; + } + } +}; + +// used by Input::EXEC struct ExecStreamReader : public Reader { std::string m_command; FILE* m_pipe = nullptr; @@ -189,21 +259,7 @@ std::unique_ptr Reader::from(const Input &input) return std::make_unique(input.m_details, true); case Input::POLL: - /* TODO: - * - * return std::make_unique(input.m_details, input.m_interval); - * - * Implement FilePoller - * FilePoller::FilePoller(path, interval); - * - * FilePoller::eof() is always false - * FilePoller::fail() can be true, if there is a read error - * - * FilePoller::line() will open the file, read one line, close file, return the line read - */ - spdlog::error("poll source value {} not yet supported", - (int)input.m_source); - std::terminate(); + return std::make_unique(input.m_details, input.m_interval); case Input::EXEC: return std::make_unique(input.m_details); @@ -222,7 +278,7 @@ std::unique_ptr Reader::from(const Input &input) * ExecWatcher::line() will execute a program, read one line, close pipe, return the line read */ spdlog::error("watch source value {} not yet supported", - (int)input.m_source); + (int)input.m_source); std::terminate(); default: From 027ae338f5880c0c6399db68c7bf8406715f4cb3 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 08:46:19 -0400 Subject: [PATCH 19/50] ExecWatcher --- cli/main.cpp | 24 +++++++-- cli/reader.cpp | 139 ++++++++++++++++++++++++++++++++++++++++--------- cli/reader.hpp | 1 + 3 files changed, 135 insertions(+), 29 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index 2077f39..a54b948 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include "config.hpp" #include "reader.hpp" @@ -12,8 +15,9 @@ int main(int argc, char *argv[]) std::cout << std::format("output_type = {}\n", config.output_type_name()); + std::vector> readers; + for (auto &input : config.m_inputs) { - std::cout << "---------------------------------------------\n"; std::cout << std::format("-> {} {} {} {} {}\n", input.plot_name(), input.source_name(), @@ -21,11 +25,25 @@ int main(int argc, char *argv[]) input.m_plot_color, input.m_interval); - auto reader = Reader::from(input); - while (*reader) { + readers.push_back(Reader::from(input)); + } + + while(std::all_of(readers.begin(), readers.end(), [](auto &reader)->bool { return *reader; })) { + + for (auto &reader : readers) { + if (reader->idle()) + continue; + auto line = reader->line(); if (line.has_value()) std::cout << *line << std::endl; } + + auto idle_view = readers | std::views::transform([](const auto& reader) { return reader->idle(); }); + double idle = std::ranges::min(idle_view); + + if (idle > 0) { + std::this_thread::sleep_for(std::chrono::duration(idle)); + } } } diff --git a/cli/reader.cpp b/cli/reader.cpp index 5c3cf03..f7a2806 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -49,6 +49,7 @@ struct FileReader : public Reader { bool fail() const override { return m_stream.fail(); } bool eof() const override { return m_stream.eof(); } + double idle() const override { return 0; } operator bool() const override { return !fail() && (!eof() || m_follow); } @@ -122,6 +123,22 @@ struct FilePoller : public Reader { bool eof() const override { return false; } operator bool() const override { return !m_fail; } + double idle() const override { + if (m_fail) { + return 0.0; + } + auto now = std::chrono::steady_clock::now(); + if (m_last_read == std::chrono::steady_clock::time_point{}) { + return 0.0; + } + double elapsed = std::chrono::duration(now - m_last_read).count(); + if (elapsed >= m_interval) { + return 0.0; + } else { + return m_interval - elapsed; + } + } + std::optional line() override { if (m_fail) { @@ -189,17 +206,10 @@ struct ExecStreamReader : public Reader { } } - bool fail() const override { - return m_fail; - } - - bool eof() const override { - return m_eof; - } - - operator bool() const override { - return !m_fail && !m_eof; - } + bool fail() const override { return m_fail; } + bool eof() const override { return m_eof; } + double idle() const override { return 0; } + operator bool() const override { return !m_fail && !m_eof; } std::optional line() override { if (m_fail || m_eof) { @@ -249,6 +259,97 @@ struct ExecStreamReader : public Reader { } }; +// used by Input::WATCH +struct ExecWatcher : public Reader { + std::string m_command; + double m_interval; + bool m_fail = false; + std::chrono::steady_clock::time_point m_last_exec{}; + + ExecWatcher(const std::string &command, double interval) + : m_command(command), m_interval(interval) + { + if (m_interval <= 0.0) { + spdlog::error("invalid watching interval: {}", m_interval); + std::exit(1); + } + } + + ~ExecWatcher() override {} + + bool fail() const override { return m_fail; } + bool eof() const override { return false; } + operator bool() const override { return !m_fail; } + + double idle() const override { + if (m_fail) { + return 0.0; + } + auto now = std::chrono::steady_clock::now(); + if (m_last_exec == std::chrono::steady_clock::time_point{}) { + return 0.0; + } + double elapsed = std::chrono::duration(now - m_last_exec).count(); + if (elapsed >= m_interval) { + return 0.0; + } else { + return m_interval - elapsed; + } + } + + std::optional line() override + { + if (m_fail) { + return {}; + } + + auto now = std::chrono::steady_clock::now(); + if (m_last_exec != std::chrono::steady_clock::time_point{} && + std::chrono::duration(now - m_last_exec).count() < m_interval) { + return {}; + } + + FILE* pipe = popen(m_command.c_str(), "r"); + if (!pipe) { + spdlog::error("failed to execute command '{}': {}", m_command, std::strerror(errno)); + m_fail = true; + return {}; + } + + char buf[4096]; + if (fgets(buf, sizeof(buf), pipe) == nullptr) { + if (ferror(pipe)) { + spdlog::warn("error reading from command '{}'", m_command); + m_fail = true; + } else { + spdlog::trace("no output from command '{}'", m_command); + } + pclose(pipe); + m_last_exec = now; + return {}; + } + + std::string line(buf); + size_t pos = line.find_last_not_of("\r\n"); + if (pos != std::string::npos) { + line.erase(pos + 1); + } else { + line.clear(); + } + + spdlog::debug("{}: {}", m_command, line); + + int status = pclose(pipe); + if (status != 0) { + spdlog::warn("command '{}' exited with status {}", m_command, status); + // Note: Not setting fail here, as we still got a line + } + + m_last_exec = now; + return line; + } +}; + std::unique_ptr Reader::from(const Input &input) { switch (input.m_source) { @@ -265,21 +366,7 @@ std::unique_ptr Reader::from(const Input &input) return std::make_unique(input.m_details); case Input::WATCH: - /* TODO: - * - * return std::make_unique(input.m_details, input.m_interval); - * - * Implement ExecWatcher - * ExecWatcher::ExecWatcher(path, interval); - * - * ExecWatcher::eof() is always false - * ExecWatcher::fail() can be true, if there is a exec/read error - * - * ExecWatcher::line() will execute a program, read one line, close pipe, return the line read - */ - spdlog::error("watch source value {} not yet supported", - (int)input.m_source); - std::terminate(); + return std::make_unique(input.m_details, input.m_interval); default: spdlog::error("invalid input source value {}", (int)input.m_source); diff --git a/cli/reader.hpp b/cli/reader.hpp index b003fee..6bf53a1 100644 --- a/cli/reader.hpp +++ b/cli/reader.hpp @@ -12,6 +12,7 @@ struct Reader { virtual bool fail() const = 0; virtual bool eof() const = 0; virtual operator bool() const = 0; + virtual double idle() const = 0; virtual std::optional line() = 0; static std::unique_ptr from(const Input &input); From 37dc36f566ca104ea2cb8e21ec0b68a2896d5c8e Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 10:33:28 -0400 Subject: [PATCH 20/50] simple plotting --- cli/config.cpp | 8 ++++++ cli/config.hpp | 1 + cli/main.cpp | 51 +++++++++++++++++++++++++++++++------ cli/plotter.hpp | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ cli/reader.cpp | 33 +++++++++++++++++------- cli/reader.hpp | 8 +++++- 6 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 cli/plotter.hpp diff --git a/cli/config.cpp b/cli/config.cpp index 7f9a166..06f3764 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -147,12 +147,20 @@ Config::Config(int argc, char *argv[]) spdlog::error("no plots defined"); std::exit(1); } + bool with_interval{}, no_interval{}; for (auto &input : m_inputs) { if (!input) { spdlog::error("incomplete plot definition"); std::exit(1); } + with_interval |= !!(input.m_interval); + no_interval |= !(input.m_interval); } + if (with_interval && no_interval) { + spdlog::error("cannot mix interval and non-interval sources"); + std::exit(1); + } + m_using_interval = with_interval; } void Input::set_source (Input::Source source, const std::string &details) diff --git a/cli/config.hpp b/cli/config.hpp index 0a55d87..76cb78a 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -53,6 +53,7 @@ struct Config { const char *m_self{}; enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; std::vector m_inputs; + bool m_using_interval{}; Config(int argc, char *argv[]); ~Config() {} diff --git a/cli/main.cpp b/cli/main.cpp index a54b948..1a492e0 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -6,19 +6,28 @@ #include "config.hpp" #include "reader.hpp" -#include "blot.hpp" +#include "plotter.hpp" + #include "spdlog/spdlog.h" int main(int argc, char *argv[]) { Config config(argc, argv); - std::cout << std::format("output_type = {}\n", config.output_type_name()); + spdlog::debug("output_type = {}", config.output_type_name()); std::vector> readers; - for (auto &input : config.m_inputs) { - std::cout << std::format("-> {} {} {} {} {}\n", + auto keep_going = [&]{ + return std::any_of(readers.begin(), readers.end(), + [](auto &reader)->bool { + return *reader; + }); + }; + + for (size_t i=0; ibool { return *reader; })) { + if (config.m_using_interval) { + spdlog::error("hang on"); + std::exit(1); + } + + Plotter plotter(config); + + while(keep_going()) { - for (auto &reader : readers) { + for (size_t i=0; iidle()) continue; auto line = reader->line(); - if (line.has_value()) - std::cout << *line << std::endl; + if (!line.has_value()) + continue; + + spdlog::trace("{}:{}: {}", input.m_details, line->number, line->text); + + double value; + auto [_,ec] = std::from_chars(line->text.data(), + line->text.data()+line->text.size(), value); + if (ec != std::errc{}) { + spdlog::error("failed to parse value from source {} line {} '{}': {}", + i, line->number, line->text, std::make_error_code(ec).message()); + std::exit(1); + } + + plotter.add(i, line->number, value); } + /* wait for the next time we have data */ + auto idle_view = readers | std::views::transform([](const auto& reader) { return reader->idle(); }); double idle = std::ranges::min(idle_view); @@ -46,4 +79,6 @@ int main(int argc, char *argv[]) std::this_thread::sleep_for(std::chrono::duration(idle)); } } + + plotter.plot(); } diff --git a/cli/plotter.hpp b/cli/plotter.hpp new file mode 100644 index 0000000..0daa030 --- /dev/null +++ b/cli/plotter.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "blot.hpp" +#include "config.hpp" + +#include "blot.hpp" +#include "spdlog/spdlog.h" + +template +class Plotter final { +protected: + struct Data { + std::vector m_xs; + std::vector m_ys; + }; + std::vector m_data; + + const Config &m_config; + size_t m_max_layers{}; + size_t m_data_history{}; + +public: + explicit Plotter(const Config &config) + : m_config(config), m_max_layers(m_config.m_inputs.size()) + { + m_data.resize(m_max_layers); + } + + void add(size_t layer, X x, Y y) { + m_data[layer].m_xs.push_back(x); + m_data[layer].m_ys.push_back(y); + } + + void plot() const { + + setlocale(LC_CTYPE, ""); + + Blot::Figure fig; + fig.set_axis_color(8); + + Blot::Dimensions term; + fig.set_screen_size(term.cols/2, term.rows/2); + + for (size_t i=0; i line() override + size_t lines() const override { return m_line_number; } + + std::optional line() override { if (m_stream.fail()) // reached failed state @@ -66,7 +68,7 @@ struct FileReader : public Reader { m_line_number ++; // successfully read next line spdlog::debug("{}:{}: {}", m_path.string(), m_line_number, line); - return line; + return Line(m_line_number, line); } spdlog::trace("{}:{}: follow={} fail={} eof={}", m_path.string(), m_line_number, @@ -99,6 +101,7 @@ struct FilePoller : public Reader { double m_interval; bool m_fail = false; std::chrono::steady_clock::time_point m_last_read{}; + size_t m_line_number{}; FilePoller(const std::string &details, double interval) : m_path(details), m_interval(interval) @@ -139,7 +142,9 @@ struct FilePoller : public Reader { } } - std::optional line() override + size_t lines() const override { return m_line_number; } + + std::optional line() override { if (m_fail) { return {}; @@ -160,9 +165,10 @@ struct FilePoller : public Reader { std::string line; if (std::getline(stream, line)) { + m_line_number ++; spdlog::debug("{}: {}", m_path.string(), line); m_last_read = now; - return line; + return Line(m_line_number, line); } else { if (stream.fail() && !stream.eof()) { spdlog::warn("{}: failed reading", m_path.string()); @@ -182,6 +188,7 @@ struct ExecStreamReader : public Reader { FILE* m_pipe = nullptr; int m_fd = -1; std::string m_buffer; + size_t m_line_number{}; bool m_eof = false; bool m_fail = false; @@ -211,7 +218,9 @@ struct ExecStreamReader : public Reader { double idle() const override { return 0; } operator bool() const override { return !m_fail && !m_eof; } - std::optional line() override { + size_t lines() const override { return m_line_number; } + + std::optional line() override { if (m_fail || m_eof) { return {}; } @@ -220,10 +229,11 @@ struct ExecStreamReader : public Reader { while (true) { size_t pos = m_buffer.find('\n'); if (pos != std::string::npos) { + m_line_number ++; std::string l = m_buffer.substr(0, pos); m_buffer.erase(0, pos + 1); spdlog::debug("{}: {}", m_command, l); - return l; + return Line(m_line_number, l); } // Read more data non-blockingly @@ -238,10 +248,11 @@ struct ExecStreamReader : public Reader { spdlog::trace("command '{}' reached EOF", m_command); if (!m_buffer.empty()) { // Return any remaining data as the last (unterminated) line + m_line_number ++; std::string l = std::move(m_buffer); m_buffer.clear(); spdlog::debug("{}: {}", m_command, l); - return l; + return Line(m_line_number, l); } return {}; } else { @@ -264,6 +275,7 @@ struct ExecWatcher : public Reader { std::string m_command; double m_interval; bool m_fail = false; + size_t m_line_number{}; std::chrono::steady_clock::time_point m_last_exec{}; ExecWatcher(const std::string &command, double interval) @@ -297,7 +309,9 @@ struct ExecWatcher : public Reader { } } - std::optional line() override + size_t lines() const override { return m_line_number; } + + std::optional line() override { if (m_fail) { return {}; @@ -345,8 +359,9 @@ struct ExecWatcher : public Reader { // Note: Not setting fail here, as we still got a line } + m_line_number ++; m_last_exec = now; - return line; + return Line(m_line_number, line); } }; diff --git a/cli/reader.hpp b/cli/reader.hpp index 6bf53a1..5483030 100644 --- a/cli/reader.hpp +++ b/cli/reader.hpp @@ -6,6 +6,11 @@ #include #include +struct Line { + size_t number; + std::string text; +}; + struct Reader { virtual ~Reader() {} @@ -13,7 +18,8 @@ struct Reader { virtual bool eof() const = 0; virtual operator bool() const = 0; virtual double idle() const = 0; - virtual std::optional line() = 0; + virtual size_t lines() const = 0; + virtual std::optional line() = 0; static std::unique_ptr from(const Input &input); }; From 74c33abcfb81e7e7f3c2f7f9bf80da65564afedd Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 10:36:33 -0400 Subject: [PATCH 21/50] fix y legend --- lib/blot_screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blot_screen.c b/lib/blot_screen.c index d52e5c2..50dabef 100644 --- a/lib/blot_screen.c +++ b/lib/blot_screen.c @@ -147,7 +147,7 @@ static bool blot_screen_plot_cans(blot_screen *scr, ytick = blot_axis_get_tick_at(y_axs, c_y, error); const char *ytick_label = ytick ? ytick->label : ""; - char axis_char = ytick ? '*' : '-'; + char axis_char = ytick ? '*' : '|'; len = swprintf(p, end-p, L"%*s %c", dsp_lft-2, ytick_label, axis_char); From 07195b81ad94f79df2f879e293547d91c00c4343 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 10:54:54 -0400 Subject: [PATCH 22/50] work with intervals --- cli/main.cpp | 23 ++++++++++++++--------- cli/plotter.hpp | 5 +++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index 1a492e0..f27aac4 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -10,6 +10,12 @@ #include "spdlog/spdlog.h" +static bool signaled = false; +static void sighandler(int sig) +{ + signaled = true; +} + int main(int argc, char *argv[]) { Config config(argc, argv); @@ -18,11 +24,14 @@ int main(int argc, char *argv[]) std::vector> readers; + signal(SIGINT, sighandler); + auto keep_going = [&]{ - return std::any_of(readers.begin(), readers.end(), - [](auto &reader)->bool { - return *reader; - }); + return !signaled + && std::any_of(readers.begin(), readers.end(), + [](auto &reader)->bool { + return *reader; + }); }; for (size_t i=0; i plotter(config); while(keep_going()) { @@ -76,6 +80,7 @@ int main(int argc, char *argv[]) double idle = std::ranges::min(idle_view); if (idle > 0) { + plotter.plot(); std::this_thread::sleep_for(std::chrono::duration(idle)); } } diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 0daa030..8e07937 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -39,7 +39,7 @@ class Plotter final { fig.set_axis_color(8); Blot::Dimensions term; - fig.set_screen_size(term.cols/2, term.rows/2); + fig.set_screen_size(term.cols, term.rows/2); for (size_t i=0; i Date: Mon, 28 Jul 2025 12:24:34 -0400 Subject: [PATCH 23/50] class, getters, auto color selection --- cli/config.cpp | 9 ++++++--- cli/config.hpp | 29 ++++++++++++++++++++++++----- cli/main.cpp | 14 +++++++------- cli/plotter.hpp | 10 ++++------ cli/reader.cpp | 26 +++++++++++++++----------- cli/reader.hpp | 3 ++- 6 files changed, 58 insertions(+), 33 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 06f3764..d3fe624 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -40,7 +40,10 @@ Config::Config(int argc, char *argv[]) auto cli_wrong = clipp::any_other(wrong); auto start_input = [&](blot_plot_type plot_type) { - m_inputs.push_back(Input{plot_type}); + blot_color color = m_inputs.empty() + ? m_first_color + : m_inputs.back().plot_color() + 1; + m_inputs.push_back(Input{plot_type, color}); }; auto cli = ( @@ -153,8 +156,8 @@ Config::Config(int argc, char *argv[]) spdlog::error("incomplete plot definition"); std::exit(1); } - with_interval |= !!(input.m_interval); - no_interval |= !(input.m_interval); + with_interval |= !!(input.interval()); + no_interval |= !(input.interval()); } if (with_interval && no_interval) { spdlog::error("cannot mix interval and non-interval sources"); diff --git a/cli/config.hpp b/cli/config.hpp index 76cb78a..61b83c8 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -7,7 +7,8 @@ #include "blot.hpp" #include "spdlog/spdlog.h" -struct Input final { +class Input final { +public: enum Source { NONE, // not yet initialized READ, // read from a file, each line is an entry @@ -17,19 +18,24 @@ struct Input final { WATCH // run program repetitively at interval, each run is one entry }; +protected: blot_plot_type m_plot_type{BLOT_LINE}; Source m_source{NONE}; std::string m_details{}; - blot_color m_plot_color{9}; + blot_color m_plot_color; double m_interval{0}; - explicit Input(blot_plot_type plot_type) : m_plot_type(plot_type) {} +public: + explicit Input(blot_plot_type plot_type, blot_color color) + : m_plot_type(plot_type), m_plot_color(color) {} ~Input() {} + blot_plot_type plot_type() const { return m_plot_type; } std::string plot_name() const { return blot_plot_type_to_string(m_plot_type); } + Source source() const { return m_source; } std::string source_name() const { switch (m_source) { case READ: return "read"; @@ -40,6 +46,10 @@ struct Input final { } } + const char * details() const { return m_details.c_str(); } + blot_color plot_color() const { return m_plot_color; } + double interval() const { return m_interval; } + /* validate if the configuration looks sane */ operator bool() const; @@ -47,15 +57,19 @@ struct Input final { void set_color (const std::string &txt); void set_interval (const std::string &txt); + }; -struct Config { +class Config final { +protected: const char *m_self{}; enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; + const static blot_color m_first_color{9}; std::vector m_inputs; bool m_using_interval{}; - Config(int argc, char *argv[]); +public: + explicit Config(int argc, char *argv[]); ~Config() {} std::string output_type_name() const { @@ -66,6 +80,11 @@ struct Config { default: return {}; } } + + size_t inputs() const { return m_inputs.size(); } + + const Input& input(size_t n) const { return m_inputs.at(n); } + Input& input(size_t n) { return m_inputs.at(n); } }; diff --git a/cli/main.cpp b/cli/main.cpp index f27aac4..b4d184c 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -34,14 +34,14 @@ int main(int argc, char *argv[]) }); }; - for (size_t i=0; iidle()) continue; @@ -60,7 +60,7 @@ int main(int argc, char *argv[]) if (!line.has_value()) continue; - spdlog::trace("{}:{}: {}", input.m_details, line->number, line->text); + spdlog::trace("{}:{}: {}", input.details(), line->number, line->text); double value; auto [_,ec] = std::from_chars(line->text.data(), diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 8e07937..3a1c428 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -21,7 +21,7 @@ class Plotter final { public: explicit Plotter(const Config &config) - : m_config(config), m_max_layers(m_config.m_inputs.size()) + : m_config(config), m_max_layers(m_config.inputs()) { m_data.resize(m_max_layers); } @@ -43,12 +43,10 @@ class Plotter final { for (size_t i=0; i Reader::from(const Input &input) { - switch (input.m_source) { + switch (input.source()) { case Input::READ: - return std::make_unique(input.m_details); + return std::make_unique(input.details()); case Input::FOLLOW: - return std::make_unique(input.m_details, true); + return std::make_unique(input.details(), true); case Input::POLL: - return std::make_unique(input.m_details, input.m_interval); + return std::make_unique(input.details(), input.interval()); case Input::EXEC: - return std::make_unique(input.m_details); + return std::make_unique(input.details()); case Input::WATCH: - return std::make_unique(input.m_details, input.m_interval); + return std::make_unique(input.details(), input.interval()); default: - spdlog::error("invalid input source value {}", (int)input.m_source); + spdlog::error("invalid input source value {}", (int)input.source()); std::terminate(); } } diff --git a/cli/reader.hpp b/cli/reader.hpp index 5483030..3e681e9 100644 --- a/cli/reader.hpp +++ b/cli/reader.hpp @@ -11,7 +11,8 @@ struct Line { std::string text; }; -struct Reader { +class Reader { +public: virtual ~Reader() {} virtual bool fail() const = 0; From 3b9863bdec9636bf3ff51666eb33c10ad8ffb06f Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 12:40:08 -0400 Subject: [PATCH 24/50] if not set, inherit interval from previous plot --- cli/config.cpp | 34 +++++++++++++++++++++++++--------- cli/config.hpp | 11 +++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index d3fe624..61273fa 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -150,8 +150,17 @@ Config::Config(int argc, char *argv[]) spdlog::error("no plots defined"); std::exit(1); } + bool with_interval{}, no_interval{}; + double prev_interval = 1; for (auto &input : m_inputs) { + if (input.needs_interval()) { + if (!input.interval()) { + input.set_interval(prev_interval); + } else { + prev_interval = input.interval(); + } + } if (!input) { spdlog::error("incomplete plot definition"); std::exit(1); @@ -193,6 +202,16 @@ void Input::set_color (const std::string &txt) m_plot_color = number; } +void Input::set_interval (double interval) +{ + if (needs_interval() && !interval) { + spdlog::warn("{} {} plot cannot have zero interval", + blot_plot_type_to_string(m_plot_type), source_name()); + std::exit(1); + } + m_interval = interval; +} + void Input::set_interval (const std::string &txt) { auto [_,ec] = std::from_chars(txt.data(), txt.data()+txt.size(), m_interval); @@ -214,30 +233,27 @@ Input::operator bool() const spdlog::warn("invalid plot type: {}", (int)m_plot_type); return false; } - bool need_interval = false; switch (m_source) { - case NONE: - spdlog::warn("{} plot does not defined an input (file or command)", - blot_plot_type_to_string(m_plot_type)); - return false; case READ: case FOLLOW: case EXEC: - break; case POLL: case WATCH: - need_interval = true; break; + case NONE: + spdlog::warn("{} plot does not defined an input (file or command)", + blot_plot_type_to_string(m_plot_type)); + return false; default: spdlog::warn("{} plot has invalid type: {}", blot_plot_type_to_string(m_plot_type), (int)m_source); return false; } - if (need_interval && !m_interval) { + if (needs_interval() && !m_interval) { spdlog::warn("{} plot has zero {} interval", blot_plot_type_to_string(m_plot_type), source_name()); return false; - } else if (!need_interval && m_interval) { + } else if (!needs_interval() && m_interval) { spdlog::warn("{} plot has non-zero {} interval (%f)", blot_plot_type_to_string(m_plot_type), source_name(), m_interval); return false; diff --git a/cli/config.hpp b/cli/config.hpp index 61b83c8..df2b751 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -46,6 +46,16 @@ class Input final { } } + bool needs_interval() const { + switch (m_source) { + case POLL: + case WATCH: + return true; + default: + return false; + } + } + const char * details() const { return m_details.c_str(); } blot_color plot_color() const { return m_plot_color; } double interval() const { return m_interval; } @@ -55,6 +65,7 @@ class Input final { void set_source (Input::Source source, const std::string &details); void set_color (const std::string &txt); + void set_interval (double interval); void set_interval (const std::string &txt); From b3706a1143d8871951a35c5e01365529894e39a3 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 13:21:13 -0400 Subject: [PATCH 25/50] fix translation from layer to canvas --- lib/blot_layer.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/blot_layer.c b/lib/blot_layer.c index 2cc33df..a2eb9a0 100644 --- a/lib/blot_layer.c +++ b/lib/blot_layer.c @@ -216,12 +216,15 @@ static bool blot_layer_scatter_int64(const blot_layer *lay, const blot_xy_limits static bool blot_layer_line(const blot_layer *lay, const blot_xy_limits *lim, blot_canvas *can, GError **error) { - double x_range = lim->x_max - lim->x_min + 1; - double y_range = lim->y_max - lim->y_min + 1; + double x_range = lim->x_max - lim->x_min; + double y_range = lim->y_max - lim->y_min; RETURN_ERRORx(x_range <= 0, false, error, ERANGE, "invalid column limits %f..%f", lim->x_min, lim->x_max); RETURN_ERRORx(x_range <= 0, false, error, ERANGE, "invalid column limits %f..%f", lim->x_min, lim->x_max); + double per_col = (double)(can->dim.cols-1) / x_range; + double per_row = (double)(can->dim.rows-1) / y_range; + bool visible = false; double px=0, py=0; @@ -234,8 +237,8 @@ static bool blot_layer_line(const blot_layer *lay, const blot_xy_limits *lim, return false; // compute location - double dx = (double)(rx - lim->x_min) * can->dim.cols / x_range; - double dy = (double)(ry - lim->y_min) * can->dim.rows / y_range; + double dx = (double)(rx - lim->x_min) * per_col; + double dy = (double)(ry - lim->y_min) * per_row; // plot it if (likely (visible)) { From 431b4c5763552870ef114c1188527a6096918914 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 13:30:13 -0400 Subject: [PATCH 26/50] fix SIGINT while sleeping --- cli/main.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cli/main.cpp b/cli/main.cpp index b4d184c..2eda5f2 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -51,6 +51,9 @@ int main(int argc, char *argv[]) while(keep_going()) { for (size_t i=0; iidle()) @@ -74,6 +77,9 @@ int main(int argc, char *argv[]) plotter.add(i, line->number, value); } + if (signaled) + return 1; + /* wait for the next time we have data */ auto idle_view = readers | std::views::transform([](const auto& reader) { return reader->idle(); }); @@ -81,7 +87,12 @@ int main(int argc, char *argv[]) if (idle > 0) { plotter.plot(); - std::this_thread::sleep_for(std::chrono::duration(idle)); + + // unfortunately std::this_thread::sleep_for() cannot be used, + // as it will complete the sleep even if SIGINT is raised. + + double useconds = idle * 1000000; + usleep(useconds); } } From ad1e05376af0de19f83b8a741e590bcac9faaeb4 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 13:34:15 -0400 Subject: [PATCH 27/50] add constructor to keep clang 14 happy --- cli/reader.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/reader.hpp b/cli/reader.hpp index 3e681e9..8bc001e 100644 --- a/cli/reader.hpp +++ b/cli/reader.hpp @@ -6,9 +6,11 @@ #include #include -struct Line { +struct Line final { size_t number; std::string text; + + explicit Line(size_t n, std::string t) : number(n), text(t) {} }; class Reader { From 5e33e5f35610dd3531a23b7772ee9cd2b2dba21c Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 13:50:16 -0400 Subject: [PATCH 28/50] is not available with clang-14, use instead --- cli/CMakeLists.txt | 26 ++++++++++++++++++-------- cli/config.cpp | 2 +- cli/main.cpp | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 2dda128..c1fab3f 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -9,20 +9,30 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/muellan/clipp.git GIT_TAG v1.2.3 ) -FetchContent_MakeAvailable(spdlog clipp) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 11.2.0 +) +FetchContent_MakeAvailable(spdlog clipp fmt) ADD_EXECUTABLE(blot - main.cpp - config.cpp - reader.cpp + main.cpp + config.cpp + reader.cpp +) + +TARGET_COMPILE_DEFINITIONS(blot PRIVATE + FMT_HEADER_ONLY ) TARGET_INCLUDE_DIRECTORIES(blot PRIVATE - ${clipp_SOURCE_DIR}/include + ${clipp_SOURCE_DIR}/include ) TARGET_LINK_LIBRARIES(blot PRIVATE - blot_a - -lm - spdlog::spdlog + blot_a + -lm + spdlog::spdlog + fmt::fmt ) diff --git a/cli/config.cpp b/cli/config.cpp index 61273fa..1c81101 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -1,12 +1,12 @@ #include "config.hpp" -#include #include #include #include #include "blot.hpp" #include "spdlog/spdlog.h" +#include "fmt/format.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-but-set-variable" diff --git a/cli/main.cpp b/cli/main.cpp index 2eda5f2..605514f 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -9,6 +8,7 @@ #include "plotter.hpp" #include "spdlog/spdlog.h" +#include "fmt/format.h" static bool signaled = false; static void sighandler(int sig) From bf4222e302ca308bbc56e989cd16a0c41e7a2829 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 15:52:14 -0400 Subject: [PATCH 29/50] add --position and --regex to select numbers out of lines of text --- cli/config.cpp | 114 +++++++++++++++++++++++++++++++------------------ cli/config.hpp | 6 +++ cli/main.cpp | 12 +++--- 3 files changed, 83 insertions(+), 49 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 1c81101..2f32e63 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -54,52 +54,60 @@ Config::Config(int argc, char *argv[]) "Plot type:" % ( - clipp::command("scatter") - .call([&]() { start_input(BLOT_SCATTER); }) - .doc("Add a scatter plot") | - clipp::command("line") - .call([&]() { start_input(BLOT_LINE); }) - .doc("Add a line/curve plot") | - clipp::command("bar") - .call([&]() { start_input(BLOT_BAR); }) - .doc("Add a bar plot") + clipp::command("scatter") + .call([&]() { start_input(BLOT_SCATTER); }) + .doc("Add a scatter plot") | + clipp::command("line") + .call([&]() { start_input(BLOT_LINE); }) + .doc("Add a line/curve plot") | + clipp::command("bar") + .call([&]() { start_input(BLOT_BAR); }) + .doc("Add a bar plot") ), /* modifiers - no heading because there is a --help bug in clipp */ + one_of( + /* source data from file or command */ + + clipp::option("-R", "--read").doc("Read file to the end, each line is a record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }), + clipp::option("-F", "--follow").doc("Read file waiting for more, each line is a record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }), + clipp::option("-P", "--poll").doc("Read file at interval, each read is one record") + & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); }), + clipp::option("-X", "--exec").doc("Run command, each line is a record") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }), + clipp::option("-W", "--watch").doc("Run command at interval, each read is one record") + & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) + ), + ( + /* how to extract values from lines */ - /* source data from file or command */ - - clipp::option("-r", "--read").doc("Read file to the end, each line is a record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }) - | - clipp::option("-f", "--follow").doc("Read file waiting for more, each line is a record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }) - | - clipp::option("-p", "--poll").doc("Read file at interval, each read is one record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); }) - | - clipp::option("-x", "--exec").doc("Run command, each line is a record") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }) - | - clipp::option("-w", "--watch").doc("Run command at interval, each read is one record") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }), - - /* augment this plots characteristics */ - - clipp::option("-c", "--color").doc("Set plot color (1..255)") - & clipp::value("color") - .call([&](const char *txt) { m_inputs.back().set_color(txt); }), - clipp::option("-i", "--interval").doc("Set interval in seconds") - & clipp::value("seconds") - .call([&](const char *txt) { m_inputs.back().set_interval(txt); }) + clipp::option("-p", "--position").doc("Use the Nth number from input line") + & clipp::value("number") + .call([&](const char *txt) { m_inputs.back().set_position(txt); }), + clipp::option("-r", "--regex").doc("Regex to match numbers from input line") + & clipp::value("regex") + .call([&](const char *txt) { m_inputs.back().set_regex(txt); }) + ), + + ( + /* augment this plots characteristics */ + + clipp::option("-c", "--color").doc("Set plot color (1..255)") + & clipp::value("color") + .call([&](const char *txt) { m_inputs.back().set_color(txt); }), + clipp::option("-i", "--interval").doc("Set interval in seconds") + & clipp::value("seconds") + .call([&](const char *txt) { m_inputs.back().set_interval(txt); }) ) ), @@ -135,12 +143,17 @@ Config::Config(int argc, char *argv[]) .append_section("EXAMPE", "\n" " blot --braille \\\n" - " line --color 10 --file x_y1_values \\\n" - " scatter --color 11 --file x_y2_values\n" + " line --color 10 --read x_y1_values \\\n" + " scatter --color 11 --read x_y2_values\n" "\n" " blot --braille \\\n" " line --color 10 --exec 'seq 1 100' \\\n" - " scatter --color 11 --file x_y_values\n" + " scatter --color 11 --read x_y_values\n" + "\n" + " blot --braille \\\n" + " line --poll /proc/loadavg --position 1 \\\n" + " line --poll /proc/loadavg --position 2 \\\n" + " line --poll /proc/loadavg --position 3\n" ) << std::endl; std::exit(0); @@ -186,6 +199,23 @@ void Input::set_source (Input::Source source, const std::string &details) m_details = details; } +void Input::set_position (const std::string &txt) +{ + unsigned number; + auto [_,ec] = std::from_chars(txt.data(), txt.data()+txt.size(), number); + if (ec != std::errc{}) { + spdlog::error("failed to parse position from '{}': {}", + txt, std::make_error_code(ec).message()); + std::exit(1); + } + m_extract.set(number); +} + +void Input::set_regex (const std::string &txt) +{ + m_extract.set(std::regex(txt)); +} + void Input::set_color (const std::string &txt) { unsigned number; diff --git a/cli/config.hpp b/cli/config.hpp index df2b751..7be5e17 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -4,6 +4,8 @@ #include #include +#include "extract.hpp" + #include "blot.hpp" #include "spdlog/spdlog.h" @@ -22,6 +24,7 @@ class Input final { blot_plot_type m_plot_type{BLOT_LINE}; Source m_source{NONE}; std::string m_details{}; + Extract m_extract; blot_color m_plot_color; double m_interval{0}; @@ -57,6 +60,7 @@ class Input final { } const char * details() const { return m_details.c_str(); } + const Extract& extract() const { return m_extract; } blot_color plot_color() const { return m_plot_color; } double interval() const { return m_interval; } @@ -64,6 +68,8 @@ class Input final { operator bool() const; void set_source (Input::Source source, const std::string &details); + void set_position (const std::string &txt); + void set_regex (const std::string &txt); void set_color (const std::string &txt); void set_interval (double interval); void set_interval (const std::string &txt); diff --git a/cli/main.cpp b/cli/main.cpp index 605514f..f462c35 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -65,16 +65,14 @@ int main(int argc, char *argv[]) spdlog::trace("{}:{}: {}", input.details(), line->number, line->text); - double value; - auto [_,ec] = std::from_chars(line->text.data(), - line->text.data()+line->text.size(), value); - if (ec != std::errc{}) { - spdlog::error("failed to parse value from source {} line {} '{}': {}", - i, line->number, line->text, std::make_error_code(ec).message()); + auto result = input.extract().parse(line->text.data()); + if (!result.count) { + spdlog::error("failed to parse value from source {} line {} '{}'", + i, line->number, line->text); std::exit(1); } - plotter.add(i, line->number, value); + plotter.add(i, line->number, result.array[0]); } if (signaled) From 7849b72b624b0e8731ebadeaa044405f0221af24 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Mon, 28 Jul 2025 22:40:17 -0400 Subject: [PATCH 30/50] some hacky things for now --- cli/main.cpp | 16 +++++++++++----- cli/plotter.hpp | 5 ++++- include/blot_types.h | 1 + lib/blot_screen.c | 11 ++++++++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index f462c35..7724918 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -66,13 +66,19 @@ int main(int argc, char *argv[]) spdlog::trace("{}:{}: {}", input.details(), line->number, line->text); auto result = input.extract().parse(line->text.data()); - if (!result.count) { - spdlog::error("failed to parse value from source {} line {} '{}'", - i, line->number, line->text); - std::exit(1); + switch (result.count) { + case 0: + spdlog::error("failed to parse value from source {} line {} '{}'", i, line->number, line->text); + std::exit(1); + case 1: + plotter.add(i, line->number, result.array[0]); + break; + case 2: + default: + plotter.add(i, result.array[0], result.array[1]); + break; } - plotter.add(i, line->number, result.array[0]); } if (signaled) diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 3a1c428..935f4dc 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -38,8 +38,10 @@ class Plotter final { Blot::Figure fig; fig.set_axis_color(8); + #if 0 Blot::Dimensions term; fig.set_screen_size(term.cols, term.rows/2); + #endif for (size_t i=0; iflags & BLOT_RENDER_LEGEND_DETAILS) { + len = swprintf(p, end-p, L"%s%lc %s %s \tcount=%u\n", + colstr, symbol, COL_RESET, + lay->label, + lay->count); + } else { + len = swprintf(p, end-p, L"%s%lc %s %s\n", colstr, symbol, COL_RESET, lay->label); + } RETURN_ERROR(len<0, false, error, "swprintf"); p += len; } From 40bea725c684ad9782193f89b84ac91e8b198d0c Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 09:50:21 -0400 Subject: [PATCH 31/50] timing for plot --- cli/config.cpp | 3 ++- cli/config.hpp | 3 +++ cli/main.cpp | 3 ++- cli/plotter.hpp | 41 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 2f32e63..f8133ed 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -27,7 +27,8 @@ Config::Config(int argc, char *argv[]) }).doc("Enable verbose output"), clipp::option("--debug").call([]{ spdlog::set_level(spdlog::level::trace); - }).doc("Enable debug output") + }).doc("Enable debug output"), + clipp::option("--timing").set(m_timing).doc("Show timing statitiscs") ); auto cli_output = "Output:" % ( diff --git a/cli/config.hpp b/cli/config.hpp index 7be5e17..562989a 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -84,6 +84,7 @@ class Config final { const static blot_color m_first_color{9}; std::vector m_inputs; bool m_using_interval{}; + bool m_timing{}; public: explicit Config(int argc, char *argv[]); @@ -102,6 +103,8 @@ class Config final { const Input& input(size_t n) const { return m_inputs.at(n); } Input& input(size_t n) { return m_inputs.at(n); } + + bool timing() const { return m_timing; } }; diff --git a/cli/main.cpp b/cli/main.cpp index 7724918..846bf90 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -96,7 +96,8 @@ int main(int argc, char *argv[]) // as it will complete the sleep even if SIGINT is raised. double useconds = idle * 1000000; - usleep(useconds); + if (useconds) + usleep(useconds); } } diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 935f4dc..332cebf 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -19,6 +19,16 @@ class Plotter final { size_t m_max_layers{}; size_t m_data_history{}; + struct { + size_t count{}; + double init{}; + double add{}; + double render{}; + double print{}; + double total{}; + } m_stats; + + public: explicit Plotter(const Config &config) : m_config(config), m_max_layers(m_config.inputs()) @@ -31,10 +41,14 @@ class Plotter final { m_data[layer].m_ys.push_back(y); } - void plot() const { + void plot() { setlocale(LC_CTYPE, ""); + bool timing = m_config.timing(); + + double t_start = timing ? blot_double_time() : 0; + Blot::Figure fig; fig.set_axis_color(8); @@ -43,6 +57,8 @@ class Plotter final { fig.set_screen_size(term.cols, term.rows/2); #endif + double t_init = timing ? blot_double_time() : 0; + for (size_t i=0; i Date: Tue, 29 Jul 2025 10:34:20 -0400 Subject: [PATCH 32/50] display timing interval --- cli/config.cpp | 71 +++++++++++++++++++++++++------------------------ cli/config.hpp | 9 ++++--- cli/main.cpp | 26 +++++++++++++++--- cli/plotter.hpp | 2 +- cli/reader.cpp | 7 +---- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index f8133ed..0b84e84 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -28,7 +28,10 @@ Config::Config(int argc, char *argv[]) clipp::option("--debug").call([]{ spdlog::set_level(spdlog::level::trace); }).doc("Enable debug output"), - clipp::option("--timing").set(m_timing).doc("Show timing statitiscs") + clipp::option("--timing").set(m_show_timing).doc("Show timing statitiscs"), + clipp::option("-i", "--interval").doc("Display interval in seconds") + & clipp::value("seconds") + .call([&](const char *txt) { m_display_interval = txt; }) ); auto cli_output = "Output:" % ( @@ -66,49 +69,47 @@ Config::Config(int argc, char *argv[]) .doc("Add a bar plot") ), - /* modifiers - no heading because there is a --help bug in clipp */ - - one_of( + "Plot data source:" % one_of( /* source data from file or command */ - clipp::option("-R", "--read").doc("Read file to the end, each line is a record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); }), - clipp::option("-F", "--follow").doc("Read file waiting for more, each line is a record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); }), - clipp::option("-P", "--poll").doc("Read file at interval, each read is one record") - & clipp::value("file") - .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); }), - clipp::option("-X", "--exec").doc("Run command, each line is a record") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); }), - clipp::option("-W", "--watch").doc("Run command at interval, each read is one record") - & clipp::value("command") - .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); }) + (clipp::option("-R", "--read") & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::READ, f); })) + .doc("Read file to the end, each line is a record"), + (clipp::option("-F", "--follow") & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::FOLLOW, f); })) + .doc("Read file waiting for more, each line is a record"), + (clipp::option("-P", "--poll") & clipp::value("file") + .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); })) + .doc("Read file at interval, each read is one record"), + (clipp::option("-X", "--exec") & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); })) + .doc("Run command, each line is a record"), + (clipp::option("-W", "--watch") & clipp::value("command") + .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); })) + .doc("Run command at interval, each read is one record") ), - ( + "Data source parsing:" % ( /* how to extract values from lines */ - clipp::option("-p", "--position").doc("Use the Nth number from input line") - & clipp::value("number") - .call([&](const char *txt) { m_inputs.back().set_position(txt); }), - clipp::option("-r", "--regex").doc("Regex to match numbers from input line") - & clipp::value("regex") - .call([&](const char *txt) { m_inputs.back().set_regex(txt); }) + (clipp::option("-p", "--position") & clipp::value("number") + .call([&](const char *txt) { m_inputs.back().set_position(txt); })) + .doc("Use the Nth number from input line"), + (clipp::option("-r", "--regex") & clipp::value("regex") + .call([&](const char *txt) { m_inputs.back().set_regex(txt); })) + .doc("Regex to match numbers from input line") ), - ( + "Plot modifiers:" % ( /* augment this plots characteristics */ - clipp::option("-c", "--color").doc("Set plot color (1..255)") - & clipp::value("color") - .call([&](const char *txt) { m_inputs.back().set_color(txt); }), - clipp::option("-i", "--interval").doc("Set interval in seconds") - & clipp::value("seconds") - .call([&](const char *txt) { m_inputs.back().set_interval(txt); }) + (clipp::option("-c", "--color") & clipp::value("color") + .call([&](const char *txt) { m_inputs.back().set_color(txt); })) + .doc("Set plot color (1..255)"), + (clipp::option("-i", "--interval") & clipp::value("seconds") + .call([&](const char *txt) { m_inputs.back().set_interval(txt); })) + .doc("Set sampling interval in seconds") ) ), @@ -134,7 +135,7 @@ Config::Config(int argc, char *argv[]) if (show_help) { Blot::Dimensions term; - unsigned doc_start = std::min(30u, term.cols/2); + unsigned doc_start = std::min(40u, term.cols/2); auto fmt = clipp::doc_formatting{} .indent_size(4) .first_column(4) @@ -186,7 +187,7 @@ Config::Config(int argc, char *argv[]) spdlog::error("cannot mix interval and non-interval sources"); std::exit(1); } - m_using_interval = with_interval; + m_using_input_interval = with_interval; } void Input::set_source (Input::Source source, const std::string &details) diff --git a/cli/config.hpp b/cli/config.hpp index 562989a..eb24c18 100644 --- a/cli/config.hpp +++ b/cli/config.hpp @@ -83,8 +83,9 @@ class Config final { enum output_type { ASCII, UNICODE, BRAILLE } m_output_type; const static blot_color m_first_color{9}; std::vector m_inputs; - bool m_using_interval{}; - bool m_timing{}; + bool m_display_interval{1}; + bool m_using_input_interval{}; + bool m_show_timing{}; public: explicit Config(int argc, char *argv[]); @@ -104,7 +105,9 @@ class Config final { const Input& input(size_t n) const { return m_inputs.at(n); } Input& input(size_t n) { return m_inputs.at(n); } - bool timing() const { return m_timing; } + bool using_input_interval() const { return m_using_input_interval; } + double display_interval() const { return m_display_interval; } + bool show_timing() const { return m_show_timing; } }; diff --git a/cli/main.cpp b/cli/main.cpp index 846bf90..83fd196 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -48,6 +48,8 @@ int main(int argc, char *argv[]) Plotter plotter(config); + double next_display = 0; + while(keep_going()) { for (size_t i=0; i next_display) { + next_display = now + interval; + do_show_plot = true; + } + } + /* wait for the next time we have data */ auto idle_view = readers | std::views::transform([](const auto& reader) { return reader->idle(); }); double idle = std::ranges::min(idle_view); if (idle > 0) { + do_show_plot = true; + sleep_after_seconds = idle; + } + + if (do_show_plot) plotter.plot(); + if (double useconds = sleep_after_seconds * 1000000) { + // unfortunately std::this_thread::sleep_for() cannot be used, // as it will complete the sleep even if SIGINT is raised. - double useconds = idle * 1000000; - if (useconds) - usleep(useconds); + usleep(useconds); } } diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 332cebf..1067be3 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -45,7 +45,7 @@ class Plotter final { setlocale(LC_CTYPE, ""); - bool timing = m_config.timing(); + bool timing = m_config.show_timing(); double t_start = timing ? blot_double_time() : 0; diff --git a/cli/reader.cpp b/cli/reader.cpp index 7d3297b..8b8a54f 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -31,12 +31,7 @@ class FileReader : public Reader { spdlog::error("{}: does not exist", m_path.string()); std::exit(1); } - if (fs::is_character_file(m_path) || fs::is_fifo(m_path)) { - if (follow) { - spdlog::error("{}: fifo/chardev cannot be used with follow", m_path.string()); - std::exit(1); - } - } else if (!fs::is_regular_file(m_path)) { + if (!fs::is_character_file(m_path) && !fs::is_fifo(m_path) && !fs::is_regular_file(m_path)) { spdlog::error("{}: is not a file/fifo/chardev", m_path.string()); std::exit(1); } From 6f5a83a94a7ad86021ae16e4c860dc3d887501a9 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 10:34:45 -0400 Subject: [PATCH 33/50] make Blot::Exception work with std::exception mechanisms --- include/blot.hpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/include/blot.hpp b/include/blot.hpp index 8c38ad2..32f3b8e 100644 --- a/include/blot.hpp +++ b/include/blot.hpp @@ -13,8 +13,8 @@ namespace Blot { /* the C code already allocated a "GError", this class just - * carries it throught the exception mechanism, then cleans up. */ -class Exception final { + * carries it through the exception mechanism, then cleans up. */ +class Exception final : public std::exception { protected: GError *m_error; public: @@ -48,7 +48,10 @@ class Exception final { operator bool() const { return m_error != nullptr; } int code() const { return m_error ? m_error->code : 0; } std::string str() const { if (m_error) return m_error->message; return {}; } - const char * c_str() const { return m_error ? m_error->message : nullptr; } + + const char * what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW override { + return m_error ? m_error->message : nullptr; + } }; From 71b39e657ff8397423099ddd14d9445e5badd9c8 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 10:38:01 -0400 Subject: [PATCH 34/50] do not try to plot w/o any data --- cli/main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/main.cpp b/cli/main.cpp index 83fd196..4d188a5 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -52,6 +52,8 @@ int main(int argc, char *argv[]) while(keep_going()) { + bool have_data = false; + for (size_t i=0; inumber, line->text); std::exit(1); case 1: + have_data = true; plotter.add(i, line->number, result.array[0]); break; case 2: default: + have_data = true; plotter.add(i, result.array[0], result.array[1]); break; } @@ -109,7 +113,7 @@ int main(int argc, char *argv[]) sleep_after_seconds = idle; } - if (do_show_plot) + if (do_show_plot && have_data) plotter.plot(); if (double useconds = sleep_after_seconds * 1000000) { From 40d3948a4742a12f631679754fea695a2e70b986 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 10:51:22 -0400 Subject: [PATCH 35/50] use COLUMNS/LINES from environment for sizing --- include/blot_utils.h | 2 ++ lib/blot_terminal.c | 9 +++++++++ lib/blot_utils.c | 15 +++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/include/blot_utils.h b/include/blot_utils.h index d1ec72f..50c0a76 100644 --- a/include/blot_utils.h +++ b/include/blot_utils.h @@ -84,3 +84,5 @@ #if !__has_builtin(__builtin_constant_p) #define __builtin_constant_p(x) (0) #endif + +BLOT_EXTERN unsigned blot_env_to_uint(const char *name, unsigned dflt); diff --git a/lib/blot_terminal.c b/lib/blot_terminal.c index 7226c00..e01ec06 100644 --- a/lib/blot_terminal.c +++ b/lib/blot_terminal.c @@ -23,6 +23,7 @@ bool blot_terminal_set_size(blot_dimensions dims, GError **error) return true; } + // caller should call blot_terminal_set_size() if the terminal size cannot be determine bool blot_terminal_get_size(blot_dimensions *dims, GError **error) { @@ -33,6 +34,14 @@ bool blot_terminal_get_size(blot_dimensions *dims, GError **error) return true; } + dims->cols = blot_env_to_uint("COLUMNS", 0); + dims->rows = blot_env_to_uint("LINES", 0); + + if (dims->cols && dims->rows) { + blot_fixed_dims = *dims; + return true; + } + struct winsize w = {}; int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); RETURN_ERROR(rc<0,false,error,"failed to get terminal size"); diff --git a/lib/blot_utils.c b/lib/blot_utils.c index 213a4fb..9d6c101 100644 --- a/lib/blot_utils.c +++ b/lib/blot_utils.c @@ -1,2 +1,17 @@ /* blot: various utility macros */ /* vim: set noet sw=8 ts=8 tw=120: */ + +#include "blot_utils.h" + +unsigned blot_env_to_uint(const char *name, unsigned dflt) +{ + const char *txt = getenv(name); + if (!txt || !*txt) + return dflt; + + unsigned long num = strtoul(txt, NULL, 0); + if (num == ULONG_MAX) + return dflt; + + return num; +} From b0ec274a1b32904245ed4355a21dc0e113a8ed72 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 11:27:29 -0400 Subject: [PATCH 36/50] do not generate color escapes if they are not needed --- lib/blot_screen.c | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/blot_screen.c b/lib/blot_screen.c index 1e15a57..ca84f89 100644 --- a/lib/blot_screen.c +++ b/lib/blot_screen.c @@ -97,7 +97,8 @@ static bool blot_screen_plot_cans(blot_screen *scr, blot_canvas *const*cans, GError **error) { - bool reset_after = false; + const int PREV_COLOR_UNUSED = -1; + int prev_color = PREV_COLOR_UNUSED; wchar_t *p = scr->data + scr->data_used, *end = scr->data + scr->data_size; int len; @@ -144,11 +145,13 @@ static bool blot_screen_plot_cans(blot_screen *scr, /* apply Y-axis color */ - const char *colstr = fg(y_axs->color); - len = swprintf(p, end-p, L"%s", colstr); - RETURN_ERROR(len<0, false, error, "swprintf"); - p += len; - reset_after = true; + if (!(scr->flags & BLOT_RENDER_NO_COLOR) && prev_color != y_axs->color) { + const char *colstr = fg(y_axs->color); + len = swprintf(p, end-p, L"%s", colstr); + RETURN_ERROR(len<0, false, error, "swprintf"); + p += len; + prev_color = y_axs->color; + } /* the next dsp_lft characters are the Y-axis label + line */ @@ -191,12 +194,12 @@ static bool blot_screen_plot_cans(blot_screen *scr, } if (top_cell) { - if (!(scr->flags & BLOT_RENDER_NO_COLOR)) { + if (!(scr->flags & BLOT_RENDER_NO_COLOR) && prev_color != top_col) { const char *colstr = fg(top_col); len = swprintf(p, end-p, L"%s", colstr); RETURN_ERROR(len<0, false, error, "swprintf"); p += len; - reset_after = true; + prev_color = top_col; } wch = top_cell; } @@ -206,11 +209,11 @@ static bool blot_screen_plot_cans(blot_screen *scr, g_assert_cmpuint((uintptr_t)p, <, (uintptr_t)end); } - if (reset_after) { + if (prev_color != PREV_COLOR_UNUSED) { len = swprintf(p, end-p, L"%s", COL_RESET); RETURN_ERROR(len<0, false, error, "swprintf"); p += len; - reset_after = false; + prev_color = PREV_COLOR_UNUSED; } *(p++) = L'\n'; @@ -222,11 +225,13 @@ static bool blot_screen_plot_cans(blot_screen *scr, if (!draw_x_axis) goto done_bot_line; - const char *colstr = fg(x_axs->color); - len = swprintf(p, end-p, L"%s", colstr); - RETURN_ERROR(len<0, false, error, "swprintf"); - p += len; - reset_after = true; + if (!(scr->flags & BLOT_RENDER_NO_COLOR) && prev_color != x_axs->color) { + const char *colstr = fg(x_axs->color); + len = swprintf(p, end-p, L"%s", colstr); + RETURN_ERROR(len<0, false, error, "swprintf"); + p += len; + prev_color = x_axs->color; + } if (s_y == dsp_bot) { /* this line is the X-axis line */ @@ -275,11 +280,11 @@ static bool blot_screen_plot_cans(blot_screen *scr, } done_bot_line: - if (reset_after) { + if (prev_color != PREV_COLOR_UNUSED) { len = swprintf(p, end-p, L"%s", COL_RESET); RETURN_ERROR(len<0, false, error, "swprintf"); p += len; - reset_after = false; + prev_color = PREV_COLOR_UNUSED; } *(p++) = L'\n'; From 0bb066370cbe7e043199bdc830f50225ed7a275c Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 11:46:47 -0400 Subject: [PATCH 37/50] fix stall in release build mode --- cli/main.cpp | 6 +----- cli/plotter.hpp | 4 ++++ cli/reader.cpp | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index 4d188a5..a456001 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -52,8 +52,6 @@ int main(int argc, char *argv[]) while(keep_going()) { - bool have_data = false; - for (size_t i=0; inumber, line->text); std::exit(1); case 1: - have_data = true; plotter.add(i, line->number, result.array[0]); break; case 2: default: - have_data = true; plotter.add(i, result.array[0], result.array[1]); break; } @@ -113,7 +109,7 @@ int main(int argc, char *argv[]) sleep_after_seconds = idle; } - if (do_show_plot && have_data) + if (do_show_plot && plotter.have_data()) plotter.plot(); if (double useconds = sleep_after_seconds * 1000000) { diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 1067be3..67e1916 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -14,6 +14,7 @@ class Plotter final { std::vector m_ys; }; std::vector m_data; + size_t m_count{}; const Config &m_config; size_t m_max_layers{}; @@ -39,8 +40,11 @@ class Plotter final { void add(size_t layer, X x, Y y) { m_data[layer].m_xs.push_back(x); m_data[layer].m_ys.push_back(y); + m_count ++; } + bool have_data() const { return m_count > 0; } + void plot() { setlocale(LC_CTYPE, ""); diff --git a/cli/reader.cpp b/cli/reader.cpp index 8b8a54f..01af6d0 100644 --- a/cli/reader.cpp +++ b/cli/reader.cpp @@ -181,6 +181,9 @@ class FilePoller : public Reader { // used by Input::EXEC class ExecStreamReader : public Reader { + static constexpr const size_t g_reserve_space = 4096; + static constexpr const size_t g_read_chunk = 4096; + std::string m_command; FILE* m_pipe = nullptr; int m_fd = -1; @@ -203,6 +206,8 @@ class ExecStreamReader : public Reader { spdlog::error("failed to set non-blocking mode for command '{}': {}", m_command, std::strerror(errno)); std::exit(1); } + + m_buffer.reserve(g_reserve_space); } ~ExecStreamReader() override { @@ -235,7 +240,7 @@ class ExecStreamReader : public Reader { } // Read more data non-blockingly - char buf[4096]; + char buf[g_read_chunk]; ssize_t bytes_read = read(m_fd, buf, sizeof(buf)); if (bytes_read > 0) { m_buffer.append(buf, bytes_read); From 02550cebfe699392ab5eb1b3c4d8c635119c20ae Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 11:49:05 -0400 Subject: [PATCH 38/50] missing extract header --- cli/extract.hpp | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 cli/extract.hpp diff --git a/cli/extract.hpp b/cli/extract.hpp new file mode 100644 index 0000000..9a87529 --- /dev/null +++ b/cli/extract.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "spdlog/spdlog.h" +#include "fmt/format.h" + +class Extract { +protected: + using Var = std::variant; + Var m_var; + + template + static std::pair __parse(const char *text, const char *end) { + T value; + auto [ptr,ec] = std::from_chars(text, end, value); + if (ec != std::errc{}) { + spdlog::error("failed to parse number({}) from '{}': {}", + typeid(T).name(), text, + std::make_error_code(ec).message()); + std::exit(1); + } + return {value, ptr}; + } + + static const char * __advance(const char *text, const char *end) { + while (text < end && !(std::isdigit(*text) || *text == '.')) + text ++; + return text; + } + +public: + void set(const auto &thing) { + m_var = thing; + } + + template + struct ParseResult { + std::array array; + size_t count{}; + }; + + template + ParseResult parse(const char *text) const { + ParseResult result; + + if (std::holds_alternative(m_var)) { + + std::string str(text); + auto &re = std::get(m_var); + + spdlog::trace("text={}", text); + + std::smatch matches; + if (!std::regex_search(str, matches, re)) + return result; + + spdlog::trace(" matches={}", matches.size()); + + for (size_t i = 1; i < matches.size(); ++i) { + const char *start = text + matches.position(i); + const char *end = start + matches.length(i); + auto [value, _] = __parse(start, end); + result.array[i-1] = value; + spdlog::trace(" array[{}] = {}", i-1, value); + } + result.count = matches.size() - 1; + + return result; + } + + unsigned start_position = 0; + if (std::holds_alternative(m_var)) + start_position = std::get(m_var); + + spdlog::trace("text={} position={}", text, start_position); + + const char *end = text+std::strlen(text); + text = __advance(text, end); + + unsigned n=0; + while (text(text, end); + if (n >= start_position) { + spdlog::trace(" array[{}] = {}", result.count, value); + result.array[result.count++] = value; + } + ++n; + text = __advance(ptr, end); + } + + return result; + } + +}; + From c67b6d48a9c61deaf27d262008c88aa95f425772 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:36:10 -0400 Subject: [PATCH 39/50] throw exception with namespace --- include/blot.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/blot.hpp b/include/blot.hpp index 32f3b8e..58fb75b 100644 --- a/include/blot.hpp +++ b/include/blot.hpp @@ -64,7 +64,7 @@ class Exception final : public std::exception { }) #else #define BLOT_THROW(code,format,args...) \ - throw Exception(g_error_new(G_UNIX_ERROR, code, "%s:%u:%s: " format, \ + throw Blot::Exception(g_error_new(G_UNIX_ERROR, code, "%s:%u:%s: " format, \ basename(__FILE__), __LINE__, __func__, ##args)) #endif From d42a9e76621effb6c5e39d4aff728cd2b4c48a63 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:36:19 -0400 Subject: [PATCH 40/50] fix margin calculation --- lib/blot_figure.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/blot_figure.c b/lib/blot_figure.c index 4513077..f8e4ac6 100644 --- a/lib/blot_figure.c +++ b/lib/blot_figure.c @@ -272,20 +272,25 @@ static blot_margins blot_figure_finalize_margins(const blot_figure *fig, /* add 3 digits of precision */ mrg.left += 4; - - // Ensure margins don't exceed dimensions - mrg.left = min_t(unsigned, mrg.left, dim->cols / 2); - mrg.bottom = min_t(unsigned, mrg.bottom, dim->rows / 2); - mrg.right = min_t(unsigned, mrg.right, dim->cols / 2); - mrg.top = min_t(unsigned, mrg.top, dim->rows / 2); } + if (flags & BLOT_RENDER_LEGEND_ABOVE) + mrg.top += fig->layer_count; + if (flags & BLOT_RENDER_LEGEND_BELOW) + mrg.top += fig->layer_count; + #if 0 - /* for testing only */ + /* don't use the top or right edge */ mrg.right += 1; mrg.top += 1; #endif + // Ensure margins don't exceed dimensions + mrg.left = min_t(unsigned, mrg.left, dim->cols / 2); + mrg.bottom = min_t(unsigned, mrg.bottom, dim->rows / 2); + mrg.right = min_t(unsigned, mrg.right, dim->cols / 2); + mrg.top = min_t(unsigned, mrg.top, dim->rows / 2); + return mrg; } From 3cd7ddca88ded02c54c0df4eac6cfe9beee42cf3 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:36:35 -0400 Subject: [PATCH 41/50] rewrite how we extract numbers from a line --- cli/extract.hpp | 186 ++++++++++++++++++++++++++++++++++++------------ cli/main.cpp | 31 ++++---- cli/plotter.hpp | 5 +- 3 files changed, 160 insertions(+), 62 deletions(-) diff --git a/cli/extract.hpp b/cli/extract.hpp index 9a87529..d56ba8c 100644 --- a/cli/extract.hpp +++ b/cli/extract.hpp @@ -1,18 +1,25 @@ #pragma once #include -#include #include #include -#include +#include #include +#include "blot.hpp" #include "spdlog/spdlog.h" #include "fmt/format.h" class Extract { +public: + template + struct ParseResult { + X x; + Y y; + }; + protected: - using Var = std::variant; + using Var = std::variant, std::regex>; Var m_var; template @@ -28,74 +35,159 @@ class Extract { return {value, ptr}; } - static const char * __advance(const char *text, const char *end) { + // advance to fist cluster of number digits + static const char * __find_start(const char *text, const char *end) { while (text < end && !(std::isdigit(*text) || *text == '.')) text ++; return text; } -public: - void set(const auto &thing) { - m_var = thing; + // skip over the next cluster of number digits + static const char * __skip_over(const char *text, const char *end) { + while (text < end && (std::isdigit(*text) || *text == '.')) + text ++; + return text; } - template - struct ParseResult { - std::array array; - size_t count{}; - }; + template + static std::optional> parse_y_position(size_t line, const char *text, unsigned y_position) { + spdlog::trace("line={} text={} x_position={} y_position", + line, text, y_position); - template - ParseResult parse(const char *text) const { - ParseResult result; + const char *end = text+std::strlen(text); + text = __find_start(text, end); + auto pos = 1; - if (std::holds_alternative(m_var)) { + while (text < end && pos < y_position) { + text = __skip_over(text, end); + text = __find_start(text, end); + pos ++; + } - std::string str(text); - auto &re = std::get(m_var); + if (!y_position || y_position == pos) { + auto [yvalue, _] = __parse(text, end); + + return ParseResult(line, yvalue); + } + + return {}; + } + + template + static std::optional> parse_xy_position(const char *text, unsigned x_position, unsigned y_position) { + + spdlog::trace("text={} x_position={} y_position", + text, x_position, y_position); - spdlog::trace("text={}", text); + const char *end = text+std::strlen(text); + text = __find_start(text, end); + auto pos = 1; + + auto first_position = std::min(x_position, y_position); + auto last_position = std::max(x_position, y_position); + + while (text < end && pos < first_position) { + text = __skip_over(text, end); + text = __find_start(text, end); + pos ++; + } - std::smatch matches; - if (!std::regex_search(str, matches, re)) - return result; + X xvalue{}; + Y yvalue{}; + unsigned have = 0; - spdlog::trace(" matches={}", matches.size()); + while (text < end && pos < last_position) { + + const char *next = nullptr; + + if (pos == x_position) { + auto [xvalue, xptr] = __parse(text, end); + have ++; + next = xptr; + } - for (size_t i = 1; i < matches.size(); ++i) { - const char *start = text + matches.position(i); - const char *end = start + matches.length(i); - auto [value, _] = __parse(start, end); - result.array[i-1] = value; - spdlog::trace(" array[{}] = {}", i-1, value); + if (pos == y_position) { + auto [yvalue, yptr] = __parse(text, end); + have ++; + next = yptr; } - result.count = matches.size() - 1; - return result; + if (have == 2) + return ParseResult(xvalue, yvalue); + + if (!next) + next = __skip_over(text, end); + + text = __find_start(next, end); + pos ++; } - unsigned start_position = 0; - if (std::holds_alternative(m_var)) - start_position = std::get(m_var); + return {}; + } - spdlog::trace("text={} position={}", text, start_position); + template + static std::optional> parse_regex(size_t line, const char *text, const std::regex &re) { - const char *end = text+std::strlen(text); - text = __advance(text, end); - - unsigned n=0; - while (text(text, end); - if (n >= start_position) { - spdlog::trace(" array[{}] = {}", result.count, value); - result.array[result.count++] = value; + std::string str(text); + + spdlog::trace("text={}", text); + + std::smatch matches; + if (!std::regex_search(str, matches, re)) + return {}; + + spdlog::trace(" matches={}", matches.size()); + + switch (matches.size()) { + case 0: + return {}; + case 1: { + const char *start = text + matches.position(1); + const char *end = start + matches.length(1); + auto [value, _] = __parse(start, end); + + return ParseResult(line, value); + } + default: { + const char *xstart = text + matches.position(1); + const char *xend = xstart + matches.length(1); + auto [xvalue, _1] = __parse(xstart, xend); + + const char *ystart = text + matches.position(1); + const char *yend = ystart + matches.length(1); + auto [yvalue, _2] = __parse(ystart, yend); + + return ParseResult(xvalue, yvalue); } - ++n; - text = __advance(ptr, end); } + } + - return result; + +public: + void set(const auto &thing) { + m_var = thing; } + template + std::optional> parse(size_t line, const char *text) const { + + if (std::holds_alternative(m_var)) { + auto &re = std::get(m_var); + return parse_regex(line, text, re); + } + + if (std::holds_alternative>(m_var)) { + auto [x,y] = std::get>(m_var); + return parse_xy_position(text, x, y); + } + + if (std::holds_alternative(m_var)) { + auto y = std::get(m_var); + return parse_y_position(line, text, y); + } + + BLOT_THROW(EINVAL,"internal error"); + } }; diff --git a/cli/main.cpp b/cli/main.cpp index a456001..84195f0 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -67,20 +67,15 @@ int main(int argc, char *argv[]) spdlog::trace("{}:{}: {}", input.details(), line->number, line->text); - auto result = input.extract().parse(line->text.data()); - switch (result.count) { - case 0: - spdlog::error("failed to parse value from source {} line {} '{}'", i, line->number, line->text); - std::exit(1); - case 1: - plotter.add(i, line->number, result.array[0]); - break; - case 2: - default: - plotter.add(i, result.array[0], result.array[1]); - break; + auto result = input.extract().parse(line->number, + line->text.data()); + if (result.has_value()) { + plotter.add(i, result->x, result->y); + } else { + spdlog::error("failed to parse value from source {} line {} '{}'", + i, line->number, line->text); + std::exit(1); } - } if (signaled) @@ -101,8 +96,18 @@ int main(int argc, char *argv[]) /* wait for the next time we have data */ + #if 0 + /* Debian 12 has an older standard library that cannot do this */ auto idle_view = readers | std::views::transform([](const auto& reader) { return reader->idle(); }); double idle = std::ranges::min(idle_view); + #else + double idle = 0; + for (size_t i=0; iidle(); + idle = i ? std::min(idle, reader_idle) : reader_idle; + } + #endif if (idle > 0) { do_show_plot = true; diff --git a/cli/plotter.hpp b/cli/plotter.hpp index 67e1916..ef36c4e 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -88,7 +88,7 @@ class Plotter final { spdlog::debug("rendered size = %zu", txt_size); - printf("%ls\n", txt); + printf("%ls", txt); fflush(stdout); double t_print = timing ? blot_double_time() : 0; @@ -101,13 +101,14 @@ class Plotter final { m_stats.print += t_print - t_render; m_stats.total += t_print - t_start; - fmt::println("time: count={} init={:.6f} add={:.6f} render={:.6f} print={:.6f} [{:.6f}]", + fmt::print("time: count={} init={:.6f} add={:.6f} render={:.6f} print={:.6f} [{:.6f}]", m_stats.count, m_stats.init / m_stats.count, m_stats.add / m_stats.count, m_stats.render / m_stats.count, m_stats.print / m_stats.count, m_stats.total / m_stats.count); + std::flush(std::cout); } } }; From 9e061ffcdb2764ed3354b453d96437335c66254e Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:41:47 -0400 Subject: [PATCH 42/50] missing CR --- cli/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/main.cpp b/cli/main.cpp index 84195f0..26b5295 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -127,4 +127,5 @@ int main(int argc, char *argv[]) } plotter.plot(); + std::puts(""); } From 726d6b094103a702d34bf7c6ff8e9f7c82b137b6 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:41:56 -0400 Subject: [PATCH 43/50] throw exception with namespace, in Debug --- include/blot.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/blot.hpp b/include/blot.hpp index 58fb75b..7215c66 100644 --- a/include/blot.hpp +++ b/include/blot.hpp @@ -60,7 +60,7 @@ class Exception final : public std::exception { GError *error = g_error_new(G_UNIX_ERROR, code, "%s:%u:%s: " format, \ basename(__FILE__), __LINE__, __func__, ##args); \ std::cerr << error->message << std::endl; \ - throw Exception(error); \ + throw Blot::Exception(error); \ }) #else #define BLOT_THROW(code,format,args...) \ From 0e59124a6bcd6444f340b66c2e89983643204a97 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 13:45:29 -0400 Subject: [PATCH 44/50] cast line to double to keep gcc happy --- cli/extract.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/extract.hpp b/cli/extract.hpp index d56ba8c..d0b9806 100644 --- a/cli/extract.hpp +++ b/cli/extract.hpp @@ -67,7 +67,7 @@ class Extract { if (!y_position || y_position == pos) { auto [yvalue, _] = __parse(text, end); - return ParseResult(line, yvalue); + return ParseResult{X(line), yvalue}; } return {}; @@ -113,7 +113,7 @@ class Extract { } if (have == 2) - return ParseResult(xvalue, yvalue); + return ParseResult{xvalue, yvalue}; if (!next) next = __skip_over(text, end); @@ -146,7 +146,7 @@ class Extract { const char *end = start + matches.length(1); auto [value, _] = __parse(start, end); - return ParseResult(line, value); + return ParseResult{X(line), value}; } default: { const char *xstart = text + matches.position(1); @@ -157,7 +157,7 @@ class Extract { const char *yend = ystart + matches.length(1); auto [yvalue, _2] = __parse(ystart, yend); - return ParseResult(xvalue, yvalue); + return ParseResult{xvalue, yvalue}; } } } From 8911ebc14cd952fe6d1d39772c3b449770f1e38e Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 15:08:38 -0400 Subject: [PATCH 45/50] parse 1 or 2 positions from pos parameter --- cli/config.cpp | 56 +++++++++++++++++++++++++++++++++++-------------- cli/extract.hpp | 18 +++++++++------- cli/plotter.hpp | 6 ++++-- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index 0b84e84..ae2788c 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "blot.hpp" @@ -29,9 +30,9 @@ Config::Config(int argc, char *argv[]) spdlog::set_level(spdlog::level::trace); }).doc("Enable debug output"), clipp::option("--timing").set(m_show_timing).doc("Show timing statitiscs"), - clipp::option("-i", "--interval").doc("Display interval in seconds") - & clipp::value("seconds") - .call([&](const char *txt) { m_display_interval = txt; }) + (clipp::option("-i", "--interval") & clipp::value("sec") + .call([&](const char *txt) { m_display_interval = txt; })) + .doc("Display interval in seconds") ); auto cli_output = "Output:" % ( @@ -81,10 +82,10 @@ Config::Config(int argc, char *argv[]) (clipp::option("-P", "--poll") & clipp::value("file") .call([&](const char *f) { m_inputs.back().set_source(Input::POLL, f); })) .doc("Read file at interval, each read is one record"), - (clipp::option("-X", "--exec") & clipp::value("command") + (clipp::option("-X", "--exec") & clipp::value("cmd") .call([&](const char *x) { m_inputs.back().set_source(Input::EXEC, x); })) .doc("Run command, each line is a record"), - (clipp::option("-W", "--watch") & clipp::value("command") + (clipp::option("-W", "--watch") & clipp::value("cmd") .call([&](const char *x) { m_inputs.back().set_source(Input::WATCH, x); })) .doc("Run command at interval, each read is one record") ), @@ -92,9 +93,9 @@ Config::Config(int argc, char *argv[]) "Data source parsing:" % ( /* how to extract values from lines */ - (clipp::option("-p", "--position") & clipp::value("number") + (clipp::option("-p", "--position") & clipp::value("y-pos|x-pos,y-pos") .call([&](const char *txt) { m_inputs.back().set_position(txt); })) - .doc("Use the Nth number from input line"), + .doc("Find numbers in input line, pick 1 or 2 positions for X and Y values"), (clipp::option("-r", "--regex") & clipp::value("regex") .call([&](const char *txt) { m_inputs.back().set_regex(txt); })) .doc("Regex to match numbers from input line") @@ -107,7 +108,7 @@ Config::Config(int argc, char *argv[]) (clipp::option("-c", "--color") & clipp::value("color") .call([&](const char *txt) { m_inputs.back().set_color(txt); })) .doc("Set plot color (1..255)"), - (clipp::option("-i", "--interval") & clipp::value("seconds") + (clipp::option("-i", "--interval") & clipp::value("sec") .call([&](const char *txt) { m_inputs.back().set_interval(txt); })) .doc("Set sampling interval in seconds") ) @@ -135,7 +136,7 @@ Config::Config(int argc, char *argv[]) if (show_help) { Blot::Dimensions term; - unsigned doc_start = std::min(40u, term.cols/2); + unsigned doc_start = std::min(32u, term.cols/2); auto fmt = clipp::doc_formatting{} .indent_size(4) .first_column(4) @@ -203,14 +204,37 @@ void Input::set_source (Input::Source source, const std::string &details) void Input::set_position (const std::string &txt) { - unsigned number; - auto [_,ec] = std::from_chars(txt.data(), txt.data()+txt.size(), number); - if (ec != std::errc{}) { - spdlog::error("failed to parse position from '{}': {}", - txt, std::make_error_code(ec).message()); - std::exit(1); + + auto result = std::views::split(txt, ',') + | std::views::transform([](auto&& sr) { + std::string_view sv{sr.begin(), sr.end()}; + + unsigned number; + auto [_,ec] = std::from_chars(sv.begin(), sv.end(), number); + if (ec != std::errc{}) { + spdlog::error("failed to parse position from '{}': {}", + sv, std::make_error_code(ec).message()); + std::exit(1); + } + return number; + }); + + std::vector positions(result.begin(), result.end()); + + printf("got %zu positions\n", positions.size()); + + switch (positions.size()) { + case 1: + m_extract.set(positions[0]); + break; + case 2: + m_extract.set(std::pair{positions[0], positions[1]}); + break; + default: + spdlog::error("unexpected count ({}) of positions found in '{}'", + positions.size(), txt); + std::exit(1); } - m_extract.set(number); } void Input::set_regex (const std::string &txt) diff --git a/cli/extract.hpp b/cli/extract.hpp index d0b9806..9ae10e9 100644 --- a/cli/extract.hpp +++ b/cli/extract.hpp @@ -51,7 +51,7 @@ class Extract { template static std::optional> parse_y_position(size_t line, const char *text, unsigned y_position) { - spdlog::trace("line={} text={} x_position={} y_position", + spdlog::trace("line={} text='{}' x_position={} y_position", line, text, y_position); const char *end = text+std::strlen(text); @@ -76,7 +76,7 @@ class Extract { template static std::optional> parse_xy_position(const char *text, unsigned x_position, unsigned y_position) { - spdlog::trace("text={} x_position={} y_position", + spdlog::trace("text='{}' x_position={} y_position={}", text, x_position, y_position); const char *end = text+std::strlen(text); @@ -96,20 +96,22 @@ class Extract { Y yvalue{}; unsigned have = 0; - while (text < end && pos < last_position) { + while (text < end && pos <= last_position) { const char *next = nullptr; if (pos == x_position) { - auto [xvalue, xptr] = __parse(text, end); + auto [val, ptr] = __parse(text, end); have ++; - next = xptr; + xvalue = val; + next = ptr; } if (pos == y_position) { - auto [yvalue, yptr] = __parse(text, end); + auto [val, ptr] = __parse(text, end); have ++; - next = yptr; + yvalue = val; + next = ptr; } if (have == 2) @@ -130,7 +132,7 @@ class Extract { std::string str(text); - spdlog::trace("text={}", text); + spdlog::trace("line={} text='{}'", line, text); std::smatch matches; if (!std::regex_search(str, matches, re)) diff --git a/cli/plotter.hpp b/cli/plotter.hpp index ef36c4e..08ed3f2 100644 --- a/cli/plotter.hpp +++ b/cli/plotter.hpp @@ -76,8 +76,10 @@ class Plotter final { blot_render_flags flags = BLOT_RENDER_BRAILLE | BLOT_RENDER_LEGEND_BELOW - | BLOT_RENDER_CLEAR - | BLOT_RENDER_LEGEND_DETAILS; + | BLOT_RENDER_CLEAR; + + if (timing) + flags = flags | BLOT_RENDER_LEGEND_DETAILS; Blot::Screen scr = fig.render(flags); From d3400dc93071a4d62606cb94c5e24ab2c785bca7 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 15:17:10 -0400 Subject: [PATCH 46/50] fix EXAMPLES --- cli/config.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/config.cpp b/cli/config.cpp index ae2788c..488c23e 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -143,15 +143,15 @@ Config::Config(int argc, char *argv[]) .doc_column(doc_start) .last_column(term.cols); std::cout << clipp::make_man_page(cli, m_self, fmt) - .append_section("EXAMPE", + .append_section("EXAMPLES", "\n" " blot --braille \\\n" - " line --color 10 --read x_y1_values \\\n" - " scatter --color 11 --read x_y2_values\n" + " line --color 10 --read x_y1_values -p 1,2 \\\n" + " scatter --color 11 --read x_y2_values -p 1,2\n" "\n" " blot --braille \\\n" - " line --color 10 --exec 'seq 1 100' \\\n" - " scatter --color 11 --read x_y_values\n" + " scatter --color 11 --read y_values \\\n" + " line --color 10 --exec 'seq 1 100'\n" "\n" " blot --braille \\\n" " line --poll /proc/loadavg --position 1 \\\n" From 99efb8589cc5fe7ff8d7530be421ba3218944e21 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 16:14:26 -0400 Subject: [PATCH 47/50] update README --- README.md | 150 ++++++++++++++++++++++++++++++++++- cli/config.cpp | 3 - images/blot-bar-read.png | Bin 0 -> 6079 bytes images/blot-line-follow.png | Bin 0 -> 4478 bytes images/blot-line-poll.png | Bin 0 -> 3790 bytes images/blot-scatter-exec.png | Bin 0 -> 10707 bytes images/plot-line-watch.png | Bin 0 -> 4435 bytes 7 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 images/blot-bar-read.png create mode 100644 images/blot-line-follow.png create mode 100644 images/blot-line-poll.png create mode 100644 images/blot-scatter-exec.png create mode 100644 images/plot-line-watch.png diff --git a/README.md b/README.md index 118ec5d..a956263 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Licensed under LGPL v2.1, or any later version. * 256 colour support * data arrays can be provided in various types (`int16`, `int32`, `int64`, `double`, or `float`) * there is a C++ wrapper, for convenience + * there is a CLI tool that can plot from files or using output of commands ## Prerequisites @@ -46,7 +47,152 @@ You can build debug (with `ASAN`) using make TYPE=Debug -## Examples +## blot CLI + +The easiest say to use `blot` is to use the CLI, which is able to read from files +or launch programs, then plot the numbers it finds. + +
+ ❯ blot --help + +```sh +SYNOPSIS + blot [-h] [-V] [-v] [--debug] [--timing] [-i ] + blot [-A|-U|-B] ((scatter|line|bar) ([-R ] | [-F ] | [-P ] | [-X ] | [-W ]) [-p ] [-r ] [-c ] [-i ])... + +OPTIONS + -h, --help This help + -V, --version Version + -v, --verbose Enable verbose output + --debug Enable debug output + --timing Show timing statitiscs + -i, --interval Display interval in seconds + + Output: + -A, --ascii ASCII output + -U, --unicode Unicode output + -B, --braille Braille output + + Plot type: + scatter Add a scatter plot + line Add a line/curve plot + bar Add a bar plot + + Plot data source: + -R, --read Read file to the end, each line is a record + -F, --follow Read file waiting for more, each line is a record + -P, --poll Read file at interval, each read is one record + -X, --exec Run command, each line is a record + -W, --watch Run command at interval, each read is one record + + Data source parsing: + -p, --position + Find numbers in input line, pick 1 or 2 positions for X and Y values + + -r, --regex Regex to match numbers from input line + + Plot modifiers: + -c, --color Set plot color (1..255) + -i, --interval Set sampling interval in seconds + +EXAMPE + + blot --braille \ + line --color 10 --read x_y1_values -p 1,2 \ + scatter --color 11 --read x_y2_values -p 1,2 + + blot --braille \ + scatter --color 11 --read y_values \ + line --color 10 --exec 'seq 1 100' + + blot --braille \ + line --poll /proc/loadavg --position 1 \ + line --poll /proc/loadavg --position 2 \ + line --poll /proc/loadavg --position 3 +``` +
+ +You need to pick a plotting mode, there are 3 choices: `scatter`, `line`, and `bar`. +You overlay multiple plots. + +Each plot needs to get data from a file or process, and there are 5 options: +- `--read ` to read lines data from a file, stop at the end. +- `--follow ` to read lines data from a file, but wait for more data (`Ctrl-C` to stop). +- `--poll ` to poll a file (like `/sys` or `/proc` files), at some interval, and plot the results. +- `--exec ` to run a program that will give us data to plot on successive lines. +- `--watch ` to run a program that outputs one number per execution, which will be accumulate and plotted. + +Let's look at some examples. + +### plot numbers from a file (read file mode) + +Let's say we have a file that contains some X,Y pairs... +``` +0 10 +1 25 +2 0 +3 40 +4 55 +``` + +We can plot this using... + +```sh +❯ blot bar --read mydata --position 1,2 +``` +The values 1 and 2 are positions in each line where `blot` will find the X,Y coordinates. + +![blot bar --read](images/blot-bar-read.png) + +### plot numbers from a log file (follow file mode) + +Let's say that we have a lot file, where we can find some magnitude values. +Let's just make something up with a script... +```sh +#!/usr/bin/env bash +while true ; do + echo $RANDOM + sleep 1 +done > mydata +``` +While the above is running, we can plot the data being generated... +```sh +❯ blot line --follow mydata +``` +The file contains only one number per line, and `blot` will use the line number as the Y coordinate. +![blot line --follow](images/blot-line-follow.png) + +### plot the system load (poll file mode) + +Recall that `/proc/loadavg` has 3 load values (1,5,15 minutes). We can plot them... + +```sh +❯ blot line --poll /proc/loadavg --position 1 --interval 0.5 \ + line --poll /proc/loadavg --position 2 \ + line --poll /proc/loadavg --position 3 +``` +![blot line --poll](images/blot-line-poll.png) + +### read X,Y from command line (exec mode) + +Here is a cool way to test your random number generator, generate two consecutive numbers and plot them against one another. + +```sh +❯ blot scatter --exec 'for ((x=0;x<10000000;x++)) ; do echo $RANDOM $RANDOM ; done' --position 1,2 +``` +![blot scatter --exec](images/blot-scatter-exec.png) + +### plot power draw of a GPU (watch mode) + +The Nvidia graphics tools provide a system management interface CLI, named `nvidia-smi`. +If we wanted to plot the average power draw, then we could do this... + +```sh +blot --timing line --watch 'nvidia-smi --id=0 -q | grep -m1 "Average Power Draw"' --interval 0.1 +``` +![blot scatter --watch](images/plot-line-watch.png) + +## Source code examples `blot` is being used in other projects as a library, but it comes with some examples. @@ -63,6 +209,8 @@ Generated from [trig.c](examples/c/c-trig.c) (see also [trig.cpp](examples/cpp/c ![trig example](images/trig.png) + + ## Missing features * different plotting modes like histograms (currently only plots line/scatter/bar) diff --git a/cli/config.cpp b/cli/config.cpp index 488c23e..52ed5b8 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -204,7 +204,6 @@ void Input::set_source (Input::Source source, const std::string &details) void Input::set_position (const std::string &txt) { - auto result = std::views::split(txt, ',') | std::views::transform([](auto&& sr) { std::string_view sv{sr.begin(), sr.end()}; @@ -221,8 +220,6 @@ void Input::set_position (const std::string &txt) std::vector positions(result.begin(), result.end()); - printf("got %zu positions\n", positions.size()); - switch (positions.size()) { case 1: m_extract.set(positions[0]); diff --git a/images/blot-bar-read.png b/images/blot-bar-read.png new file mode 100644 index 0000000000000000000000000000000000000000..e192e81d9cd29139b12fa78f78d50857a035e429 GIT binary patch literal 6079 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 Y2s__pg+(?e8Zxd8E<^Q7bQ~Z33%RL3=Kufz literal 0 HcmV?d00001 diff --git a/images/blot-line-follow.png b/images/blot-line-follow.png new file mode 100644 index 0000000000000000000000000000000000000000..9529b450f28b7065042d267d8c29eef9f2356423 GIT binary patch literal 4478 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 RmdV1B_Eq$i!b^8T{sW<^f42Yt literal 0 HcmV?d00001 diff --git a/images/blot-scatter-exec.png b/images/blot-scatter-exec.png new file mode 100644 index 0000000000000000000000000000000000000000..a4d25a16cf9378ffc9f02f891f426d18e5d03514 GIT binary patch literal 10707 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 literal 0 HcmV?d00001 diff --git a/images/plot-line-watch.png b/images/plot-line-watch.png new file mode 100644 index 0000000000000000000000000000000000000000..4fccf7cff9c7fc746995b43a20323be00fd7c3f0 GIT binary patch literal 4435 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 Date: Tue, 29 Jul 2025 16:17:01 -0400 Subject: [PATCH 48/50] update README --- README.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a956263..aad7c71 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,30 @@ # blot -Blot is a plotting library written in C (with a C++ wrapper), that plots data onto a string buffer. +Blot is a plotting library written in C, that plots data onto a string buffer. That's right, there are no images, just text -- see examples below. +There is a C++ wrapper provided, as well as a CLI tool. + Site: [bartman.github.io/blot](https://bartman.github.io/blot/) GitHub: [github.com/bartman/blot](https://github.com/bartman/blot/) -Copyright © 2021 Bart Trojanowski +Copyright © 2021-2025 Bart Trojanowski Licensed under LGPL v2.1, or any later version. ## Noteworthy features - * plots to the console as text (by calling `puts()`) + * plots to the console as text (by calling `puts()`/`printf()`) * very very fast (compared to python alternatives) * very very memory usage friendly * can plot multiple datasets on one canvas * uses familiar figure based API (similar to existing python plotting frameworks) * supports braille plotting (like [plotille](https://github.com/tammoippen/plotille)) - * 256 colour support * data arrays can be provided in various types (`int16`, `int32`, `int64`, `double`, or `float`) - * there is a C++ wrapper, for convenience + * there is a C++ wrapper, with limited features, for convenience * there is a CLI tool that can plot from files or using output of commands ## Prerequisites @@ -47,11 +48,14 @@ You can build debug (with `ASAN`) using make TYPE=Debug +Run `make help` for a full list. + ## blot CLI The easiest say to use `blot` is to use the CLI, which is able to read from files or launch programs, then plot the numbers it finds. +See the online help for a full list of features...
❯ blot --help @@ -112,8 +116,8 @@ EXAMPE ```
-You need to pick a plotting mode, there are 3 choices: `scatter`, `line`, and `bar`. -You overlay multiple plots. +First, pick a plotting mode, there are 3 choices: `scatter`, `line`, and `bar`. +Multiple plots can be overlayed. Each plot needs to get data from a file or process, and there are 5 options: - `--read ` to read lines data from a file, stop at the end. @@ -146,7 +150,7 @@ The values 1 and 2 are positions in each line where `blot` will find the X,Y coo ### plot numbers from a log file (follow file mode) -Let's say that we have a lot file, where we can find some magnitude values. +Let's say that we have a log file, with some magnitude values. Let's just make something up with a script... ```sh #!/usr/bin/env bash @@ -162,6 +166,8 @@ While the above is running, we can plot the data being generated... The file contains only one number per line, and `blot` will use the line number as the Y coordinate. ![blot line --follow](images/blot-line-follow.png) +Plot will update continuously, until `Ctrl-C` is used to stop. + ### plot the system load (poll file mode) Recall that `/proc/loadavg` has 3 load values (1,5,15 minutes). We can plot them... @@ -194,8 +200,8 @@ blot --timing line --watch 'nvidia-smi --id=0 -q | grep -m1 "Average Power Draw" ## Source code examples -`blot` is being used in other projects as a library, but it comes with some -examples. +`blot` is being used in other projects as a library, and it comes with many +examples for the C and C++ usage. Generated from [simple.c](examples/c/c-simple.c) (see also [simple.cpp](examples/cpp/cpp-simple.cpp) for C++ wrapper usage) @@ -217,6 +223,7 @@ Generated from [trig.c](examples/c/c-trig.c) (see also [trig.cpp](examples/cpp/c * improve axis line and numbering (currently not very accurate) * add axis labels and minor ticks (configurable) * draw origin lines and minor tick lines (configurable) + * better 256 colour support ### Ideas: From b79c8f0c1a96db2019d8af7d91954ce1d1401d59 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 16:36:18 -0400 Subject: [PATCH 49/50] non fallback implementation of --position parsing --- cli/config.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cli/config.cpp b/cli/config.cpp index 52ed5b8..cc24475 100644 --- a/cli/config.cpp +++ b/cli/config.cpp @@ -204,6 +204,8 @@ void Input::set_source (Input::Source source, const std::string &details) void Input::set_position (const std::string &txt) { + /* ranges and views are fun, but not all standard libraries support it */ +#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L && defined(__GLIBCXX__) && __GLIBCXX__ >= 20230601 auto result = std::views::split(txt, ',') | std::views::transform([](auto&& sr) { std::string_view sv{sr.begin(), sr.end()}; @@ -227,11 +229,51 @@ void Input::set_position (const std::string &txt) case 2: m_extract.set(std::pair{positions[0], positions[1]}); break; + default: + spdlog::error("unexpected count ({}) of positions found in '{}'", + positions.size(), txt); + std::exit(1); + } +#else + std::vector positions; + std::string_view sv = txt; + size_t start = 0; + + while (true) { + size_t comma_pos = sv.find(',', start); + std::string_view part = (comma_pos == std::string_view::npos) + ? sv.substr(start) + : sv.substr(start, comma_pos - start); + + unsigned number; + auto [ptr, ec] = std::from_chars(part.begin(), part.end(), number); + if (ec != std::errc{}) { + spdlog::error("failed to parse position from '{}': {}", + part, std::make_error_code(ec).message()); + std::exit(1); + } + positions.push_back(number); + + if (comma_pos == std::string_view::npos) { + break; + } + start = comma_pos + 1; + } + + switch (positions.size()) { + case 1: + m_extract.set(positions[0]); + break; + case 2: + m_extract.set(std::pair{positions[0], positions[1]}); + break; default: spdlog::error("unexpected count ({}) of positions found in '{}'", positions.size(), txt); std::exit(1); } + +#endif } void Input::set_regex (const std::string &txt) From f96d926118a4927921e517bab8408f6ec3f9d8c2 Mon Sep 17 00:00:00 2001 From: Bart Trojanowski Date: Tue, 29 Jul 2025 16:40:13 -0400 Subject: [PATCH 50/50] fix a signed/unsigned comparison build issue with gcc --- cli/extract.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/extract.hpp b/cli/extract.hpp index 9ae10e9..cf30869 100644 --- a/cli/extract.hpp +++ b/cli/extract.hpp @@ -56,7 +56,7 @@ class Extract { const char *end = text+std::strlen(text); text = __find_start(text, end); - auto pos = 1; + auto pos = 1u ; while (text < end && pos < y_position) { text = __skip_over(text, end); @@ -81,7 +81,7 @@ class Extract { const char *end = text+std::strlen(text); text = __find_start(text, end); - auto pos = 1; + auto pos = 1u; auto first_position = std::min(x_position, y_position); auto last_position = std::max(x_position, y_position);