ghostscope_ui/components/command_panel/
optimized_input.rs

1use crate::action::{Action, CursorDirection};
2use crate::model::panel_state::{CommandPanelState, InputState, InteractionMode, JkEscapeState};
3use ratatui::crossterm::event::KeyEvent;
4use std::time::Instant;
5
6/// Optimized input handler with vim-like controls
7#[derive(Debug)]
8pub struct OptimizedInputHandler {
9    // jk escape sequence handling
10    jk_timeout_ms: u64,
11
12    // Input debouncing (though we use frame-based rendering now)
13    last_input_time: Instant,
14}
15
16impl OptimizedInputHandler {
17    pub fn new() -> Self {
18        Self {
19            jk_timeout_ms: 150,
20            last_input_time: Instant::now(),
21        }
22    }
23
24    /// Handle key event with priority-based routing (delegates to InputHandler)
25    pub fn handle_key_event(
26        &mut self,
27        state: &mut CommandPanelState,
28        key: KeyEvent,
29    ) -> Vec<Action> {
30        self.last_input_time = Instant::now();
31
32        // Delegate to the main InputHandler for priority-based handling
33        crate::components::command_panel::InputHandler::handle_key_event(state, key)
34    }
35
36    /// Handle character input in different modes
37    pub fn handle_char_input(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
38        tracing::debug!(
39            "handle_char_input: received char='{}' (code={}), mode={:?}",
40            ch,
41            ch as u32,
42            state.mode
43        );
44        self.last_input_time = Instant::now();
45
46        let result = match state.mode {
47            InteractionMode::Input => self.handle_input_mode_char(state, ch),
48            InteractionMode::Command => self.handle_command_mode_char(state, ch),
49            InteractionMode::ScriptEditor => self.handle_script_mode_char(state, ch),
50        };
51
52        tracing::debug!(
53            "handle_char_input: after processing, input_text='{}', cursor_pos={}",
54            state.input_text,
55            state.cursor_position
56        );
57        result
58    }
59
60    /// Insert a full string at the current cursor position efficiently (batch paste)
61    /// - In Input mode: inserts as single line (newlines converted to spaces), updates suggestion once
62    /// - In ScriptEditor: delegates to ScriptEditor for multi-line aware insertion
63    pub fn insert_str(&mut self, state: &mut CommandPanelState, text: &str) -> Vec<Action> {
64        self.last_input_time = Instant::now();
65
66        match state.mode {
67            InteractionMode::Input => {
68                // Cancel any pending jk escape
69                state.jk_escape_state = JkEscapeState::None;
70                state.jk_timer = None;
71
72                // Normalize pasted text to a single line for command input
73                // Convert CRLF/CR to LF, then LF to space
74                let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
75                let sanitized = normalized.replace('\n', " ");
76                if sanitized.is_empty() {
77                    return Vec::new();
78                }
79
80                let byte_pos = self.char_pos_to_byte_pos(&state.input_text, state.cursor_position);
81                state.input_text.insert_str(byte_pos, &sanitized);
82                state.cursor_position += sanitized.chars().count();
83
84                // Update auto suggestion once after batch insert
85                state.update_auto_suggestion();
86                Vec::new()
87            }
88            InteractionMode::ScriptEditor => {
89                // Delegate to script editor for multi-line aware insertion
90                use crate::components::command_panel::ScriptEditor;
91                ScriptEditor::insert_text(state, text)
92            }
93            InteractionMode::Command => Vec::new(),
94        }
95    }
96
97    /// Handle character input in Input mode (normal typing)
98    fn handle_input_mode_char(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
99        // Handle jk escape sequence first
100        match self.handle_jk_escape(state, ch) {
101            JkResult::Continue => {
102                // Normal character insertion
103                self.insert_char_at_cursor(state, ch);
104                Vec::new()
105            }
106            JkResult::WaitForK => {
107                // Don't insert 'j' yet, just wait for potential 'k'
108                Vec::new()
109            }
110            JkResult::InsertJThenChar => {
111                // Insert pending 'j' then current character
112                self.insert_char_at_cursor(state, 'j');
113                self.insert_char_at_cursor(state, ch);
114                Vec::new()
115            }
116            JkResult::SwitchToCommand => {
117                // Switch to command mode
118                vec![Action::EnterCommandMode]
119            }
120        }
121    }
122
123    /// Handle character input in Command mode (vim-like navigation)
124    fn handle_command_mode_char(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
125        match ch {
126            // Movement commands (vim-like)
127            'j' => {
128                self.move_history_down(state);
129                Vec::new()
130            }
131            'k' => {
132                self.move_history_up(state);
133                Vec::new()
134            }
135            'h' => {
136                // Move cursor left in current line
137                if state.command_cursor_column > 0 {
138                    state.command_cursor_column -= 1;
139                }
140                Vec::new()
141            }
142            'l' => {
143                // Move cursor right in current line
144                self.move_cursor_right_in_command(state);
145                Vec::new()
146            }
147            'g' => {
148                // Go to top
149                self.go_to_top(state);
150                Vec::new()
151            }
152            'G' => {
153                // Go to bottom
154                self.go_to_bottom(state);
155                Vec::new()
156            }
157            '0' | '^' => {
158                // Go to beginning of line
159                state.command_cursor_column = 0;
160                Vec::new()
161            }
162            '$' => {
163                // Go to end of line
164                self.move_to_end_of_line(state);
165                Vec::new()
166            }
167
168            // Mode switching
169            'i' => {
170                // Enter insert mode at current cursor position
171                self.copy_current_line_to_input_if_needed(state);
172                vec![Action::EnterInputMode]
173            }
174            'a' => {
175                // Enter insert mode (after cursor)
176                self.copy_current_line_to_input_if_needed(state);
177                self.move_cursor_right_if_possible(state);
178                vec![Action::EnterInputMode]
179            }
180            'A' => {
181                // Enter insert mode at end of line
182                self.copy_current_line_to_input_if_needed(state);
183                self.move_to_end_of_input(state);
184                vec![Action::EnterInputMode]
185            }
186            'I' => {
187                // Enter insert mode at beginning of line
188                self.copy_current_line_to_input_if_needed(state);
189                state.cursor_position = 0;
190                vec![Action::EnterInputMode]
191            }
192            'o' => {
193                // Open new line below and enter insert mode
194                state.input_text.clear();
195                state.cursor_position = 0;
196                vec![Action::EnterInputMode]
197            }
198            'O' => {
199                // Open new line above and enter insert mode (same as 'o' for command input)
200                state.input_text.clear();
201                state.cursor_position = 0;
202                vec![Action::EnterInputMode]
203            }
204
205            // Command execution
206            '\n' | '\r' => {
207                // Execute current command (if we're on a command line)
208                self.execute_current_command(state)
209            }
210
211            // Copy and edit commands
212            'y' => {
213                // Yank (copy) current line to input
214                self.copy_current_line_to_input_if_needed(state);
215                Vec::new()
216            }
217
218            // Search in history
219            '/' => {
220                // Start search in command history (to be implemented later)
221                Vec::new()
222            }
223
224            // Escape (stay in command mode, but clear any selection)
225            '\u{1b}' => {
226                // Clear any visual selection or multi-char commands
227                Vec::new()
228            }
229
230            // Ignore other characters in command mode
231            _ => Vec::new(),
232        }
233    }
234
235    /// Handle character input in Script Editor mode
236    fn handle_script_mode_char(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
237        use crate::components::command_panel::ScriptEditor;
238        ScriptEditor::insert_char(state, ch)
239    }
240
241    /// Handle jk escape sequence for vim-like mode switching
242    fn handle_jk_escape(&mut self, state: &mut CommandPanelState, ch: char) -> JkResult {
243        match state.jk_escape_state {
244            JkEscapeState::None => {
245                if ch == 'j' {
246                    state.jk_escape_state = JkEscapeState::J;
247                    state.jk_timer = Some(Instant::now());
248                    JkResult::WaitForK
249                } else {
250                    JkResult::Continue
251                }
252            }
253            JkEscapeState::J => {
254                state.jk_escape_state = JkEscapeState::None;
255                state.jk_timer = None;
256
257                if ch == 'k' {
258                    JkResult::SwitchToCommand
259                } else {
260                    JkResult::InsertJThenChar
261                }
262            }
263        }
264    }
265
266    /// Check for jk timeout and handle it
267    pub fn check_jk_timeout(&mut self, state: &mut CommandPanelState) -> bool {
268        if let JkEscapeState::J = state.jk_escape_state {
269            if let Some(timer) = state.jk_timer {
270                if timer.elapsed().as_millis() > self.jk_timeout_ms as u128 {
271                    // Timeout - insert the pending 'j'
272                    state.jk_escape_state = JkEscapeState::None;
273                    state.jk_timer = None;
274                    self.insert_char_at_cursor(state, 'j');
275                    return true;
276                }
277            }
278        }
279        false
280    }
281
282    /// Insert character at current cursor position
283    fn insert_char_at_cursor(&self, state: &mut CommandPanelState, ch: char) {
284        tracing::debug!(
285            "insert_char_at_cursor: inserting '{}' at cursor_pos={}, before: '{}'",
286            ch,
287            state.cursor_position,
288            state.input_text
289        );
290        let byte_pos = self.char_pos_to_byte_pos(&state.input_text, state.cursor_position);
291        state.input_text.insert(byte_pos, ch);
292        state.cursor_position += 1;
293
294        // Update auto suggestion after character insertion
295        state.update_auto_suggestion();
296
297        tracing::debug!(
298            "insert_char_at_cursor: after insertion: '{}', cursor_pos={}",
299            state.input_text,
300            state.cursor_position
301        );
302    }
303
304    /// Move in history (j = down, k = up) - line by line as requested
305    fn move_history_down(&self, state: &mut CommandPanelState) {
306        let total_lines = self.get_total_display_lines(state);
307        if state.command_cursor_line + 1 < total_lines {
308            state.command_cursor_line += 1;
309            self.adjust_cursor_column_for_line(state);
310        }
311    }
312
313    fn move_history_up(&self, state: &mut CommandPanelState) {
314        if state.command_cursor_line > 0 {
315            state.command_cursor_line -= 1;
316            self.adjust_cursor_column_for_line(state);
317        }
318    }
319
320    /// Navigate to top of history
321    fn go_to_top(&self, state: &mut CommandPanelState) {
322        state.command_cursor_line = 0;
323        state.command_cursor_column = 0;
324    }
325
326    /// Navigate to bottom of history (current input)
327    fn go_to_bottom(&self, state: &mut CommandPanelState) {
328        state.command_cursor_line = self.get_total_display_lines(state).saturating_sub(1);
329        self.adjust_cursor_column_for_line(state);
330    }
331
332    /// Execute command at current cursor position
333    fn execute_current_command(&self, state: &mut CommandPanelState) -> Vec<Action> {
334        // If we're on the current input line, submit it
335        let total_lines = self.get_total_display_lines(state);
336        if state.command_cursor_line + 1 >= total_lines {
337            // On current input line
338            if !state.input_text.trim().is_empty() {
339                vec![Action::SubmitCommand]
340            } else {
341                Vec::new()
342            }
343        } else {
344            // On a history line - copy it to current input
345            if let Some(content) = self.get_line_content_at_cursor(state) {
346                state.input_text = content;
347                state.cursor_position = state.input_text.chars().count();
348                vec![Action::EnterInputMode]
349            } else {
350                Vec::new()
351            }
352        }
353    }
354
355    /// Move cursor right in command mode
356    fn move_cursor_right_in_command(&self, state: &mut CommandPanelState) {
357        if let Some(line_content) = self.get_line_content_at_cursor(state) {
358            if state.command_cursor_column < line_content.chars().count() {
359                state.command_cursor_column += 1;
360            }
361        }
362    }
363
364    /// Move to end of current line
365    fn move_to_end_of_line(&self, state: &mut CommandPanelState) {
366        if let Some(line_content) = self.get_line_content_at_cursor(state) {
367            state.command_cursor_column = line_content.chars().count();
368        }
369    }
370
371    /// Move to end of input text
372    fn move_to_end_of_input(&self, state: &mut CommandPanelState) {
373        state.cursor_position = state.input_text.chars().count();
374    }
375
376    /// Move cursor right if possible
377    fn move_cursor_right_if_possible(&self, state: &mut CommandPanelState) {
378        let input_len = state.input_text.chars().count();
379        if state.cursor_position < input_len {
380            state.cursor_position += 1;
381        }
382    }
383
384    /// Adjust cursor column when moving between lines
385    fn adjust_cursor_column_for_line(&self, state: &mut CommandPanelState) {
386        if let Some(line_content) = self.get_line_content_at_cursor(state) {
387            let line_len = line_content.chars().count();
388            state.command_cursor_column = state.command_cursor_column.min(line_len);
389        }
390    }
391
392    /// Get content of line at current cursor position
393    fn get_line_content_at_cursor(&self, state: &CommandPanelState) -> Option<String> {
394        // Build the display lines structure similar to what the renderer does
395        let mut display_lines = Vec::new();
396
397        // Add command history
398        for item in &state.command_history {
399            // Command line - use (ghostscope) prompt for consistency
400            let command_line = format!("(ghostscope) {}", item.command);
401            display_lines.push(command_line);
402
403            // Response lines (if any)
404            if let Some(ref response) = item.response {
405                for response_line in response.lines() {
406                    display_lines.push(response_line.to_string());
407                }
408            }
409        }
410
411        // Current input line
412        if matches!(state.input_state, InputState::Ready) {
413            let prompt = "(ghostscope) ";
414            let input_line = format!("{prompt}{input_text}", input_text = state.input_text);
415            display_lines.push(input_line);
416        }
417
418        // Return the line at current cursor position
419        display_lines.get(state.command_cursor_line).cloned()
420    }
421
422    /// Get total number of display lines
423    fn get_total_display_lines(&self, state: &CommandPanelState) -> usize {
424        // Calculate total lines: history + responses + current input
425        let mut total = 0;
426
427        for item in &state.command_history {
428            total += 1; // Command line
429            if let Some(ref response) = item.response {
430                total += response.lines().count();
431            }
432        }
433
434        total += 1; // Current input line
435        total
436    }
437
438    /// Handle basic movement (arrow keys, etc.)
439    pub fn handle_movement(
440        &mut self,
441        state: &mut CommandPanelState,
442        direction: CursorDirection,
443    ) -> Vec<Action> {
444        match state.mode {
445            InteractionMode::Input => self.handle_input_movement(state, direction),
446            InteractionMode::Command => self.handle_command_movement(state, direction),
447            InteractionMode::ScriptEditor => self.handle_script_movement(state, direction),
448        }
449    }
450
451    /// Handle movement in input mode
452    fn handle_input_movement(
453        &self,
454        state: &mut CommandPanelState,
455        direction: CursorDirection,
456    ) -> Vec<Action> {
457        match direction {
458            CursorDirection::Left => {
459                if state.cursor_position > 0 {
460                    state.cursor_position -= 1;
461                }
462            }
463            CursorDirection::Right => {
464                let input_len = state.input_text.chars().count();
465                if state.cursor_position < input_len {
466                    state.cursor_position += 1;
467                }
468            }
469            CursorDirection::Up => {
470                return self.history_up(state);
471            }
472            CursorDirection::Down => {
473                return self.history_down(state);
474            }
475            CursorDirection::Home => {
476                state.cursor_position = 0;
477            }
478            CursorDirection::End => {
479                state.cursor_position = state.input_text.chars().count();
480            }
481        }
482        Vec::new()
483    }
484
485    /// Handle movement in command mode (vim-like)
486    fn handle_command_movement(
487        &self,
488        state: &mut CommandPanelState,
489        direction: CursorDirection,
490    ) -> Vec<Action> {
491        match direction {
492            CursorDirection::Left => {
493                if state.command_cursor_column > 0 {
494                    state.command_cursor_column -= 1;
495                }
496            }
497            CursorDirection::Right => {
498                self.move_cursor_right_in_command(state);
499            }
500            CursorDirection::Up => {
501                self.move_history_up(state);
502            }
503            CursorDirection::Down => {
504                self.move_history_down(state);
505            }
506            CursorDirection::Home => {
507                state.command_cursor_column = 0;
508            }
509            CursorDirection::End => {
510                self.move_to_end_of_line(state);
511            }
512        }
513        Vec::new()
514    }
515
516    /// Handle movement in script editor mode
517    fn handle_script_movement(
518        &self,
519        state: &mut CommandPanelState,
520        direction: CursorDirection,
521    ) -> Vec<Action> {
522        use crate::components::command_panel::ScriptEditor;
523        match direction {
524            CursorDirection::Left => ScriptEditor::move_cursor_left(state),
525            CursorDirection::Right => ScriptEditor::move_cursor_right(state),
526            CursorDirection::Up => ScriptEditor::move_cursor_up(state),
527            CursorDirection::Down => ScriptEditor::move_cursor_down(state),
528            CursorDirection::Home => ScriptEditor::move_to_beginning(state),
529            CursorDirection::End => ScriptEditor::move_to_end(state),
530        }
531    }
532
533    /// Handle Enter key for script editor
534    pub fn handle_enter(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
535        match state.mode {
536            InteractionMode::ScriptEditor => {
537                use crate::components::command_panel::ScriptEditor;
538                ScriptEditor::insert_newline(state)
539            }
540            _ => Vec::new(), // Enter handling for other modes done elsewhere
541        }
542    }
543
544    /// Handle Delete key (Ctrl+D) - delete character at cursor position
545    pub fn handle_delete(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
546        match state.mode {
547            InteractionMode::Input => {
548                // Delete character at cursor position
549                if state.cursor_position < state.input_text.chars().count() {
550                    let chars: Vec<char> = state.input_text.chars().collect();
551                    let before: String = chars[..state.cursor_position].iter().collect();
552                    let after: String = chars[state.cursor_position + 1..].iter().collect();
553                    state.input_text = format!("{before}{after}");
554                }
555                Vec::new()
556            }
557            InteractionMode::Command => {
558                // In command mode, delete doesn't do anything
559                Vec::new()
560            }
561            InteractionMode::ScriptEditor => {
562                // Delete character in script editor
563                if let Some(ref mut cache) = state.script_cache {
564                    if cache.cursor_line < cache.lines.len() {
565                        let line = &cache.lines[cache.cursor_line];
566                        if cache.cursor_col < line.chars().count() {
567                            let chars: Vec<char> = line.chars().collect();
568                            let before: String = chars[..cache.cursor_col].iter().collect();
569                            let after: String = chars[cache.cursor_col + 1..].iter().collect();
570                            cache.lines[cache.cursor_line] = format!("{before}{after}");
571                        }
572                    }
573                }
574                Vec::new()
575            }
576        }
577    }
578
579    /// Handle Tab key for script editor
580    pub fn handle_tab(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
581        match state.mode {
582            InteractionMode::ScriptEditor => {
583                use crate::components::command_panel::ScriptEditor;
584                ScriptEditor::insert_tab(state)
585            }
586            _ => Vec::new(), // Tab handling for other modes done elsewhere
587        }
588    }
589
590    /// Handle command submission (Enter key)
591    pub fn handle_submit(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
592        match state.mode {
593            InteractionMode::Input => {
594                // Submit the current input as a command
595                let command = state.input_text.clone();
596
597                // Always add command to history (even if empty) to show user feedback
598                self.add_command_to_history(state, &command);
599
600                // Reset history navigation to start from newest command next time
601                state.history_index = None;
602
603                // Clear input after command submission
604                state.input_text.clear();
605                state.cursor_position = 0;
606                state.auto_suggestion.clear();
607
608                if !command.trim().is_empty() {
609                    // Parse and execute non-empty commands
610                    use crate::components::command_panel::CommandParser;
611                    CommandParser::parse_command(state, &command)
612                } else {
613                    // Empty command - no action needed, just shows empty line in history
614                    Vec::new()
615                }
616            }
617            InteractionMode::ScriptEditor => {
618                // Enter in script editor should insert newline
619                self.handle_enter(state)
620            }
621            InteractionMode::Command => {
622                // Command mode: copy current line to input and submit
623                self.copy_current_line_to_input_if_needed(state);
624
625                let command = state.input_text.clone();
626                if !command.trim().is_empty() {
627                    // Switch back to input mode and submit
628                    state.mode = InteractionMode::Input;
629                    use crate::components::command_panel::CommandParser;
630                    CommandParser::parse_command(state, &command)
631                } else {
632                    Vec::new()
633                }
634            }
635        }
636    }
637
638    /// Handle history up (traditional arrow key behavior)
639    fn history_up(&self, state: &mut CommandPanelState) -> Vec<Action> {
640        if state.command_history.is_empty() {
641            return Vec::new();
642        }
643
644        match state.history_index {
645            None => {
646                // First time accessing history
647                if !state.input_text.is_empty() {
648                    state.unsent_input_backup = Some(state.input_text.clone());
649                }
650                state.history_index = Some(state.command_history.len() - 1);
651            }
652            Some(current_index) => {
653                if current_index > 0 {
654                    state.history_index = Some(current_index - 1);
655                }
656            }
657        }
658
659        // Load the selected history item
660        if let Some(index) = state.history_index {
661            if let Some(item) = state.command_history.get(index) {
662                state.input_text = item.command.clone();
663                state.cursor_position = state.input_text.chars().count();
664            }
665        }
666
667        Vec::new()
668    }
669
670    /// Handle history down (traditional arrow key behavior)
671    fn history_down(&self, state: &mut CommandPanelState) -> Vec<Action> {
672        match state.history_index {
673            None => Vec::new(), // Not in history mode
674            Some(current_index) => {
675                let max_index = state.command_history.len() - 1;
676                if current_index < max_index {
677                    state.history_index = Some(current_index + 1);
678                    if let Some(item) = state.command_history.get(current_index + 1) {
679                        state.input_text = item.command.clone();
680                        state.cursor_position = state.input_text.chars().count();
681                    }
682                } else {
683                    // Reached the end of history, restore unsent input or clear
684                    state.history_index = None;
685                    if let Some(backup) = state.unsent_input_backup.take() {
686                        state.input_text = backup;
687                    } else {
688                        state.input_text.clear();
689                    }
690                    state.cursor_position = state.input_text.chars().count();
691                }
692                Vec::new()
693            }
694        }
695    }
696
697    /// Handle backspace/delete
698    pub fn handle_backspace(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
699        match state.mode {
700            InteractionMode::Input => {
701                if state.cursor_position > 0 {
702                    state.cursor_position -= 1;
703                    let byte_pos =
704                        self.char_pos_to_byte_pos(&state.input_text, state.cursor_position);
705                    if byte_pos < state.input_text.len() {
706                        let mut end_pos = byte_pos + 1;
707                        while end_pos < state.input_text.len()
708                            && !state.input_text.is_char_boundary(end_pos)
709                        {
710                            end_pos += 1;
711                        }
712                        state.input_text.drain(byte_pos..end_pos);
713                    }
714                    // Update auto suggestion after character deletion
715                    state.update_auto_suggestion();
716                }
717            }
718            InteractionMode::Command => {
719                // In command mode, backspace might switch back to input mode
720                // For now, ignore
721            }
722            InteractionMode::ScriptEditor => {
723                use crate::components::command_panel::ScriptEditor;
724                return ScriptEditor::delete_char(state);
725            }
726        }
727        Vec::new()
728    }
729
730    /// Utility: Convert character position to byte position
731    fn char_pos_to_byte_pos(&self, text: &str, char_pos: usize) -> usize {
732        text.char_indices()
733            .nth(char_pos)
734            .map_or(text.len(), |(pos, _)| pos)
735    }
736
737    /// Copy current history line to input if we're not on the input line
738    fn copy_current_line_to_input_if_needed(&self, state: &mut CommandPanelState) {
739        let total_lines = self.get_total_display_lines(state);
740
741        // If we're not on the last line (current input), copy the current line
742        if state.command_cursor_line + 1 < total_lines {
743            if let Some(content) = self.get_line_content_at_cursor(state) {
744                // Extract command part if it's a command line (remove prompt)
745                if let Some(stripped) = content.strip_prefix("(ghostscope) ") {
746                    state.input_text = stripped.to_string(); // "(ghostscope) ".len() = 13
747                } else {
748                    // For response lines, copy as-is (user might want to edit it as a command)
749                    state.input_text = content;
750                }
751                state.cursor_position = state.input_text.chars().count();
752            }
753        }
754    }
755
756    /// Add a command to the command history
757    fn add_command_to_history(&self, state: &mut CommandPanelState, command: &str) {
758        // Use the unified method from CommandPanelState
759        state.add_command_entry(command);
760    }
761}
762
763impl Default for OptimizedInputHandler {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769/// Result of jk escape sequence processing
770#[derive(Debug, PartialEq)]
771enum JkResult {
772    Continue,        // Normal processing
773    WaitForK,        // 'j' was pressed, waiting for potential 'k'
774    InsertJThenChar, // Insert pending 'j' then current char
775    SwitchToCommand, // Switch to command mode
776}