reratui_runtime/
terminal.rs

1//! Terminal management for Reratui runtime
2//!
3//! This module provides terminal initialization, cleanup, and management
4//! functionality for TUI applications.
5
6use crossterm::{
7    event::{DisableMouseCapture, EnableMouseCapture},
8    execute,
9    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
10};
11use ratatui::{Terminal, backend::CrosstermBackend};
12use std::io::{self, Stdout};
13
14/// A managed terminal instance that handles setup and cleanup
15pub struct ManagedTerminal {
16    terminal: Terminal<CrosstermBackend<Stdout>>,
17}
18
19impl ManagedTerminal {
20    /// Initialize a new terminal with proper setup
21    pub fn new() -> io::Result<Self> {
22        // Enable raw mode for input handling
23        enable_raw_mode()?;
24
25        // Get stdout
26        let mut stdout = io::stdout();
27
28        // Enter alternate screen to preserve terminal state
29        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
30
31        // Create the terminal backend
32        let backend = CrosstermBackend::new(stdout);
33        let terminal = Terminal::new(backend)?;
34
35        Ok(Self { terminal })
36    }
37
38    /// Get a mutable reference to the terminal
39    pub fn terminal_mut(&mut self) -> &mut Terminal<CrosstermBackend<Stdout>> {
40        &mut self.terminal
41    }
42
43    /// Get the terminal size
44    pub fn size(&self) -> io::Result<ratatui::layout::Rect> {
45        self.terminal
46            .size()
47            .map(|size| ratatui::layout::Rect::new(0, 0, size.width, size.height))
48    }
49
50    /// Clear the terminal
51    pub fn clear(&mut self) -> io::Result<()> {
52        self.terminal.clear()
53    }
54
55    /// Draw the terminal with a closure
56    pub fn draw<F>(&mut self, f: F) -> io::Result<()>
57    where
58        F: FnOnce(&mut ratatui::Frame),
59    {
60        self.terminal.draw(f)?;
61        Ok(())
62    }
63}
64
65impl Drop for ManagedTerminal {
66    /// Cleanup terminal state when dropped
67    fn drop(&mut self) {
68        // Restore terminal state
69        let _ = disable_raw_mode();
70        let _ = execute!(
71            self.terminal.backend_mut(),
72            LeaveAlternateScreen,
73            DisableMouseCapture
74        );
75        let _ = self.terminal.show_cursor();
76    }
77}
78
79/// Initialize terminal for TUI applications
80pub fn setup_terminal() -> io::Result<ManagedTerminal> {
81    ManagedTerminal::new()
82}
83
84/// Restore terminal to original state
85pub fn restore_terminal() -> io::Result<()> {
86    // Disable raw mode
87    disable_raw_mode()?;
88
89    // Leave alternate screen and disable mouse capture
90    execute!(
91        std::io::stdout(),
92        LeaveAlternateScreen,
93        DisableMouseCapture,
94        crossterm::cursor::Show
95    )?;
96
97    Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::sync::{Arc, Mutex};
104    use std::thread;
105    use std::time::Duration;
106
107    /// Test that ManagedTerminal can be created and dropped safely
108    #[test]
109    fn test_managed_terminal_creation_and_cleanup() {
110        // This test verifies basic terminal lifecycle
111        // Note: In CI environments, this might fail due to lack of TTY
112        // but it's useful for local development
113
114        // We can't easily test actual terminal setup in unit tests
115        // since it requires a real terminal, so we test the API structure
116        // Placeholder - actual terminal tests need integration environment
117    }
118
119    /// Test terminal size conversion from Size to Rect
120    #[test]
121    fn test_size_conversion() {
122        // Test the logic we use in the size() method
123        let width = 80u16;
124        let height = 24u16;
125
126        let rect = ratatui::layout::Rect::new(0, 0, width, height);
127
128        assert_eq!(rect.x, 0);
129        assert_eq!(rect.y, 0);
130        assert_eq!(rect.width, width);
131        assert_eq!(rect.height, height);
132    }
133
134    /// Test that setup_terminal function exists and has correct signature
135    #[test]
136    fn test_setup_terminal_signature() {
137        // Verify the function signature compiles
138        let _setup_fn: fn() -> io::Result<ManagedTerminal> = setup_terminal;
139    }
140
141    /// Test that restore_terminal function exists and has correct signature
142    #[test]
143    fn test_restore_terminal_signature() {
144        // Verify the function signature compiles
145        let _restore_fn: fn() -> io::Result<()> = restore_terminal;
146    }
147
148    /// Test ManagedTerminal method signatures
149    #[test]
150    fn test_managed_terminal_methods() {
151        // Test that all expected methods exist with correct signatures
152        // This is a compile-time test
153
154        // We can't actually create a ManagedTerminal in tests without a TTY,
155        // but we can verify the method signatures exist
156        fn _test_methods(mut terminal: ManagedTerminal) -> io::Result<()> {
157            let _size = terminal.size()?;
158            terminal.clear()?;
159            terminal.draw(|_frame| {})?;
160            let _term_ref = terminal.terminal_mut();
161            Ok(())
162        }
163
164        // Compilation success means methods exist
165    }
166
167    /// Test error handling scenarios
168    #[test]
169    fn test_error_handling() {
170        // Test that our error types are compatible
171        let _io_error: io::Error = io::Error::other("test");
172
173        // Verify our functions return the expected error types
174        fn _test_error_types() {
175            let _: io::Result<ManagedTerminal> = setup_terminal();
176            let _: io::Result<()> = restore_terminal();
177        }
178    }
179
180    /// Test thread safety considerations
181    #[test]
182    fn test_thread_safety() {
183        // Test that our types can be used safely across threads
184        let counter = Arc::new(Mutex::new(0));
185        let counter_clone = Arc::clone(&counter);
186
187        let handle = thread::spawn(move || {
188            let mut num = counter_clone.lock().unwrap();
189            *num += 1;
190
191            // Test that our functions can be called from different threads
192            let _setup_fn = setup_terminal;
193            let _restore_fn = restore_terminal;
194        });
195
196        handle.join().unwrap();
197        assert_eq!(*counter.lock().unwrap(), 1);
198    }
199
200    /// Test Drop implementation behavior
201    #[test]
202    fn test_drop_implementation() {
203        // Verify that ManagedTerminal has drop behavior
204        // This is important for RAII cleanup
205        use std::mem;
206
207        // Test that ManagedTerminal needs drop (has custom Drop implementation)
208        assert!(mem::needs_drop::<ManagedTerminal>());
209
210        // We can't test the actual Drop behavior without a real terminal,
211        // but we can verify the type requires cleanup
212    }
213
214    /// Performance test for rapid terminal operations
215    #[test]
216    fn test_performance_characteristics() {
217        // Test that our terminal operations complete in reasonable time
218        let start = std::time::Instant::now();
219
220        // Simulate the work our functions would do
221        for _ in 0..1000 {
222            let _rect = ratatui::layout::Rect::new(0, 0, 80, 24);
223        }
224
225        let duration = start.elapsed();
226
227        // Should complete very quickly since it's just struct creation
228        assert!(duration < Duration::from_millis(10));
229    }
230
231    /// Test memory usage patterns
232    #[test]
233    fn test_memory_usage() {
234        // Test that our structs have reasonable memory footprint
235        use std::mem;
236
237        // ManagedTerminal should be relatively small
238        let terminal_size = mem::size_of::<ManagedTerminal>();
239
240        // Should be reasonable size (less than 1KB)
241        assert!(terminal_size < 1024);
242
243        // Test that Rect creation is efficient
244        let rect_size = mem::size_of::<ratatui::layout::Rect>();
245        assert!(rect_size <= 8); // Should be just 4 u16s
246    }
247}