diff --git a/.clang-format b/.clang-format index a676325..b45eb8b 100644 --- a/.clang-format +++ b/.clang-format @@ -1,14 +1,13 @@ -Language: Cpp BasedOnStyle: LLVM IndentWidth: 4 UseTab: Never ColumnLimit: 100 AllowShortFunctionsOnASingleLine: None -AllowShortIfStatementsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: WithoutElse AllowShortLoopsOnASingleLine: false BraceWrapping: AfterClass: true - AfterControlStatement: true + AfterControlStatement: Always AfterFunction: true AfterNamespace: true AfterStruct: true @@ -20,5 +19,5 @@ PointerAlignment: Left SpaceAfterCStyleCast: true SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements -AlignConsecutiveAssignments: false +AlignConsecutiveAssignments: None AlignTrailingComments: true diff --git a/.gitignore b/.gitignore index 9902b8c..36f6d39 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,6 @@ core src/* !src/*.cpp !src/*.hpp +!src/** +!src/**/*.cpp +!src/**/*.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 59ba546..9050090 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(cpp-workspace VERSION 0.2.0 LANGUAGES CXX) +project(cpp-workspace VERSION 0.2.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -12,11 +12,19 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) add_executable(cpp-workspace-app - src/main.cpp - src/ollama_client.cpp - src/progress_spinner.cpp - src/user-interface.cpp - src/text-utils.cpp + src/main.cpp + src/interface/progress_spinner.cpp + src/interface/user-interface.cpp + src/ollama/ollama_client.cpp + src/utils/text-utils.cpp +) + +target_include_directories(cpp-workspace-app + PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/interface + ${CMAKE_SOURCE_DIR}/src/ollama + ${CMAKE_SOURCE_DIR}/src/utils ) find_package(fmt REQUIRED) diff --git a/src/progress_spinner.cpp b/src/interface/progress_spinner.cpp similarity index 100% rename from src/progress_spinner.cpp rename to src/interface/progress_spinner.cpp diff --git a/src/progress_spinner.hpp b/src/interface/progress_spinner.hpp similarity index 100% rename from src/progress_spinner.hpp rename to src/interface/progress_spinner.hpp diff --git a/src/user-interface.cpp b/src/interface/user-interface.cpp similarity index 84% rename from src/user-interface.cpp rename to src/interface/user-interface.cpp index 3d7bc64..dc60fb3 100644 --- a/src/user-interface.cpp +++ b/src/interface/user-interface.cpp @@ -257,6 +257,88 @@ void UserInterface::print_args_noninteractive(const std::string& model, const st printf("\n"); } +void UserInterface::print_model_list(const std::vector& models) { + std::string buffer; + buffer += "\nšŸ›ˆ Select a model from the list below:\n"; + for (size_t i = 0; i < models.size(); i += 1) { + buffer += "[" + std::to_string(i) + "] " + models[i] + "\n"; + } + + this->print(buffer, UserInterface::MESSAGE_TYPE_NORMAL); +} + +void UserInterface::print_model_list_noninteractive(const std::vector& models) { + printf("\nšŸ›ˆ Select a model from the list below:\n"); + for (size_t i = 0; i < models.size(); i += 1) { + printf("[%zu] %s\n", i, models[i].c_str()); + } +} + +void UserInterface::select_model(const std::vector& models, std::string* model) { + int index{}; + + if (models.size() == 0) { + this->print_error("There are no available models to select from.\n"); + exit(1); + } + + this->request_redraw(); + + this->input_buffer.clear(); + int ch; + + while ((ch = getch()) != '\n') { + if (ch == KEY_BACKSPACE || ch == 127) { + if (!input_buffer.empty()) { + input_buffer.pop_back(); + input_cursor_x--; + this->request_redraw(); + } + } else if (ch != ERR && isprint(ch)) { + if (input_buffer.length() < COLS - 40) { // Leave room for prompt + input_buffer.push_back(ch); + input_cursor_x++; + this->request_redraw(); + } + } + } + + if (this->input_buffer == "q" || this->input_buffer == "Q") { + this->end_session(EXIT_SUCCESS); + } + + index = stoi(this->input_buffer); + + if (index < 0 || index >= models.size()) { + std::string message = std::format("Model index %d does not exist.\n", index); + this->print_error(message); + this->select_model(models, model); + } else { + *model = models[index]; + } +} + +void UserInterface::select_model_noninteractive(const std::vector& models, + std::string* model) { + int index{}; + int read_result{}; + + if (models.size() == 0) { + fprintf(stderr, "There are no available models to select from.\n"); + exit(1); + } + + while (read_result != 1 || index < 0 || index >= models.size()) { + if (index < 0 || index >= models.size()) { + fprintf(stderr, "Model index %d does not exist.\n", index); + } + printf("Model index: "); + read_result = scanf("%d", &index); + } + + *model = models[index]; +} + bool UserInterface::next_prompt(std::string* prompt) { bool done = false; @@ -374,8 +456,7 @@ void UserInterface::draw_output_area() { // Render from bottom to top (reverse order) for (auto line_it = display_lines.rbegin(); line_it != display_lines.rend(); ++line_it) { - if (current_line <= layout.output_y) - break; + if (current_line <= layout.output_y) break; if (type == UserInterface::MESSAGE_TYPE_ERROR) { wattron(stdscr, COLOR_PAIR(ERROR_PAIR) | A_BOLD); diff --git a/src/user-interface.hpp b/src/interface/user-interface.hpp similarity index 90% rename from src/user-interface.hpp rename to src/interface/user-interface.hpp index e4372ba..f35b241 100644 --- a/src/user-interface.hpp +++ b/src/interface/user-interface.hpp @@ -1,6 +1,6 @@ #pragma once -#include "text-utils.hpp" +#include "utils/text-utils.hpp" #include #include #include @@ -221,6 +221,7 @@ class UserInterface { */ void print_args(const std::string& model, const std::string& prompt, bool stream, bool interactive, bool dark_theme); + /** * Static version of print_args() for non-interactive mode. * Outputs arguments to stdout instead of the UI. @@ -233,6 +234,33 @@ class UserInterface { static void print_args_noninteractive(const std::string& model, const std::string& prompt, bool stream, bool interactive, bool dark_theme); + /** + * Prints available models and their corresponding indexes for selection. + * @param models The list of available local models. + */ + void print_model_list(const std::vector& models); + + /** + * Static version of print_model_list() for non-interactive mode. + * @param models The list of available local models. + */ + static void print_model_list_noninteractive(const std::vector& models); + + /** + * Prompts for user input to select a model form the list of available local models. + * @param models The list of available local models. + * @param model Pointer to a std::string to receive the user input. + */ + void select_model(const std::vector& models, std::string* model); + + /** + * Static version of the select_model() for non-interactive mode. + * @param models The list of available local models. + * @param model Pointer to a std::string to receive the user input. + */ + static void select_model_noninteractive(const std::vector& models, + std::string* model); + /** * Waits for and retrieves the next user input prompt (interactive mode). * Blocks until the user presses Enter and a prompt is available. diff --git a/src/main.cpp b/src/main.cpp index 0efb37e..472ec62 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ -#include "ollama_client.hpp" -#include "user-interface.hpp" +#include "interface/user-interface.hpp" +#include "ollama/ollama_client.hpp" #include #include @@ -69,7 +69,26 @@ int main(int argc, const char* argv[]) { OllamaClient client = OllamaClient(&ui); - client.query_model_list(); + std::vector models = client.query_model_list(); + bool force_model_selection = true; + for (size_t i = 0; i < models.size(); i += 1) { + if (models[i] == model) { + force_model_selection = false; + } + } + + if (force_model_selection == true) { + if (interactive == true) { + ui.print_model_list(models); + ui.select_model(models, &model); + ui.print_args(model, prompt, stream, interactive, dark_theme); + } else { + UserInterface::print_model_list_noninteractive(models); + UserInterface::select_model_noninteractive(models, &model); + UserInterface::print_args_noninteractive(model, prompt, stream, interactive, + dark_theme); + } + } struct OllamaPromptConfig config; config.model = model; diff --git a/src/ollama_client.cpp b/src/ollama/ollama_client.cpp similarity index 98% rename from src/ollama_client.cpp rename to src/ollama/ollama_client.cpp index c5716aa..71721a1 100644 --- a/src/ollama_client.cpp +++ b/src/ollama/ollama_client.cpp @@ -396,7 +396,7 @@ void OllamaClient::query_model(OllamaPromptConfig config) { * * @see OllamaPromptConfig for model information structure */ -void OllamaClient::query_model_list() { +std::vector OllamaClient::query_model_list() { this->init_curl(); this->spinner->start(); @@ -425,6 +425,7 @@ void OllamaClient::query_model_list() { curl_slist_free_all(this->headers); this->headers = nullptr; + std::vector models; std::string message{}; if (result != CURLE_OK) { message = std::format("CURL error: {}\n", curl_easy_strerror(result)); @@ -484,6 +485,10 @@ void OllamaClient::query_model_list() { ? std::to_string(model["size"].get() / 1024 / 1024) + " Mb" : "size unknown"; + if (name != "unknown") { + models.push_back(name); + } + message = std::format("{}\t ({}) \t{}\n", modified_at, size, name); if (this->ui->is_running() == false) { printf("%s", message.c_str()); @@ -502,4 +507,6 @@ void OllamaClient::query_model_list() { this->ui->end_session(EXIT_FAILURE); } } + + return models; } diff --git a/src/ollama_client.hpp b/src/ollama/ollama_client.hpp similarity index 97% rename from src/ollama_client.hpp rename to src/ollama/ollama_client.hpp index 76f96f7..d77f3ba 100644 --- a/src/ollama_client.hpp +++ b/src/ollama/ollama_client.hpp @@ -1,7 +1,7 @@ #pragma once -#include "progress_spinner.hpp" -#include "user-interface.hpp" +#include "interface/progress_spinner.hpp" +#include "interface/user-interface.hpp" #include #include @@ -199,6 +199,7 @@ class OllamaClient { * each model with its information formatted as a table. * * @note On error, displays an error message and exits or ends the session. + * @returns The list of available local models. */ - void query_model_list(); + std::vector query_model_list(); }; diff --git a/src/text-utils.cpp b/src/utils/text-utils.cpp similarity index 100% rename from src/text-utils.cpp rename to src/utils/text-utils.cpp diff --git a/src/text-utils.hpp b/src/utils/text-utils.hpp similarity index 100% rename from src/text-utils.hpp rename to src/utils/text-utils.hpp diff --git a/tools/shell/format.sh b/tools/shell/format.sh index c67d0a6..47e16a2 100644 --- a/tools/shell/format.sh +++ b/tools/shell/format.sh @@ -2,7 +2,21 @@ set -euo pipefail -clang-format src/* -i -Werror +# Change to repo root (handles running from any directory) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +echo "Running from: $(pwd)" +echo "---" +echo "Directory structure:" +ls -la src/ +echo "---" +echo "All .cpp and .hpp files:" +find src -type f \( -name "*.cpp" -o -name "*.hpp" \) +echo "---" + +find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -exec clang-format -i -Werror {} + cppcheck --std=c++20 \ --enable=all \ @@ -10,4 +24,7 @@ cppcheck --std=c++20 \ --suppress=missingIncludeSystem \ --suppress=unusedLabel \ -I src/ \ + -I src/interface \ + -I src/ollama \ + -I src/utils \ src/