hojicha_runtime/program/
terminal_manager.rs1use crate::program::MouseMode;
4use crossterm::{
5 execute,
6 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use ratatui::{backend::CrosstermBackend, Terminal};
9use std::io::{self, Stdout};
10
11#[derive(Debug, Clone)]
13pub struct TerminalConfig {
14 pub alt_screen: bool,
16 pub mouse_mode: MouseMode,
18 pub bracketed_paste: bool,
20 pub focus_reporting: bool,
22 pub headless: bool,
24}
25
26impl Default for TerminalConfig {
27 fn default() -> Self {
28 Self {
29 alt_screen: true,
30 mouse_mode: MouseMode::None,
31 bracketed_paste: false,
32 focus_reporting: false,
33 headless: false,
34 }
35 }
36}
37
38pub struct TerminalManager {
40 terminal: Option<Terminal<CrosstermBackend<Stdout>>>,
41 config: TerminalConfig,
42 alt_screen_was_active: bool,
43 is_released: bool,
44}
45
46impl TerminalManager {
47 pub fn new(config: TerminalConfig) -> io::Result<Self> {
49 let terminal = if !config.headless {
50 Some(Self::setup_terminal(&config)?)
51 } else {
52 None
53 };
54
55 Ok(Self {
56 terminal,
57 config,
58 alt_screen_was_active: false,
59 is_released: false,
60 })
61 }
62
63 fn setup_terminal(config: &TerminalConfig) -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
65 let mut stdout = io::stdout();
66
67 enable_raw_mode()?;
69
70 if config.alt_screen {
72 execute!(stdout, EnterAlternateScreen)?;
73 }
74
75 match config.mouse_mode {
77 MouseMode::CellMotion => {
78 execute!(stdout, crossterm::event::EnableMouseCapture)?;
79 }
80 MouseMode::AllMotion => {
81 execute!(
82 stdout,
83 crossterm::event::EnableMouseCapture,
84 crossterm::cursor::Show,
85 crossterm::cursor::Hide,
86 )?;
87 }
88 MouseMode::None => {}
89 }
90
91 if config.bracketed_paste {
93 execute!(stdout, crossterm::event::EnableBracketedPaste)?;
94 }
95
96 if config.focus_reporting {
98 execute!(stdout, crossterm::event::EnableFocusChange)?;
99 }
100
101 let backend = CrosstermBackend::new(stdout);
102 let mut terminal = Terminal::new(backend)?;
103 terminal.hide_cursor()?;
104
105 Ok(terminal)
106 }
107
108 pub fn terminal(&self) -> Option<&Terminal<CrosstermBackend<Stdout>>> {
110 self.terminal.as_ref()
111 }
112
113 pub fn terminal_mut(&mut self) -> Option<&mut Terminal<CrosstermBackend<Stdout>>> {
115 self.terminal.as_mut()
116 }
117
118 pub fn release(&mut self) -> io::Result<()> {
120 if self.is_released || self.config.headless {
121 return Ok(());
122 }
123
124 self.alt_screen_was_active = self.config.alt_screen;
126
127 if let Some(ref mut terminal) = self.terminal {
129 terminal.show_cursor()?;
130 }
131
132 if self.config.alt_screen {
134 execute!(io::stdout(), LeaveAlternateScreen)?;
135 }
136
137 disable_raw_mode()?;
139
140 self.is_released = true;
141 Ok(())
142 }
143
144 pub fn restore(&mut self) -> io::Result<()> {
146 if !self.is_released || self.config.headless {
147 return Ok(());
148 }
149
150 enable_raw_mode()?;
152
153 if self.alt_screen_was_active {
155 execute!(io::stdout(), EnterAlternateScreen)?;
156 }
157
158 if let Some(ref mut terminal) = self.terminal {
160 terminal.hide_cursor()?;
161 terminal.clear()?;
163 }
164
165 self.is_released = false;
166 Ok(())
167 }
168
169 pub fn is_released(&self) -> bool {
171 self.is_released
172 }
173
174 pub fn cleanup(&mut self) -> io::Result<()> {
176 if self.config.headless {
177 return Ok(());
178 }
179
180 if let Some(ref mut terminal) = self.terminal {
182 let _ = terminal.show_cursor();
183 }
184
185 let mut stdout = io::stdout();
187
188 if self.config.focus_reporting {
189 let _ = execute!(stdout, crossterm::event::DisableFocusChange);
190 }
191
192 if self.config.bracketed_paste {
193 let _ = execute!(stdout, crossterm::event::DisableBracketedPaste);
194 }
195
196 if self.config.mouse_mode != MouseMode::None {
197 let _ = execute!(stdout, crossterm::event::DisableMouseCapture);
198 }
199
200 if self.config.alt_screen && !self.is_released {
201 let _ = execute!(stdout, LeaveAlternateScreen);
202 }
203
204 let _ = disable_raw_mode();
206
207 Ok(())
208 }
209
210 pub fn draw<F>(&mut self, f: F) -> io::Result<()>
212 where
213 F: FnOnce(&mut ratatui::Frame),
214 {
215 if let Some(ref mut terminal) = self.terminal {
216 terminal.draw(f)?;
217 }
218 Ok(())
219 }
220
221 pub fn size(&self) -> io::Result<ratatui::layout::Rect> {
223 if let Some(ref terminal) = self.terminal {
224 let size = terminal.size()?;
225 Ok(ratatui::layout::Rect::new(0, 0, size.width, size.height))
226 } else {
227 Ok(ratatui::layout::Rect::new(0, 0, 80, 24))
229 }
230 }
231
232 pub fn clear(&mut self) -> io::Result<()> {
234 if let Some(ref mut terminal) = self.terminal {
235 terminal.clear()?;
236 }
237 Ok(())
238 }
239}
240
241impl Drop for TerminalManager {
242 fn drop(&mut self) {
243 let _ = self.cleanup();
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_terminal_config_default() {
253 let config = TerminalConfig::default();
254 assert!(config.alt_screen);
255 assert_eq!(config.mouse_mode, MouseMode::None);
256 assert!(!config.bracketed_paste);
257 assert!(!config.focus_reporting);
258 assert!(!config.headless);
259 }
260
261 #[test]
262 fn test_terminal_manager_headless() {
263 let config = TerminalConfig {
264 headless: true,
265 ..Default::default()
266 };
267
268 let manager = TerminalManager::new(config).unwrap();
269 assert!(manager.terminal().is_none());
270 assert!(!manager.is_released());
271 }
272
273 #[test]
274 fn test_terminal_manager_release_restore() {
275 let config = TerminalConfig {
276 headless: true,
277 ..Default::default()
278 };
279
280 let mut manager = TerminalManager::new(config).unwrap();
281
282 assert!(manager.release().is_ok());
284 assert!(!manager.is_released()); assert!(manager.restore().is_ok());
288 assert!(!manager.is_released());
289 }
290
291 #[test]
292 fn test_terminal_manager_size_headless() {
293 let config = TerminalConfig {
294 headless: true,
295 ..Default::default()
296 };
297
298 let manager = TerminalManager::new(config).unwrap();
299 let size = manager.size().unwrap();
300
301 assert_eq!(size.width, 80);
303 assert_eq!(size.height, 24);
304 }
305
306 #[test]
307 fn test_terminal_manager_clear_headless() {
308 let config = TerminalConfig {
309 headless: true,
310 ..Default::default()
311 };
312
313 let mut manager = TerminalManager::new(config).unwrap();
314
315 assert!(manager.clear().is_ok());
317 }
318
319 #[test]
320 fn test_terminal_manager_draw_headless() {
321 let config = TerminalConfig {
322 headless: true,
323 ..Default::default()
324 };
325
326 let mut manager = TerminalManager::new(config).unwrap();
327
328 assert!(manager
330 .draw(|_f| {
331 })
333 .is_ok());
334 }
335
336 #[test]
337 fn test_terminal_manager_cleanup() {
338 let config = TerminalConfig {
339 headless: true,
340 ..Default::default()
341 };
342
343 let mut manager = TerminalManager::new(config).unwrap();
344
345 assert!(manager.cleanup().is_ok());
347 }
348
349 #[test]
350 fn test_terminal_manager_drop() {
351 let config = TerminalConfig {
352 headless: true,
353 ..Default::default()
354 };
355
356 {
357 let _manager = TerminalManager::new(config).unwrap();
358 }
360 }
362
363 #[test]
364 fn test_terminal_config_variations() {
365 let configs = vec![
366 TerminalConfig {
367 alt_screen: false,
368 mouse_mode: MouseMode::CellMotion,
369 bracketed_paste: true,
370 focus_reporting: true,
371 headless: true,
372 },
373 TerminalConfig {
374 alt_screen: true,
375 mouse_mode: MouseMode::AllMotion,
376 bracketed_paste: false,
377 focus_reporting: false,
378 headless: true,
379 },
380 ];
381
382 for config in configs {
383 let manager = TerminalManager::new(config).unwrap();
384 assert!(manager.terminal().is_none()); }
386 }
387}