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}