excel_cli/ui/
handlers.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use tui_textarea::{Input, Key, TextArea};
3
4use crate::app::{AppState, InputMode};
5
6pub fn handle_key_event(app_state: &mut AppState, key: KeyEvent) {
7    match app_state.input_mode {
8        InputMode::Normal => {
9            if key.modifiers.contains(KeyModifiers::CONTROL)
10                || key.modifiers.contains(KeyModifiers::SUPER)
11            {
12                handle_ctrl_key(app_state, key.code);
13            } else {
14                handle_normal_mode(app_state, key.code);
15            }
16        }
17        InputMode::Editing => handle_editing_mode(app_state, key),
18        InputMode::Command => handle_command_mode(app_state, key.code),
19        InputMode::CommandInLazyLoading => handle_command_in_lazy_loading_mode(app_state, key.code),
20        InputMode::SearchForward => handle_search_mode(app_state, key.code),
21        InputMode::SearchBackward => handle_search_mode(app_state, key.code),
22        InputMode::Help => handle_help_mode(app_state, key.code),
23        InputMode::LazyLoading => handle_lazy_loading_mode(app_state, key.code),
24    }
25}
26
27// Handles both Ctrl+key and Command+key (on Mac) combinations
28fn handle_ctrl_key(app_state: &mut AppState, key_code: KeyCode) {
29    match key_code {
30        KeyCode::Left => {
31            app_state.jump_to_prev_non_empty_cell_left();
32        }
33        KeyCode::Right => {
34            app_state.jump_to_prev_non_empty_cell_right();
35        }
36        KeyCode::Up => {
37            app_state.jump_to_prev_non_empty_cell_up();
38        }
39        KeyCode::Down => {
40            app_state.jump_to_prev_non_empty_cell_down();
41        }
42        KeyCode::Char('r') => {
43            if let Err(e) = app_state.redo() {
44                app_state.add_notification(format!("Redo failed: {e}"));
45            }
46        }
47        _ => {}
48    }
49}
50
51fn handle_command_mode(app_state: &mut AppState, key_code: KeyCode) {
52    match key_code {
53        KeyCode::Enter => app_state.execute_command(),
54        KeyCode::Esc => app_state.cancel_input(),
55        KeyCode::Backspace => app_state.delete_char_from_input(),
56        KeyCode::Char(c) => app_state.add_char_to_input(c),
57        _ => {}
58    }
59}
60
61fn handle_command_in_lazy_loading_mode(app_state: &mut AppState, key_code: KeyCode) {
62    match key_code {
63        KeyCode::Enter => {
64            // Execute the command but stay in lazy loading mode if needed
65            let current_index = app_state.workbook.get_current_sheet_index();
66            let is_sheet_loaded = app_state.workbook.is_sheet_loaded(current_index);
67
68            // Execute the command
69            app_state.execute_command();
70
71            // If the sheet is still not loaded after command execution, switch back to LazyLoading mode
72            if !is_sheet_loaded
73                && !app_state
74                    .workbook
75                    .is_sheet_loaded(app_state.workbook.get_current_sheet_index())
76                && matches!(app_state.input_mode, InputMode::Normal)
77            {
78                app_state.input_mode = InputMode::LazyLoading;
79            }
80        }
81        KeyCode::Esc => {
82            // Return to LazyLoading mode
83            app_state.input_mode = InputMode::LazyLoading;
84            app_state.input_buffer = String::new();
85        }
86        KeyCode::Backspace => app_state.delete_char_from_input(),
87        KeyCode::Char(c) => app_state.add_char_to_input(c),
88        _ => {}
89    }
90}
91
92fn handle_normal_mode(app_state: &mut AppState, key_code: KeyCode) {
93    match key_code {
94        KeyCode::Enter => {
95            app_state.g_pressed = false;
96
97            // Check if the current sheet is loaded
98            let index = app_state.workbook.get_current_sheet_index();
99            let sheet_name = app_state.workbook.get_current_sheet_name();
100
101            if app_state.workbook.is_lazy_loading() && !app_state.workbook.is_sheet_loaded(index) {
102                // If the sheet is not loaded, load it first
103                if let Err(e) = app_state.workbook.ensure_sheet_loaded(index, &sheet_name) {
104                    app_state.add_notification(format!("Failed to load sheet: {e}"));
105                } else {
106                    app_state.start_editing();
107                }
108            } else {
109                app_state.start_editing();
110            }
111        }
112        KeyCode::Char('h') => {
113            app_state.g_pressed = false;
114            app_state.move_cursor(0, -1);
115        }
116        KeyCode::Char('j') => {
117            app_state.g_pressed = false;
118            app_state.move_cursor(1, 0);
119        }
120        KeyCode::Char('k') => {
121            app_state.g_pressed = false;
122            app_state.move_cursor(-1, 0);
123        }
124        KeyCode::Char('l') => {
125            app_state.g_pressed = false;
126            app_state.move_cursor(0, 1);
127        }
128        KeyCode::Char('u') => {
129            app_state.g_pressed = false;
130            if let Err(e) = app_state.undo() {
131                app_state.add_notification(format!("Undo failed: {e}"));
132            }
133        }
134        KeyCode::Char('=' | '+') => {
135            app_state.g_pressed = false;
136            app_state.adjust_info_panel_height(1);
137        }
138        KeyCode::Char('-') => {
139            app_state.g_pressed = false;
140            app_state.adjust_info_panel_height(-1);
141        }
142        KeyCode::Char('[') => {
143            app_state.g_pressed = false;
144            if let Err(e) = app_state.prev_sheet() {
145                app_state.add_notification(format!("Failed to switch to previous sheet: {e}"));
146            }
147        }
148        KeyCode::Char(']') => {
149            app_state.g_pressed = false;
150            if let Err(e) = app_state.next_sheet() {
151                app_state.add_notification(format!("Failed to switch to next sheet: {e}"));
152            }
153        }
154        KeyCode::Char('g') => {
155            if app_state.g_pressed {
156                app_state.jump_to_first_row();
157                app_state.g_pressed = false;
158            } else {
159                app_state.g_pressed = true;
160            }
161        }
162        KeyCode::Char('G') => {
163            app_state.g_pressed = false;
164            app_state.jump_to_last_row();
165        }
166        KeyCode::Char('0') => {
167            app_state.g_pressed = false;
168            app_state.jump_to_first_column();
169        }
170        KeyCode::Char('^') => {
171            app_state.g_pressed = false;
172            app_state.jump_to_first_non_empty_column();
173        }
174        KeyCode::Char('$') => {
175            app_state.g_pressed = false;
176            app_state.jump_to_last_column();
177        }
178        KeyCode::Char('y') => {
179            app_state.g_pressed = false;
180            app_state.copy_cell();
181        }
182        KeyCode::Char('d') => {
183            app_state.g_pressed = false;
184            if let Err(e) = app_state.cut_cell() {
185                app_state.add_notification(format!("Cut failed: {e}"));
186            }
187        }
188        KeyCode::Char('p') => {
189            app_state.g_pressed = false;
190            if let Err(e) = app_state.paste_cell() {
191                app_state.add_notification(format!("Paste failed: {e}"));
192            }
193        }
194        KeyCode::Char(':') => {
195            app_state.g_pressed = false;
196            app_state.start_command_mode();
197        }
198        KeyCode::Char('/') => {
199            app_state.g_pressed = false;
200            app_state.start_search_forward();
201        }
202        KeyCode::Char('?') => {
203            app_state.g_pressed = false;
204            app_state.start_search_backward();
205        }
206        KeyCode::Char('n') => {
207            app_state.g_pressed = false;
208            if !app_state.search_results.is_empty() {
209                app_state.jump_to_next_search_result();
210            } else if !app_state.search_query.is_empty() {
211                // Re-run the last search if we have a query but no results
212                app_state.search_results = app_state.find_all_matches(&app_state.search_query);
213                if !app_state.search_results.is_empty() {
214                    app_state.jump_to_next_search_result();
215                }
216            }
217        }
218
219        KeyCode::Char('N') => {
220            app_state.g_pressed = false;
221            if !app_state.search_results.is_empty() {
222                app_state.jump_to_prev_search_result();
223            } else if !app_state.search_query.is_empty() {
224                // Re-run the last search if we have a query but no results
225                app_state.search_results = app_state.find_all_matches(&app_state.search_query);
226                if !app_state.search_results.is_empty() {
227                    app_state.jump_to_prev_search_result();
228                }
229            }
230        }
231
232        KeyCode::Left => {
233            app_state.g_pressed = false;
234            app_state.move_cursor(0, -1);
235        }
236        KeyCode::Right => {
237            app_state.g_pressed = false;
238            app_state.move_cursor(0, 1);
239        }
240        KeyCode::Up => {
241            app_state.g_pressed = false;
242            app_state.move_cursor(-1, 0);
243        }
244        KeyCode::Down => {
245            app_state.g_pressed = false;
246            app_state.move_cursor(1, 0);
247        }
248        _ => {
249            app_state.g_pressed = false;
250        }
251    }
252}
253
254fn handle_editing_mode(app_state: &mut AppState, key: KeyEvent) {
255    // Convert KeyEvent to Input for tui-textarea
256    let input = Input {
257        key: key_code_to_tui_key(key.code),
258        ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
259        alt: key.modifiers.contains(KeyModifiers::ALT),
260        shift: key.modifiers.contains(KeyModifiers::SHIFT),
261    };
262
263    if let Err(e) = app_state.handle_vim_input(input) {
264        app_state.add_notification(format!("Vim input error: {e}"));
265    }
266}
267
268fn handle_search_mode(app_state: &mut AppState, key_code: KeyCode) {
269    match key_code {
270        KeyCode::Enter => app_state.execute_search(),
271        KeyCode::Esc => {
272            app_state.input_mode = InputMode::Normal;
273            app_state.input_buffer = String::new();
274            app_state.text_area = TextArea::default();
275        }
276        _ => {
277            let input = Input {
278                key: key_code_to_tui_key(key_code),
279                ctrl: false,
280                alt: false,
281                shift: false,
282            };
283            app_state.text_area.input(input);
284        }
285    }
286}
287
288// Convert crossterm::event::KeyCode to tui_textarea::Key
289fn key_code_to_tui_key(key_code: KeyCode) -> Key {
290    match key_code {
291        KeyCode::Backspace => Key::Backspace,
292        KeyCode::Enter => Key::Enter,
293        KeyCode::Left => Key::Left,
294        KeyCode::Right => Key::Right,
295        KeyCode::Up => Key::Up,
296        KeyCode::Down => Key::Down,
297        KeyCode::Home => Key::Home,
298        KeyCode::End => Key::End,
299        KeyCode::PageUp => Key::PageUp,
300        KeyCode::PageDown => Key::PageDown,
301        KeyCode::Tab => Key::Tab,
302        KeyCode::Delete => Key::Delete,
303        // BackTab and Insert not supported in tui-textarea
304        KeyCode::BackTab | KeyCode::Insert => Key::Null,
305        KeyCode::Esc => Key::Esc,
306        KeyCode::Char(c) => Key::Char(c),
307        KeyCode::F(n) => Key::F(n),
308        _ => Key::Null,
309    }
310}
311
312fn handle_lazy_loading_mode(app_state: &mut AppState, key_code: KeyCode) {
313    match key_code {
314        KeyCode::Enter => {
315            let index = app_state.workbook.get_current_sheet_index();
316            let sheet_name = app_state.workbook.get_current_sheet_name();
317
318            // Load the sheet
319            if let Err(e) = app_state.workbook.ensure_sheet_loaded(index, &sheet_name) {
320                app_state.add_notification(format!("Failed to load sheet: {e}"));
321            }
322
323            app_state.input_mode = InputMode::Normal;
324        }
325        KeyCode::Char('[') => {
326            // Switch to previous sheet
327            let current_index = app_state.workbook.get_current_sheet_index();
328
329            if current_index == 0 {
330                app_state.add_notification("Already at the first sheet".to_string());
331            } else {
332                // The method will automatically set the input mode to LazyLoading if the sheet is not loaded
333                if let Err(e) = app_state.switch_sheet_by_index(current_index - 1) {
334                    app_state.add_notification(format!("Failed to switch to previous sheet: {e}"));
335                }
336            }
337        }
338        KeyCode::Char(']') => {
339            // Switch to next sheet
340            let current_index = app_state.workbook.get_current_sheet_index();
341            let sheet_count = app_state.workbook.get_sheet_names().len();
342
343            if current_index >= sheet_count - 1 {
344                app_state.add_notification("Already at the last sheet".to_string());
345            } else {
346                // The method will automatically set the input mode to LazyLoading if the sheet is not loaded
347                if let Err(e) = app_state.switch_sheet_by_index(current_index + 1) {
348                    app_state.add_notification(format!("Failed to switch to next sheet: {e}"));
349                }
350            }
351        }
352        KeyCode::Char(':') => {
353            // Allow entering command mode from lazy loading mode
354            app_state.start_command_in_lazy_loading_mode();
355        }
356        _ => {
357            app_state.add_notification(
358                "Press Enter to load the sheet data, or use [ and ] to switch sheets".to_string(),
359            );
360        }
361    }
362}
363
364fn handle_help_mode(app_state: &mut AppState, key_code: KeyCode) {
365    let line_count = app_state.help_text.lines().count();
366
367    let visible_lines = app_state.help_visible_lines;
368
369    let max_scroll = line_count.saturating_sub(visible_lines).max(0);
370
371    match key_code {
372        KeyCode::Enter | KeyCode::Esc => {
373            app_state.input_mode = InputMode::Normal;
374        }
375        KeyCode::Char('j') | KeyCode::Down => {
376            // Scroll down, but not beyond the last line
377            app_state.help_scroll = (app_state.help_scroll + 1).min(max_scroll);
378        }
379        KeyCode::Char('k') | KeyCode::Up => {
380            // Scroll up
381            app_state.help_scroll = app_state.help_scroll.saturating_sub(1);
382        }
383        KeyCode::Home => {
384            // Scroll to the top
385            app_state.help_scroll = 0;
386        }
387        KeyCode::End => {
388            // Scroll to the bottom
389            app_state.help_scroll = max_scroll;
390        }
391        _ => {}
392    }
393}