ghostscope_ui/components/app/event_loop.rs
1use super::App;
2use crate::action::{Action, PanelType};
3use crate::components::loading::LoadingState;
4use anyhow::Result;
5use crossterm::{
6 event::{DisableBracketedPaste, Event, EventStream, KeyCode, KeyEventKind},
7 execute,
8 terminal::{disable_raw_mode, LeaveAlternateScreen},
9};
10use futures_util::StreamExt;
11use tracing::debug;
12
13impl App {
14 pub async fn run(&mut self) -> Result<()> {
15 debug!("Starting new TEA-based TUI application");
16
17 // Create async event stream (proper crossterm async support)
18 let mut event_stream = EventStream::new();
19 let mut needs_render = true;
20
21 // Create a timeout for loading - if no runtime response, go to ready
22 const LOADING_TIMEOUT_SECS: u64 = 30;
23 let loading_timeout =
24 tokio::time::sleep(tokio::time::Duration::from_secs(LOADING_TIMEOUT_SECS));
25 tokio::pin!(loading_timeout);
26
27 // Create a 1-second interval for loading UI updates (elapsed time, spinner, etc.)
28 let mut loading_ui_ticker = tokio::time::interval(tokio::time::Duration::from_secs(1));
29 loading_ui_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
30
31 // Periodic housekeeping ticker for lightweight timeout/cleanup checks.
32 // Use an interval instead of recreating sleep futures in each select iteration.
33 let mut housekeeping_ticker = tokio::time::interval(tokio::time::Duration::from_millis(50));
34 housekeeping_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
35
36 // Initial render
37 self.terminal.draw(|f| Self::draw_ui(f, &mut self.state))?;
38
39 loop {
40 // Handle events using select! to monitor multiple sources
41 tokio::select! {
42 // Handle crossterm events (keyboard, mouse, resize) - proper async
43 Some(event_result) = event_stream.next() => {
44 match event_result {
45 Ok(event) => {
46 if let Event::Key(key) = &event {
47 tracing::debug!("Raw crossterm event: {:?}", key);
48 }
49 if let Err(e) = self.handle_event(event).await {
50 tracing::error!("Error handling terminal event: {}", e);
51 }
52 needs_render = true;
53 }
54 Err(e) => {
55 tracing::error!("Error reading terminal events: {}", e);
56 break;
57 }
58 }
59 }
60
61 // Handle runtime status messages
62 Some(status) = self.state.event_registry.status_receiver.recv() => {
63 self.handle_runtime_status(status).await;
64 needs_render = true;
65 }
66
67 // Handle trace events
68 Some(trace_event) = self.state.event_registry.trace_receiver.recv() => {
69 self.handle_trace_event(trace_event).await;
70 needs_render = true;
71 }
72
73 // Loading timeout - show error in loading UI
74 () = &mut loading_timeout, if !self.state.loading_state.is_ready() && !self.state.loading_state.is_failed() => {
75 tracing::info!("No runtime response after {} seconds, connection timeout", LOADING_TIMEOUT_SECS);
76 self.state.set_loading_state(LoadingState::Failed("Connection timeout - no runtime response".to_string()));
77 needs_render = true;
78 }
79
80 // Update loading UI periodically (elapsed time, spinner animation)
81 _ = loading_ui_ticker.tick(), if self.state.is_loading() => {
82 // Just trigger a redraw to update elapsed time and spinner
83 // No state changes needed - the UI will read fresh elapsed time on render
84 needs_render = true;
85 }
86
87 // Check for jk escape sequence timeout and periodic cleanup
88 _ = housekeeping_ticker.tick() => {
89 // Check jk timeout
90 if crate::components::command_panel::input_handler::InputHandler::check_jk_timeout(&mut self.state.command_panel) {
91 needs_render = true;
92 }
93
94 // Check for command response timeout
95 if let crate::model::panel_state::InputState::WaitingResponse { sent_time, command, .. } = &self.state.command_panel.input_state {
96 const COMMAND_TIMEOUT_SECS: u64 = 5;
97 if sent_time.elapsed().as_secs() >= COMMAND_TIMEOUT_SECS {
98 let timeout_msg = format!("Command timeout: '{command}' - no response after {COMMAND_TIMEOUT_SECS} seconds");
99 self.clear_waiting_state();
100 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
101 &mut self.state.command_panel,
102 timeout_msg,
103 crate::components::command_panel::style_builder::StylePresets::ERROR,
104 crate::action::ResponseType::Error,
105 );
106 needs_render = true;
107 }
108 }
109
110 // Periodic cleanup of file completion cache
111 self.state.command_panel.cleanup_file_completion_cache();
112 }
113 }
114
115 // Render only when needed (event-driven)
116 if needs_render {
117 self.terminal.draw(|f| Self::draw_ui(f, &mut self.state))?;
118 needs_render = false;
119 }
120
121 // Check for quit condition
122 if self.should_quit || self.state.should_quit {
123 break;
124 }
125 }
126
127 // Send shutdown command to runtime before cleanup
128 if let Err(e) = self
129 .state
130 .event_registry
131 .command_sender
132 .send(crate::events::RuntimeCommand::Shutdown)
133 {
134 tracing::warn!("Failed to send shutdown command to runtime: {}", e);
135 }
136
137 self.cleanup().await
138 }
139
140 /// Handle terminal events and convert to actions
141 async fn handle_event(&mut self, event: Event) -> Result<bool> {
142 let mut actions_to_process = Vec::new();
143
144 match event {
145 Event::Key(key) => {
146 tracing::debug!(
147 "Event received: key={:?}, is_loading={}",
148 key,
149 self.state.is_loading()
150 );
151 if key.kind == KeyEventKind::Press {
152 // Always handle input - loading state should not block user interaction
153 // Loading is purely a visual indication
154
155 // Handle window navigation mode first
156 if self.state.ui.focus.expecting_window_nav {
157 match key.code {
158 KeyCode::Char('h') => {
159 actions_to_process.push(Action::WindowNavMove(
160 crate::action::WindowDirection::Left,
161 ));
162 actions_to_process.push(Action::ExitWindowNavMode);
163 }
164 KeyCode::Char('j') => {
165 actions_to_process.push(Action::WindowNavMove(
166 crate::action::WindowDirection::Down,
167 ));
168 actions_to_process.push(Action::ExitWindowNavMode);
169 }
170 KeyCode::Char('k') => {
171 actions_to_process.push(Action::WindowNavMove(
172 crate::action::WindowDirection::Up,
173 ));
174 actions_to_process.push(Action::ExitWindowNavMode);
175 }
176 KeyCode::Char('l') => {
177 actions_to_process.push(Action::WindowNavMove(
178 crate::action::WindowDirection::Right,
179 ));
180 actions_to_process.push(Action::ExitWindowNavMode);
181 }
182 KeyCode::Char('v') => {
183 actions_to_process.push(Action::SwitchLayout);
184 actions_to_process.push(Action::ExitWindowNavMode);
185 }
186 KeyCode::Char('z') => {
187 actions_to_process.push(Action::ToggleFullscreen);
188 actions_to_process.push(Action::ExitWindowNavMode);
189 }
190 _ => {
191 // Any other key cancels window navigation
192 actions_to_process.push(Action::ExitWindowNavMode);
193 }
194 }
195 }
196
197 // Normal key handling
198
199 // Clear Ctrl+C flag for any key that's not Ctrl+C
200 let is_ctrl_c = matches!(key.code, KeyCode::Char('c'))
201 && key
202 .modifiers
203 .contains(crossterm::event::KeyModifiers::CONTROL);
204 if !is_ctrl_c {
205 self.state.expecting_second_ctrl_c = false;
206 }
207
208 match key.code {
209 KeyCode::Char('c')
210 if key
211 .modifiers
212 .contains(crossterm::event::KeyModifiers::CONTROL) =>
213 {
214 // Use the new centralized Ctrl+C handler
215 let ctrl_c_actions = self.handle_ctrl_c();
216 actions_to_process.extend(ctrl_c_actions);
217 }
218 KeyCode::Char('w')
219 if key
220 .modifiers
221 .contains(crossterm::event::KeyModifiers::CONTROL) =>
222 {
223 // Handle Ctrl+W based on current focus and mode - priority order matters!
224 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
225 && self.state.source_panel.mode
226 == crate::model::panel_state::SourcePanelMode::FileSearch
227 {
228 // HIGHEST PRIORITY: File search delete word
229 if let Some(ref cache) =
230 self.state.command_panel.file_completion_cache
231 {
232 let delete_actions = crate::components::source_panel::SourceSearch::delete_word_file_search(
233 &mut self.state.source_panel,
234 cache,
235 );
236 actions_to_process.extend(delete_actions);
237 }
238 } else if self.state.ui.focus.current_panel
239 == crate::action::PanelType::InteractiveCommand
240 {
241 match self.state.command_panel.mode {
242 crate::model::panel_state::InteractionMode::Input => {
243 actions_to_process.push(Action::DeletePreviousWord);
244 }
245 crate::model::panel_state::InteractionMode::ScriptEditor => {
246 actions_to_process.push(Action::DeletePreviousWord);
247 }
248 _ => {
249 // In command mode, use for window navigation
250 actions_to_process.push(Action::EnterWindowNavMode);
251 }
252 }
253 } else {
254 // In other panels, use for window navigation
255 actions_to_process.push(Action::EnterWindowNavMode);
256 }
257 }
258 KeyCode::Tab => {
259 // Handle Tab based on current panel and mode - priority order matters!
260 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
261 && self.state.source_panel.mode
262 == crate::model::panel_state::SourcePanelMode::FileSearch
263 {
264 // HIGHEST PRIORITY: File search navigation
265 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
266 &mut self.state.source_panel,
267 );
268 actions_to_process.extend(move_actions);
269 } else if self.state.ui.focus.current_panel
270 == crate::action::PanelType::InteractiveCommand
271 && self.state.command_panel.mode
272 == crate::model::panel_state::InteractionMode::ScriptEditor
273 {
274 // Script editor Tab inserts spaces
275 actions_to_process.push(Action::InsertTab);
276 } else if self.state.ui.focus.current_panel
277 == crate::action::PanelType::InteractiveCommand
278 && self.state.command_panel.mode
279 == crate::model::panel_state::InteractionMode::Input
280 {
281 // COMMAND INPUT MODE: Let Tab go to focused panel handler for auto-suggestion
282 let panel_actions = self.handle_focused_panel_input(key)?;
283 actions_to_process.extend(panel_actions);
284 } else {
285 // Normal Tab behavior: cycle focus
286 actions_to_process.push(Action::FocusNext);
287 }
288 }
289 KeyCode::BackTab => {
290 // Handle Shift+Tab based on current panel and mode
291 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
292 && self.state.source_panel.mode
293 == crate::model::panel_state::SourcePanelMode::FileSearch
294 {
295 // HIGHEST PRIORITY: File search navigation (up)
296 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_up(
297 &mut self.state.source_panel,
298 );
299 actions_to_process.extend(move_actions);
300 } else {
301 // Normal Shift+Tab behavior: cycle focus backward
302 actions_to_process.push(Action::FocusPrevious);
303 }
304 }
305 KeyCode::F(1) => {
306 actions_to_process.push(Action::ToggleFullscreen);
307 }
308 KeyCode::F(2) => {
309 actions_to_process.push(Action::SwitchLayout);
310 }
311 _ => {
312 // Forward to focused panel handler
313 let panel_actions = self.handle_focused_panel_input(key)?;
314 actions_to_process.extend(panel_actions);
315 }
316 }
317 }
318 }
319 Event::Resize(width, height) => {
320 actions_to_process.push(Action::Resize(width, height));
321 }
322 Event::Paste(pasted) => {
323 tracing::debug!("Event received: paste_len={}", pasted.len());
324 // Batch insert pasted text depending on focused panel and mode
325 match self.state.ui.focus.current_panel {
326 PanelType::InteractiveCommand => {
327 match self.state.command_panel.mode {
328 crate::model::panel_state::InteractionMode::Input => {
329 let actions = self
330 .state
331 .command_input_handler
332 .insert_str(&mut self.state.command_panel, &pasted);
333 actions_to_process.extend(actions);
334 self.state.command_renderer.mark_pending_updates();
335 }
336 crate::model::panel_state::InteractionMode::ScriptEditor => {
337 let actions =
338 crate::components::command_panel::ScriptEditor::insert_text(
339 &mut self.state.command_panel,
340 &pasted,
341 );
342 actions_to_process.extend(actions);
343 self.state.command_renderer.mark_pending_updates();
344 }
345 crate::model::panel_state::InteractionMode::Command => {
346 // Ignore paste in command mode
347 }
348 }
349 }
350 _ => {
351 // Ignore paste in other panels
352 }
353 }
354 }
355 _ => {}
356 }
357
358 // Process all actions
359 for action in actions_to_process {
360 let is_quit = matches!(action, Action::Quit);
361 let additional_actions = self.handle_action(action)?;
362
363 // Process any additional actions returned
364 for additional_action in additional_actions {
365 self.handle_action(additional_action)?;
366 }
367
368 if is_quit || self.state.should_quit {
369 return Ok(true);
370 }
371 }
372
373 Ok(false)
374 }
375
376 /// Handle input for the currently focused panel
377 fn handle_focused_panel_input(
378 &mut self,
379 key: crossterm::event::KeyEvent,
380 ) -> Result<Vec<Action>> {
381 let mut actions = Vec::new();
382
383 match self.state.ui.focus.current_panel {
384 PanelType::InteractiveCommand => {
385 // First, try the new unified key event handler for history and suggestions
386 let unified_actions = self
387 .state
388 .command_input_handler
389 .handle_key_event(&mut self.state.command_panel, key);
390
391 if !unified_actions.is_empty() {
392 // The unified handler handled the key, mark for updates and return
393 self.state.command_renderer.mark_pending_updates();
394 return Ok(unified_actions);
395 }
396
397 // Fall back to existing character-based handling
398 match key.code {
399 KeyCode::Char(c) => {
400 tracing::debug!(
401 "App received char='{}' (code={}), modifiers={:?}, current_panel={:?}",
402 c,
403 c as u32,
404 key.modifiers,
405 self.state.ui.focus.current_panel
406 );
407 // Handle Ctrl+key combinations first
408 if key
409 .modifiers
410 .contains(crossterm::event::KeyModifiers::CONTROL)
411 {
412 match c {
413 's' => {
414 // Ctrl+S - only submit script in script mode
415 if matches!(
416 self.state.command_panel.mode,
417 crate::model::panel_state::InteractionMode::ScriptEditor
418 ) {
419 actions.push(Action::SubmitScript);
420 }
421 }
422 'a' => {
423 match self.state.command_panel.mode {
424 crate::model::panel_state::InteractionMode::ScriptEditor => {
425 // Ctrl+A - move to beginning of current line in script mode
426 let script_actions = crate::components::command_panel::ScriptEditor::move_to_beginning(
427 &mut self.state.command_panel,
428 );
429 actions.extend(script_actions);
430 }
431 _ => {
432 // Ctrl+A - move to beginning of line in input/command mode
433 actions.push(Action::MoveCursor(crate::action::CursorDirection::Home));
434 }
435 }
436 }
437 'e' => {
438 match self.state.command_panel.mode {
439 crate::model::panel_state::InteractionMode::ScriptEditor => {
440 // Ctrl+E - move to end of current line in script mode
441 let script_actions = crate::components::command_panel::ScriptEditor::move_to_end(
442 &mut self.state.command_panel,
443 );
444 actions.extend(script_actions);
445 }
446 _ => {
447 // Ctrl+E - move to end of line in input/command mode
448 actions.push(Action::MoveCursor(crate::action::CursorDirection::End));
449 }
450 }
451 }
452 'f' => {
453 match self.state.command_panel.mode {
454 crate::model::panel_state::InteractionMode::ScriptEditor => {
455 // Ctrl+F - move cursor right (forward one character) in script mode
456 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_right(
457 &mut self.state.command_panel,
458 );
459 actions.extend(script_actions);
460 }
461 _ => {
462 // Ctrl+F - move cursor right in input/command mode
463 actions.push(Action::MoveCursor(crate::action::CursorDirection::Right));
464 }
465 }
466 }
467 'b' => {
468 match self.state.command_panel.mode {
469 crate::model::panel_state::InteractionMode::ScriptEditor => {
470 // Ctrl+B - move cursor left (back one character) in script mode
471 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_left(
472 &mut self.state.command_panel,
473 );
474 actions.extend(script_actions);
475 }
476 _ => {
477 // Ctrl+B - move cursor left in input/command mode
478 actions.push(Action::MoveCursor(crate::action::CursorDirection::Left));
479 }
480 }
481 }
482 'u' => {
483 match self.state.command_panel.mode {
484 crate::model::panel_state::InteractionMode::ScriptEditor => {
485 // Ctrl+U - delete from cursor to line start in script mode
486 let script_actions = crate::components::command_panel::ScriptEditor::delete_to_line_start(
487 &mut self.state.command_panel,
488 );
489 actions.extend(script_actions);
490 }
491 crate::model::panel_state::InteractionMode::Command => {
492 // Ctrl+U - half page up in command mode (fast scroll)
493 actions.push(Action::CommandHalfPageUp);
494 }
495 _ => {
496 // Ctrl+U - delete to beginning in input mode
497 actions.push(Action::DeleteToBeginning);
498 }
499 }
500 }
501 'd' => {
502 match self.state.command_panel.mode {
503 crate::model::panel_state::InteractionMode::Command => {
504 // Ctrl+D - half page down in command mode (fast scroll)
505 actions.push(Action::CommandHalfPageDown);
506 }
507 _ => {
508 // Ctrl+D might be used for other purposes in other modes
509 }
510 }
511 }
512 'k' => {
513 match self.state.command_panel.mode {
514 crate::model::panel_state::InteractionMode::ScriptEditor => {
515 // Ctrl+K - delete from cursor to line end in script mode
516 let script_actions = crate::components::command_panel::ScriptEditor::delete_to_end(
517 &mut self.state.command_panel,
518 );
519 actions.extend(script_actions);
520 }
521 _ => {
522 // Ctrl+K - delete to end in input/command mode
523 actions.push(Action::DeleteToEnd);
524 }
525 }
526 }
527 'w' => {
528 match self.state.command_panel.mode {
529 crate::model::panel_state::InteractionMode::ScriptEditor => {
530 // Ctrl+W - delete previous word in script mode
531 let script_actions = crate::components::command_panel::ScriptEditor::delete_previous_word(
532 &mut self.state.command_panel,
533 );
534 actions.extend(script_actions);
535 }
536 _ => {
537 // Ctrl+W - delete previous word in input/command mode
538 actions.push(Action::DeletePreviousWord);
539 }
540 }
541 }
542 'p' => {
543 match self.state.command_panel.mode {
544 crate::model::panel_state::InteractionMode::Input => {
545 // Ctrl+P - go to previous command in input mode
546 actions.push(Action::HistoryPrevious);
547 }
548 crate::model::panel_state::InteractionMode::ScriptEditor => {
549 // Ctrl+P - move cursor up (previous line) in script mode
550 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_up(
551 &mut self.state.command_panel,
552 );
553 actions.extend(script_actions);
554 }
555 _ => {
556 // Other modes: use original behavior
557 actions.push(Action::HistoryUp);
558 }
559 }
560 }
561 'n' => {
562 match self.state.command_panel.mode {
563 crate::model::panel_state::InteractionMode::Input => {
564 // Ctrl+N - go to next command in input mode
565 actions.push(Action::HistoryNext);
566 }
567 crate::model::panel_state::InteractionMode::ScriptEditor => {
568 // Ctrl+N - move cursor down (next line) in script mode
569 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_down(
570 &mut self.state.command_panel,
571 );
572 actions.extend(script_actions);
573 }
574 _ => {
575 // Other modes: use original behavior
576 actions.push(Action::HistoryDown);
577 }
578 }
579 }
580 'i' => actions.push(Action::InsertTab),
581 'h' => {
582 match self.state.command_panel.mode {
583 crate::model::panel_state::InteractionMode::ScriptEditor => {
584 // Ctrl+H - delete character (backspace) in script mode
585 let script_actions = crate::components::command_panel::ScriptEditor::delete_char_at_cursor(
586 &mut self.state.command_panel,
587 );
588 actions.extend(script_actions);
589 }
590 _ => {
591 // Ctrl+H - Backspace in input/command mode
592 let handler_actions = self
593 .state
594 .command_input_handler
595 .handle_backspace(&mut self.state.command_panel);
596 actions.extend(handler_actions);
597 self.state.command_renderer.mark_pending_updates();
598 }
599 }
600 }
601 _ => {
602 // Use optimized input handler for regular character input
603 let handler_actions = self
604 .state
605 .command_input_handler
606 .handle_char_input(&mut self.state.command_panel, c);
607 actions.extend(handler_actions);
608 self.state.command_renderer.mark_pending_updates();
609 }
610 }
611 } else {
612 // Handle non-Ctrl character input based on mode
613 match self.state.command_panel.mode {
614 crate::model::panel_state::InteractionMode::Command => {
615 // In command mode, handle vim-style navigation
616 match c {
617 'j' => {
618 // Move cursor down in unified line view
619 actions.push(Action::CommandCursorDown);
620 }
621 'k' => {
622 // Move cursor up in unified line view
623 actions.push(Action::CommandCursorUp);
624 }
625 'h' => {
626 // Move cursor left in current line
627 actions.push(Action::CommandCursorLeft);
628 }
629 'l' => {
630 // Move cursor right in current line
631 actions.push(Action::CommandCursorRight);
632 }
633 'i' => {
634 // Exit command mode and return to previous mode
635 actions.push(Action::ExitCommandMode);
636 }
637 'g' => {
638 // Go to top of history (vim style)
639 self.state.command_panel.command_cursor_line = 0;
640 self.state.command_panel.command_cursor_column = 0;
641 self.state.command_renderer.mark_pending_updates();
642 }
643 'G' => {
644 // Go to the last line of the entire content, including current input
645 // Use wrapped lines to handle text that exceeds panel width
646 let wrapped_lines = self
647 .state
648 .command_panel
649 .get_command_mode_wrapped_lines(
650 self.state.command_panel_width,
651 );
652
653 if !wrapped_lines.is_empty() {
654 let last_line =
655 wrapped_lines.len().saturating_sub(1);
656 self.state.command_panel.command_cursor_line =
657 last_line;
658 // Set column to end of the last line
659 self.state.command_panel.command_cursor_column =
660 wrapped_lines[last_line].chars().count();
661 }
662 self.state.command_renderer.mark_pending_updates();
663 }
664 '$' => {
665 // Go to end of current line
666 if self.state.command_panel.command_cursor_line
667 < self.state.command_panel.command_history.len()
668 {
669 self.state.command_panel.command_cursor_column =
670 self.state.command_panel.command_history[self
671 .state
672 .command_panel
673 .command_cursor_line]
674 .command
675 .chars()
676 .count();
677 }
678 self.state.command_renderer.mark_pending_updates();
679 }
680 '0' => {
681 // Go to beginning of current line
682 self.state.command_panel.command_cursor_column = 0;
683 self.state.command_renderer.mark_pending_updates();
684 }
685 _ => {
686 // For other characters in command mode, do nothing or handle as needed
687 }
688 }
689 }
690 _ => {
691 // For input and script modes, use normal input handler
692 let handler_actions = self
693 .state
694 .command_input_handler
695 .handle_char_input(&mut self.state.command_panel, c);
696 actions.extend(handler_actions);
697 self.state.command_renderer.mark_pending_updates();
698 }
699 }
700 }
701 }
702 KeyCode::Backspace => {
703 let handler_actions = self
704 .state
705 .command_input_handler
706 .handle_backspace(&mut self.state.command_panel);
707 actions.extend(handler_actions);
708 self.state.command_renderer.mark_pending_updates();
709 }
710 KeyCode::Enter => {
711 actions.push(Action::SubmitCommand);
712 }
713 KeyCode::Up
714 | KeyCode::Down
715 | KeyCode::Left
716 | KeyCode::Right
717 | KeyCode::Home
718 | KeyCode::End => {
719 let direction = match key.code {
720 KeyCode::Up => crate::action::CursorDirection::Up,
721 KeyCode::Down => crate::action::CursorDirection::Down,
722 KeyCode::Left => crate::action::CursorDirection::Left,
723 KeyCode::Right => crate::action::CursorDirection::Right,
724 KeyCode::Home => crate::action::CursorDirection::Home,
725 KeyCode::End => crate::action::CursorDirection::End,
726 _ => unreachable!(),
727 };
728 let handler_actions = self
729 .state
730 .command_input_handler
731 .handle_movement(&mut self.state.command_panel, direction);
732 actions.extend(handler_actions);
733 self.state.command_renderer.mark_pending_updates();
734 }
735 KeyCode::Esc => {
736 // Handle Esc based on current mode
737 match self.state.command_panel.mode {
738 crate::model::panel_state::InteractionMode::ScriptEditor => {
739 // Script mode: Esc exits to input mode (traditional behavior)
740 actions.push(Action::ExitScriptMode);
741 }
742 crate::model::panel_state::InteractionMode::Input => {
743 // Input mode: Esc enters command mode
744 actions.push(Action::EnterCommandMode);
745 }
746 crate::model::panel_state::InteractionMode::Command => {
747 // Already in command mode, do nothing
748 }
749 }
750 }
751 _ => {}
752 }
753 }
754 PanelType::Source => {
755 // Handle source panel input based on current mode
756 match self.state.source_panel.mode {
757 crate::model::panel_state::SourcePanelMode::Normal => match key.code {
758 KeyCode::Up => {
759 actions
760 .push(Action::NavigateSource(crate::action::SourceNavigation::Up));
761 }
762 KeyCode::Down => {
763 actions.push(Action::NavigateSource(
764 crate::action::SourceNavigation::Down,
765 ));
766 }
767 KeyCode::Left => {
768 actions.push(Action::NavigateSource(
769 crate::action::SourceNavigation::Left,
770 ));
771 }
772 KeyCode::Right => {
773 actions.push(Action::NavigateSource(
774 crate::action::SourceNavigation::Right,
775 ));
776 }
777 KeyCode::PageUp => {
778 actions.push(Action::NavigateSource(
779 crate::action::SourceNavigation::PageUp,
780 ));
781 }
782 KeyCode::PageDown => {
783 actions.push(Action::NavigateSource(
784 crate::action::SourceNavigation::PageDown,
785 ));
786 }
787 KeyCode::Char('/') => {
788 actions.push(Action::EnterTextSearch);
789 }
790 KeyCode::Char('o') => {
791 actions.push(Action::EnterFileSearch);
792 }
793 KeyCode::Char('g') => {
794 actions.push(Action::SourceGoToLine);
795 }
796 KeyCode::Char('G') => {
797 actions.push(Action::SourceGoToBottom);
798 }
799 KeyCode::Char('h') => {
800 actions.push(Action::NavigateSource(
801 crate::action::SourceNavigation::Left,
802 ));
803 }
804 KeyCode::Char('j') => {
805 actions.push(Action::NavigateSource(
806 crate::action::SourceNavigation::Down,
807 ));
808 }
809 KeyCode::Char('k') => {
810 actions
811 .push(Action::NavigateSource(crate::action::SourceNavigation::Up));
812 }
813 KeyCode::Char('l') => {
814 actions.push(Action::NavigateSource(
815 crate::action::SourceNavigation::Right,
816 ));
817 }
818 KeyCode::Char('n') => {
819 actions.push(Action::NavigateSource(
820 crate::action::SourceNavigation::NextMatch,
821 ));
822 }
823 KeyCode::Char('N') => {
824 actions.push(Action::NavigateSource(
825 crate::action::SourceNavigation::PrevMatch,
826 ));
827 }
828 KeyCode::Char('w') => {
829 actions.push(Action::NavigateSource(
830 crate::action::SourceNavigation::WordForward,
831 ));
832 }
833 KeyCode::Char('b') => {
834 actions.push(Action::NavigateSource(
835 crate::action::SourceNavigation::WordBackward,
836 ));
837 }
838 KeyCode::Char('^') => {
839 actions.push(Action::NavigateSource(
840 crate::action::SourceNavigation::LineStart,
841 ));
842 }
843 KeyCode::Char('$') => {
844 actions.push(Action::NavigateSource(
845 crate::action::SourceNavigation::LineEnd,
846 ));
847 }
848 KeyCode::Char(' ') => {
849 // Space key - set trace at current line
850 actions.push(Action::SetTraceFromSourceLine);
851 }
852 KeyCode::Char(c) => {
853 // Handle Ctrl+key combinations in source panel
854 if key
855 .modifiers
856 .contains(crossterm::event::KeyModifiers::CONTROL)
857 {
858 match c {
859 'd' => {
860 // Ctrl+D - half page down (10 lines)
861 actions.push(Action::NavigateSource(
862 crate::action::SourceNavigation::HalfPageDown,
863 ));
864 }
865 'u' => {
866 // Ctrl+U - half page up (10 lines)
867 actions.push(Action::NavigateSource(
868 crate::action::SourceNavigation::HalfPageUp,
869 ));
870 }
871 _ => {}
872 }
873 } else if c.is_ascii_digit() {
874 actions.push(Action::SourceNumberInput(c));
875 }
876 }
877 KeyCode::Esc => {
878 // Clear all search highlights and navigation state (like vim)
879 let clear_actions =
880 crate::components::source_panel::SourceNavigation::clear_all_state(
881 &mut self.state.source_panel,
882 );
883 actions.extend(clear_actions);
884 }
885 _ => {}
886 },
887 crate::model::panel_state::SourcePanelMode::TextSearch => match key.code {
888 KeyCode::Char(c) => {
889 actions.push(Action::SourceSearchInput(c));
890 }
891 KeyCode::Backspace => {
892 actions.push(Action::SourceSearchBackspace);
893 }
894 KeyCode::Enter => {
895 actions.push(Action::SourceSearchConfirm);
896 }
897 KeyCode::Esc => {
898 actions.push(Action::ExitTextSearch);
899 }
900 _ => {}
901 },
902 crate::model::panel_state::SourcePanelMode::FileSearch => match key.code {
903 KeyCode::Char(c) => {
904 // Handle Ctrl+key combinations in file search
905 if key
906 .modifiers
907 .contains(crossterm::event::KeyModifiers::CONTROL)
908 {
909 match c {
910 'n' => {
911 // Ctrl+N - move down in file search
912 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
913 &mut self.state.source_panel,
914 );
915 actions.extend(move_actions);
916 }
917 'p' => {
918 // Ctrl+P - move up in file search
919 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_up(
920 &mut self.state.source_panel,
921 );
922 actions.extend(move_actions);
923 }
924 'd' => {
925 // Ctrl+D - page down in file search (move down multiple items)
926 for _ in 0..5 {
927 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
928 &mut self.state.source_panel,
929 );
930 actions.extend(move_actions);
931 }
932 }
933 'u' => {
934 // Ctrl+U - clear entire query
935 if let Some(ref cache) =
936 self.state.command_panel.file_completion_cache
937 {
938 let clear_actions = crate::components::source_panel::SourceSearch::clear_file_search_query(
939 &mut self.state.source_panel,
940 cache,
941 );
942 actions.extend(clear_actions);
943 }
944 }
945 'a' => {
946 // Ctrl+A - move cursor to beginning
947 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_to_start(
948 &mut self.state.source_panel,
949 );
950 actions.extend(move_actions);
951 }
952 'e' => {
953 // Ctrl+E - move cursor to end
954 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_to_end(
955 &mut self.state.source_panel,
956 );
957 actions.extend(move_actions);
958 }
959 'w' => {
960 // Ctrl+W - delete previous word
961 if let Some(ref cache) =
962 self.state.command_panel.file_completion_cache
963 {
964 let delete_actions = crate::components::source_panel::SourceSearch::delete_word_file_search(
965 &mut self.state.source_panel,
966 cache,
967 );
968 actions.extend(delete_actions);
969 }
970 }
971 'b' => {
972 // Ctrl+B - move cursor left
973 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_left(
974 &mut self.state.source_panel,
975 );
976 actions.extend(move_actions);
977 }
978 'f' => {
979 // Ctrl+F - move cursor right
980 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_right(
981 &mut self.state.source_panel,
982 );
983 actions.extend(move_actions);
984 }
985 'h' => {
986 // Ctrl+H - delete previous character (same as backspace)
987 actions.push(Action::SourceFileSearchBackspace);
988 }
989 _ => {
990 // Regular character input
991 actions.push(Action::SourceFileSearchInput(c));
992 }
993 }
994 } else {
995 // Regular character input
996 actions.push(Action::SourceFileSearchInput(c));
997 }
998 }
999 KeyCode::Backspace => {
1000 actions.push(Action::SourceFileSearchBackspace);
1001 }
1002 KeyCode::Enter => {
1003 actions.push(Action::SourceFileSearchConfirm);
1004 }
1005 KeyCode::Up => {
1006 // Arrow Up - move up in file search
1007 let move_actions =
1008 crate::components::source_panel::SourceSearch::move_file_search_up(
1009 &mut self.state.source_panel,
1010 );
1011 actions.extend(move_actions);
1012 }
1013 KeyCode::Down => {
1014 // Arrow Down - move down in file search
1015 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
1016 &mut self.state.source_panel,
1017 );
1018 actions.extend(move_actions);
1019 }
1020 KeyCode::Esc => {
1021 actions.push(Action::ExitFileSearch);
1022 }
1023 _ => {}
1024 },
1025 }
1026 }
1027 PanelType::EbpfInfo => {
1028 // Handle eBPF panel input using the dedicated handler
1029 let panel_actions = self
1030 .state
1031 .ebpf_panel_handler
1032 .handle_key_event(&mut self.state.ebpf_panel, key);
1033 actions.extend(panel_actions);
1034 }
1035 }
1036 Ok(actions)
1037 }
1038
1039 fn handle_ctrl_c(&mut self) -> Vec<Action> {
1040 // If eBPF panel is in expanded view, close it on single Ctrl+C
1041 if self.state.ui.focus.current_panel == crate::action::PanelType::EbpfInfo
1042 && self.state.ebpf_panel.is_expanded()
1043 {
1044 self.state.ebpf_panel.close_expanded();
1045 // Do not treat as first press for quitting
1046 self.state.expecting_second_ctrl_c = false;
1047 return vec![];
1048 }
1049 // Check if this is a double Ctrl+C press (consecutive, no timeout)
1050 let is_double_press = self.state.expecting_second_ctrl_c;
1051
1052 // Set flag for next Ctrl+C press
1053 self.state.expecting_second_ctrl_c = true;
1054
1055 // Handle double press - always quit
1056 if is_double_press {
1057 tracing::info!("Double Ctrl+C detected, quitting application");
1058 return vec![Action::Quit];
1059 }
1060
1061 // Single Ctrl+C - handle based on current context
1062 match self.state.ui.focus.current_panel {
1063 crate::action::PanelType::InteractiveCommand => {
1064 // Command panel specific handling
1065 if self.state.command_panel.is_in_history_search() {
1066 // In history search mode - exit search directly
1067 self.state.command_panel.exit_history_search();
1068 self.state.command_panel.input_text.clear();
1069 self.state.command_panel.cursor_position = 0;
1070 // Don't add empty response - would overwrite previous command's response
1071 vec![]
1072 } else {
1073 match self.state.command_panel.mode {
1074 crate::model::panel_state::InteractionMode::ScriptEditor => {
1075 // In script mode - exit to input mode
1076 vec![Action::ExitScriptMode]
1077 }
1078 crate::model::panel_state::InteractionMode::Input => {
1079 // In input mode - clear input and add "quit" command
1080 self.state.command_panel.input_text.clear();
1081 self.state.command_panel.cursor_position = 0;
1082 self.state.command_panel.input_text = "quit".to_string();
1083 self.state.command_panel.cursor_position = 4;
1084 // Clear auto-suggestion to prevent suggestions after "quit"
1085 self.state.command_panel.auto_suggestion.clear();
1086 // Don't add response here - it would attach to previous command in history
1087 // User will see "quit" in input box, which is clear enough
1088 vec![]
1089 }
1090 _ => {
1091 // Other modes - no action needed
1092 vec![]
1093 }
1094 }
1095 }
1096 }
1097 crate::action::PanelType::Source => {
1098 if self.state.source_panel.mode
1099 == crate::model::panel_state::SourcePanelMode::FileSearch
1100 {
1101 // In file search mode - exit file search
1102 vec![Action::ExitFileSearch]
1103 } else {
1104 // Normal source panel - no action needed
1105 vec![]
1106 }
1107 }
1108 _ => {
1109 // Other panels - no action needed
1110 vec![]
1111 }
1112 }
1113 }
1114
1115 /// Cleanup terminal
1116 async fn cleanup(&mut self) -> Result<()> {
1117 disable_raw_mode()?;
1118 // Disable bracketed paste before leaving alternate screen
1119 execute!(self.terminal.backend_mut(), DisableBracketedPaste)?;
1120 execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
1121 // Mouse capture was not enabled, so no need to disable it
1122 self.terminal.show_cursor()?;
1123 Ok(())
1124 }
1125}