//! Shared application state types used across TUI frontends. use std::fmt; /// High-level application state reported by the UI loop. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AppState { Running, Quit, } /// Vim-style input modes supported by the TUI. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InputMode { Normal, Editing, ProviderSelection, ModelSelection, Help, Visual, Command, SessionBrowser, ThemeBrowser, RepoSearch, SymbolSearch, } impl fmt::Display for InputMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { InputMode::Normal => "Normal", InputMode::Editing => "Editing", InputMode::ModelSelection => "Model", InputMode::ProviderSelection => "Provider", InputMode::Help => "Help", InputMode::Visual => "Visual", InputMode::Command => "Command", InputMode::SessionBrowser => "Sessions", InputMode::ThemeBrowser => "Themes", InputMode::RepoSearch => "Search", InputMode::SymbolSearch => "Symbols", }; f.write_str(label) } } /// Represents which panel is currently focused in the TUI layout. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FocusedPanel { Files, Chat, Thinking, Input, Code, } /// Auto-scroll state manager for scrollable panels. #[derive(Debug, Clone)] pub struct AutoScroll { pub scroll: usize, pub content_len: usize, pub stick_to_bottom: bool, } impl Default for AutoScroll { fn default() -> Self { Self { scroll: 0, content_len: 0, stick_to_bottom: true, } } } impl AutoScroll { /// Update scroll position based on viewport height. pub fn on_viewport(&mut self, viewport_h: usize) { let max = self.content_len.saturating_sub(viewport_h); if self.stick_to_bottom { self.scroll = max; } else { self.scroll = self.scroll.min(max); } } /// Handle user scroll input. pub fn on_user_scroll(&mut self, delta: isize, viewport_h: usize) { let max = self.content_len.saturating_sub(viewport_h) as isize; let s = (self.scroll as isize + delta).clamp(0, max) as usize; self.scroll = s; self.stick_to_bottom = s as isize == max; } pub fn scroll_half_page_down(&mut self, viewport_h: usize) { let delta = (viewport_h / 2) as isize; self.on_user_scroll(delta, viewport_h); } pub fn scroll_half_page_up(&mut self, viewport_h: usize) { let delta = -((viewport_h / 2) as isize); self.on_user_scroll(delta, viewport_h); } pub fn scroll_full_page_down(&mut self, viewport_h: usize) { let delta = viewport_h as isize; self.on_user_scroll(delta, viewport_h); } pub fn scroll_full_page_up(&mut self, viewport_h: usize) { let delta = -(viewport_h as isize); self.on_user_scroll(delta, viewport_h); } pub fn jump_to_top(&mut self) { self.scroll = 0; self.stick_to_bottom = false; } pub fn jump_to_bottom(&mut self, viewport_h: usize) { self.stick_to_bottom = true; self.on_viewport(viewport_h); } } /// Visual selection state for text selection. #[derive(Debug, Clone, Default)] pub struct VisualSelection { pub start: Option<(usize, usize)>, pub end: Option<(usize, usize)>, } impl VisualSelection { pub fn new() -> Self { Self::default() } pub fn start_at(&mut self, pos: (usize, usize)) { self.start = Some(pos); self.end = Some(pos); } pub fn extend_to(&mut self, pos: (usize, usize)) { self.end = Some(pos); } pub fn clear(&mut self) { self.start = None; self.end = None; } pub fn is_active(&self) -> bool { self.start.is_some() && self.end.is_some() } pub fn get_normalized(&self) -> Option<((usize, usize), (usize, usize))> { if let (Some(s), Some(e)) = (self.start, self.end) { if s.0 < e.0 || (s.0 == e.0 && s.1 <= e.1) { Some((s, e)) } else { Some((e, s)) } } else { None } } } /// Cursor position helper for navigating scrollable content. #[derive(Debug, Clone, Copy, Default)] pub struct CursorPosition { pub row: usize, pub col: usize, } impl CursorPosition { pub fn new(row: usize, col: usize) -> Self { Self { row, col } } pub fn move_up(&mut self, amount: usize) { self.row = self.row.saturating_sub(amount); } pub fn move_down(&mut self, amount: usize, max: usize) { self.row = (self.row + amount).min(max); } pub fn move_left(&mut self, amount: usize) { self.col = self.col.saturating_sub(amount); } pub fn move_right(&mut self, amount: usize, max: usize) { self.col = (self.col + amount).min(max); } pub fn as_tuple(&self) -> (usize, usize) { (self.row, self.col) } }