ghostscope_ui/components/command_panel/
optimized_input.rs1use crate::action::{Action, CursorDirection};
2use crate::model::panel_state::{CommandPanelState, InputState, InteractionMode, JkEscapeState};
3use ratatui::crossterm::event::KeyEvent;
4use std::time::Instant;
5
6#[derive(Debug)]
8pub struct OptimizedInputHandler {
9 jk_timeout_ms: u64,
11
12 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 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 crate::components::command_panel::InputHandler::handle_key_event(state, key)
34 }
35
36 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 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 state.jk_escape_state = JkEscapeState::None;
70 state.jk_timer = None;
71
72 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 state.update_auto_suggestion();
86 Vec::new()
87 }
88 InteractionMode::ScriptEditor => {
89 use crate::components::command_panel::ScriptEditor;
91 ScriptEditor::insert_text(state, text)
92 }
93 InteractionMode::Command => Vec::new(),
94 }
95 }
96
97 fn handle_input_mode_char(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
99 match self.handle_jk_escape(state, ch) {
101 JkResult::Continue => {
102 self.insert_char_at_cursor(state, ch);
104 Vec::new()
105 }
106 JkResult::WaitForK => {
107 Vec::new()
109 }
110 JkResult::InsertJThenChar => {
111 self.insert_char_at_cursor(state, 'j');
113 self.insert_char_at_cursor(state, ch);
114 Vec::new()
115 }
116 JkResult::SwitchToCommand => {
117 vec![Action::EnterCommandMode]
119 }
120 }
121 }
122
123 fn handle_command_mode_char(&mut self, state: &mut CommandPanelState, ch: char) -> Vec<Action> {
125 match ch {
126 '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 if state.command_cursor_column > 0 {
138 state.command_cursor_column -= 1;
139 }
140 Vec::new()
141 }
142 'l' => {
143 self.move_cursor_right_in_command(state);
145 Vec::new()
146 }
147 'g' => {
148 self.go_to_top(state);
150 Vec::new()
151 }
152 'G' => {
153 self.go_to_bottom(state);
155 Vec::new()
156 }
157 '0' | '^' => {
158 state.command_cursor_column = 0;
160 Vec::new()
161 }
162 '$' => {
163 self.move_to_end_of_line(state);
165 Vec::new()
166 }
167
168 'i' => {
170 self.copy_current_line_to_input_if_needed(state);
172 vec![Action::EnterInputMode]
173 }
174 'a' => {
175 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 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 self.copy_current_line_to_input_if_needed(state);
189 state.cursor_position = 0;
190 vec![Action::EnterInputMode]
191 }
192 'o' => {
193 state.input_text.clear();
195 state.cursor_position = 0;
196 vec![Action::EnterInputMode]
197 }
198 'O' => {
199 state.input_text.clear();
201 state.cursor_position = 0;
202 vec![Action::EnterInputMode]
203 }
204
205 '\n' | '\r' => {
207 self.execute_current_command(state)
209 }
210
211 'y' => {
213 self.copy_current_line_to_input_if_needed(state);
215 Vec::new()
216 }
217
218 '/' => {
220 Vec::new()
222 }
223
224 '\u{1b}' => {
226 Vec::new()
228 }
229
230 _ => Vec::new(),
232 }
233 }
234
235 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 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 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 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 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 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 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 fn go_to_top(&self, state: &mut CommandPanelState) {
322 state.command_cursor_line = 0;
323 state.command_cursor_column = 0;
324 }
325
326 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 fn execute_current_command(&self, state: &mut CommandPanelState) -> Vec<Action> {
334 let total_lines = self.get_total_display_lines(state);
336 if state.command_cursor_line + 1 >= total_lines {
337 if !state.input_text.trim().is_empty() {
339 vec![Action::SubmitCommand]
340 } else {
341 Vec::new()
342 }
343 } else {
344 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 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 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 fn move_to_end_of_input(&self, state: &mut CommandPanelState) {
373 state.cursor_position = state.input_text.chars().count();
374 }
375
376 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 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 fn get_line_content_at_cursor(&self, state: &CommandPanelState) -> Option<String> {
394 let mut display_lines = Vec::new();
396
397 for item in &state.command_history {
399 let command_line = format!("(ghostscope) {}", item.command);
401 display_lines.push(command_line);
402
403 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 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 display_lines.get(state.command_cursor_line).cloned()
420 }
421
422 fn get_total_display_lines(&self, state: &CommandPanelState) -> usize {
424 let mut total = 0;
426
427 for item in &state.command_history {
428 total += 1; if let Some(ref response) = item.response {
430 total += response.lines().count();
431 }
432 }
433
434 total += 1; total
436 }
437
438 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 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 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 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 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(), }
542 }
543
544 pub fn handle_delete(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
546 match state.mode {
547 InteractionMode::Input => {
548 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 Vec::new()
560 }
561 InteractionMode::ScriptEditor => {
562 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 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(), }
588 }
589
590 pub fn handle_submit(&mut self, state: &mut CommandPanelState) -> Vec<Action> {
592 match state.mode {
593 InteractionMode::Input => {
594 let command = state.input_text.clone();
596
597 self.add_command_to_history(state, &command);
599
600 state.history_index = None;
602
603 state.input_text.clear();
605 state.cursor_position = 0;
606 state.auto_suggestion.clear();
607
608 if !command.trim().is_empty() {
609 use crate::components::command_panel::CommandParser;
611 CommandParser::parse_command(state, &command)
612 } else {
613 Vec::new()
615 }
616 }
617 InteractionMode::ScriptEditor => {
618 self.handle_enter(state)
620 }
621 InteractionMode::Command => {
622 self.copy_current_line_to_input_if_needed(state);
624
625 let command = state.input_text.clone();
626 if !command.trim().is_empty() {
627 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 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 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 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 fn history_down(&self, state: &mut CommandPanelState) -> Vec<Action> {
672 match state.history_index {
673 None => Vec::new(), 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 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 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 state.update_auto_suggestion();
716 }
717 }
718 InteractionMode::Command => {
719 }
722 InteractionMode::ScriptEditor => {
723 use crate::components::command_panel::ScriptEditor;
724 return ScriptEditor::delete_char(state);
725 }
726 }
727 Vec::new()
728 }
729
730 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 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 state.command_cursor_line + 1 < total_lines {
743 if let Some(content) = self.get_line_content_at_cursor(state) {
744 if let Some(stripped) = content.strip_prefix("(ghostscope) ") {
746 state.input_text = stripped.to_string(); } else {
748 state.input_text = content;
750 }
751 state.cursor_position = state.input_text.chars().count();
752 }
753 }
754 }
755
756 fn add_command_to_history(&self, state: &mut CommandPanelState, command: &str) {
758 state.add_command_entry(command);
760 }
761}
762
763impl Default for OptimizedInputHandler {
764 fn default() -> Self {
765 Self::new()
766 }
767}
768
769#[derive(Debug, PartialEq)]
771enum JkResult {
772 Continue, WaitForK, InsertJThenChar, SwitchToCommand, }