Skip to main content

cli_ui/prompt/
engine.rs

1#![allow(missing_docs)]
2//! Input engine — interactive (crossterm) or fallback (numeric stdin).
3
4use super::error::{PromptError, Result};
5
6pub fn is_interactive() -> bool {
7    use std::io::IsTerminal;
8    std::io::stderr().is_terminal() && std::io::stdin().is_terminal()
9}
10
11// ── Key enum ─────────────────────────────────────────────────────────────────
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum Key {
15    Up,
16    Down,
17    Left,
18    Right,
19    Home,
20    End,
21    PageUp,
22    PageDown,
23    Enter,
24    Tab,
25    /// Any printable character. Space arrives here as `Char(' ')`.
26    Char(char),
27    Backspace,
28    Delete,
29    Escape,
30    /// Ctrl+C or Ctrl+D — triggers Interrupted error
31    Interrupt,
32    /// Ctrl+W — delete word backward
33    DeleteWordBack,
34}
35
36// ── Interactive engine ────────────────────────────────────────────────────────
37
38pub mod interactive {
39    use super::{Key, PromptError, Result};
40    use crossterm::event::{self, Event, KeyCode, KeyModifiers};
41
42    pub fn read_key() -> Result<Key> {
43        loop {
44            match event::read().map_err(PromptError::Io)? {
45                Event::Key(ke) => {
46                    if ke.modifiers.contains(KeyModifiers::CONTROL) {
47                        match ke.code {
48                            KeyCode::Char('c') | KeyCode::Char('d') => {
49                                return Ok(Key::Interrupt);
50                            }
51                            KeyCode::Char('w') => return Ok(Key::DeleteWordBack),
52                            KeyCode::Char('a') => return Ok(Key::Home),
53                            KeyCode::Char('e') => return Ok(Key::End),
54                            _ => continue,
55                        }
56                    }
57                    return Ok(match ke.code {
58                        KeyCode::Up => Key::Up,
59                        KeyCode::Down => Key::Down,
60                        KeyCode::Left => Key::Left,
61                        KeyCode::Right => Key::Right,
62                        KeyCode::Home => Key::Home,
63                        KeyCode::End => Key::End,
64                        KeyCode::PageUp => Key::PageUp,
65                        KeyCode::PageDown => Key::PageDown,
66                        KeyCode::Enter => Key::Enter,
67                        KeyCode::Tab => Key::Tab,
68                        KeyCode::Char(c) => Key::Char(c),
69                        KeyCode::Backspace => Key::Backspace,
70                        KeyCode::Delete => Key::Delete,
71                        KeyCode::Esc => Key::Escape,
72                        _ => continue,
73                    });
74                }
75                _ => continue,
76            }
77        }
78    }
79
80    pub fn enter_raw() -> Result<()> {
81        crossterm::terminal::enable_raw_mode().map_err(PromptError::Io)
82    }
83
84    pub fn leave_raw() -> Result<()> {
85        crossterm::terminal::disable_raw_mode().map_err(PromptError::Io)
86    }
87}
88
89// ── Fallback engine ───────────────────────────────────────────────────────────
90
91pub mod fallback {
92    use super::{PromptError, Result};
93    use std::io::BufRead;
94
95    pub fn read_line_raw() -> Result<String> {
96        let mut line = String::new();
97        std::io::stdin()
98            .lock()
99            .read_line(&mut line)
100            .map_err(PromptError::Io)?;
101        if line.is_empty() {
102            return Err(PromptError::Interrupted);
103        }
104        Ok(line.trim().to_string())
105    }
106}