Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions src/ollama_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ OllamaClient::OllamaClient(UserInterface* ui) {
* Output is routed through the UserInterface (if available) or stdout/stderr.
*/
void OllamaClient::check_ollama() {
std::string message{};
if (system("which ollama > /dev/null 2>&1")) {
std::string message = "Ollama binary does not exist.\nInstallation instructions: "
"https://docs.ollama.com/linux\n";
message = "Ollama binary does not exist.\nInstallation instructions: "
"https://docs.ollama.com/linux\n";
if (this->ui->is_running() == false) {
fprintf(stderr, "❌ %s", message.c_str());
exit(0);
Expand All @@ -75,7 +76,7 @@ void OllamaClient::check_ollama() {
this->ui->end_session(EXIT_FAILURE);
}
} else {
std::string message = "Ollama binary exists.\n";
message = "Ollama binary exists.\n";
if (this->ui->is_running() == false) {
printf("✅ %s", message.c_str());
} else {
Expand All @@ -92,8 +93,9 @@ void OllamaClient::check_ollama() {
*/
void OllamaClient::init_curl() {
this->curl = curl_easy_init();
std::string message{};
if (!this->curl) {
std::string message = "Failed to initialize CURL.\n";
message = "Failed to initialize CURL.\n";
if (this->ui->is_running() == false) {
fprintf(stderr, "❌ %s", message.c_str());
exit(1);
Expand All @@ -102,7 +104,7 @@ void OllamaClient::init_curl() {
this->ui->end_session(EXIT_FAILURE);
}
} else {
std::string message = "CURL initialized.\n";
message = "CURL initialized.\n";
if (this->ui->is_running() == false) {
printf("✅ %s", message.c_str());
} else {
Expand Down Expand Up @@ -203,16 +205,30 @@ void OllamaClient::process_response_stream(std::string response) {
std::istringstream stream(response);
std::string line;

std::cout << "Response: ";
std::cout.flush();
std::string accumulated{};

std::string text = "🦙 llama:\n";
if (this->ui->is_running() == false) {
this->ui->print_message_noninteractive(text);
} else {
accumulated += text;
}

while (std::getline(stream, line)) {
if (!line.empty()) {
try {
json chunk = json::parse(line);
std::string text = chunk["response"].get<std::string>();
std::cout << text;
std::cout.flush();
text = chunk["response"].get<std::string>();
if (this->ui->is_running() == false) {
this->ui->print_message_noninteractive(text);
} else {
accumulated += text;

if (text.find('\n') != std::string::npos || accumulated.length() > 256) {
this->ui->print_message(accumulated);
accumulated.clear();
}
}

if (chunk["done"].get<bool>()) {
break;
Expand All @@ -230,7 +246,12 @@ void OllamaClient::process_response_stream(std::string response) {
}
}

std::cout << std::endl;
text = "\n---\n";
if (this->ui->is_running() == false) {
this->ui->print_message_noninteractive(text);
} else {
this->ui->print_message(text);
}
}

/**
Expand Down
20 changes: 20 additions & 0 deletions src/user-interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ bool UserInterface::next_prompt_noninteractive(std::string* prompt) {
return done;
}

/**
* Prints a message using ncurses.
*/
void UserInterface::print(const std::string& text, int type) {
{
std::lock_guard<std::mutex> lock(this->ui_mutex);
Expand All @@ -260,18 +263,35 @@ void UserInterface::print(const std::string& text, int type) {
this->request_redraw();
}

/**
* Prints an error message using ncurses.
*/
void UserInterface::print_error(const std::string& text) {
this->print(text, UserInterface::MESSAGE_TYPE_ERROR);
}

/**
* Prints a success message using ncurses.
*/
void UserInterface::print_success(const std::string& text) {
this->print(text, UserInterface::MESSAGE_TYPE_SUCCESS);
}

/**
* Prints a normal message using ncurses.
*/
void UserInterface::print_message(const std::string& text) {
this->print(text, UserInterface::MESSAGE_TYPE_NORMAL);
}

/**
* Prints a message using `std::cout`.
*/
void UserInterface::print_message_noninteractive(const std::string& text) {
std::cout << text;
std::cout.flush();
}

void UserInterface::draw_output_area() {
// Clear the output area
for (int i = 0; i < layout.output_height; i++) {
Expand Down
187 changes: 187 additions & 0 deletions src/user-interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,59 +56,246 @@ class UserInterface {
std::atomic<int> progress_percent{0};

// Initialization methods.

/**
* Initializes the ncurses library for terminal UI rendering.
* Sets up color pairs, configures terminal mode (raw mode, no echo),
* hides cursor, enables keypad input, and initializes screen dimensions.
*/
void init_ncurses(void);

/**
* Cleans up and restores the terminal to its original state.
* Disables raw mode, re-enables echo, restores cursor visibility,
* and deallocates ncurses resources.
*/
void cleanup_ncurses(void);

// Renderer methods.

/**
* Spawns a dedicated background thread that runs render_loop().
* The thread handles periodic UI redraws without blocking input handling.
*/
void start_render_thread(void);

/**
* Signals the render thread to stop and waits for it to join.
* Sets thread_running to false and notifies the condition variable.
*/
void stop_render_thread(void);

/**
* Main loop executed by the render thread.
* Waits for redraw requests (via condition_variable), then redraws
* the entire UI if the terminal wasn't resized. Exits when thread_running is false.
* Thread-safe: uses ui_mutex to protect rendering operations.
*/
void render_loop(void);

// Layout manpulation methods.

/**
* Redraws the entire user interface.
* Clears the screen and calls draw_input_area(), draw_output_area(),
* draw_spinner(), and refreshes the display.
*/
void draw_ui(void);

/**
* Draws the progress spinner at the designated position.
* Displays an animated character based on the current spinner frame.
*/
void draw_spinner(void);

/**
* Recalculates layout positions and dimensions based on current terminal size.
* Called when SIGWINCH (terminal resize) is detected.
* Updates the Layout struct fields (input_y, output_y, status_y, etc.).
*/
void update_layout();

// Input area methods.

/**
* Renders the input prompt and user input buffer.
* Displays the prompt text and the current contents of input_buffer,
* with cursor positioned at input_cursor_x.
*/
void draw_input_area();

/**
* Clears the input area of the screen.
* Used before redrawing to prevent display artifacts.
*/
void clear_input_area();

// Output area methods.

/**
* Renders all messages in the output buffer.
* Iterates through output_buffer and draws each message with its
* associated type (normal, error, success) which may affect coloring.
*/
void draw_output_area();

public:
/**
* Constructor: Initializes the UserInterface object.
* Sets up initial state, layout, and registers signal handlers
* but does NOT start the render thread (that happens in run()).
*/
UserInterface();

/**
* Destructor: Cleans up resources.
* Stops the render thread and restores the terminal via cleanup_ncurses().
*/
~UserInterface();

/**
* Returns whether the UI is currently running.
* Reads the volatile 'running' flag in a thread-safe manner.
* @return true if the UI is running, false otherwise.
*/
bool is_running();

/**
* Static signal handler for SIGWINCH (terminal resize).
* Sets resize_flag to 1 to trigger layout recalculation.
* Must be static to comply with C signal API.
* @param sig The signal number (unused, present for signal handler compliance).
*/
static void handle_resize(int sig);

/**
* Static signal handler for SIGINT (Ctrl+C).
* Sets running to 0 to gracefully shut down the UI.
* Must be static to comply with C signal API.
* @param sig The signal number (unused, present for signal handler compliance).
*/
static void handle_interrupt(int sig);

/**
* Starts the interactive user interface.
* Registers signal handlers, initializes ncurses, starts the render thread,
* and enters the main input loop. Blocks until running becomes false.
*/
void run(void);

/**
* Ends the current session and exits.
* Stops the render thread, cleans up ncurses, and terminates the application.
* @param exit_code The program exit code to return to the operating system.
*/
void end_session(int exit_code);

/**
* Displays configuration/arguments at the start of the session (interactive mode).
* Outputs model, prompt, and mode information to the UI.
* @param model The model being used.
* @param prompt The initial prompt text.
* @param stream Whether streaming is enabled.
* @param interactive Whether interactive mode is enabled.
*/
void print_args(const std::string& model, const std::string& prompt, bool stream,
bool interactive);
/**
* Static version of print_args() for non-interactive mode.
* Outputs arguments to stdout instead of the UI.
* @param model The model being used.
* @param prompt The initial prompt text.
* @param stream Whether streaming is enabled.
* @param interactive Whether interactive mode is enabled.
*/
static void print_args_noninteractive(const std::string& model, const std::string& prompt,
bool stream, bool interactive);

/**
* Waits for and retrieves the next user input prompt (interactive mode).
* Blocks until the user presses Enter and a prompt is available.
* @param prompt Pointer to a std::string to receive the user input.
* @return true if a prompt was successfully read, false if the session ended.
*/
bool next_prompt(std::string* prompt);

/**
* Static version of next_prompt() for non-interactive mode.
* Reads from stdin instead of the interactive UI.
* @param prompt Pointer to a std::string to receive the user input.
* @return true if input was read, false on EOF or error.
*/
static bool next_prompt_noninteractive(std::string* prompt);

/**
* Prints a message to the output area with a specified type.
* Appends the message to output_buffer and triggers a redraw.
* @param text The message to display.
* @param type The message type (MESSAGE_TYPE_NORMAL, MESSAGE_TYPE_ERROR, or
* MESSAGE_TYPE_SUCCESS). Defaults to MESSAGE_TYPE_NORMAL if not specified.
*/
void print(const std::string& text, int type = UserInterface::MESSAGE_TYPE_NORMAL);

/**
* Convenience method: prints an error message (MESSAGE_TYPE_ERROR).
* @param text The error message to display.
*/
void print_error(const std::string& text);

/**
* Convenience method: prints a success message (MESSAGE_TYPE_SUCCESS).
* @param text The success message to display.
*/
void print_success(const std::string& text);

/**
* Convenience method: prints a normal message (MESSAGE_TYPE_NORMAL).
* @param text The message to display.
*/
void print_message(const std::string& text);

void print_message_noninteractive(const std::string& text);

/**
* Requests that the UI be redrawn on the next render cycle.
* Sets should_redraw to true and notifies the render thread via condition variable.
* Safe to call from multiple threads.
*/
void request_redraw(void);

// Spinner methods.

/**
* Activates the progress spinner animation.
* Sets spinner_active to true.
*/
void start_spinner(void);

/**
* Deactivates the progress spinner.
* Sets spinner_active to false.
*/
void stop_spinner(void);

/**
* Advances the spinner animation to the next frame.
* Increments spinner_frame (wraps around after reaching max).
* Called periodically during render_loop().
*/
void advance_spinner_frame(void);

/**
* Returns the current state of the spinner.
* @return true if the spinner is currently active, false otherwise.
*/
bool is_spinner_active(void) const;

/**
* Sets the progress percentage displayed with the spinner.
* @param percent Progress as a percentage (0–100).
*/
void set_progress(int percent);

// Grant friend access to ProgressSpinner
friend class ProgressSpinner;
};
Loading