1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use unicode_segmentation::UnicodeSegmentation;
5
6#[derive(Debug, Clone, Default)]
8pub struct InputState {
9 buffer: String,
11 cursor: usize,
13 error: Option<String>,
15 pub context_path: Option<std::path::PathBuf>,
17}
18
19impl InputState {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn with_initial(value: &str) -> Self {
27 Self {
28 buffer: value.to_string(),
29 cursor: value.len(),
30 error: None,
31 context_path: None,
32 }
33 }
34
35 pub fn buffer(&self) -> &str {
37 &self.buffer
38 }
39
40 pub fn cursor(&self) -> usize {
42 self.cursor
43 }
44
45 pub fn error(&self) -> Option<&str> {
47 self.error.as_deref()
48 }
49
50 pub fn set_error(&mut self, error: impl Into<String>) {
52 self.error = Some(error.into());
53 }
54
55 pub fn clear_error(&mut self) {
57 self.error = None;
58 }
59
60 pub fn handle_key(&mut self, key: KeyEvent) -> InputResult {
64 self.clear_error();
65
66 match (key.code, key.modifiers) {
67 (KeyCode::Enter, _) => {
69 let value = self.buffer.clone();
70 InputResult::Submit(value)
71 }
72
73 (KeyCode::Esc, _) => InputResult::Cancel,
75
76 (KeyCode::Backspace, _) => {
78 if self.cursor > 0 {
79 let start = self.previous_boundary();
80 self.buffer.replace_range(start..self.cursor, "");
81 self.cursor = start;
82 self.snap_cursor_forward();
83 }
84 InputResult::Continue
85 }
86
87 (KeyCode::Delete, _) => {
89 if self.cursor < self.buffer.len() {
90 let end = self.next_boundary();
91 self.buffer.replace_range(self.cursor..end, "");
92 self.snap_cursor_forward();
93 }
94 InputResult::Continue
95 }
96
97 (KeyCode::Left, _) => {
99 self.cursor = self.previous_boundary();
100 InputResult::Continue
101 }
102
103 (KeyCode::Right, _) => {
105 self.cursor = self.next_boundary();
106 InputResult::Continue
107 }
108
109 (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
111 self.cursor = 0;
112 InputResult::Continue
113 }
114
115 (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
117 self.cursor = self.buffer.len();
118 InputResult::Continue
119 }
120
121 (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
123 self.buffer.clear();
124 self.cursor = 0;
125 InputResult::Continue
126 }
127
128 (KeyCode::Char('k'), KeyModifiers::CONTROL) => {
130 self.buffer.truncate(self.cursor);
131 InputResult::Continue
132 }
133
134 (KeyCode::Char('w'), KeyModifiers::CONTROL) => {
136 if self.cursor > 0 {
137 let mut start = self.cursor;
138 let mut found_word = false;
139 for (index, grapheme) in self.buffer[..self.cursor].grapheme_indices(true).rev()
140 {
141 let whitespace = grapheme.chars().all(char::is_whitespace);
142 if found_word && whitespace {
143 break;
144 }
145 found_word |= !whitespace;
146 start = index;
147 }
148 self.buffer.replace_range(start..self.cursor, "");
149 self.cursor = start;
150 self.snap_cursor_forward();
151 }
152 InputResult::Continue
153 }
154
155 (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
157 self.buffer.insert(self.cursor, c);
158 self.cursor += c.len_utf8();
159 self.snap_cursor_forward();
160 InputResult::Continue
161 }
162
163 _ => InputResult::Continue,
165 }
166 }
167
168 pub fn validate_filename(&self) -> Result<(), String> {
172 gravityfile_ops::validate_filename(&self.buffer)
173 }
174
175 fn previous_boundary(&self) -> usize {
176 self.buffer
177 .grapheme_indices(true)
178 .map(|(index, _)| index)
179 .take_while(|&index| index < self.cursor)
180 .last()
181 .unwrap_or(0)
182 }
183
184 fn next_boundary(&self) -> usize {
185 self.buffer
186 .grapheme_indices(true)
187 .map(|(index, _)| index)
188 .find(|&index| index > self.cursor)
189 .unwrap_or(self.buffer.len())
190 }
191
192 fn snap_cursor_forward(&mut self) {
193 self.cursor = self
194 .buffer
195 .grapheme_indices(true)
196 .map(|(index, _)| index)
197 .find(|&index| index >= self.cursor)
198 .unwrap_or(self.buffer.len());
199 }
200}
201
202#[derive(Debug, Clone)]
204pub enum InputResult {
205 Continue,
207 Cancel,
209 Submit(String),
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
217
218 fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
219 KeyEvent {
220 code,
221 modifiers,
222 kind: KeyEventKind::Press,
223 state: KeyEventState::NONE,
224 }
225 }
226
227 #[test]
228 fn unicode_editing_preserves_grapheme_boundaries() {
229 use ratatui::{buffer::Buffer, layout::Rect, style::Modifier, widgets::Widget};
230 for value in ["é日", "e\u{301}x", "👩💻🇺🇸1️⃣", "a\u{2003}word "] {
231 let mut input = InputState::new();
232 for character in value.chars() {
233 input.handle_key(key_event(KeyCode::Char(character), KeyModifiers::NONE));
234 assert!(input.buffer.is_char_boundary(input.cursor));
235 for width in [0, 1, 6, 20, 80] {
236 let area = Rect::new(0, 0, width, 24);
237 let mut buffer = Buffer::empty(area);
238 crate::ui::modals::InputModal::new(
239 &crate::theme::Theme::default(),
240 &input,
241 "Rename",
242 "Name",
243 )
244 .render(area, &mut buffer);
245 }
246 }
247 assert_eq!(input.buffer(), value);
248 let boundaries: Vec<_> = value
249 .grapheme_indices(true)
250 .map(|(index, _)| index)
251 .chain(std::iter::once(value.len()))
252 .collect();
253 for &boundary in boundaries.iter().rev().skip(1) {
254 input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
255 assert_eq!(input.cursor(), boundary);
256 }
257 for &boundary in boundaries.iter().skip(1) {
258 input.handle_key(key_event(KeyCode::Right, KeyModifiers::NONE));
259 assert_eq!(input.cursor(), boundary);
260 }
261 for remaining in (0..value.graphemes(true).count()).rev() {
262 input.handle_key(key_event(KeyCode::Backspace, KeyModifiers::NONE));
263 assert_eq!(input.buffer.graphemes(true).count(), remaining);
264 }
265 let mut input = InputState::with_initial(value);
266 input.handle_key(key_event(KeyCode::Home, KeyModifiers::NONE));
267 for remaining in (0..value.graphemes(true).count()).rev() {
268 input.handle_key(key_event(KeyCode::Delete, KeyModifiers::NONE));
269 assert_eq!(input.buffer.graphemes(true).count(), remaining);
270 }
271 }
272 let mut input = InputState::with_initial("é\u{2003}word ");
273 input.handle_key(key_event(KeyCode::Char('w'), KeyModifiers::CONTROL));
274 assert_eq!(input.buffer(), "é\u{2003}");
275 input.handle_key(key_event(KeyCode::Char('w'), KeyModifiers::CONTROL));
276 assert!(input.buffer().is_empty());
277 let mut input = InputState::with_initial("🇺x🇸");
278 input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
279 input.handle_key(key_event(KeyCode::Backspace, KeyModifiers::NONE));
280 assert_eq!(input.buffer(), "🇺🇸");
281 assert_eq!(input.cursor(), input.buffer().len());
282 let mut input = InputState::with_initial("é日");
283 input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
284 let area = Rect::new(0, 0, 80, 24);
285 let mut buffer = Buffer::empty(area);
286 crate::ui::modals::InputModal::new(
287 &crate::theme::Theme::default(),
288 &input,
289 "Rename",
290 "Name",
291 )
292 .render(area, &mut buffer);
293 assert!(
294 buffer
295 .content()
296 .iter()
297 .any(|cell| cell.symbol() == "日" && cell.modifier.contains(Modifier::REVERSED))
298 );
299 }
300
301 #[test]
302 fn test_input_basic() {
303 let mut input = InputState::new();
304
305 input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
307 input.handle_key(key_event(KeyCode::Char('e'), KeyModifiers::NONE));
308 input.handle_key(key_event(KeyCode::Char('s'), KeyModifiers::NONE));
309 input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
310
311 assert_eq!(input.buffer(), "test");
312 assert_eq!(input.cursor(), 4);
313 }
314
315 #[test]
316 fn test_input_backspace() {
317 let mut input = InputState::with_initial("test");
318
319 input.handle_key(key_event(KeyCode::Backspace, KeyModifiers::NONE));
320
321 assert_eq!(input.buffer(), "tes");
322 assert_eq!(input.cursor(), 3);
323 }
324
325 #[test]
326 fn test_input_cursor_movement() {
327 let mut input = InputState::with_initial("test");
328
329 input.handle_key(key_event(KeyCode::Home, KeyModifiers::NONE));
331 assert_eq!(input.cursor(), 0);
332
333 input.handle_key(key_event(KeyCode::End, KeyModifiers::NONE));
335 assert_eq!(input.cursor(), 4);
336
337 input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
339 assert_eq!(input.cursor(), 3);
340
341 input.handle_key(key_event(KeyCode::Right, KeyModifiers::NONE));
343 assert_eq!(input.cursor(), 4);
344 }
345
346 #[test]
347 fn test_validate_filename() {
348 let mut input = InputState::new();
349
350 input.buffer = "test.txt".to_string();
352 assert!(input.validate_filename().is_ok());
353
354 input.buffer = ".hidden".to_string();
355 assert!(input.validate_filename().is_ok());
356
357 input.buffer = "my-file_v2".to_string();
358 assert!(input.validate_filename().is_ok());
359
360 input.buffer = "".to_string();
362 assert!(input.validate_filename().is_err());
363
364 input.buffer = "test/file".to_string();
365 assert!(input.validate_filename().is_err());
366
367 input.buffer = ".".to_string();
368 assert!(input.validate_filename().is_err());
369
370 input.buffer = "..".to_string();
371 assert!(input.validate_filename().is_err());
372 }
373
374 #[test]
375 fn test_submit_and_cancel() {
376 let mut input = InputState::with_initial("test");
377
378 let result = input.handle_key(key_event(KeyCode::Enter, KeyModifiers::NONE));
379 assert!(matches!(result, InputResult::Submit(s) if s == "test"));
380
381 let result = input.handle_key(key_event(KeyCode::Esc, KeyModifiers::NONE));
382 assert!(matches!(result, InputResult::Cancel));
383 }
384}