hojicha_runtime/program/
terminal_manager.rs

1//! Terminal management using crossterm for direct control
2//!
3//! This module handles raw terminal setup, alternate screen management,
4//! and terminal state restoration for clean program exits.
5
6use crate::program::MouseMode;
7use crossterm::{
8    cursor, execute,
9    terminal::{
10        self, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
11    },
12};
13use std::io::{self, Stdout, Write};
14
15/// Configuration for terminal setup
16#[derive(Debug, Clone)]
17pub struct TerminalConfig {
18    /// Whether to use alternate screen buffer
19    pub alt_screen: bool,
20    /// Mouse input mode configuration
21    pub mouse_mode: MouseMode,
22    /// Whether to enable bracketed paste mode
23    pub bracketed_paste: bool,
24    /// Whether to enable terminal focus reporting
25    pub focus_reporting: bool,
26    /// Whether to run in headless mode (no terminal setup)
27    pub headless: bool,
28}
29
30impl Default for TerminalConfig {
31    fn default() -> Self {
32        Self {
33            alt_screen: true,
34            mouse_mode: MouseMode::None,
35            bracketed_paste: false,
36            focus_reporting: false,
37            headless: false,
38        }
39    }
40}
41
42/// Manages terminal setup, teardown, and state
43pub struct TerminalManager {
44    stdout: Option<Stdout>,
45    config: TerminalConfig,
46    is_released: bool,
47}
48
49impl TerminalManager {
50    /// Create a new terminal manager
51    pub fn new(config: TerminalConfig) -> io::Result<Self> {
52        let headless = config.headless;
53        let mut manager = Self {
54            stdout: if !headless { Some(io::stdout()) } else { None },
55            config,
56            is_released: false,
57        };
58
59        if !headless {
60            manager.setup()?;
61        }
62
63        Ok(manager)
64    }
65
66    /// Set up the terminal with the given configuration
67    fn setup(&mut self) -> io::Result<()> {
68        if let Some(ref mut stdout) = self.stdout {
69            // Enable raw mode
70            enable_raw_mode()?;
71
72            // Enter alternate screen if requested
73            if self.config.alt_screen {
74                execute!(stdout, EnterAlternateScreen)?;
75            }
76
77            // Clear screen
78            execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
79            execute!(stdout, cursor::MoveTo(0, 0))?;
80
81            // Hide cursor by default (components can show it when needed)
82            execute!(stdout, cursor::Hide)?;
83
84            // Set up mouse mode
85            match self.config.mouse_mode {
86                MouseMode::CellMotion => {
87                    execute!(stdout, crossterm::event::EnableMouseCapture)?;
88                }
89                MouseMode::AllMotion => {
90                    execute!(stdout, crossterm::event::EnableMouseCapture,)?;
91                }
92                MouseMode::None => {}
93            }
94
95            // Enable bracketed paste if requested
96            if self.config.bracketed_paste {
97                execute!(stdout, crossterm::event::EnableBracketedPaste)?;
98            }
99
100            // Enable focus reporting if requested
101            if self.config.focus_reporting {
102                execute!(stdout, crossterm::event::EnableFocusChange)?;
103            }
104
105            stdout.flush()?;
106        }
107
108        Ok(())
109    }
110
111    /// Clean up terminal settings
112    pub fn cleanup(&mut self) -> io::Result<()> {
113        if self.is_released || self.config.headless {
114            return Ok(());
115        }
116
117        if let Some(ref mut stdout) = self.stdout {
118            // Show cursor
119            execute!(stdout, cursor::Show)?;
120
121            // Disable focus reporting
122            if self.config.focus_reporting {
123                execute!(stdout, crossterm::event::DisableFocusChange)?;
124            }
125
126            // Disable bracketed paste
127            if self.config.bracketed_paste {
128                execute!(stdout, crossterm::event::DisableBracketedPaste)?;
129            }
130
131            // Disable mouse capture
132            if self.config.mouse_mode != MouseMode::None {
133                execute!(stdout, crossterm::event::DisableMouseCapture)?;
134            }
135
136            // Leave alternate screen
137            if self.config.alt_screen {
138                execute!(stdout, LeaveAlternateScreen)?;
139            }
140
141            // Disable raw mode
142            disable_raw_mode()?;
143
144            stdout.flush()?;
145        }
146
147        self.is_released = true;
148        Ok(())
149    }
150
151    /// Get a mutable reference to stdout
152    pub fn stdout_mut(&mut self) -> Option<&mut Stdout> {
153        self.stdout.as_mut()
154    }
155
156    /// Check if terminal is set up
157    pub fn is_setup(&self) -> bool {
158        !self.is_released && self.stdout.is_some()
159    }
160
161    /// Get terminal size
162    pub fn size(&self) -> io::Result<(u16, u16)> {
163        terminal::size()
164    }
165
166    /// Release the terminal (alias for cleanup)
167    pub fn release(&mut self) -> io::Result<()> {
168        self.cleanup()
169    }
170
171    /// Restore the terminal (alias for cleanup)
172    pub fn restore(&mut self) -> io::Result<()> {
173        self.cleanup()
174    }
175}
176
177impl Drop for TerminalManager {
178    fn drop(&mut self) {
179        // Attempt cleanup, but don't panic if it fails
180        let _ = self.cleanup();
181    }
182}