lazyllama 0.5.2

A lightweight TUI client for Ollama with markdown support and smart scrolling.
Documentation
/*
 *  _                      _      _
 * | |    __ _  ______  __| |    | | __ _ _ __ ___   __ _
 * | |   / _` ||_  /\ \/ /| |    | |/ _` | '_ ` _ \ / _` |
 * | |__| (_| | / /  \  / | |___ | | (_| | | | | | | (_| |
 * |_____\__,_|/___| /_/  |_____||_|\__,_|_| |_| |_|\__,_|
 *
 * Copyright (C) 2026 Raimo Geisel
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

//! # LazyLlama
//!
//! A lightweight Terminal User Interface (TUI) client for Ollama AI models.
//! Provides real-time streaming responses, syntax highlighting for code blocks,
//! and automatic logging of chat sessions.
//!
//! ## Features
//! 
//! - Real-time streaming of AI model responses
//! - Markdown and code syntax highlighting
//! - Smart scrolling with autoscroll and manual modes
//! - Model switching with separate buffer management per model
//! - Automatic session logging
//!
//! ## Usage
//!
//! Run the application and use the following controls:
//! - `Ctrl+Q`: Quit the application
//! - `Ctrl+C`: Clear current model's chat history
//! - `Ctrl+S`: Toggle autoscroll mode
//! - `Ctrl+Arrow Up/Down`: Switch between AI models
//! - `Arrow Up/Down`: Move cursor between lines in multiline input
//! - `Page Up/Down`: Manual scrolling
//! - `Enter`: Send message to AI
//!
//! Each AI model maintains separate input buffers, chat histories, and scroll positions.

mod app;
mod ui;
mod utils;

use crate::app::App;
use anyhow::Result;
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, time::Duration};

/// Main entry point for the LazyLlama application.
///
/// Initializes the terminal interface, sets up the event loop, and handles user input.
/// The function configures crossterm for raw mode terminal input, creates a ratatui
/// terminal backend, and manages the application lifecycle including proper cleanup
/// and history saving upon exit.
///
/// # Returns
///
/// Returns `Ok(())` on successful execution or an `anyhow::Error` if initialization
/// or runtime errors occur.
///
/// # Event Handling
///
/// The main loop processes the following key combinations:
/// - `Ctrl+Q`: Graceful application exit
/// - `Ctrl+C`: Clear current model's buffer
/// - `Ctrl+S`: Toggle autoscroll behavior
/// - `Ctrl+Up/Down Arrow`: Switch between AI models with buffer persistence
/// - `Up/Down Arrow`: Navigate cursor between lines in multiline input
/// - `Page Up/Down`: Manual scrolling with autoscroll disable
/// - `Enter`: Send query to selected AI model
/// - `Backspace`: Delete characters from input
/// - `Character keys`: Add text to input buffer
///
/// # Error Handling
///
/// Properly handles terminal setup/teardown and ensures cleanup even on errors.
#[tokio::main]
async fn main() -> Result<()> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let mut app = App::new().await;
    let mut should_quit = false;

    // Initial draw
    terminal.draw(|f| ui::ui(f, &mut app))?;

    while !should_quit {
        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                // Windows-specific fix: Only process KeyPress events to prevent double input
                if key.kind != KeyEventKind::Press {
                    continue;
                }

                if app.debug_keys {
                    app.debug_last_key = Some(format!("{:?}", key));
                }
                
                let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
                let is_shift = key.modifiers.contains(KeyModifiers::SHIFT);
                
                // Handle settings dialog navigation first if it's open
                if app.show_settings_dialog {
                    let num_themes = app::SyntaxTheme::all().len();
                    match key.code {
                        KeyCode::Esc | KeyCode::Char('q') => {
                            app.show_settings_dialog = false;
                            continue;
                        }
                        KeyCode::Up => {
                            app.settings_selection = app.settings_selection.saturating_sub(1);
                            continue;
                        }
                        KeyCode::Down => {
                            if app.settings_selection < num_themes - 1 {
                                app.settings_selection += 1;
                            }
                            continue;
                        }
                        KeyCode::Enter => {
                            let themes = app::SyntaxTheme::all();
                            if let Some(theme) = themes.get(app.settings_selection) {
                                app.settings.syntax_theme = theme.clone();
                                // Save settings to disk
                                let _ = utils::save_settings(&app.settings);
                            }
                            app.show_settings_dialog = false;
                            continue;
                        }
                        _ => continue, // Ignore other keys when dialog is open
                    }
                }
                
                match (key.code, is_ctrl, is_shift) {
                    // Quit application
                    (KeyCode::Char('q'), true, false) => should_quit = true,
                    
                    // Clear history
                    (KeyCode::Char('c'), true, false) => {
                        // Lösche nur den aktuellen Modell-Buffer
                        app.history.clear();
                        app.scroll = 0;
                        app.autoscroll = true;
                        app.save_current_model_buffers();
                    }
                    
                    // Copy selection (Ctrl+Shift+C)
                    (KeyCode::Char('c'), true, true) | (KeyCode::Char('C'), true, _) => {
                        if let Err(e) = app.copy_selection() {
                            // Silently ignore errors (e.g., no selection)
                            if app.debug_keys {
                                app.debug_last_key = Some(format!("Copy failed: {}", e));
                            }
                        }
                    }
                    
                    // Paste from clipboard (Ctrl+Shift+V)
                    (KeyCode::Char('v'), true, true) | (KeyCode::Char('V'), true, _) => {
                        if let Err(e) = app.paste_from_clipboard() {
                            // Silently ignore errors
                            if app.debug_keys {
                                app.debug_last_key = Some(format!("Paste failed: {}", e));
                            }
                        }
                    }
                    
                    // Toggle autoscroll
                    (KeyCode::Char('s'), true, false) => app.autoscroll = !app.autoscroll,
                    
                    // Open settings dialog (Ctrl+O)
                    (KeyCode::Char('o'), true, false) => {
                        app.show_settings_dialog = !app.show_settings_dialog;
                        if app.show_settings_dialog {
                            app.settings_selection = 0; // Reset selection when opening
                        }
                    }
                    
                    // Cursor movement with Ctrl+Shift (word selection)
                    (KeyCode::Left, true, true) => {
                        app.move_cursor_word_left_with_selection();
                    }
                    (KeyCode::Right, true, true) => {
                        app.move_cursor_word_right_with_selection();
                    }
                    
                    // Cursor movement with Shift (character selection)
                    (KeyCode::Left, false, true) => {
                        app.move_cursor_left_with_selection();
                    }
                    (KeyCode::Right, false, true) => {
                        app.move_cursor_right_with_selection();
                    }
                    
                    // Home/End with Shift (select to start/end)
                    (KeyCode::Home, _, true) => {
                        app.move_cursor_home_with_selection();
                    }
                    (KeyCode::End, _, true) => {
                        app.move_cursor_end_with_selection();
                    }
                    
                    // Cursor movement with Ctrl (word jump)
                    (KeyCode::Left, true, false) => {
                        app.move_cursor_word_left();
                    }
                    (KeyCode::Right, true, false) => {
                        app.move_cursor_word_right();
                    }
                    
                    // Home/End without modifiers
                    (KeyCode::Home, false, false) => {
                        app.move_cursor_home();
                    }
                    (KeyCode::End, false, false) => {
                        app.move_cursor_end();
                    }
                    
                    // Model selection with Ctrl+Up/Down
                    (KeyCode::Up, true, false) => {
                        app.select_previous_model();
                    }
                    (KeyCode::Down, true, false) => {
                        app.select_next_model();
                    }
                    
                    // Cursor line movement with Up/Down
                    (KeyCode::Up, false, false) => {
                        app.move_cursor_up();
                    }
                    (KeyCode::Down, false, false) => {
                        app.move_cursor_down();
                    }
                    
                    // Page scrolling
                    (KeyCode::PageUp, _, _) => {
                        app.autoscroll = false;
                        app.scroll = app.scroll.saturating_sub(5);
                    }
                    (KeyCode::PageDown, _, _) => {
                        app.autoscroll = false;
                        app.scroll = app.scroll.saturating_add(5);
                    }
                    
                    // Normal cursor movement (no modifiers)
                    (KeyCode::Left, false, false) => {
                        app.move_cursor_left();
                    }
                    (KeyCode::Right, false, false) => {
                        app.move_cursor_right();
                    }
                    
                    // Word deletion
                    (KeyCode::Backspace, true, _) => {
                        app.delete_word_left();
                    }
                    (KeyCode::Char('h'), true, _) => {
                        // Kitty maps Ctrl+Backspace to Ctrl+H, so handle it as word deletion.
                        app.delete_word_left();
                    }
                    (KeyCode::Delete, true, _) => {
                        app.delete_word_right();
                    }
                    
                    // Normal deletion
                    (KeyCode::Delete, false, false) => {
                        app.delete_forward();
                    }
                    (KeyCode::Backspace, false, _) => {
                        app.backspace();
                    }
                    
                    // Enter handling
                    (KeyCode::Enter, _, true) => {
                        // Shift+Enter: Insert newline for multiline input
                        app.insert_char('\n');
                    }
                    (KeyCode::Enter, _, false) => {
                        // Normal Enter: Send query
                        if !app.input.is_empty() && !app.is_loading {
                            app.send_query(&mut terminal).await?;
                        }
                    }
                    
                    // Character input
                    (KeyCode::Char(c), false, _) => {
                        app.insert_char(c);
                    }
                    
                    _ => {}
                }
                
                // Only redraw after an actual event occurred
                terminal.draw(|f| ui::ui(f, &mut app))?;
            }
        } else if app.is_loading {
            // Redraw during loading for spinner animation
            terminal.draw(|f| ui::ui(f, &mut app))?;
        } else if app.update_cursor_blink() {
            terminal.draw(|f| ui::ui(f, &mut app))?;
        }
    }

    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    
    // Speichere die aktuellen Buffer vor dem Beenden
    app.save_current_model_buffers();
    
    // Speichere sowohl die allgemeine History als auch die modellspezifischen Histories
    utils::save_history_to_file(&app.history)?;
    utils::save_model_histories(&app.model_histories)?;
    Ok(())
}