npm_run_scripts/utils/
terminal.rs

1//! Terminal utilities.
2
3use std::io::{self, Write};
4
5use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
6use crossterm::{cursor, execute};
7
8/// Minimum terminal dimensions.
9pub const MIN_WIDTH: u16 = 40;
10pub const MIN_HEIGHT: u16 = 10;
11
12/// Terminal size information.
13#[derive(Debug, Clone, Copy)]
14pub struct TerminalSize {
15    /// Width in columns.
16    pub width: u16,
17    /// Height in rows.
18    pub height: u16,
19}
20
21impl TerminalSize {
22    /// Check if the terminal size meets minimum requirements.
23    pub fn is_valid(&self) -> bool {
24        self.width >= MIN_WIDTH && self.height >= MIN_HEIGHT
25    }
26
27    /// Calculate the number of columns for the scripts grid.
28    pub fn grid_columns(&self) -> usize {
29        match self.width {
30            0..60 => 1,
31            60..90 => 2,
32            90..120 => 3,
33            120..160 => 4,
34            _ => 5,
35        }
36    }
37}
38
39/// Check the terminal size.
40///
41/// Returns the current terminal size, or None if it cannot be determined.
42pub fn check_terminal_size() -> Option<TerminalSize> {
43    terminal::size()
44        .ok()
45        .map(|(width, height)| TerminalSize { width, height })
46}
47
48/// Enable raw mode for TUI.
49///
50/// This enables character-by-character input without echo,
51/// which is required for TUI applications.
52///
53/// # Errors
54///
55/// Returns an error if raw mode cannot be enabled.
56pub fn enable_raw_mode() -> io::Result<()> {
57    terminal::enable_raw_mode()
58}
59
60/// Disable raw mode.
61///
62/// Restores the terminal to its normal state where input
63/// is line-buffered and echoed.
64///
65/// # Errors
66///
67/// Returns an error if raw mode cannot be disabled.
68pub fn disable_raw_mode() -> io::Result<()> {
69    terminal::disable_raw_mode()
70}
71
72/// Check if the terminal is currently in raw mode.
73pub fn is_raw_mode_enabled() -> bool {
74    terminal::is_raw_mode_enabled().unwrap_or(false)
75}
76
77/// Enter the alternate screen buffer.
78///
79/// This preserves the current terminal content and provides
80/// a clean screen for TUI rendering. When the TUI exits,
81/// the original content can be restored.
82///
83/// # Errors
84///
85/// Returns an error if the alternate screen cannot be entered.
86pub fn enter_alternate_screen() -> io::Result<()> {
87    execute!(io::stdout(), EnterAlternateScreen)?;
88    Ok(())
89}
90
91/// Leave the alternate screen buffer.
92///
93/// Restores the original terminal content that was saved
94/// when entering the alternate screen.
95///
96/// # Errors
97///
98/// Returns an error if the alternate screen cannot be left.
99pub fn leave_alternate_screen() -> io::Result<()> {
100    execute!(io::stdout(), LeaveAlternateScreen)?;
101    Ok(())
102}
103
104/// Show the cursor.
105///
106/// # Errors
107///
108/// Returns an error if the cursor cannot be shown.
109pub fn show_cursor() -> io::Result<()> {
110    execute!(io::stdout(), cursor::Show)?;
111    Ok(())
112}
113
114/// Hide the cursor.
115///
116/// # Errors
117///
118/// Returns an error if the cursor cannot be hidden.
119pub fn hide_cursor() -> io::Result<()> {
120    execute!(io::stdout(), cursor::Hide)?;
121    Ok(())
122}
123
124/// Prepare terminal for script execution.
125///
126/// This function should be called before running a script to ensure
127/// the terminal is in a normal state:
128/// - Disables raw mode
129/// - Leaves alternate screen
130/// - Shows cursor
131///
132/// # Errors
133///
134/// Returns an error if terminal cleanup fails.
135pub fn prepare_for_script_execution() -> io::Result<()> {
136    // Only disable raw mode if it's enabled
137    if is_raw_mode_enabled() {
138        disable_raw_mode()?;
139    }
140
141    // Leave alternate screen
142    leave_alternate_screen()?;
143
144    // Show cursor
145    show_cursor()?;
146
147    // Flush stdout
148    io::stdout().flush()?;
149
150    Ok(())
151}
152
153/// Restore terminal for TUI.
154///
155/// This function should be called after a script finishes to restore
156/// the terminal to TUI mode:
157/// - Enables raw mode
158/// - Enters alternate screen
159/// - Hides cursor
160///
161/// # Errors
162///
163/// Returns an error if terminal setup fails.
164pub fn restore_for_tui() -> io::Result<()> {
165    // Enter alternate screen
166    enter_alternate_screen()?;
167
168    // Enable raw mode
169    enable_raw_mode()?;
170
171    // Hide cursor
172    hide_cursor()?;
173
174    // Flush stdout
175    io::stdout().flush()?;
176
177    Ok(())
178}
179
180/// Cleanup terminal completely.
181///
182/// This function should be called when the application exits to ensure
183/// the terminal is in a clean state:
184/// - Disables raw mode
185/// - Leaves alternate screen
186/// - Shows cursor
187///
188/// This is similar to `prepare_for_script_execution` but doesn't check
189/// raw mode status first - it always tries to clean up.
190///
191/// # Errors
192///
193/// Returns an error if terminal cleanup fails.
194pub fn cleanup_terminal() -> io::Result<()> {
195    // Always try to disable raw mode
196    let _ = disable_raw_mode();
197
198    // Leave alternate screen
199    let _ = leave_alternate_screen();
200
201    // Show cursor
202    let _ = show_cursor();
203
204    // Flush stdout
205    io::stdout().flush()?;
206
207    Ok(())
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_terminal_size_validity() {
216        let valid = TerminalSize {
217            width: 80,
218            height: 24,
219        };
220        assert!(valid.is_valid());
221
222        let too_small = TerminalSize {
223            width: 30,
224            height: 5,
225        };
226        assert!(!too_small.is_valid());
227    }
228
229    #[test]
230    fn test_grid_columns() {
231        assert_eq!(
232            TerminalSize {
233                width: 50,
234                height: 24
235            }
236            .grid_columns(),
237            1
238        );
239        assert_eq!(
240            TerminalSize {
241                width: 80,
242                height: 24
243            }
244            .grid_columns(),
245            2
246        );
247        assert_eq!(
248            TerminalSize {
249                width: 100,
250                height: 24
251            }
252            .grid_columns(),
253            3
254        );
255        assert_eq!(
256            TerminalSize {
257                width: 140,
258                height: 24
259            }
260            .grid_columns(),
261            4
262        );
263        assert_eq!(
264            TerminalSize {
265                width: 200,
266                height: 24
267            }
268            .grid_columns(),
269            5
270        );
271    }
272}