stynx-code-tui 3.12.1

Terminal user interface with ratatui for interactive sessions
Documentation
pub mod clipboard;
pub mod dialogs;
pub mod event;
pub mod layout;
pub mod persistence;
pub mod render;
pub mod state;
pub mod theme;
pub mod tool_ui_impl;
pub mod util;
pub mod widgets;

pub use event::event_handler::{EventHandler, UiAction};
pub use render::renderer::Renderer;
pub use state::{
    AppState, ConversationState, DialogOption, DisplayMessage, DisplayToolUse, InputMode,
    InputState, ModalKind, ModalState, PermissionChoice, SelectKind, ToolUseStatus,
};

use std::io;
use std::sync::atomic::{AtomicBool, Ordering};

use crossterm::{
    event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};

pub struct TuiApp {
    terminal: Terminal<CrosstermBackend<io::Stdout>>,
    pub state: AppState,
    in_alt: bool,
}

pub fn restore_terminal() {
    let _ = disable_raw_mode();
    let _ = execute!(
        io::stdout(),
        DisableMouseCapture,
        DisableBracketedPaste,
        LeaveAlternateScreen,
    );
}

fn install_panic_hook() {
    use std::sync::Once;
    static INSTALLED: Once = Once::new();
    INSTALLED.call_once(|| {
        let prev = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |info| {
            restore_terminal();
            prev(info);
        }));
    });
}

impl TuiApp {
    pub fn new() -> io::Result<Self> {
        install_panic_hook();
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)?;
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend)?;
        Ok(Self { terminal, state: AppState::new(), in_alt: true })
    }

    pub fn draw(&mut self) -> io::Result<()> {
        let Self { terminal, state, .. } = self;
        terminal.draw(|frame| Renderer::draw(frame, state))?;
        Ok(())
    }

    pub fn leave_alt(&mut self) {
        if self.in_alt {
            disable_raw_mode().ok();
            execute!(self.terminal.backend_mut(), DisableMouseCapture, DisableBracketedPaste, LeaveAlternateScreen).ok();
            self.in_alt = false;
        }
    }

    pub fn enter_alt(&mut self) {
        if !self.in_alt {
            enable_raw_mode().ok();
            execute!(self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste).ok();
            self.terminal.clear().ok();
            self.in_alt = true;
        }
    }

    pub fn is_in_alt(&self) -> bool { self.in_alt }

    pub fn tick_spinner(&mut self) {
        if self.state.is_streaming {
            self.state.spinner_tick = self.state.spinner_tick.wrapping_add(1);
            if self.state.spinner_tick % 8 == 0 {
                self.state.spinner_frame = self.state.spinner_frame.wrapping_add(1) % 10;
            }
        }
        self.state.toasts.tick();
    }

    pub fn sync_pause(&mut self, paused: &AtomicBool) {
        if paused.load(Ordering::Relaxed) {
            self.leave_alt();
        } else if !self.in_alt {
            self.enter_alt();
        }
    }
}

impl Drop for TuiApp {
    fn drop(&mut self) { self.leave_alt(); }
}