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