gravityfile_tui/app/
input.rs

1//! Input state for text input modes (rename, create).
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5/// State for text input operations.
6#[derive(Debug, Clone, Default)]
7pub struct InputState {
8    /// The current input buffer.
9    buffer: String,
10    /// Cursor position within the buffer.
11    cursor: usize,
12    /// Original value (for rename operations to show what's being renamed).
13    original: Option<String>,
14    /// Validation error message.
15    error: Option<String>,
16    /// Path context (parent directory for create, full path for rename).
17    pub context_path: Option<std::path::PathBuf>,
18}
19
20impl InputState {
21    /// Create a new empty input state.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Create an input state with an initial value (for rename).
27    pub fn with_initial(value: &str) -> Self {
28        Self {
29            buffer: value.to_string(),
30            cursor: value.len(),
31            original: Some(value.to_string()),
32            error: None,
33            context_path: None,
34        }
35    }
36
37    /// Create an input state with context path.
38    pub fn with_context(context_path: std::path::PathBuf) -> Self {
39        Self {
40            context_path: Some(context_path),
41            ..Default::default()
42        }
43    }
44
45    /// Get the current buffer contents.
46    pub fn buffer(&self) -> &str {
47        &self.buffer
48    }
49
50    /// Get the cursor position.
51    pub fn cursor(&self) -> usize {
52        self.cursor
53    }
54
55    /// Get the original value (if any).
56    pub fn original(&self) -> Option<&str> {
57        self.original.as_deref()
58    }
59
60    /// Get the current error message (if any).
61    pub fn error(&self) -> Option<&str> {
62        self.error.as_deref()
63    }
64
65    /// Set an error message.
66    pub fn set_error(&mut self, error: impl Into<String>) {
67        self.error = Some(error.into());
68    }
69
70    /// Clear the error message.
71    pub fn clear_error(&mut self) {
72        self.error = None;
73    }
74
75    /// Handle a key event.
76    ///
77    /// Returns the result of handling the key.
78    pub fn handle_key(&mut self, key: KeyEvent) -> InputResult {
79        self.clear_error();
80
81        match (key.code, key.modifiers) {
82            // Submit
83            (KeyCode::Enter, _) => {
84                let value = self.buffer.clone();
85                InputResult::Submit(value)
86            }
87
88            // Cancel
89            (KeyCode::Esc, _) => InputResult::Cancel,
90
91            // Backspace - delete character before cursor
92            (KeyCode::Backspace, _) => {
93                if self.cursor > 0 {
94                    self.cursor -= 1;
95                    self.buffer.remove(self.cursor);
96                }
97                InputResult::Continue
98            }
99
100            // Delete - delete character at cursor
101            (KeyCode::Delete, _) => {
102                if self.cursor < self.buffer.len() {
103                    self.buffer.remove(self.cursor);
104                }
105                InputResult::Continue
106            }
107
108            // Left arrow - move cursor left
109            (KeyCode::Left, _) => {
110                self.cursor = self.cursor.saturating_sub(1);
111                InputResult::Continue
112            }
113
114            // Right arrow - move cursor right
115            (KeyCode::Right, _) => {
116                self.cursor = (self.cursor + 1).min(self.buffer.len());
117                InputResult::Continue
118            }
119
120            // Home or Ctrl-A - move to start
121            (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
122                self.cursor = 0;
123                InputResult::Continue
124            }
125
126            // End or Ctrl-E - move to end
127            (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
128                self.cursor = self.buffer.len();
129                InputResult::Continue
130            }
131
132            // Ctrl-U - clear line
133            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
134                self.buffer.clear();
135                self.cursor = 0;
136                InputResult::Continue
137            }
138
139            // Ctrl-K - delete from cursor to end
140            (KeyCode::Char('k'), KeyModifiers::CONTROL) => {
141                self.buffer.truncate(self.cursor);
142                InputResult::Continue
143            }
144
145            // Ctrl-W - delete word before cursor
146            (KeyCode::Char('w'), KeyModifiers::CONTROL) => {
147                if self.cursor > 0 {
148                    // Find the start of the word
149                    let before = &self.buffer[..self.cursor];
150                    let word_start = before
151                        .rfind(|c: char| c.is_whitespace())
152                        .map(|i| i + 1)
153                        .unwrap_or(0);
154                    self.buffer.replace_range(word_start..self.cursor, "");
155                    self.cursor = word_start;
156                }
157                InputResult::Continue
158            }
159
160            // Regular character input
161            (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
162                self.buffer.insert(self.cursor, c);
163                self.cursor += 1;
164                InputResult::Continue
165            }
166
167            // Ignore other keys
168            _ => InputResult::Continue,
169        }
170    }
171
172    /// Validate the buffer as a filename.
173    ///
174    /// Returns Ok(()) if valid, Err with message if invalid.
175    pub fn validate_filename(&self) -> Result<(), String> {
176        let name = &self.buffer;
177
178        if name.is_empty() {
179            return Err("Name cannot be empty".into());
180        }
181
182        if name.len() > 255 {
183            return Err("Name is too long (max 255 characters)".into());
184        }
185
186        // Check for invalid characters (Unix)
187        if name.contains('/') {
188            return Err("Name cannot contain '/'".into());
189        }
190
191        if name.contains('\0') {
192            return Err("Name cannot contain null character".into());
193        }
194
195        // Check for . and .. which are reserved
196        if name == "." || name == ".." {
197            return Err("'.' and '..' are reserved names".into());
198        }
199
200        // Check for leading/trailing spaces (problematic)
201        if name.starts_with(' ') || name.ends_with(' ') {
202            return Err("Name cannot start or end with spaces".into());
203        }
204
205        // Check for trailing dots (problematic on Windows)
206        if name.ends_with('.') && name != "." {
207            return Err("Name cannot end with a dot".into());
208        }
209
210        Ok(())
211    }
212
213    /// Check if the buffer has changed from the original value.
214    pub fn has_changed(&self) -> bool {
215        self.original.as_deref() != Some(&self.buffer)
216    }
217}
218
219/// Result of handling input.
220#[derive(Debug, Clone)]
221pub enum InputResult {
222    /// Continue accepting input.
223    Continue,
224    /// User cancelled the input.
225    Cancel,
226    /// User submitted the input with this value.
227    Submit(String),
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
234
235    fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
236        KeyEvent {
237            code,
238            modifiers,
239            kind: KeyEventKind::Press,
240            state: KeyEventState::NONE,
241        }
242    }
243
244    #[test]
245    fn test_input_basic() {
246        let mut input = InputState::new();
247
248        // Type "test"
249        input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
250        input.handle_key(key_event(KeyCode::Char('e'), KeyModifiers::NONE));
251        input.handle_key(key_event(KeyCode::Char('s'), KeyModifiers::NONE));
252        input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
253
254        assert_eq!(input.buffer(), "test");
255        assert_eq!(input.cursor(), 4);
256    }
257
258    #[test]
259    fn test_input_backspace() {
260        let mut input = InputState::with_initial("test");
261
262        input.handle_key(key_event(KeyCode::Backspace, KeyModifiers::NONE));
263
264        assert_eq!(input.buffer(), "tes");
265        assert_eq!(input.cursor(), 3);
266    }
267
268    #[test]
269    fn test_input_cursor_movement() {
270        let mut input = InputState::with_initial("test");
271
272        // Move to start
273        input.handle_key(key_event(KeyCode::Home, KeyModifiers::NONE));
274        assert_eq!(input.cursor(), 0);
275
276        // Move to end
277        input.handle_key(key_event(KeyCode::End, KeyModifiers::NONE));
278        assert_eq!(input.cursor(), 4);
279
280        // Move left
281        input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
282        assert_eq!(input.cursor(), 3);
283
284        // Move right
285        input.handle_key(key_event(KeyCode::Right, KeyModifiers::NONE));
286        assert_eq!(input.cursor(), 4);
287    }
288
289    #[test]
290    fn test_validate_filename() {
291        let mut input = InputState::new();
292
293        // Valid names
294        input.buffer = "test.txt".to_string();
295        assert!(input.validate_filename().is_ok());
296
297        input.buffer = ".hidden".to_string();
298        assert!(input.validate_filename().is_ok());
299
300        input.buffer = "my-file_v2".to_string();
301        assert!(input.validate_filename().is_ok());
302
303        // Invalid names
304        input.buffer = "".to_string();
305        assert!(input.validate_filename().is_err());
306
307        input.buffer = "test/file".to_string();
308        assert!(input.validate_filename().is_err());
309
310        input.buffer = ".".to_string();
311        assert!(input.validate_filename().is_err());
312
313        input.buffer = "..".to_string();
314        assert!(input.validate_filename().is_err());
315    }
316
317    #[test]
318    fn test_submit_and_cancel() {
319        let mut input = InputState::with_initial("test");
320
321        let result = input.handle_key(key_event(KeyCode::Enter, KeyModifiers::NONE));
322        assert!(matches!(result, InputResult::Submit(s) if s == "test"));
323
324        let result = input.handle_key(key_event(KeyCode::Esc, KeyModifiers::NONE));
325        assert!(matches!(result, InputResult::Cancel));
326    }
327}