diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fc6567..c4d6277 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ 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 ) find_package(fmt REQUIRED) diff --git a/src/main.cpp b/src/main.cpp index d8de3f1..fb7a29c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,38 +2,38 @@ #include int main(int argc, const char* argv[]) { - std::string model_arg = "gemma3n:latest"; - std::string prompt_arg = "Hey! What's up!"; - bool stream_arg = false; + std::string model = "gemma3n:latest"; + std::string prompt = "Hey! What's up!"; + bool stream = false; for (int i = 1; i < argc; i += 1) { std::string arg = argv[i]; if (arg.starts_with("--model=") == 0) { - model_arg = arg.substr(8); + model = arg.substr(8); } else if (arg.starts_with("--prompt=") == 0) { - prompt_arg = arg.substr(9); + prompt = arg.substr(9); } else if (arg.starts_with("--stream=") == 0) { - stream_arg = arg.substr(9) == "true" ? true : false; + stream = arg.substr(9) == "true" ? true : false; } } printf("\n"); - printf("model_arg = %s\n", model_arg.c_str()); - printf("prompt_arg = %s\n", prompt_arg.c_str()); - printf("stream_arg = %s\n", stream_arg ? "true" : "false"); + printf("🛈 model = %s\n", model.c_str()); + printf("🛈 prompt = %s\n", prompt.c_str()); + printf("🛈 stream = %s\n", stream ? "true" : "false"); printf("\n"); - if (prompt_arg.length() == 0) { + if (prompt.length() == 0) { printf("Enter your prompt: "); - getline(std::cin, prompt_arg); + getline(std::cin, prompt); } OllamaClient client = OllamaClient(); struct OllamaPromptConfig config; - config.model = model_arg; - config.prompt = prompt_arg; - config.stream = stream_arg; + config.model = model; + config.prompt = prompt; + config.stream = stream; client.curl_request(config); diff --git a/src/ollama_client.cpp b/src/ollama_client.cpp index d140391..78c1bce 100644 --- a/src/ollama_client.cpp +++ b/src/ollama_client.cpp @@ -1,8 +1,10 @@ #include "ollama_client.hpp" +#include #include #include #include #include +#include using nlohmann::json_abi_v3_11_2::json; @@ -30,7 +32,7 @@ void OllamaClient::check_ollama() { "https://docs.ollama.com/linux\n"); exit(0); } else { - printf("[ok] Ollama binary exists.\n"); + printf("✅ Ollama binary exists.\n"); } }; @@ -41,15 +43,31 @@ void OllamaClient::init_curl() { fprintf(stderr, "Failed to initialize CURL.\n"); exit(1); } else { - printf("[ok] CURL initialized.\n"); + printf("✅ CURL initialized.\n"); } } +// Trims leading whitespace and trailing whitespace from a json response. +std::string OllamaClient::trim_response(std::string response) { + response.erase(response.begin(), + std::find_if(response.begin(), response.end(), + [](unsigned char ch) { return !std::isspace(ch); })); + + response.erase(std::find_if(response.rbegin(), response.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + response.end()); + return response; +} + // Process a response. void OllamaClient::process_response(std::string response) { try { json response_json = json::parse(response); - std::cout << "Response: " << response_json["response"].get() << std::endl; + + std::cout << "🦙 llama:\n" + << this->trim_response(response_json["response"].get()) << "\n---\n" + << std::endl; } catch (const json::exception& err) { fprintf(stderr, "JSON parse error: %s\n", err.what()); } @@ -97,6 +115,9 @@ void OllamaClient::curl_request(OllamaPromptConfig config) { curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(this->curl, CURLOPT_XFERINFOFUNCTION, ProgressSpinner::progress_callback); + curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 0L); + this->headers = curl_slist_append(this->headers, "Content-Type: application/json"); curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, this->headers); diff --git a/src/ollama_client.hpp b/src/ollama_client.hpp index 04f84f6..17276c5 100644 --- a/src/ollama_client.hpp +++ b/src/ollama_client.hpp @@ -1,3 +1,4 @@ +#include "progress_spinner.hpp" #include #include @@ -12,9 +13,13 @@ class OllamaClient { CURL* curl; std::string url; struct curl_slist* headers; + ProgressSpinner spinner{ProgressSpinner()}; void check_ollama(); void init_curl(); + + std::string trim_response(std::string response); + void process_response(std::string response); void process_response_stream(std::string response); diff --git a/src/progress_spinner.cpp b/src/progress_spinner.cpp new file mode 100644 index 0000000..6f216f4 --- /dev/null +++ b/src/progress_spinner.cpp @@ -0,0 +1,73 @@ +#include "progress_spinner.hpp" +#include +#include +#include +#include +#include + +ProgressSpinner::ProgressSpinner() { + this->active = false; +} + +ProgressSpinner::ProgressSpinner(bool activate) { + if (activate == true) { + this->active = activate; + start(); + } +} + +ProgressSpinner::~ProgressSpinner() { + stop(); +} + +// Starts the progress spinner. +void ProgressSpinner::start() { + active = true; + spinner_thread = std::thread([this]() { + const char spinner_chars[] = {'|', '/', '-', '\\'}; + int i = 0; + while (active) { + std::cout << "\r" << spinner_chars[i++ % 4] << " Loading..." << std::flush; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + std::cout << "\r✓ Complete\n" << std::endl; + }); +} + +// Stops the progress spinner. +void ProgressSpinner::stop() { + active = false; + if (spinner_thread.joinable()) { + spinner_thread.join(); + } +} + +/** + * CURL progress callback. + * + * @example + * ```cpp + * CURL* curl = curl_easy_init(); + * curl_easy_setopt(this->curl, CURLOPT_XFERINFOFUNCTION, ProgressSpinner::progress_callback); + * curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 0L); + * curl_easy_perform(curl); + * ``` + */ +int ProgressSpinner::progress_callback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow) { + if (dltotal > 0) { + // If size is known, show progress bar + int progress = (dlnow * 100) / dltotal; + if (progress == 100) { + std::cout << "\r✓ Complete " << std::flush; + } else { + std::cout << "\rProgress: " << progress << "% " << std::flush; + } + } else { + // Indeterminate - show spinner + const char spinner_chars[] = {'|', '/', '-', '\\'}; + static int i = 0; + std::cout << "\r" << spinner_chars[i++ % 4] << " Loading..." << std::flush; + } + return 0; +} diff --git a/src/progress_spinner.hpp b/src/progress_spinner.hpp new file mode 100644 index 0000000..5857b8d --- /dev/null +++ b/src/progress_spinner.hpp @@ -0,0 +1,39 @@ +#pragma once + +#ifndef PROGRESS_SPINNER_HPP +#define PROGRESS_SPINNER_HPP + +#include +#include +#include +#include + +/** + * Progress spinner for asynchronous operations. + * + * @example manual mode + * ```cpp + * ProgressSpinner spinner = ProgressSpinner(); + * spinner.start(); + * curl_easy_perform(curl_handle); + * spinner.stop(); + * ``` + */ +class ProgressSpinner { + private: + std::atomic active{false}; + std::thread spinner_thread; + + public: + ProgressSpinner(); + explicit ProgressSpinner(bool activate); + ~ProgressSpinner(); + + void start(); + void stop(); + + static int progress_callback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow); +}; + +#endif