cargo_e/
e_prompts.rs

1#![allow(unused_variables)]
2use anyhow::Result;
3#[cfg(feature = "tui")]
4use crossterm::event::{poll, read, Event, KeyCode};
5use std::error::Error;
6use std::time::Duration;
7
8/// A RAII guard that enables raw mode and disables it when dropped.
9#[allow(dead_code)]
10struct RawModeGuard;
11
12impl RawModeGuard {
13    #[allow(dead_code)]
14    fn new() -> Result<Self> {
15        #[cfg(feature = "tui")]
16        crossterm::terminal::enable_raw_mode()?;
17        Ok(Self)
18    }
19}
20
21impl Drop for RawModeGuard {
22    fn drop(&mut self) {
23        #[cfg(feature = "tui")]
24        let _ = crossterm::terminal::disable_raw_mode();
25    }
26}
27
28/// Prompts the user with the given message and waits up to `wait_secs` seconds
29/// for a key press. Returns `Ok(Some(c))` if a key is pressed, or `Ok(None)`
30/// if the timeout expires.
31pub fn prompt(message: &str, wait_secs: u64) -> Result<Option<char>> {
32    if !message.trim().is_empty() {
33        println!("{}", message);
34    }
35    use std::io::IsTerminal;
36    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
37        // println!("Non-interactive mode detected; skipping prompt.");
38        return Ok(None);
39    }
40
41    // When the "tui" feature is enabled, use raw mode.
42    #[cfg(feature = "tui")]
43    {
44        let timeout = Duration::from_secs(wait_secs);
45        drain_events().ok(); // Clear any pending events.
46                             // Enable raw mode and ensure it will be disabled when the guard is dropped.
47        let _raw_guard = RawModeGuard::new()?;
48        let result = if poll(timeout)? {
49            if let Event::Key(key_event) = read()? {
50                if let KeyCode::Char(c) = key_event.code {
51                    // Check if it's the Ctrl+C character, which is often '\x03'
52                    if c == '\x03' {
53                        // Ctrl+C
54                        return Err(anyhow::anyhow!("Ctrl+C pressed").into()); // Propagate as error to handle
55                    }
56                    Some(c.to_ascii_lowercase())
57                } else {
58                    None
59                }
60            } else {
61                None
62            }
63        } else {
64            None
65        };
66        Ok(result)
67    }
68
69    // Otherwise, use normal line input.
70    #[cfg(not(feature = "tui"))]
71    {
72        use std::io::{self, BufRead};
73        use std::sync::mpsc;
74        use std::thread;
75
76        let (tx, rx) = mpsc::channel();
77        thread::spawn(move || {
78            let stdin = io::stdin();
79            let mut line = String::new();
80            // This call will block until input is received.
81            let _ = stdin.lock().read_line(&mut line);
82            let _ = tx.send(line);
83        });
84
85        match rx.recv_timeout(Duration::from_secs(wait_secs)) {
86            Ok(line) => Ok(line.trim().chars().next().map(|c| c.to_ascii_lowercase())),
87            Err(_) => Ok(None),
88        }
89    }
90}
91
92/// Reads an entire line from the user with a timeout of `wait_secs`.
93/// Returns Ok(Some(String)) if input is received, or Ok(None) if the timeout expires.
94pub fn prompt_line(message: &str, wait_secs: u64) -> Result<Option<String>, Box<dyn Error>> {
95    if !message.trim().is_empty() {
96        println!("{}", message);
97    }
98    use std::io::IsTerminal;
99    if !std::io::stdin().is_terminal() {
100        // println!("Non-interactive mode detected; skipping prompt.");
101        return Ok(None);
102    }
103
104    #[cfg(not(feature = "tui"))]
105    {
106        use std::io::{self, BufRead};
107        use std::sync::mpsc;
108        use std::thread;
109
110        let (tx, rx) = mpsc::channel();
111        thread::spawn(move || {
112            let stdin = io::stdin();
113            let mut line = String::new();
114            // This call will block until input is received.
115            let _ = stdin.lock().read_line(&mut line);
116            let _ = tx.send(line);
117        });
118
119        match rx.recv_timeout(Duration::from_secs(wait_secs)) {
120            Ok(line) => Ok(Some(line.trim().to_string())),
121            Err(_) => Ok(None),
122        }
123    }
124
125    #[cfg(feature = "tui")]
126    {
127        // In TUI raw mode, we collect key events until Enter is pressed.
128        use crossterm::event::{poll, read, Event, KeyCode};
129        use std::io::{self, Write};
130        // Enable raw mode and ensure it will be disabled when the guard is dropped.
131        #[cfg(feature = "tui")]
132        let _raw_guard = RawModeGuard::new()?;
133        let mut input = String::new();
134        let start = std::time::Instant::now();
135        loop {
136            let elapsed = start.elapsed().as_secs();
137            if elapsed >= wait_secs {
138                println!("Timeout reached; no input received.");
139                return Ok(None);
140            }
141            let remaining = Duration::from_secs(wait_secs - elapsed);
142            if poll(remaining)? {
143                if let Event::Key(key_event) = read()? {
144                    // Only process key press events
145                    if key_event.kind != crossterm::event::KeyEventKind::Press {
146                        continue;
147                    }
148                    match key_event.code {
149                        KeyCode::Enter => break,
150                        KeyCode::Char(c) => {
151                            input.push(c);
152                            print!("{}", c);
153                            use std::io::{self, Write};
154                            io::stdout().flush()?;
155                        }
156                        KeyCode::Backspace => {
157                            input.pop();
158                            print!("\r{} \r", input);
159                            io::stdout().flush()?;
160                        }
161                        _ => {}
162                    }
163                }
164            }
165        }
166        println!();
167        Ok(Some(input.trim().to_string()))
168    }
169}
170
171use std::io::{self, BufRead};
172use std::sync::mpsc;
173use std::thread;
174
175/// Reads a line from standard input with a timeout.
176/// Returns Ok(Some(String)) if a line is read before the timeout,
177/// Ok(None) if the timeout expires, or an error.
178pub fn read_line_with_timeout(wait_secs: u64) -> io::Result<Option<String>> {
179    let timeout = Duration::from_secs(wait_secs);
180    let (tx, rx) = mpsc::channel();
181    thread::spawn(move || {
182        let stdin = io::stdin();
183        let mut line = String::new();
184        let _ = stdin.lock().read_line(&mut line);
185        let _ = tx.send(line);
186    });
187    match rx.recv_timeout(timeout) {
188        Ok(line) => Ok(Some(line.trim().to_string())),
189        Err(mpsc::RecvTimeoutError::Timeout) => Ok(None),
190        Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
191    }
192}
193
194/// Prompts the user for a full line of input using crossterm events with a timeout.
195/// This works cross-platform and avoids leftover input from a lingering blocking thread.
196pub fn prompt_line_with_poll(wait_secs: u64) -> Result<Option<String>, Box<dyn std::error::Error>> {
197    #[cfg(feature = "tui")]
198    {
199        // Enable raw mode and ensure it will be disabled when the guard is dropped.
200        #[cfg(feature = "tui")]
201        let _raw_guard = RawModeGuard::new()?;
202        let timeout = Duration::from_secs(wait_secs);
203        let start = std::time::Instant::now();
204        let mut input = String::new();
205        loop {
206            let elapsed = start.elapsed();
207            if elapsed >= timeout {
208                return Ok(None);
209            }
210            // Poll for an event for the remaining time.
211            let remaining = timeout - elapsed;
212            if poll(remaining)? {
213                if let Event::Key(crossterm::event::KeyEvent { code, kind, .. }) = read()? {
214                    // Only process key press events
215                    if kind != crossterm::event::KeyEventKind::Press {
216                        continue;
217                    }
218                    match code {
219                        KeyCode::Enter => break,
220                        KeyCode::Char(c) => {
221                            input.push(c);
222                            // Optionally echo the character (if desired).
223                            print!("{}", c);
224                            io::Write::flush(&mut io::stdout())?;
225                        }
226                        KeyCode::Backspace => {
227                            input.pop();
228                            // Optionally update the display.
229                            print!("\r{}\r", " ".repeat(input.len() + 1));
230                            print!("{}", input);
231                            io::Write::flush(&mut io::stdout())?;
232                        }
233                        _ => {} // Ignore other keys.
234                    }
235                }
236            }
237        }
238        Ok(Some(input))
239    }
240    #[cfg(not(feature = "tui"))]
241    {
242        return Ok(read_line_with_timeout(wait_secs).unwrap_or_default());
243    }
244}
245
246/// Prompts the user for a full line of input using crossterm events (raw mode) with a timeout.
247///
248/// - `wait_secs`: seconds to wait for input.
249/// - `quick_exit`: a slice of characters that, if pressed, immediately cause the function to return that key (as a string).
250/// - `allowed_chars`: an optional slice of allowed characters (case-insensitive). If provided, only these characters are accepted.
251///
252/// In this example, we use numeric digits as allowed characters.
253pub fn prompt_line_with_poll_opts(
254    wait_secs: u64,
255    quick_exit: &[char],
256    allowed_chars: Option<&[char]>,
257) -> Result<Option<String>, Box<dyn Error + Send + Sync>> {
258    #[cfg(feature = "tui")]
259    {
260        let timeout = Duration::from_secs(wait_secs);
261        let _raw_guard = RawModeGuard::new()?;
262        let start = std::time::Instant::now();
263        let mut input = String::new();
264
265        loop {
266            let elapsed = start.elapsed();
267            if elapsed >= timeout {
268                return Ok(None);
269            }
270            let remaining = timeout - elapsed;
271            if poll(remaining)? {
272                if let Event::Key(crossterm::event::KeyEvent { code, kind, .. }) = read()? {
273                    // Only process key press events
274                    if kind != crossterm::event::KeyEventKind::Press {
275                        continue;
276                    }
277                    match code {
278                        KeyCode::Enter => {
279                            drain_events().ok();
280                            break;
281                        } // End input on Enter.
282                        KeyCode::Char(c) => {
283                            // Check quick exit keys (case-insensitive)
284                            if quick_exit.iter().any(|&qe| qe.eq_ignore_ascii_case(&c)) {
285                                input.push(c);
286                                drain_events().ok(); // Clear any pending events.
287                                return Ok(Some(input.to_string()));
288                            }
289                            // If allowed_chars is provided, only accept those.
290                            if let Some(allowed) = allowed_chars {
291                                if !allowed.iter().any(|&a| a.eq_ignore_ascii_case(&c)) {
292                                    // Ignore characters that aren't allowed.
293                                    continue;
294                                }
295                            }
296                            input.push(c);
297                            use crossterm::{
298                                cursor::MoveToColumn,
299                                execute,
300                                terminal::{Clear, ClearType},
301                            };
302                            execute!(io::stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0))?;
303                            print!("{}", input);
304                            io::Write::flush(&mut io::stdout())?;
305                        }
306                        KeyCode::Backspace => {
307                            if !input.is_empty() {
308                                input.pop();
309                                // Move cursor back one, overwrite with space, and move back again.
310                                print!("\x08 \x08");
311                                io::Write::flush(&mut io::stdout())?;
312                            }
313                        }
314                        _ => {} // Ignore other keys.
315                    }
316                }
317            }
318        }
319        // println!();
320        Ok(Some(input.trim().to_string()))
321    }
322
323    #[cfg(not(feature = "tui"))]
324    {
325        return Ok(read_line_with_timeout(wait_secs).unwrap_or_default());
326    }
327}
328
329/// Drain *all* pending input events so that the next `poll` really waits
330#[allow(dead_code)]
331fn drain_events() -> Result<()> {
332    // keep pulling until there’s nothing left
333    #[cfg(feature = "tui")]
334    while poll(Duration::from_millis(0))? {
335        let _ = read()?;
336    }
337    Ok(())
338}
339
340/// Prompts the user with a yes/no question and returns true if the user answers yes, false if no,
341/// or None if the timeout expires or an error occurs.
342pub fn yesno(prompt_message: &str, default: Option<bool>) -> Result<Option<bool>> {
343    let prompt_with_default = match default {
344        Some(true) => format!("{} (Y/n)? ", prompt_message),
345        Some(false) => format!("{} (y/N)? ", prompt_message),
346        None => format!("{} (y/n)? ", prompt_message),
347    };
348
349    let result = match prompt(&prompt_with_default, 10)? {
350        Some(c) => match c {
351            'y' => Some(true),
352            'n' => Some(false),
353            _ => {
354                // Handle invalid input (e.g., by re-prompting or returning None)
355                println!("Invalid input. Please enter 'y' or 'n'.");
356                return Ok(None); // Or potentially re-prompt here.
357            }
358        },
359        None => {
360            // Timeout occurred
361            match default {
362                Some(value) => Some(value),
363                None => None,
364            }
365        }
366    };
367    Ok(result)
368}