From 19425f8efddae3e64a175689c70c313d8a468475 Mon Sep 17 00:00:00 2001 From: rfprod Date: Mon, 2 Nov 2026 22:33:27 +0400 Subject: [PATCH] feat(program): argument parser and `I/O` operations refactoring - [x] implement argument parsing logic in a dedicated `ArgsUtils`; - [x] use `ParsedArgs` struct to encapsulate all parsed arguments; - [x] implement handler map for `O(1)` argument key lookup and processing; - [x] add `print_help()` function for displaying usage information; - [x] refactor `main.cpp` to use `ArgsUtils` for argument handling; - [x] refactored `I/O` from `std` streams to `fprintf` for consistency; - [x] document CLI arguments with parameter table; --- CMakeLists.txt | 3 +- README.md | 64 ++++++++++++----- src/interface/progress_spinner.cpp | 9 ++- src/interface/user_interface.cpp | 20 +++--- src/main.cpp | 112 +++++++++++++---------------- src/utils/args_utils.cpp | 90 +++++++++++++++++++++++ src/utils/args_utils.hpp | 45 ++++++++++++ 7 files changed, 246 insertions(+), 97 deletions(-) create mode 100644 src/utils/args_utils.cpp create mode 100644 src/utils/args_utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 394286c..5d74a29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(cpp-workspace VERSION 0.4.1 LANGUAGES CXX) +project(cpp-workspace VERSION 0.4.2 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -17,6 +17,7 @@ add_executable(cpp-workspace-app src/interface/user_interface.cpp src/ollama/ollama_client.cpp src/storage/qdrant_client.cpp + src/utils/args_utils.cpp src/utils/curl_utils.cpp src/utils/text_utils.cpp src/utils/system_utils.cpp diff --git a/README.md b/README.md index 982c010..a9636d8 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,14 @@ In order to run your own copy of the project it is recommended to satisfly the f ```bash git config --global core.autocrlf false git config --global core.eol lf + ``` ### Integrated development environment 🏆 **Visual Studio Code** - recommended for all operating systems Suggested extensions: + - [C/C++ Extension Pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools-extension-pack) - [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools) @@ -66,12 +68,12 @@ See [`.vscode/extensions`](.vscode/extensions.json) for the complete list. ### Core dependencies -- [Git]((https://git-scm.com/)) +- [Git](<(https://git-scm.com/)>) - [Python](https://www.python.org/) or [NodeJS](https://nodejs.org/) _for conventional commits_ - [Bash 5](https://www.gnu.org/software/bash/) _for scripting_ - [CMake](https://cmake.org/) - A **Build system** (one of): - - [Make](https://en.wikipedia.org/wiki/Make_(software)) + - [Make]() - [Ninja](https://ninja-build.org/) - A **C++ compiler** (one of): - [GCC](https://gcc.gnu.org/) @@ -95,10 +97,12 @@ export QDRANT_API_KEY_READ_ONLY="your-secure-read-only-key" This project requires [Commitizen](https://commitizen.org/) for standardized commit messages. **Setup:** + - **Linux/macOS:** `brew install commitizen` or `pip install commitizen` - **Windows:** Use WSL2 and follow Linux instructions, or install via [MSYS2](https://www.msys2.org/) **Committing:** + ```bash cz commit # Interactive commit prompt ``` @@ -109,21 +113,21 @@ cz commit # Interactive commit prompt 1. Install - ```bash - bash ./tools/shell/install.sh --help - ``` + ```bash + bash ./tools/shell/install.sh --help + ``` 2. Build - ```bash - bash ./tools/shell/build.sh --help - ``` + ```bash + bash ./tools/shell/build.sh --help + ``` 3. Format - ```bash - bash ./tools/shell/format.sh - ``` + ```bash + bash ./tools/shell/format.sh + ``` ### Manual build @@ -143,9 +147,31 @@ rm -rf ./build/* ./CMakeUserPresets.json ;\ cmake --build --preset conan-release ``` +### Runtime arguments + +This app accepts the following command-line arguments to customize behavior: + +| Argument | Type | Default | Description | +| -------------------------------- | ------- | ----------------------- | ----------------------------------------------------- | +| `--model=` | String | `gemma3n:latest` | The LLM model to use for inference. | +| `--embedding_model=` | String | `embeddinggemma:latest` | The model to use for generating embeddings. | +| `--prompt=` | String | `Hey! What's up!` | Initial prompt or query to send to the model. | +| `--stream=` | Boolean | `false` | Enable streaming responses. | +| `--rag=` | Boolean | `false` | Enable Retrieval-Augmented Generation (RAG) mode. | +| `--interactive=` | Boolean | `false` | Enable interactive mode for multi-turn conversations. | +| `--dark_theme=` | Boolean | `false` | Use dark theme for the ncurses interface. | +| `--skip_embedding=` | Boolean | `false` | Skip embedding creation. | +| `--help=` | Boolean | `false` | Print help. | + +### Notes + +- Boolean arguments accept **`true`** or **`false`** (case-sensitive lowercase) +- String arguments (`--model`, `--embedding_model`, `--prompt`) accept any alphanumeric string; no spaces or special characters are recommended +- Arguments can be provided in any order + ### Run the app -Source .env first, then run the app +Source `.env` first (see the [Environment variables](#environment-variables) section), then run the app ```bash source .env; \ @@ -154,12 +180,12 @@ source .env; \ ### Quick Start -1. Install Ollama and pull two models, e.g. [__embeddinggemma:latest__](https://ollama.com/library/embeddinggemma) and [__gemma3n:latest__](https://ollama.com/library/gemma3n:latest). +1. Install Ollama and pull two models, e.g. [**embeddinggemma:latest**](https://ollama.com/library/embeddinggemma) and [**gemma3n:latest**](https://ollama.com/library/gemma3n:latest). 2. Build the project or [download the latest release](https://github.com/rfprod/cpp-workspace/releases): - ```bash - bash tools/shell/build.sh --clean --release --exe-args '--skip_embedding=true' - ``` + ```bash + bash tools/shell/build.sh --clean --release --exe-args '--skip_embedding=true' + ``` 3. Run in interactive RAG mode: - ```bash - ./build/Release/bin/cpp-workspace-app --interactive=true --rag=true - ``` + ```bash + ./build/Release/bin/cpp-workspace-app --interactive=true --rag=true + ``` diff --git a/src/interface/progress_spinner.cpp b/src/interface/progress_spinner.cpp index 5f74cf2..3f23e5b 100644 --- a/src/interface/progress_spinner.cpp +++ b/src/interface/progress_spinner.cpp @@ -63,7 +63,8 @@ void ProgressSpinner::start() { const char spinner_chars[] = {'|', '/', '-', '\\'}; int i = 0; while (this->active) { - std::cout << "\r" << spinner_chars[i++ % 4] << " Loading..." << std::flush; + fprintf(stdout, "\r%c Loading...", spinner_chars[i++ % 4]); + fflush(stdout); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } UserInterface::print_message_noninteractive("\n"); @@ -117,10 +118,12 @@ int ProgressSpinner::progress_callback_impl(curl_off_t dltotal, curl_off_t dlnow if (dltotal > 0) { int progress = (dlnow * 100) / dltotal; - std::cout << "\r▰▰▰▱ Progress: " << progress << "% " << std::flush; + fprintf(stdout, "\r▰▰▰▱ Progress %d%% ", progress); + fflush(stdout); } else { static int i = 0; - std::cout << "\r" << spinner_chars[i++ % 4] << " Loading..." << std::flush; + fprintf(stdout, "\r%c Loading...", spinner_chars[i++ % 4]); + fflush(stdout); } return 0; } diff --git a/src/interface/user_interface.cpp b/src/interface/user_interface.cpp index 0a440bd..7e5bece 100644 --- a/src/interface/user_interface.cpp +++ b/src/interface/user_interface.cpp @@ -132,7 +132,7 @@ void UserInterface::init_ncurses(void) { const WINDOW* w = initscr(); if (!w) { const char* term = getenv("TERM"); - std::cerr << "Failed to initialize ncurses with TERM=" << (term ? term : "unset") << "\n"; + fprintf(stderr, "Failed to initialize ncurses with TERM=%s\n", term ? term : "unset"); std::exit(EXIT_FAILURE); } @@ -141,13 +141,13 @@ void UserInterface::init_ncurses(void) { // Setup terminal. if (cbreak() == ERR || noecho() == ERR) { endwin(); - std::cerr << "Terminal setup failed\n"; + fprintf(stderr, "Terminal setup failed\n"); std::exit(EXIT_FAILURE); } // Enable function keys and arrow keys. if (keypad(stdscr, TRUE) == ERR) { - std::cerr << "Warning: keypad() not supported\n"; + fprintf(stderr, "Warning: keypad() not supported\n"); } // Block on input (getch waits for input). @@ -155,7 +155,7 @@ void UserInterface::init_ncurses(void) { // Cursor hiding (may not be supported). if (curs_set(0) == ERR) { - std::cerr << "Warning: cursor control not supported on this terminal\n"; + fprintf(stderr, "Warning: cursor control not supported on this terminal\n"); } if (has_colors()) { @@ -1106,8 +1106,8 @@ void UserInterface::print_message(const std::string& text, bool stream) { * @see print_error() */ void UserInterface::print_error_noninteractive(const std::string& text) { - std::cerr << "❌ " << text; - std::cerr.flush(); + fprintf(stderr, "❌ %s", text.c_str()); + fflush(stderr); } /** @@ -1123,8 +1123,8 @@ void UserInterface::print_error_noninteractive(const std::string& text) { * @see print_error() */ void UserInterface::print_success_noninteractive(const std::string& text) { - std::cout << "✅ " << text; - std::cout.flush(); + fprintf(stdout, "✅ %s", text.c_str()); + fflush(stdout); } /** @@ -1139,8 +1139,8 @@ void UserInterface::print_success_noninteractive(const std::string& text) { * @see print_message() */ void UserInterface::print_message_noninteractive(const std::string& text) { - std::cout << text; - std::cout.flush(); + fprintf(stdout, "%s", text.c_str()); + fflush(stdout); } /** diff --git a/src/main.cpp b/src/main.cpp index f529b97..84a48c6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "interface/user_interface.hpp" #include "ollama/ollama_client.hpp" #include "storage/qdrant_client.hpp" +#include "utils/args_utils.hpp" #include "utils/curl_utils.hpp" #include "utils/embedding_utils.hpp" #include "utils/retrieving_utils.hpp" @@ -11,42 +12,18 @@ #include int main(int argc, const char* argv[]) { - std::string model = "gemma3n:latest"; - std::string embedding_model = "embeddinggemma:latest"; - std::string prompt = "Hey! What's up!"; - bool stream = false; - bool rag = false; - bool interactive = false; - bool dark_theme = true; - bool skip_embedding = false; - bool done = false; std::string message{}; - std::string arg{}; - for (int i = 1; i < argc; i += 1) { - arg = argv[i]; - if (arg.starts_with("--model=") == true) { - model = arg.substr(8); - } else if (arg.starts_with("--embedding_model=") == true) { - embedding_model = arg.substr(18); - } else if (arg.starts_with("--prompt=") == true) { - prompt = arg.substr(9); - } else if (arg.starts_with("--stream=") == true) { - stream = arg.substr(9) == "true" ? true : false; - } else if (arg.starts_with("--rag=") == true) { - rag = arg.substr(6) == "true" ? true : false; - } else if (arg.starts_with("--interactive=") == true) { - interactive = arg.substr(14) == "true" ? true : false; - } else if (arg.starts_with("--dark_theme=") == true) { - dark_theme = arg.substr(13) == "true" ? true : false; - } else if (arg.starts_with("--skip_embedding=") == true) { - skip_embedding = arg.substr(17) == "true" ? true : false; - } + ArgsUtils args_utils; + struct ParsedArgs parsed_args = args_utils.parse_args(&argc, argv); + + if (parsed_args.help == true) { + args_utils.print_help(); } - UserInterface ui(dark_theme); - if (interactive == true) { + UserInterface ui(parsed_args.dark_theme); + if (parsed_args.interactive == true) { message = std::format("\nInteractive mode debug info.\n" "---\n" "isatty(0)={}, isatty(1)={}, isatty(2)={}\n" @@ -63,23 +40,25 @@ int main(int argc, const char* argv[]) { UserInterface::print_message_noninteractive(message); ui.run(); - ui.print_args(model, embedding_model, prompt, stream, rag, interactive, dark_theme, skip_embedding); + ui.print_args(parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, parsed_args.stream, + parsed_args.rag, parsed_args.interactive, parsed_args.dark_theme, parsed_args.skip_embedding); - while (prompt.length() == 0) { - done = ui.next_prompt(&prompt); + while (parsed_args.prompt.length() == 0) { + done = ui.next_prompt(&parsed_args.prompt); if (done == true) { ui.end_session(EXIT_SUCCESS); } } } else { - UserInterface::print_args_noninteractive(model, embedding_model, prompt, stream, rag, interactive, dark_theme, - skip_embedding); + UserInterface::print_args_noninteractive(parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, + parsed_args.stream, parsed_args.rag, parsed_args.interactive, + parsed_args.dark_theme, parsed_args.skip_embedding); - if (prompt.length() == 0) { - done = UserInterface::next_prompt_noninteractive(&prompt); + if (parsed_args.prompt.length() == 0) { + done = UserInterface::next_prompt_noninteractive(&parsed_args.prompt); - if (prompt.length() == 0) { + if (parsed_args.prompt.length() == 0) { UserInterface::print_message_noninteractive("\n🏁 Empty prompt. The session has ended.\n"); exit(0); } @@ -110,77 +89,82 @@ int main(int argc, const char* argv[]) { std::vector models = ollama_client.query_model_list(); bool force_model_selection = true; for (size_t i = 0; i < models.size(); i += 1) { - if (models[i] == model) { + if (models[i] == parsed_args.model) { force_model_selection = false; } } if (force_model_selection == true) { message = "⚠ A Language model is not selected.\n"; - if (interactive == true) { + if (parsed_args.interactive == true) { ui.print_message(message); ui.print_model_list(models); - ui.select_model(models, &model); - ui.print_args(model, embedding_model, prompt, stream, rag, interactive, dark_theme, skip_embedding); + ui.select_model(models, &parsed_args.model); + ui.print_args(parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, parsed_args.stream, + parsed_args.rag, parsed_args.interactive, parsed_args.dark_theme, parsed_args.skip_embedding); } else { UserInterface::print_message_noninteractive(message); UserInterface::print_model_list_noninteractive(models); - UserInterface::select_model_noninteractive(models, &model); - UserInterface::print_args_noninteractive(model, embedding_model, prompt, stream, rag, interactive, - dark_theme, skip_embedding); + UserInterface::select_model_noninteractive(models, &parsed_args.model); + UserInterface::print_args_noninteractive(parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, + parsed_args.stream, parsed_args.rag, parsed_args.interactive, + parsed_args.dark_theme, parsed_args.skip_embedding); } } - if (skip_embedding == false) { + if (parsed_args.skip_embedding == false) { bool force_embedding_model_selection = true; for (size_t i = 0; i < models.size(); i += 1) { - if (models[i] == embedding_model) { + if (models[i] == parsed_args.embedding_model) { force_embedding_model_selection = false; } } if (force_embedding_model_selection == true) { message = "⚠ An Embedding model is not selected.\n"; - if (interactive == true) { + if (parsed_args.interactive == true) { ui.print_message(message); ui.print_model_list(models); - ui.select_model(models, &embedding_model); - ui.print_args(model, embedding_model, prompt, stream, rag, interactive, dark_theme, skip_embedding); + ui.select_model(models, &parsed_args.embedding_model); + ui.print_args(parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, parsed_args.stream, + parsed_args.rag, parsed_args.interactive, parsed_args.dark_theme, + parsed_args.skip_embedding); } else { UserInterface::print_message_noninteractive(message); UserInterface::print_model_list_noninteractive(models); - UserInterface::select_model_noninteractive(models, &embedding_model); - UserInterface::print_args_noninteractive(model, embedding_model, prompt, stream, rag, interactive, - dark_theme, skip_embedding); + UserInterface::select_model_noninteractive(models, &parsed_args.embedding_model); + UserInterface::print_args_noninteractive( + parsed_args.model, parsed_args.embedding_model, parsed_args.prompt, parsed_args.stream, + parsed_args.rag, parsed_args.interactive, parsed_args.dark_theme, parsed_args.skip_embedding); } } } - ollama_config.model = model; - ollama_config.embedding_model = embedding_model; - ollama_config.prompt = prompt; - ollama_config.stream = stream; + ollama_config.model = parsed_args.model; + ollama_config.embedding_model = parsed_args.embedding_model; + ollama_config.prompt = parsed_args.prompt; + ollama_config.stream = parsed_args.stream; - if (skip_embedding == false) { + if (parsed_args.skip_embedding == false) { std::vector urls = { "https://raw.githubusercontent.com/rfprod/nx-ng-starter/refs/heads/main/README.md"}; embedding_utils.create_embedings(urls); } - if (rag == false) { + if (parsed_args.rag == false) { ollama_client.query_model(); } else { retrieving_utils.run(); } - if (interactive == true) { + if (parsed_args.interactive == true) { while (done == false) { - done = ui.next_prompt(&prompt); + done = ui.next_prompt(&parsed_args.prompt); if (done == true) { continue; } - ollama_config.prompt = prompt; - if (rag == false) { + ollama_config.prompt = parsed_args.prompt; + if (parsed_args.rag == false) { ollama_client.query_model(); } else { retrieving_utils.run(); diff --git a/src/utils/args_utils.cpp b/src/utils/args_utils.cpp new file mode 100644 index 0000000..767e461 --- /dev/null +++ b/src/utils/args_utils.cpp @@ -0,0 +1,90 @@ +#include "args_utils.hpp" + +/** + * Initializes the ArgsUtils instance with command-line argument handlers. + * Sets up a map of argument keys to handler functions that parse and assign values to ParsedArgs. + */ +ArgsUtils::ArgsUtils() { + this->handlers = { + {"--model", [&](const std::string& val, struct ParsedArgs* args) { args->model = val; }}, + {"--embedding_model", [&](const std::string& val, struct ParsedArgs* args) { args->embedding_model = val; }}, + {"--prompt", [&](const std::string& val, struct ParsedArgs* args) { args->prompt = val; }}, + {"--stream", [&](const std::string& val, struct ParsedArgs* args) { args->stream = val == "true"; }}, + {"--rag", [&](const std::string& val, struct ParsedArgs* args) { args->rag = val == "true"; }}, + {"--interactive", [&](const std::string& val, struct ParsedArgs* args) { args->interactive = val == "true"; }}, + {"--dark_theme", [&](const std::string& val, struct ParsedArgs* args) { args->dark_theme = val == "true"; }}, + {"--skip_embedding", + [&](const std::string& val, struct ParsedArgs* args) { args->skip_embedding = val == "true"; }}, + {"--help", [&](const std::string& val, struct ParsedArgs* args) { args->help = val == "true"; }}, + }; +} + +/** + * Parses command-line arguments and returns a ParsedArgs struct. + * Initializes all arguments with default values, then overrides them with values from the command line. + * Expects arguments in the format --key=value. + * + * @param argc Pointer to argument count from main() + * @param argv Array of argument strings from main() + * @return ParsedArgs struct containing all parsed argument values + */ +struct ParsedArgs ArgsUtils::parse_args(const int* argc, const char* argv[]) { + struct ParsedArgs parsed_args {}; + parsed_args.model = "gemma3n:latest"; + parsed_args.embedding_model = "embeddinggemma:latest"; + parsed_args.prompt = "Hey! What's up!"; + parsed_args.stream = false; + parsed_args.rag = false; + parsed_args.interactive = false; + parsed_args.dark_theme = false; + parsed_args.skip_embedding = false; + parsed_args.help = false; + + std::string arg{}; + for (int i = 1; i < *argc; i += 1) { + arg = argv[i]; + size_t eq_pos = arg.find('='); + + if (eq_pos == std::string::npos) { + continue; + } + + std::string key = arg.substr(0, eq_pos); + std::string val = arg.substr(eq_pos + 1); + + if (handlers.count(key)) { + handlers[key](val, &parsed_args); + } + } + + return parsed_args; +} + +/** + * Prints usage information for all supported runtime arguments and exits. + * Displays each argument name, type, description, and default value. + */ +void ArgsUtils::print_help() { + printf("Usage: app [OPTIONS]\n\n" + "Options:\n" + " --model= The LLM model to use for inference.\n" + " (default: gemma3n:latest)\n\n" + " --embedding_model= The model to use for generating embeddings/\n" + " (default: embeddinggemma:latest)\n\n" + " --prompt= Initial prompt or query to send to the model.\n" + " (default: Hey! What's up!)\n\n" + " --stream= Enable streaming responses.\n" + " (default: false)\n\n" + " --rag= Enable Retrieval-Augmented Generation (RAG) mode.\n" + " (default: false)\n\n" + " --interactive= Enable interactive mode for multi-turn conversations.\n" + " (default: false)\n\n" + " --dark_theme= Use dark theme for the ncurses interface.\n" + " (default: false)\n\n" + " --skip_embedding= Skip embedding creation.\n" + " (default: false)\n\n" + " --help= Print help.\n" + " (default: false)\n"); + + std::exit(0); +} diff --git a/src/utils/args_utils.hpp b/src/utils/args_utils.hpp new file mode 100644 index 0000000..690b3b8 --- /dev/null +++ b/src/utils/args_utils.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +/** + * Represents parsed command-line arguments. + * Contains all supported runtime arguments with their parsed values. + * Boolean fields default to false; string fields default to empty strings. + */ +struct ParsedArgs { + std::string model; + std::string embedding_model; + std::string prompt; + bool stream; + bool rag; + bool interactive; + bool ncurses; + bool dark_theme; + bool skip_embedding; + bool help; +}; + +/** + * Utility class for parsing command-line arguments. + * Uses a handler map to associate argument keys with parsing functions, + * enabling O(1) lookup and flexible argument handling. + */ +class ArgsUtils { + private: + /** + * Maps command-line argument keys to handler functions. + * Each handler processes the argument value and updates the corresponding field in ParsedArgs. + * Provides O(1) lookup when parsing arguments. + */ + std::unordered_map> handlers; + + public: + ArgsUtils(); + + struct ParsedArgs parse_args(const int* argc, const char* argv[]); + + void print_help(); +};