lc/utils/
input.rs

1use anyhow::Result;
2use colored::Colorize;
3use crossterm::{
4    event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
5    terminal::{disable_raw_mode, enable_raw_mode},
6};
7use std::io::{self, Write};
8
9/// Multi-line input handler that supports Shift+Enter for new lines
10pub struct MultiLineInput {
11    lines: Vec<String>,
12    current_line: String,
13    cursor_pos: usize,
14}
15
16impl MultiLineInput {
17    pub fn new() -> Self {
18        Self {
19            lines: Vec::new(),
20            current_line: String::new(),
21            cursor_pos: 0,
22        }
23    }
24
25    /// Read multi-line input from the terminal
26    /// - Enter: Submit the input
27    /// - Shift+Enter: Add a new line
28    /// - Ctrl+C: Cancel input (returns empty string)
29    /// - Backspace: Delete character
30    /// - Arrow keys: Navigate (basic support)
31    pub fn read_input(&mut self, prompt: &str) -> Result<String> {
32        print!("{} ", prompt);
33        io::stdout().flush()?;
34
35        // Ensure we can enable raw mode
36        if let Err(e) = enable_raw_mode() {
37            eprintln!(
38                "Warning: Failed to enable raw mode: {}. Falling back to simple input.",
39                e
40            );
41            return self.fallback_input();
42        }
43
44        let result = self.read_input_raw();
45
46        // Always disable raw mode, even if there was an error
47        let _ = disable_raw_mode();
48
49        result
50    }
51
52    fn read_input_raw(&mut self) -> Result<String> {
53        loop {
54            let event = event::read()?;
55            if let Event::Key(key_event) = event {
56                // Only process key press events, ignore key release
57                if key_event.kind == KeyEventKind::Press {
58                    match self.handle_key_event(key_event)? {
59                        InputAction::Continue => continue,
60                        InputAction::Submit => {
61                            // Add current line to lines if it's not empty
62                            if !self.current_line.is_empty() {
63                                self.lines.push(self.current_line.clone());
64                            }
65
66                            // Join all lines and return
67                            let result = self.lines.join("\n");
68
69                            // Clear state for next use
70                            self.lines.clear();
71                            self.current_line.clear();
72                            self.cursor_pos = 0;
73
74                            println!(); // Move to next line after input
75                            return Ok(result);
76                        }
77                        InputAction::Cancel => {
78                            // Clear state and return empty string
79                            self.lines.clear();
80                            self.current_line.clear();
81                            self.cursor_pos = 0;
82
83                            println!(); // Move to next line
84                            return Ok(String::new());
85                        }
86                        InputAction::NewLine => {
87                            // Add current line to lines and start a new line
88                            self.lines.push(self.current_line.clone());
89                            self.current_line.clear();
90                            self.cursor_pos = 0;
91
92                            // Print newline and show continuation prompt at beginning of line
93                            print!("\r\n{}   ", "...".dimmed());
94                            io::stdout().flush()?;
95                        }
96                    }
97                }
98            }
99        }
100    }
101
102    fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<InputAction> {
103        // Debug: print key event details
104        if std::env::var("LC_DEBUG_INPUT").is_ok() {
105            eprintln!("[DEBUG] Key event: {:?}", key_event);
106        }
107        match key_event.code {
108            KeyCode::Enter => {
109                if key_event.modifiers.contains(KeyModifiers::SHIFT) {
110                    // Shift+Enter: New line
111                    Ok(InputAction::NewLine)
112                } else {
113                    // Enter: Submit
114                    Ok(InputAction::Submit)
115                }
116            }
117            KeyCode::Char('j') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
118                // Ctrl+J: Alternative for new line (common in some terminals)
119                Ok(InputAction::NewLine)
120            }
121            KeyCode::Char('\n') => {
122                // Direct newline character (some terminals send this for Shift+Enter)
123                Ok(InputAction::NewLine)
124            }
125            KeyCode::Char('c') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
126                // Ctrl+C: Cancel
127                Ok(InputAction::Cancel)
128            }
129            KeyCode::Char(c) => {
130                // Insert character at cursor position
131                self.current_line.insert(self.cursor_pos, c);
132                self.cursor_pos += 1;
133
134                // Print the character
135                print!("{}", c);
136                io::stdout().flush()?;
137
138                Ok(InputAction::Continue)
139            }
140            KeyCode::Backspace => {
141                if self.cursor_pos > 0 {
142                    // Remove character before cursor
143                    self.current_line.remove(self.cursor_pos - 1);
144                    self.cursor_pos -= 1;
145
146                    // Move cursor back, print space to clear character, move back again
147                    print!("\x08 \x08");
148                    io::stdout().flush()?;
149                } else if !self.lines.is_empty() {
150                    // If at beginning of current line and there are previous lines,
151                    // move to end of previous line
152                    let prev_line = self.lines.pop().unwrap();
153
154                    // Clear current line display
155                    print!("\r{}   \r", " ".repeat(10));
156
157                    // Restore previous line
158                    self.cursor_pos = prev_line.len();
159                    self.current_line = prev_line;
160
161                    // Redraw prompt and current line
162                    if self.lines.is_empty() {
163                        print!("You: {}", self.current_line);
164                    } else {
165                        print!("...   {}", self.current_line);
166                    }
167                    io::stdout().flush()?;
168                }
169
170                Ok(InputAction::Continue)
171            }
172            KeyCode::Left => {
173                if self.cursor_pos > 0 {
174                    self.cursor_pos -= 1;
175                    print!("\x08"); // Move cursor left
176                    io::stdout().flush()?;
177                }
178                Ok(InputAction::Continue)
179            }
180            KeyCode::Right => {
181                if self.cursor_pos < self.current_line.len() {
182                    self.cursor_pos += 1;
183                    print!("\x1b[C"); // Move cursor right
184                    io::stdout().flush()?;
185                }
186                Ok(InputAction::Continue)
187            }
188            _ => Ok(InputAction::Continue),
189        }
190    }
191
192    /// Fallback to simple input when raw mode fails
193    fn fallback_input(&mut self) -> Result<String> {
194        eprintln!("Multi-line input (Shift+Enter) will not be available.");
195        let mut input = String::new();
196        io::stdin().read_line(&mut input)?;
197        Ok(input.trim().to_string())
198    }
199}
200
201#[derive(Debug)]
202enum InputAction {
203    Continue,
204    Submit,
205    Cancel,
206    NewLine,
207}
208
209impl Default for MultiLineInput {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_multiline_input_creation() {
221        let input = MultiLineInput::new();
222        assert!(input.lines.is_empty());
223        assert!(input.current_line.is_empty());
224        assert_eq!(input.cursor_pos, 0);
225    }
226}