1use crate::action::{Action, PanelType};
2use crate::components::loading::{LoadingState, LoadingUI};
3use crate::events::EventRegistry;
4use crate::model::ui_state::LayoutMode;
5use crate::model::AppState;
6use anyhow::Result;
7use crossterm::{
8 event::{
9 DisableBracketedPaste, EnableBracketedPaste, Event, EventStream, KeyCode, KeyEventKind,
10 },
11 execute,
12 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
13};
14use futures_util::StreamExt;
15use ratatui::{
16 backend::CrosstermBackend,
17 layout::{Constraint, Direction, Layout, Rect},
18 widgets::{Block, BorderType, Borders},
19 Frame, Terminal,
20};
21use std::io;
22use tracing::debug;
23
24pub struct App {
26 terminal: Terminal<CrosstermBackend<io::Stdout>>,
27 state: AppState,
28 should_quit: bool,
29}
30
31impl App {
32 pub async fn new(event_registry: EventRegistry, layout_mode: LayoutMode) -> Result<Self> {
34 enable_raw_mode()?;
36 let mut stdout = io::stdout();
37 execute!(stdout, EnterAlternateScreen)?;
38 execute!(stdout, EnableBracketedPaste)?;
40 let backend = CrosstermBackend::new(stdout);
42 let terminal = Terminal::new(backend)?;
43
44 let mut state = AppState::new(event_registry, layout_mode);
45
46 if state.ui.config.show_source_panel {
48 if let Err(e) = state
49 .event_registry
50 .command_sender
51 .send(crate::events::RuntimeCommand::RequestSourceCode)
52 {
53 tracing::warn!("Failed to send initial source code request: {}", e);
54 } else {
55 state.set_loading_state(
57 crate::components::loading::LoadingState::ConnectingToRuntime,
58 );
59 }
60 }
61
62 Ok(Self {
63 terminal,
64 state,
65 should_quit: false,
66 })
67 }
68
69 pub async fn new_with_config(
71 event_registry: EventRegistry,
72 ui_config: crate::model::ui_state::UiConfig,
73 ) -> Result<Self> {
74 enable_raw_mode()?;
76 let mut stdout = io::stdout();
77 execute!(stdout, EnterAlternateScreen)?;
78 execute!(stdout, EnableBracketedPaste)?;
80 let backend = CrosstermBackend::new(stdout);
82 let terminal = Terminal::new(backend)?;
83
84 let mut state = AppState::new_with_config(event_registry, ui_config);
85
86 if state.ui.config.show_source_panel {
88 if let Err(e) = state
89 .event_registry
90 .command_sender
91 .send(crate::events::RuntimeCommand::RequestSourceCode)
92 {
93 tracing::warn!("Failed to send initial source code request: {}", e);
94 } else {
95 state.set_loading_state(
97 crate::components::loading::LoadingState::ConnectingToRuntime,
98 );
99 }
100 }
101
102 Ok(Self {
103 terminal,
104 state,
105 should_quit: false,
106 })
107 }
108
109 pub async fn run(&mut self) -> Result<()> {
111 debug!("Starting new TEA-based TUI application");
112
113 let mut event_stream = EventStream::new();
115 let mut needs_render = true;
116
117 const LOADING_TIMEOUT_SECS: u64 = 30;
119 let loading_timeout =
120 tokio::time::sleep(tokio::time::Duration::from_secs(LOADING_TIMEOUT_SECS));
121 tokio::pin!(loading_timeout);
122
123 let mut loading_ui_ticker = tokio::time::interval(tokio::time::Duration::from_secs(1));
125 loading_ui_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
126
127 self.terminal.draw(|f| Self::draw_ui(f, &mut self.state))?;
129
130 loop {
131 tokio::select! {
133 Some(event_result) = event_stream.next() => {
135 match event_result {
136 Ok(event) => {
137 if let Event::Key(key) = &event {
138 tracing::debug!("Raw crossterm event: {:?}", key);
139 }
140 if let Err(e) = self.handle_event(event).await {
141 tracing::error!("Error handling terminal event: {}", e);
142 }
143 needs_render = true;
144 }
145 Err(e) => {
146 tracing::error!("Error reading terminal events: {}", e);
147 break;
148 }
149 }
150 }
151
152 Some(status) = self.state.event_registry.status_receiver.recv() => {
154 self.handle_runtime_status(status).await;
155 needs_render = true;
156 }
157
158 Some(trace_event) = self.state.event_registry.trace_receiver.recv() => {
160 self.handle_trace_event(trace_event).await;
161 needs_render = true;
162 }
163
164 () = &mut loading_timeout, if !self.state.loading_state.is_ready() && !self.state.loading_state.is_failed() => {
166 tracing::info!("No runtime response after {} seconds, connection timeout", LOADING_TIMEOUT_SECS);
167 self.state.set_loading_state(LoadingState::Failed("Connection timeout - no runtime response".to_string()));
168 needs_render = true;
169 }
170
171 _ = loading_ui_ticker.tick(), if self.state.is_loading() => {
173 needs_render = true;
176 }
177
178 _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {
180 if crate::components::command_panel::input_handler::InputHandler::check_jk_timeout(&mut self.state.command_panel) {
182 needs_render = true;
183 }
184
185 if let crate::model::panel_state::InputState::WaitingResponse { sent_time, command, .. } = &self.state.command_panel.input_state {
187 const COMMAND_TIMEOUT_SECS: u64 = 5;
188 if sent_time.elapsed().as_secs() >= COMMAND_TIMEOUT_SECS {
189 let timeout_msg = format!("Command timeout: '{command}' - no response after {COMMAND_TIMEOUT_SECS} seconds");
190 self.clear_waiting_state();
191 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
192 &mut self.state.command_panel,
193 timeout_msg,
194 crate::components::command_panel::style_builder::StylePresets::ERROR,
195 crate::action::ResponseType::Error,
196 );
197 needs_render = true;
198 }
199 }
200
201 self.state.command_panel.cleanup_file_completion_cache();
203 }
204 }
205
206 if needs_render {
208 self.terminal.draw(|f| Self::draw_ui(f, &mut self.state))?;
209 needs_render = false;
210 }
211
212 if self.should_quit || self.state.should_quit {
214 break;
215 }
216 }
217
218 if let Err(e) = self
220 .state
221 .event_registry
222 .command_sender
223 .send(crate::events::RuntimeCommand::Shutdown)
224 {
225 tracing::warn!("Failed to send shutdown command to runtime: {}", e);
226 }
227
228 self.cleanup().await
229 }
230
231 async fn handle_event(&mut self, event: Event) -> Result<bool> {
233 let mut actions_to_process = Vec::new();
234
235 match event {
236 Event::Key(key) => {
237 tracing::debug!(
238 "Event received: key={:?}, is_loading={}",
239 key,
240 self.state.is_loading()
241 );
242 if key.kind == KeyEventKind::Press {
243 if self.state.ui.focus.expecting_window_nav {
248 match key.code {
249 KeyCode::Char('h') => {
250 actions_to_process.push(Action::WindowNavMove(
251 crate::action::WindowDirection::Left,
252 ));
253 actions_to_process.push(Action::ExitWindowNavMode);
254 }
255 KeyCode::Char('j') => {
256 actions_to_process.push(Action::WindowNavMove(
257 crate::action::WindowDirection::Down,
258 ));
259 actions_to_process.push(Action::ExitWindowNavMode);
260 }
261 KeyCode::Char('k') => {
262 actions_to_process.push(Action::WindowNavMove(
263 crate::action::WindowDirection::Up,
264 ));
265 actions_to_process.push(Action::ExitWindowNavMode);
266 }
267 KeyCode::Char('l') => {
268 actions_to_process.push(Action::WindowNavMove(
269 crate::action::WindowDirection::Right,
270 ));
271 actions_to_process.push(Action::ExitWindowNavMode);
272 }
273 KeyCode::Char('v') => {
274 actions_to_process.push(Action::SwitchLayout);
275 actions_to_process.push(Action::ExitWindowNavMode);
276 }
277 KeyCode::Char('z') => {
278 actions_to_process.push(Action::ToggleFullscreen);
279 actions_to_process.push(Action::ExitWindowNavMode);
280 }
281 _ => {
282 actions_to_process.push(Action::ExitWindowNavMode);
284 }
285 }
286 }
287
288 let is_ctrl_c = matches!(key.code, KeyCode::Char('c'))
292 && key
293 .modifiers
294 .contains(crossterm::event::KeyModifiers::CONTROL);
295 if !is_ctrl_c {
296 self.state.expecting_second_ctrl_c = false;
297 }
298
299 match key.code {
300 KeyCode::Char('c')
301 if key
302 .modifiers
303 .contains(crossterm::event::KeyModifiers::CONTROL) =>
304 {
305 let ctrl_c_actions = self.handle_ctrl_c();
307 actions_to_process.extend(ctrl_c_actions);
308 }
309 KeyCode::Char('w')
310 if key
311 .modifiers
312 .contains(crossterm::event::KeyModifiers::CONTROL) =>
313 {
314 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
316 && self.state.source_panel.mode
317 == crate::model::panel_state::SourcePanelMode::FileSearch
318 {
319 if let Some(ref cache) =
321 self.state.command_panel.file_completion_cache
322 {
323 let delete_actions = crate::components::source_panel::SourceSearch::delete_word_file_search(
324 &mut self.state.source_panel,
325 cache,
326 );
327 actions_to_process.extend(delete_actions);
328 }
329 } else if self.state.ui.focus.current_panel
330 == crate::action::PanelType::InteractiveCommand
331 {
332 match self.state.command_panel.mode {
333 crate::model::panel_state::InteractionMode::Input => {
334 actions_to_process.push(Action::DeletePreviousWord);
335 }
336 crate::model::panel_state::InteractionMode::ScriptEditor => {
337 actions_to_process.push(Action::DeletePreviousWord);
338 }
339 _ => {
340 actions_to_process.push(Action::EnterWindowNavMode);
342 }
343 }
344 } else {
345 actions_to_process.push(Action::EnterWindowNavMode);
347 }
348 }
349 KeyCode::Tab => {
350 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
352 && self.state.source_panel.mode
353 == crate::model::panel_state::SourcePanelMode::FileSearch
354 {
355 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
357 &mut self.state.source_panel,
358 );
359 actions_to_process.extend(move_actions);
360 } else if self.state.ui.focus.current_panel
361 == crate::action::PanelType::InteractiveCommand
362 && self.state.command_panel.mode
363 == crate::model::panel_state::InteractionMode::ScriptEditor
364 {
365 actions_to_process.push(Action::InsertTab);
367 } else if self.state.ui.focus.current_panel
368 == crate::action::PanelType::InteractiveCommand
369 && self.state.command_panel.mode
370 == crate::model::panel_state::InteractionMode::Input
371 {
372 let panel_actions = self.handle_focused_panel_input(key)?;
374 actions_to_process.extend(panel_actions);
375 } else {
376 actions_to_process.push(Action::FocusNext);
378 }
379 }
380 KeyCode::BackTab => {
381 if self.state.ui.focus.current_panel == crate::action::PanelType::Source
383 && self.state.source_panel.mode
384 == crate::model::panel_state::SourcePanelMode::FileSearch
385 {
386 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_up(
388 &mut self.state.source_panel,
389 );
390 actions_to_process.extend(move_actions);
391 } else {
392 actions_to_process.push(Action::FocusPrevious);
394 }
395 }
396 KeyCode::F(1) => {
397 actions_to_process.push(Action::ToggleFullscreen);
398 }
399 KeyCode::F(2) => {
400 actions_to_process.push(Action::SwitchLayout);
401 }
402 _ => {
403 let panel_actions = self.handle_focused_panel_input(key)?;
405 actions_to_process.extend(panel_actions);
406 }
407 }
408 }
409 }
410 Event::Resize(width, height) => {
411 actions_to_process.push(Action::Resize(width, height));
412 }
413 Event::Paste(pasted) => {
414 tracing::debug!("Event received: paste_len={}", pasted.len());
415 match self.state.ui.focus.current_panel {
417 PanelType::InteractiveCommand => {
418 match self.state.command_panel.mode {
419 crate::model::panel_state::InteractionMode::Input => {
420 let actions = self
421 .state
422 .command_input_handler
423 .insert_str(&mut self.state.command_panel, &pasted);
424 actions_to_process.extend(actions);
425 self.state.command_renderer.mark_pending_updates();
426 }
427 crate::model::panel_state::InteractionMode::ScriptEditor => {
428 let actions =
429 crate::components::command_panel::ScriptEditor::insert_text(
430 &mut self.state.command_panel,
431 &pasted,
432 );
433 actions_to_process.extend(actions);
434 self.state.command_renderer.mark_pending_updates();
435 }
436 crate::model::panel_state::InteractionMode::Command => {
437 }
439 }
440 }
441 _ => {
442 }
444 }
445 }
446 _ => {}
447 }
448
449 for action in actions_to_process {
451 let is_quit = matches!(action, Action::Quit);
452 let additional_actions = self.handle_action(action)?;
453
454 for additional_action in additional_actions {
456 self.handle_action(additional_action)?;
457 }
458
459 if is_quit || self.state.should_quit {
460 return Ok(true);
461 }
462 }
463
464 Ok(false)
465 }
466
467 fn handle_focused_panel_input(
469 &mut self,
470 key: crossterm::event::KeyEvent,
471 ) -> Result<Vec<Action>> {
472 let mut actions = Vec::new();
473
474 match self.state.ui.focus.current_panel {
475 PanelType::InteractiveCommand => {
476 let unified_actions = self
478 .state
479 .command_input_handler
480 .handle_key_event(&mut self.state.command_panel, key);
481
482 if !unified_actions.is_empty() {
483 self.state.command_renderer.mark_pending_updates();
485 return Ok(unified_actions);
486 }
487
488 match key.code {
490 KeyCode::Char(c) => {
491 tracing::debug!(
492 "App received char='{}' (code={}), modifiers={:?}, current_panel={:?}",
493 c,
494 c as u32,
495 key.modifiers,
496 self.state.ui.focus.current_panel
497 );
498 if key
500 .modifiers
501 .contains(crossterm::event::KeyModifiers::CONTROL)
502 {
503 match c {
504 's' => {
505 if matches!(
507 self.state.command_panel.mode,
508 crate::model::panel_state::InteractionMode::ScriptEditor
509 ) {
510 actions.push(Action::SubmitScript);
511 }
512 }
513 'a' => {
514 match self.state.command_panel.mode {
515 crate::model::panel_state::InteractionMode::ScriptEditor => {
516 let script_actions = crate::components::command_panel::ScriptEditor::move_to_beginning(
518 &mut self.state.command_panel,
519 );
520 actions.extend(script_actions);
521 }
522 _ => {
523 actions.push(Action::MoveCursor(crate::action::CursorDirection::Home));
525 }
526 }
527 }
528 'e' => {
529 match self.state.command_panel.mode {
530 crate::model::panel_state::InteractionMode::ScriptEditor => {
531 let script_actions = crate::components::command_panel::ScriptEditor::move_to_end(
533 &mut self.state.command_panel,
534 );
535 actions.extend(script_actions);
536 }
537 _ => {
538 actions.push(Action::MoveCursor(crate::action::CursorDirection::End));
540 }
541 }
542 }
543 'f' => {
544 match self.state.command_panel.mode {
545 crate::model::panel_state::InteractionMode::ScriptEditor => {
546 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_right(
548 &mut self.state.command_panel,
549 );
550 actions.extend(script_actions);
551 }
552 _ => {
553 actions.push(Action::MoveCursor(crate::action::CursorDirection::Right));
555 }
556 }
557 }
558 'b' => {
559 match self.state.command_panel.mode {
560 crate::model::panel_state::InteractionMode::ScriptEditor => {
561 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_left(
563 &mut self.state.command_panel,
564 );
565 actions.extend(script_actions);
566 }
567 _ => {
568 actions.push(Action::MoveCursor(crate::action::CursorDirection::Left));
570 }
571 }
572 }
573 'u' => {
574 match self.state.command_panel.mode {
575 crate::model::panel_state::InteractionMode::ScriptEditor => {
576 let script_actions = crate::components::command_panel::ScriptEditor::delete_to_line_start(
578 &mut self.state.command_panel,
579 );
580 actions.extend(script_actions);
581 }
582 crate::model::panel_state::InteractionMode::Command => {
583 actions.push(Action::CommandHalfPageUp);
585 }
586 _ => {
587 actions.push(Action::DeleteToBeginning);
589 }
590 }
591 }
592 'd' => {
593 match self.state.command_panel.mode {
594 crate::model::panel_state::InteractionMode::Command => {
595 actions.push(Action::CommandHalfPageDown);
597 }
598 _ => {
599 }
601 }
602 }
603 'k' => {
604 match self.state.command_panel.mode {
605 crate::model::panel_state::InteractionMode::ScriptEditor => {
606 let script_actions = crate::components::command_panel::ScriptEditor::delete_to_end(
608 &mut self.state.command_panel,
609 );
610 actions.extend(script_actions);
611 }
612 _ => {
613 actions.push(Action::DeleteToEnd);
615 }
616 }
617 }
618 'w' => {
619 match self.state.command_panel.mode {
620 crate::model::panel_state::InteractionMode::ScriptEditor => {
621 let script_actions = crate::components::command_panel::ScriptEditor::delete_previous_word(
623 &mut self.state.command_panel,
624 );
625 actions.extend(script_actions);
626 }
627 _ => {
628 actions.push(Action::DeletePreviousWord);
630 }
631 }
632 }
633 'p' => {
634 match self.state.command_panel.mode {
635 crate::model::panel_state::InteractionMode::Input => {
636 actions.push(Action::HistoryPrevious);
638 }
639 crate::model::panel_state::InteractionMode::ScriptEditor => {
640 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_up(
642 &mut self.state.command_panel,
643 );
644 actions.extend(script_actions);
645 }
646 _ => {
647 actions.push(Action::HistoryUp);
649 }
650 }
651 }
652 'n' => {
653 match self.state.command_panel.mode {
654 crate::model::panel_state::InteractionMode::Input => {
655 actions.push(Action::HistoryNext);
657 }
658 crate::model::panel_state::InteractionMode::ScriptEditor => {
659 let script_actions = crate::components::command_panel::ScriptEditor::move_cursor_down(
661 &mut self.state.command_panel,
662 );
663 actions.extend(script_actions);
664 }
665 _ => {
666 actions.push(Action::HistoryDown);
668 }
669 }
670 }
671 'i' => actions.push(Action::InsertTab),
672 'h' => {
673 match self.state.command_panel.mode {
674 crate::model::panel_state::InteractionMode::ScriptEditor => {
675 let script_actions = crate::components::command_panel::ScriptEditor::delete_char_at_cursor(
677 &mut self.state.command_panel,
678 );
679 actions.extend(script_actions);
680 }
681 _ => {
682 let handler_actions = self
684 .state
685 .command_input_handler
686 .handle_backspace(&mut self.state.command_panel);
687 actions.extend(handler_actions);
688 self.state.command_renderer.mark_pending_updates();
689 }
690 }
691 }
692 _ => {
693 let handler_actions = self
695 .state
696 .command_input_handler
697 .handle_char_input(&mut self.state.command_panel, c);
698 actions.extend(handler_actions);
699 self.state.command_renderer.mark_pending_updates();
700 }
701 }
702 } else {
703 match self.state.command_panel.mode {
705 crate::model::panel_state::InteractionMode::Command => {
706 match c {
708 'j' => {
709 actions.push(Action::CommandCursorDown);
711 }
712 'k' => {
713 actions.push(Action::CommandCursorUp);
715 }
716 'h' => {
717 actions.push(Action::CommandCursorLeft);
719 }
720 'l' => {
721 actions.push(Action::CommandCursorRight);
723 }
724 'i' => {
725 actions.push(Action::ExitCommandMode);
727 }
728 'g' => {
729 self.state.command_panel.command_cursor_line = 0;
731 self.state.command_panel.command_cursor_column = 0;
732 self.state.command_renderer.mark_pending_updates();
733 }
734 'G' => {
735 let wrapped_lines = self
738 .state
739 .command_panel
740 .get_command_mode_wrapped_lines(
741 self.state.command_panel_width,
742 );
743
744 if !wrapped_lines.is_empty() {
745 let last_line =
746 wrapped_lines.len().saturating_sub(1);
747 self.state.command_panel.command_cursor_line =
748 last_line;
749 self.state.command_panel.command_cursor_column =
751 wrapped_lines[last_line].chars().count();
752 }
753 self.state.command_renderer.mark_pending_updates();
754 }
755 '$' => {
756 if self.state.command_panel.command_cursor_line
758 < self.state.command_panel.command_history.len()
759 {
760 self.state.command_panel.command_cursor_column =
761 self.state.command_panel.command_history[self
762 .state
763 .command_panel
764 .command_cursor_line]
765 .command
766 .chars()
767 .count();
768 }
769 self.state.command_renderer.mark_pending_updates();
770 }
771 '0' => {
772 self.state.command_panel.command_cursor_column = 0;
774 self.state.command_renderer.mark_pending_updates();
775 }
776 _ => {
777 }
779 }
780 }
781 _ => {
782 let handler_actions = self
784 .state
785 .command_input_handler
786 .handle_char_input(&mut self.state.command_panel, c);
787 actions.extend(handler_actions);
788 self.state.command_renderer.mark_pending_updates();
789 }
790 }
791 }
792 }
793 KeyCode::Backspace => {
794 let handler_actions = self
795 .state
796 .command_input_handler
797 .handle_backspace(&mut self.state.command_panel);
798 actions.extend(handler_actions);
799 self.state.command_renderer.mark_pending_updates();
800 }
801 KeyCode::Enter => {
802 actions.push(Action::SubmitCommand);
803 }
804 KeyCode::Up
805 | KeyCode::Down
806 | KeyCode::Left
807 | KeyCode::Right
808 | KeyCode::Home
809 | KeyCode::End => {
810 let direction = match key.code {
811 KeyCode::Up => crate::action::CursorDirection::Up,
812 KeyCode::Down => crate::action::CursorDirection::Down,
813 KeyCode::Left => crate::action::CursorDirection::Left,
814 KeyCode::Right => crate::action::CursorDirection::Right,
815 KeyCode::Home => crate::action::CursorDirection::Home,
816 KeyCode::End => crate::action::CursorDirection::End,
817 _ => unreachable!(),
818 };
819 let handler_actions = self
820 .state
821 .command_input_handler
822 .handle_movement(&mut self.state.command_panel, direction);
823 actions.extend(handler_actions);
824 self.state.command_renderer.mark_pending_updates();
825 }
826 KeyCode::Esc => {
827 match self.state.command_panel.mode {
829 crate::model::panel_state::InteractionMode::ScriptEditor => {
830 actions.push(Action::ExitScriptMode);
832 }
833 crate::model::panel_state::InteractionMode::Input => {
834 actions.push(Action::EnterCommandMode);
836 }
837 crate::model::panel_state::InteractionMode::Command => {
838 }
840 }
841 }
842 _ => {}
843 }
844 }
845 PanelType::Source => {
846 match self.state.source_panel.mode {
848 crate::model::panel_state::SourcePanelMode::Normal => match key.code {
849 KeyCode::Up => {
850 actions
851 .push(Action::NavigateSource(crate::action::SourceNavigation::Up));
852 }
853 KeyCode::Down => {
854 actions.push(Action::NavigateSource(
855 crate::action::SourceNavigation::Down,
856 ));
857 }
858 KeyCode::Left => {
859 actions.push(Action::NavigateSource(
860 crate::action::SourceNavigation::Left,
861 ));
862 }
863 KeyCode::Right => {
864 actions.push(Action::NavigateSource(
865 crate::action::SourceNavigation::Right,
866 ));
867 }
868 KeyCode::PageUp => {
869 actions.push(Action::NavigateSource(
870 crate::action::SourceNavigation::PageUp,
871 ));
872 }
873 KeyCode::PageDown => {
874 actions.push(Action::NavigateSource(
875 crate::action::SourceNavigation::PageDown,
876 ));
877 }
878 KeyCode::Char('/') => {
879 actions.push(Action::EnterTextSearch);
880 }
881 KeyCode::Char('o') => {
882 actions.push(Action::EnterFileSearch);
883 }
884 KeyCode::Char('g') => {
885 actions.push(Action::SourceGoToLine);
886 }
887 KeyCode::Char('G') => {
888 actions.push(Action::SourceGoToBottom);
889 }
890 KeyCode::Char('h') => {
891 actions.push(Action::NavigateSource(
892 crate::action::SourceNavigation::Left,
893 ));
894 }
895 KeyCode::Char('j') => {
896 actions.push(Action::NavigateSource(
897 crate::action::SourceNavigation::Down,
898 ));
899 }
900 KeyCode::Char('k') => {
901 actions
902 .push(Action::NavigateSource(crate::action::SourceNavigation::Up));
903 }
904 KeyCode::Char('l') => {
905 actions.push(Action::NavigateSource(
906 crate::action::SourceNavigation::Right,
907 ));
908 }
909 KeyCode::Char('n') => {
910 actions.push(Action::NavigateSource(
911 crate::action::SourceNavigation::NextMatch,
912 ));
913 }
914 KeyCode::Char('N') => {
915 actions.push(Action::NavigateSource(
916 crate::action::SourceNavigation::PrevMatch,
917 ));
918 }
919 KeyCode::Char('w') => {
920 actions.push(Action::NavigateSource(
921 crate::action::SourceNavigation::WordForward,
922 ));
923 }
924 KeyCode::Char('b') => {
925 actions.push(Action::NavigateSource(
926 crate::action::SourceNavigation::WordBackward,
927 ));
928 }
929 KeyCode::Char('^') => {
930 actions.push(Action::NavigateSource(
931 crate::action::SourceNavigation::LineStart,
932 ));
933 }
934 KeyCode::Char('$') => {
935 actions.push(Action::NavigateSource(
936 crate::action::SourceNavigation::LineEnd,
937 ));
938 }
939 KeyCode::Char(' ') => {
940 actions.push(Action::SetTraceFromSourceLine);
942 }
943 KeyCode::Char(c) => {
944 if key
946 .modifiers
947 .contains(crossterm::event::KeyModifiers::CONTROL)
948 {
949 match c {
950 'd' => {
951 actions.push(Action::NavigateSource(
953 crate::action::SourceNavigation::HalfPageDown,
954 ));
955 }
956 'u' => {
957 actions.push(Action::NavigateSource(
959 crate::action::SourceNavigation::HalfPageUp,
960 ));
961 }
962 _ => {}
963 }
964 } else if c.is_ascii_digit() {
965 actions.push(Action::SourceNumberInput(c));
966 }
967 }
968 KeyCode::Esc => {
969 let clear_actions =
971 crate::components::source_panel::SourceNavigation::clear_all_state(
972 &mut self.state.source_panel,
973 );
974 actions.extend(clear_actions);
975 }
976 _ => {}
977 },
978 crate::model::panel_state::SourcePanelMode::TextSearch => match key.code {
979 KeyCode::Char(c) => {
980 actions.push(Action::SourceSearchInput(c));
981 }
982 KeyCode::Backspace => {
983 actions.push(Action::SourceSearchBackspace);
984 }
985 KeyCode::Enter => {
986 actions.push(Action::SourceSearchConfirm);
987 }
988 KeyCode::Esc => {
989 actions.push(Action::ExitTextSearch);
990 }
991 _ => {}
992 },
993 crate::model::panel_state::SourcePanelMode::FileSearch => match key.code {
994 KeyCode::Char(c) => {
995 if key
997 .modifiers
998 .contains(crossterm::event::KeyModifiers::CONTROL)
999 {
1000 match c {
1001 'n' => {
1002 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
1004 &mut self.state.source_panel,
1005 );
1006 actions.extend(move_actions);
1007 }
1008 'p' => {
1009 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_up(
1011 &mut self.state.source_panel,
1012 );
1013 actions.extend(move_actions);
1014 }
1015 'd' => {
1016 for _ in 0..5 {
1018 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
1019 &mut self.state.source_panel,
1020 );
1021 actions.extend(move_actions);
1022 }
1023 }
1024 'u' => {
1025 if let Some(ref cache) =
1027 self.state.command_panel.file_completion_cache
1028 {
1029 let clear_actions = crate::components::source_panel::SourceSearch::clear_file_search_query(
1030 &mut self.state.source_panel,
1031 cache,
1032 );
1033 actions.extend(clear_actions);
1034 }
1035 }
1036 'a' => {
1037 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_to_start(
1039 &mut self.state.source_panel,
1040 );
1041 actions.extend(move_actions);
1042 }
1043 'e' => {
1044 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_to_end(
1046 &mut self.state.source_panel,
1047 );
1048 actions.extend(move_actions);
1049 }
1050 'w' => {
1051 if let Some(ref cache) =
1053 self.state.command_panel.file_completion_cache
1054 {
1055 let delete_actions = crate::components::source_panel::SourceSearch::delete_word_file_search(
1056 &mut self.state.source_panel,
1057 cache,
1058 );
1059 actions.extend(delete_actions);
1060 }
1061 }
1062 'b' => {
1063 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_left(
1065 &mut self.state.source_panel,
1066 );
1067 actions.extend(move_actions);
1068 }
1069 'f' => {
1070 let move_actions = crate::components::source_panel::SourceSearch::move_cursor_right(
1072 &mut self.state.source_panel,
1073 );
1074 actions.extend(move_actions);
1075 }
1076 'h' => {
1077 actions.push(Action::SourceFileSearchBackspace);
1079 }
1080 _ => {
1081 actions.push(Action::SourceFileSearchInput(c));
1083 }
1084 }
1085 } else {
1086 actions.push(Action::SourceFileSearchInput(c));
1088 }
1089 }
1090 KeyCode::Backspace => {
1091 actions.push(Action::SourceFileSearchBackspace);
1092 }
1093 KeyCode::Enter => {
1094 actions.push(Action::SourceFileSearchConfirm);
1095 }
1096 KeyCode::Up => {
1097 let move_actions =
1099 crate::components::source_panel::SourceSearch::move_file_search_up(
1100 &mut self.state.source_panel,
1101 );
1102 actions.extend(move_actions);
1103 }
1104 KeyCode::Down => {
1105 let move_actions = crate::components::source_panel::SourceSearch::move_file_search_down(
1107 &mut self.state.source_panel,
1108 );
1109 actions.extend(move_actions);
1110 }
1111 KeyCode::Esc => {
1112 actions.push(Action::ExitFileSearch);
1113 }
1114 _ => {}
1115 },
1116 }
1117 }
1118 PanelType::EbpfInfo => {
1119 let panel_actions = self
1121 .state
1122 .ebpf_panel_handler
1123 .handle_key_event(&mut self.state.ebpf_panel, key);
1124 actions.extend(panel_actions);
1125 }
1126 }
1127 Ok(actions)
1128 }
1129
1130 fn handle_action(&mut self, action: Action) -> Result<Vec<Action>> {
1132 debug!("Handling action: {:?}", action);
1133 let mut additional_actions = Vec::new();
1134
1135 match action {
1136 Action::Quit => {
1137 self.state.should_quit = true;
1138 }
1139 Action::Resize(width, height) => {
1140 tracing::debug!("Terminal resized to {}x{}", width, height);
1142 }
1145 Action::FocusNext => {
1146 let src_enabled = self.state.ui.config.show_source_panel;
1147 self.state.ui.focus.cycle_next(src_enabled);
1148 }
1149 Action::FocusPrevious => {
1150 let src_enabled = self.state.ui.config.show_source_panel;
1151 self.state.ui.focus.cycle_previous(src_enabled);
1152 }
1153 Action::FocusPanel(panel) => {
1154 if panel == crate::action::PanelType::Source
1155 && !self.state.ui.config.show_source_panel
1156 {
1157 self.state
1159 .ui
1160 .focus
1161 .set_panel(crate::action::PanelType::InteractiveCommand);
1162 } else {
1163 self.state.ui.focus.set_panel(panel);
1164 }
1165 }
1166 Action::ToggleFullscreen => {
1167 self.state.ui.layout.toggle_fullscreen();
1168 }
1169 Action::SwitchLayout => {
1170 self.state.ui.layout.switch_mode();
1171 }
1172 Action::EnterWindowNavMode => {
1173 self.state.ui.focus.expecting_window_nav = true;
1174 }
1175 Action::ExitWindowNavMode => {
1176 self.state.ui.focus.expecting_window_nav = false;
1177 }
1178 Action::WindowNavMove(direction) => {
1179 let src_enabled = self.state.ui.config.show_source_panel;
1180 self.state.ui.focus.move_focus_in_direction(
1181 direction,
1182 self.state.ui.layout.mode,
1183 src_enabled,
1184 );
1185 }
1186 Action::SetSourcePanelVisibility(show) => {
1187 let currently_shown = self.state.ui.config.show_source_panel;
1188 if show == currently_shown {
1189 return Ok(Vec::new());
1190 }
1191 self.state.ui.config.show_source_panel = show;
1192 if show {
1193 if let Err(e) = self
1195 .state
1196 .event_registry
1197 .command_sender
1198 .send(crate::events::RuntimeCommand::RequestSourceCode)
1199 {
1200 tracing::warn!("Failed to send source request after enabling: {}", e);
1201 }
1202 let plain =
1204 "ā
Source panel enabled. Use 'ui source off' to hide it.".to_string();
1205 let styled = vec![
1206 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1207 .styled(
1208 plain.clone(),
1209 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1210 )
1211 .build(),
1212 ];
1213 additional_actions.push(Action::AddResponseWithStyle {
1214 content: plain,
1215 styled_lines: Some(styled),
1216 response_type: crate::action::ResponseType::Success,
1217 });
1218 } else {
1219 if self.state.ui.focus.current_panel == crate::action::PanelType::Source {
1221 self.state
1222 .ui
1223 .focus
1224 .set_panel(crate::action::PanelType::InteractiveCommand);
1225 }
1226 if self.state.ui.layout.is_fullscreen
1227 && matches!(
1228 self.state.ui.focus.current_panel,
1229 crate::action::PanelType::Source
1230 )
1231 {
1232 self.state.ui.layout.is_fullscreen = false;
1233 }
1234 let plain = "ā
Source panel disabled. Panels: eBPF output + command. Use 'ui source on' to enable.".to_string();
1236 let styled = vec![
1237 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1238 .styled(
1239 plain.clone(),
1240 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1241 )
1242 .build(),
1243 ];
1244 additional_actions.push(Action::AddResponseWithStyle {
1245 content: plain,
1246 styled_lines: Some(styled),
1247 response_type: crate::action::ResponseType::Success,
1248 });
1249 }
1250 }
1251 Action::InsertChar(c) => {
1252 let actions = crate::components::command_panel::InputHandler::insert_char(
1253 &mut self.state.command_panel,
1254 c,
1255 );
1256 additional_actions.extend(actions);
1257 }
1258 Action::DeleteChar => {
1259 let actions = crate::components::command_panel::InputHandler::delete_char(
1260 &mut self.state.command_panel,
1261 );
1262 additional_actions.extend(actions);
1263 }
1264 Action::MoveCursor(direction) => {
1265 let actions = crate::components::command_panel::InputHandler::move_cursor(
1266 &mut self.state.command_panel,
1267 direction,
1268 );
1269 additional_actions.extend(actions);
1270 }
1271 Action::SubmitCommand => {
1272 let actions = self
1273 .state
1274 .command_input_handler
1275 .handle_submit(&mut self.state.command_panel);
1276 additional_actions.extend(actions);
1277 self.state.command_renderer.mark_pending_updates();
1278
1279 if self.state.realtime_session_logger.enabled {
1281 if let Some(command) = self
1282 .state
1283 .command_panel
1284 .command_history
1285 .last()
1286 .map(|item| item.command.clone())
1287 {
1288 if let Err(e) = self.write_command_to_session_log(&command) {
1289 tracing::error!("Failed to write command to session log: {}", e);
1290 }
1291 }
1292 }
1293 }
1294 Action::SubmitCommandWithText { command } => {
1295 self.state.command_panel.add_command_to_history(&command);
1298
1299 self.state.command_panel.input_text = command.clone();
1301 let actions = self
1302 .state
1303 .command_input_handler
1304 .handle_submit(&mut self.state.command_panel);
1305 additional_actions.extend(actions);
1306 self.state.command_renderer.mark_pending_updates();
1307
1308 if self.state.realtime_session_logger.enabled {
1310 if let Err(e) = self.write_command_to_session_log(&command) {
1311 tracing::error!("Failed to write command to session log: {}", e);
1312 }
1313 }
1314 }
1315 Action::HistoryUp => {
1316 }
1318 Action::HistoryDown => {
1319 }
1321 Action::HistoryPrevious => {
1322 self.state.command_panel.history_previous();
1323 self.state.command_renderer.mark_pending_updates();
1324 }
1325 Action::HistoryNext => {
1326 self.state.command_panel.history_next();
1327 self.state.command_renderer.mark_pending_updates();
1328 }
1329 Action::EnterCommandMode => {
1330 self.state
1331 .command_panel
1332 .enter_command_mode(self.state.command_panel_width);
1333 }
1334 Action::ExitCommandMode => {
1335 self.state.command_panel.exit_command_mode();
1336 }
1337 Action::EnterInputMode => {
1338 self.state.command_panel.mode = crate::model::panel_state::InteractionMode::Input;
1339 }
1340 Action::CommandCursorUp => {
1341 self.state.command_panel.move_command_cursor_up();
1342 self.state.command_renderer.mark_pending_updates();
1343 }
1344 Action::CommandCursorDown => {
1345 self.state.command_panel.move_command_cursor_down();
1346 self.state.command_renderer.mark_pending_updates();
1347 }
1348 Action::CommandCursorLeft => {
1349 self.state.command_panel.move_command_cursor_left();
1350 self.state.command_renderer.mark_pending_updates();
1351 }
1352 Action::CommandCursorRight => {
1353 self.state.command_panel.move_command_cursor_right();
1354 self.state.command_renderer.mark_pending_updates();
1355 }
1356 Action::CommandHalfPageUp => {
1357 self.state.command_panel.move_command_half_page_up();
1358 self.state.command_renderer.mark_pending_updates();
1359 }
1360 Action::CommandHalfPageDown => {
1361 self.state.command_panel.move_command_half_page_down();
1362 self.state.command_renderer.mark_pending_updates();
1363 }
1364 Action::EnterScriptMode(command) => {
1365 let actions = crate::components::command_panel::ScriptEditor::enter_script_mode(
1366 &mut self.state.command_panel,
1367 &command,
1368 );
1369 additional_actions.extend(actions);
1370 self.state.command_renderer.mark_pending_updates();
1371 }
1372 Action::ExitScriptMode => {
1373 let actions = crate::components::command_panel::ScriptEditor::exit_script_mode(
1374 &mut self.state.command_panel,
1375 );
1376 additional_actions.extend(actions);
1377 self.state.command_renderer.mark_pending_updates();
1378 }
1379 Action::SubmitScript => {
1380 let actions = crate::components::command_panel::ScriptEditor::submit_script(
1381 &mut self.state.command_panel,
1382 );
1383 additional_actions.extend(actions);
1384 self.state.command_renderer.mark_pending_updates();
1385 }
1386 Action::CancelScript => {
1387 let actions = crate::components::command_panel::ScriptEditor::exit_script_mode(
1388 &mut self.state.command_panel,
1389 );
1390 additional_actions.extend(actions);
1391 self.state.command_renderer.mark_pending_updates();
1392 }
1393 Action::AddResponseWithStyle {
1394 content,
1395 styled_lines,
1396 response_type,
1397 } => {
1398 if self.state.realtime_session_logger.enabled {
1400 if let Err(e) = self.write_response_to_session_log(&content) {
1401 tracing::error!("Failed to write response to session log: {}", e);
1402 }
1403 }
1404 crate::components::command_panel::ResponseFormatter::add_response_with_style(
1405 &mut self.state.command_panel,
1406 content,
1407 styled_lines,
1408 response_type,
1409 );
1410 self.state.command_renderer.mark_pending_updates();
1411 }
1412 Action::AddStyledWelcomeMessage {
1414 styled_lines,
1415 response_type,
1416 } => {
1417 self.state
1419 .command_panel
1420 .add_styled_welcome_lines(styled_lines, response_type);
1421 self.state.command_renderer.mark_pending_updates();
1422 }
1423 Action::SendRuntimeCommand(cmd) => {
1424 debug!("Sending runtime command: {:?}", cmd);
1426 if let Err(e) = self.state.event_registry.command_sender.send(cmd) {
1427 tracing::error!("Failed to send runtime command: {}", e);
1428 let plain = format!("ā Failed to send command to runtime: {e}");
1430 let styled = vec![
1431 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1432 .styled(plain.clone(), crate::components::command_panel::style_builder::StylePresets::ERROR)
1433 .build(),
1434 ];
1435 let error_action = Action::AddResponseWithStyle {
1436 content: plain,
1437 styled_lines: Some(styled),
1438 response_type: crate::action::ResponseType::Error,
1439 };
1440 additional_actions.push(error_action);
1441 }
1442 }
1443 Action::HandleRuntimeStatus(status) => {
1444 debug!("Would handle runtime status: {:?}", status);
1446 }
1447 Action::DeletePreviousWord => match self.state.command_panel.mode {
1448 crate::model::panel_state::InteractionMode::ScriptEditor => {
1449 let actions =
1450 crate::components::command_panel::ScriptEditor::delete_previous_word(
1451 &mut self.state.command_panel,
1452 );
1453 additional_actions.extend(actions);
1454 }
1455 _ => {
1456 let actions =
1457 crate::components::command_panel::InputHandler::delete_previous_word(
1458 &mut self.state.command_panel,
1459 );
1460 additional_actions.extend(actions);
1461 }
1462 },
1463 Action::DeleteToEnd => {
1464 let actions = crate::components::command_panel::InputHandler::delete_to_end(
1465 &mut self.state.command_panel,
1466 );
1467 additional_actions.extend(actions);
1468 }
1469 Action::DeleteToBeginning => {
1470 let actions = crate::components::command_panel::InputHandler::delete_to_beginning(
1471 &mut self.state.command_panel,
1472 );
1473 additional_actions.extend(actions);
1474 }
1475 Action::InsertTab => {
1476 if self.state.command_panel.mode
1477 == crate::model::panel_state::InteractionMode::ScriptEditor
1478 {
1479 let actions = crate::components::command_panel::ScriptEditor::insert_tab(
1480 &mut self.state.command_panel,
1481 );
1482 additional_actions.extend(actions);
1483 }
1484 }
1485 Action::InsertNewline => {
1486 if self.state.command_panel.mode
1487 == crate::model::panel_state::InteractionMode::ScriptEditor
1488 {
1489 let actions = crate::components::command_panel::ScriptEditor::insert_newline(
1490 &mut self.state.command_panel,
1491 );
1492 additional_actions.extend(actions);
1493 }
1494 }
1495 Action::NavigateSource(direction) => {
1497 let actions = match direction {
1498 crate::action::SourceNavigation::Up => {
1499 crate::components::source_panel::SourceNavigation::move_up(
1500 &mut self.state.source_panel,
1501 )
1502 }
1503 crate::action::SourceNavigation::Down => {
1504 crate::components::source_panel::SourceNavigation::move_down(
1505 &mut self.state.source_panel,
1506 )
1507 }
1508 crate::action::SourceNavigation::Left => {
1509 crate::components::source_panel::SourceNavigation::move_left(
1510 &mut self.state.source_panel,
1511 )
1512 }
1513 crate::action::SourceNavigation::Right => {
1514 crate::components::source_panel::SourceNavigation::move_right(
1515 &mut self.state.source_panel,
1516 )
1517 }
1518 crate::action::SourceNavigation::PageUp => {
1519 crate::components::source_panel::SourceNavigation::move_up_fast(
1520 &mut self.state.source_panel,
1521 )
1522 }
1523 crate::action::SourceNavigation::PageDown => {
1524 crate::components::source_panel::SourceNavigation::move_down_fast(
1525 &mut self.state.source_panel,
1526 )
1527 }
1528 crate::action::SourceNavigation::HalfPageUp => {
1529 crate::components::source_panel::SourceNavigation::move_half_page_up(
1530 &mut self.state.source_panel,
1531 )
1532 }
1533 crate::action::SourceNavigation::HalfPageDown => {
1534 crate::components::source_panel::SourceNavigation::move_half_page_down(
1535 &mut self.state.source_panel,
1536 )
1537 }
1538 crate::action::SourceNavigation::GoToLine(line) => {
1539 crate::components::source_panel::SourceNavigation::go_to_line(
1540 &mut self.state.source_panel,
1541 line,
1542 )
1543 }
1544 crate::action::SourceNavigation::NextMatch => {
1545 crate::components::source_panel::SourceSearch::next_match(
1546 &mut self.state.source_panel,
1547 )
1548 }
1549 crate::action::SourceNavigation::PrevMatch => {
1550 crate::components::source_panel::SourceSearch::prev_match(
1551 &mut self.state.source_panel,
1552 )
1553 }
1554 crate::action::SourceNavigation::WordForward => {
1555 crate::components::source_panel::SourceNavigation::move_word_forward(
1556 &mut self.state.source_panel,
1557 )
1558 }
1559 crate::action::SourceNavigation::WordBackward => {
1560 crate::components::source_panel::SourceNavigation::move_word_backward(
1561 &mut self.state.source_panel,
1562 )
1563 }
1564 crate::action::SourceNavigation::LineStart => {
1565 crate::components::source_panel::SourceNavigation::move_to_line_start(
1566 &mut self.state.source_panel,
1567 )
1568 }
1569 crate::action::SourceNavigation::LineEnd => {
1570 crate::components::source_panel::SourceNavigation::move_to_line_end(
1571 &mut self.state.source_panel,
1572 )
1573 }
1574 };
1575 additional_actions.extend(actions);
1576 }
1577 Action::LoadSource { path, line } => {
1578 let actions = crate::components::source_panel::SourceNavigation::load_source(
1579 &mut self.state.source_panel,
1580 path,
1581 line,
1582 );
1583 additional_actions.extend(actions);
1584 }
1585 Action::EnterTextSearch => {
1586 let actions = crate::components::source_panel::SourceSearch::enter_search_mode(
1587 &mut self.state.source_panel,
1588 );
1589 additional_actions.extend(actions);
1590 }
1591 Action::ExitTextSearch => {
1592 let actions = crate::components::source_panel::SourceSearch::exit_search_mode(
1593 &mut self.state.source_panel,
1594 );
1595 additional_actions.extend(actions);
1596 }
1597 Action::EnterFileSearch => {
1598 let actions = crate::components::source_panel::SourceSearch::enter_file_search_mode(
1599 &mut self.state.source_panel,
1600 );
1601 additional_actions.extend(actions);
1602
1603 if let Some(ref mut cache) = self.state.command_panel.file_completion_cache {
1605 if !cache.is_empty() {
1606 tracing::debug!("Using cached file list for source panel search");
1608 let files = cache.get_all_files().to_vec();
1609 let actions =
1610 crate::components::source_panel::SourceSearch::set_file_search_files(
1611 &mut self.state.source_panel,
1612 cache,
1613 files,
1614 );
1615 additional_actions.extend(actions);
1616 }
1617 } else {
1618 tracing::debug!("No cached files available, requesting from runtime");
1620 self.state.route_file_info_to_file_search = true;
1621 if let Err(e) = self
1622 .state
1623 .event_registry
1624 .command_sender
1625 .send(crate::events::RuntimeCommand::InfoSource)
1626 {
1627 tracing::error!("Failed to send InfoSource command: {}", e);
1628 self.state.route_file_info_to_file_search = false;
1630 let error_actions =
1631 crate::components::source_panel::SourceSearch::set_file_search_error(
1632 &mut self.state.source_panel,
1633 "Failed to request file list".to_string(),
1634 );
1635 additional_actions.extend(error_actions);
1636 }
1637 }
1638 }
1639 Action::ExitFileSearch => {
1640 let actions = crate::components::source_panel::SourceSearch::exit_file_search_mode(
1641 &mut self.state.source_panel,
1642 );
1643 additional_actions.extend(actions);
1644 }
1645 Action::SourceSearchInput(ch) => {
1646 let actions = crate::components::source_panel::SourceSearch::push_search_char(
1647 &mut self.state.source_panel,
1648 ch,
1649 );
1650 additional_actions.extend(actions);
1651 }
1652 Action::SourceSearchBackspace => {
1653 let actions = crate::components::source_panel::SourceSearch::backspace_search(
1654 &mut self.state.source_panel,
1655 );
1656 additional_actions.extend(actions);
1657 }
1658 Action::SourceSearchConfirm => {
1659 let actions = crate::components::source_panel::SourceSearch::confirm_search(
1660 &mut self.state.source_panel,
1661 );
1662 additional_actions.extend(actions);
1663 }
1664 Action::SourceFileSearchInput(ch) => {
1665 if let Some(ref cache) = self.state.command_panel.file_completion_cache {
1666 let actions =
1667 crate::components::source_panel::SourceSearch::push_file_search_char(
1668 &mut self.state.source_panel,
1669 cache,
1670 ch,
1671 );
1672 additional_actions.extend(actions);
1673 }
1674 }
1675 Action::SourceFileSearchBackspace => {
1676 if let Some(ref cache) = self.state.command_panel.file_completion_cache {
1677 let actions =
1678 crate::components::source_panel::SourceSearch::backspace_file_search(
1679 &mut self.state.source_panel,
1680 cache,
1681 );
1682 additional_actions.extend(actions);
1683 }
1684 }
1685 Action::SourceFileSearchConfirm => {
1686 if let Some(ref cache) = self.state.command_panel.file_completion_cache {
1687 if let Some(selected_file) =
1688 crate::components::source_panel::SourceSearch::confirm_file_search(
1689 &mut self.state.source_panel,
1690 cache,
1691 )
1692 {
1693 additional_actions.push(Action::LoadSource {
1695 path: selected_file,
1696 line: None,
1697 });
1698 }
1699 }
1700 }
1701 Action::SourceNumberInput(ch) => {
1702 let actions =
1703 crate::components::source_panel::SourceNavigation::handle_number_input(
1704 &mut self.state.source_panel,
1705 ch,
1706 );
1707 additional_actions.extend(actions);
1708 }
1709 Action::SourceGoToLine => {
1710 let actions = crate::components::source_panel::SourceNavigation::handle_g_key(
1711 &mut self.state.source_panel,
1712 );
1713 additional_actions.extend(actions);
1714 }
1715 Action::SourceGoToBottom => {
1716 let actions = crate::components::source_panel::SourceNavigation::handle_shift_g_key(
1717 &mut self.state.source_panel,
1718 );
1719 additional_actions.extend(actions);
1720 }
1721 Action::SetTraceFromSourceLine => {
1722 if let Some(file_path) = &self.state.source_panel.file_path {
1724 let line_num = self.state.source_panel.cursor_line + 1; let trace_command = format!("trace {file_path}:{line_num}");
1730
1731 self.state.ui.focus.current_panel = PanelType::InteractiveCommand;
1733
1734 self.state.command_panel.add_command_entry(&trace_command);
1736
1737 self.state.command_panel.input_text.clear();
1739 self.state.command_panel.cursor_position = 0;
1740
1741 additional_actions.push(Action::EnterScriptMode(trace_command));
1743 }
1744 }
1745 Action::SaveEbpfOutput { filename } => {
1746 let (content, response_type, style_preset) =
1748 match self.start_realtime_output_logging(filename) {
1749 Ok(file_path) => (
1750 format!(
1751 "ā
Realtime eBPF output logging started: {}",
1752 file_path.display()
1753 ),
1754 crate::action::ResponseType::Success,
1755 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1756 ),
1757 Err(e) => (
1758 format!("ā Failed to start output logging: {e}"),
1759 crate::action::ResponseType::Error,
1760 crate::components::command_panel::style_builder::StylePresets::ERROR,
1761 ),
1762 };
1763
1764 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
1766 &mut self.state.command_panel,
1767 content,
1768 style_preset,
1769 response_type,
1770 );
1771 self.state.command_renderer.mark_pending_updates();
1772 }
1773 Action::SaveCommandSession { filename } => {
1774 let (content, response_type, style_preset) =
1776 match self.start_realtime_session_logging(filename) {
1777 Ok(file_path) => (
1778 format!(
1779 "ā
Realtime session logging started: {}",
1780 file_path.display()
1781 ),
1782 crate::action::ResponseType::Success,
1783 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1784 ),
1785 Err(e) => (
1786 format!("ā Failed to start session logging: {e}"),
1787 crate::action::ResponseType::Error,
1788 crate::components::command_panel::style_builder::StylePresets::ERROR,
1789 ),
1790 };
1791
1792 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
1794 &mut self.state.command_panel,
1795 content,
1796 style_preset,
1797 response_type,
1798 );
1799 self.state.command_renderer.mark_pending_updates();
1800 }
1801 Action::StopSaveOutput => {
1802 let (content, response_type, style_preset) =
1804 match self.state.realtime_output_logger.stop() {
1805 Ok(()) => (
1806 "ā
Realtime eBPF output logging stopped".to_string(),
1807 crate::action::ResponseType::Success,
1808 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1809 ),
1810 Err(e) => (
1811 format!("ā Failed to stop output logging: {e}"),
1812 crate::action::ResponseType::Error,
1813 crate::components::command_panel::style_builder::StylePresets::ERROR,
1814 ),
1815 };
1816
1817 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
1819 &mut self.state.command_panel,
1820 content,
1821 style_preset,
1822 response_type,
1823 );
1824 self.state.command_renderer.mark_pending_updates();
1825 }
1826 Action::StopSaveSession => {
1827 let (content, response_type, style_preset) =
1829 match self.state.realtime_session_logger.stop() {
1830 Ok(()) => (
1831 "ā
Realtime session logging stopped".to_string(),
1832 crate::action::ResponseType::Success,
1833 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
1834 ),
1835 Err(e) => (
1836 format!("ā Failed to stop session logging: {e}"),
1837 crate::action::ResponseType::Error,
1838 crate::components::command_panel::style_builder::StylePresets::ERROR,
1839 ),
1840 };
1841
1842 crate::components::command_panel::ResponseFormatter::add_simple_styled_response(
1844 &mut self.state.command_panel,
1845 content,
1846 style_preset,
1847 response_type,
1848 );
1849 self.state.command_renderer.mark_pending_updates();
1850 }
1851 Action::NoOp => {
1852 }
1854 _ => {
1855 debug!("Action not yet implemented: {:?}", action);
1856 }
1857 }
1858
1859 Ok(additional_actions)
1860 }
1861
1862 pub fn add_module_to_loading(&mut self, module_path: String) {
1864 self.state.loading_ui.progress.add_module(module_path);
1865 }
1866
1867 pub fn start_module_loading(&mut self, module_path: &str) {
1869 self.state
1870 .loading_ui
1871 .progress
1872 .start_module_loading(module_path);
1873 }
1874
1875 pub fn complete_module_loading(
1877 &mut self,
1878 module_path: &str,
1879 functions: usize,
1880 variables: usize,
1881 types: usize,
1882 ) {
1883 use crate::components::loading::ModuleStats;
1884 let stats = ModuleStats {
1885 functions,
1886 variables,
1887 types,
1888 };
1889 self.state
1890 .loading_ui
1891 .progress
1892 .complete_module(module_path, stats);
1893 }
1894
1895 pub fn fail_module_loading(&mut self, module_path: &str, error: String) {
1897 self.state
1898 .loading_ui
1899 .progress
1900 .fail_module(module_path, error);
1901 }
1902
1903 pub fn set_target_pid(&mut self, pid: u32) {
1905 self.state.target_pid = Some(pid);
1906 }
1907
1908 pub fn transition_to_ready_with_completion(&mut self) {
1910 self.add_loading_completion_summary();
1911 self.state.set_loading_state(LoadingState::Ready);
1912 }
1913
1914 fn sync_files_to_command_panel(&mut self, files: Vec<String>) {
1916 tracing::debug!(
1917 "Syncing {} files to command panel completion cache",
1918 files.len()
1919 );
1920 if !files.is_empty() {
1921 tracing::debug!(
1922 "First 5 files: {:?}",
1923 files.iter().take(5).collect::<Vec<_>>()
1924 );
1925 }
1926
1927 if let Some(cache) = &mut self.state.command_panel.file_completion_cache {
1929 let updated = cache.sync_from_source_panel(&files);
1931 tracing::debug!("Updated existing file completion cache: {}", updated);
1932 } else {
1933 if !files.is_empty() {
1935 tracing::debug!(
1936 "Creating new file completion cache with {} files",
1937 files.len()
1938 );
1939 self.state.command_panel.file_completion_cache = Some(
1940 crate::components::command_panel::file_completion::FileCompletionCache::new(
1941 &files,
1942 ),
1943 );
1944 tracing::debug!("File completion cache created successfully");
1945 } else {
1946 tracing::debug!("No files to create cache with");
1947 }
1948 }
1949 }
1950
1951 pub fn add_loading_completion_summary(&mut self) {
1953 let total_time = self.state.loading_ui.progress.elapsed_time();
1954
1955 let mut styled_lines = self.state.loading_ui.create_welcome_message(total_time);
1957
1958 if let Some(pid) = self.state.target_pid {
1960 use ratatui::style::{Color, Style};
1961 use ratatui::text::{Line, Span};
1962
1963 let mut enhanced_lines = Vec::new();
1965 let mut found_dwarf_stats = false;
1966 for line in styled_lines {
1967 enhanced_lines.push(line.clone());
1968 let line_text: String = line
1970 .spans
1971 .iter()
1972 .map(|span| span.content.as_ref())
1973 .collect();
1974 if !found_dwarf_stats && line_text.starts_with("ā¢") && line_text.contains("indexed")
1975 {
1976 found_dwarf_stats = true;
1977 enhanced_lines.push(Line::from("")); enhanced_lines.push(Line::from(Span::styled(
1979 format!("Attached to process {pid}"),
1980 Style::default().fg(Color::White),
1981 )));
1982 }
1984 }
1985 styled_lines = enhanced_lines;
1986 }
1987
1988 let action = Action::AddStyledWelcomeMessage {
1992 styled_lines,
1993 response_type: crate::action::ResponseType::Info,
1994 };
1995 if let Err(e) = self.handle_action(action) {
1996 tracing::error!("Failed to add completion summary: {}", e);
1997 }
1998 }
1999
2000 fn draw_ui(f: &mut Frame, state: &mut AppState) {
2002 let size = f.area();
2003
2004 if state.is_loading() {
2006 if matches!(state.loading_state, LoadingState::LoadingSymbols { .. }) {
2008 LoadingUI::render_dwarf_loading(
2009 f,
2010 &mut state.loading_ui,
2011 &state.loading_state,
2012 state.target_pid,
2013 );
2014 } else {
2015 LoadingUI::render_simple(
2017 f,
2018 &mut state.loading_ui,
2019 state.loading_state.message(),
2020 state.loading_state.progress(),
2021 );
2022 }
2023 return;
2024 }
2025
2026 if state.ui.layout.is_fullscreen {
2027 match state.ui.focus.current_panel {
2029 PanelType::Source => {
2030 if state.ui.config.show_source_panel {
2031 Self::draw_source_panel(f, size, state);
2032 } else {
2033 Self::draw_command_panel(f, size, state);
2035 }
2036 }
2037 PanelType::EbpfInfo => {
2038 Self::draw_ebpf_panel(f, size, state);
2039 }
2040 PanelType::InteractiveCommand => {
2041 Self::draw_command_panel(f, size, state);
2042 }
2043 }
2044 } else {
2045 if state.ui.config.show_source_panel {
2047 let ratios = &state.ui.config.panel_ratios;
2049 let total_ratio: u32 = ratios.iter().map(|&x| x as u32).sum();
2050
2051 let chunks = match state.ui.layout.mode {
2052 LayoutMode::Horizontal => {
2053 Layout::default()
2054 .direction(Direction::Horizontal)
2055 .constraints(
2056 [
2057 Constraint::Ratio(ratios[0] as u32, total_ratio), Constraint::Ratio(ratios[1] as u32, total_ratio), Constraint::Ratio(ratios[2] as u32, total_ratio), ]
2061 .as_ref(),
2062 )
2063 .split(size)
2064 }
2065 LayoutMode::Vertical => {
2066 Layout::default()
2067 .direction(Direction::Vertical)
2068 .constraints(
2069 [
2070 Constraint::Ratio(ratios[0] as u32, total_ratio), Constraint::Ratio(ratios[1] as u32, total_ratio), Constraint::Ratio(ratios[2] as u32, total_ratio), ]
2074 .as_ref(),
2075 )
2076 .split(size)
2077 }
2078 };
2079
2080 Self::draw_source_panel(f, chunks[0], state);
2082 Self::draw_ebpf_panel(f, chunks[1], state);
2083 Self::draw_command_panel(f, chunks[2], state);
2084 } else {
2085 let ratios2 = state.ui.config.two_panel_ratios;
2087 let total2: u32 = (ratios2[0] as u32) + (ratios2[1] as u32);
2088
2089 let chunks = match state.ui.layout.mode {
2090 LayoutMode::Horizontal => {
2091 Layout::default()
2092 .direction(Direction::Horizontal)
2093 .constraints(
2094 [
2095 Constraint::Ratio(ratios2[0] as u32, total2), Constraint::Ratio(ratios2[1] as u32, total2), ]
2098 .as_ref(),
2099 )
2100 .split(size)
2101 }
2102 LayoutMode::Vertical => {
2103 Layout::default()
2104 .direction(Direction::Vertical)
2105 .constraints(
2106 [
2107 Constraint::Ratio(ratios2[0] as u32, total2), Constraint::Ratio(ratios2[1] as u32, total2), ]
2110 .as_ref(),
2111 )
2112 .split(size)
2113 }
2114 };
2115
2116 Self::draw_ebpf_panel(f, chunks[0], state);
2117 Self::draw_command_panel(f, chunks[1], state);
2118 }
2119 }
2120 }
2121
2122 fn draw_source_panel(f: &mut Frame, area: Rect, state: &AppState) {
2124 let is_focused = state.ui.focus.is_focused(PanelType::Source);
2125 let mut source_state = state.source_panel.clone();
2127
2128 let empty_cache = crate::components::command_panel::FileCompletionCache::default();
2130 let cache = state
2131 .command_panel
2132 .file_completion_cache
2133 .as_ref()
2134 .unwrap_or(&empty_cache);
2135
2136 crate::components::source_panel::SourceRenderer::render(
2137 f,
2138 area,
2139 &mut source_state,
2140 cache,
2141 is_focused,
2142 );
2143 }
2144
2145 fn draw_ebpf_panel(f: &mut Frame, area: Rect, state: &mut AppState) {
2147 let is_focused = state.ui.focus.is_focused(PanelType::EbpfInfo);
2148 state
2149 .ebpf_panel_renderer
2150 .render(&mut state.ebpf_panel, f, area, is_focused);
2151 }
2152
2153 fn draw_command_panel(f: &mut Frame, area: Rect, state: &mut AppState) {
2155 let old_width = state.command_panel.cached_panel_width;
2157 state.command_panel_width = area.width.saturating_sub(2); state
2161 .command_panel
2162 .remap_command_cursor_on_width_change(old_width, state.command_panel_width);
2163
2164 state
2166 .command_panel
2167 .update_panel_width(state.command_panel_width);
2168
2169 let is_focused = state.ui.focus.is_focused(PanelType::InteractiveCommand);
2170 let border_style = if is_focused {
2171 crate::ui::themes::UIThemes::panel_focused()
2172 } else {
2173 crate::ui::themes::UIThemes::panel_unfocused()
2174 };
2175
2176 let block = Block::default()
2177 .title(crate::ui::strings::UIStrings::COMMAND_PANEL_TITLE)
2178 .borders(Borders::ALL)
2179 .border_type(BorderType::Rounded)
2180 .border_style(border_style);
2181
2182 f.render_widget(block, area);
2183
2184 state
2186 .command_renderer
2187 .render(f, area, &state.command_panel, is_focused);
2188 }
2189
2190 async fn handle_runtime_status(&mut self, status: crate::events::RuntimeStatus) {
2192 use crate::components::loading::LoadingState;
2193 use crate::events::RuntimeStatus;
2194
2195 match &status {
2197 RuntimeStatus::DwarfLoadingStarted => {
2198 self.state.set_loading_state(LoadingState::LoadingSymbols {
2199 progress: Some(0.0),
2200 });
2201 }
2202 RuntimeStatus::DwarfLoadingCompleted { .. } => {
2203 if self.state.ui.config.show_source_panel {
2204 self.state
2205 .set_loading_state(LoadingState::LoadingSourceCode);
2206 } else {
2207 self.transition_to_ready_with_completion();
2209 tracing::debug!(
2211 "Source panel hidden on startup; requesting file list for completion cache"
2212 );
2213 if let Err(e) = self
2214 .state
2215 .event_registry
2216 .command_sender
2217 .send(crate::events::RuntimeCommand::InfoSource)
2218 {
2219 tracing::warn!("Failed to auto-request file list: {}", e);
2220 }
2221 }
2222 }
2223 RuntimeStatus::DwarfLoadingFailed(error) => {
2224 self.state
2225 .set_loading_state(LoadingState::Failed(error.clone()));
2226 }
2227 RuntimeStatus::DwarfModuleDiscovered {
2229 module_path,
2230 total_modules: _,
2231 } => {
2232 self.state
2234 .loading_ui
2235 .progress
2236 .add_module(module_path.clone());
2237 }
2238 RuntimeStatus::DwarfModuleLoadingStarted {
2239 module_path,
2240 current,
2241 total,
2242 } => {
2243 self.state
2245 .loading_ui
2246 .progress
2247 .start_module_loading(module_path);
2248 let progress = (*current as f64) / (*total as f64);
2250 self.state.set_loading_state(LoadingState::LoadingSymbols {
2251 progress: Some(progress),
2252 });
2253 }
2254 RuntimeStatus::DwarfModuleLoadingCompleted {
2255 module_path,
2256 stats,
2257 current,
2258 total,
2259 } => {
2260 let module_stats = crate::components::loading::ModuleStats {
2262 functions: stats.functions,
2263 variables: stats.variables,
2264 types: stats.types,
2265 };
2266 self.state
2267 .loading_ui
2268 .progress
2269 .complete_module(module_path, module_stats);
2270 let progress = (*current as f64) / (*total as f64);
2272 self.state.set_loading_state(LoadingState::LoadingSymbols {
2273 progress: Some(progress),
2274 });
2275 }
2276 RuntimeStatus::DwarfModuleLoadingFailed {
2277 module_path,
2278 error,
2279 current: _,
2280 total: _,
2281 } => {
2282 self.state
2284 .loading_ui
2285 .progress
2286 .fail_module(module_path, error.clone());
2287 }
2288 RuntimeStatus::SourceCodeLoaded(_) => {
2289 self.transition_to_ready_with_completion();
2291 }
2292 RuntimeStatus::SourceCodeLoadFailed(error) => {
2293 self.state
2294 .set_loading_state(LoadingState::Failed(error.clone()));
2295
2296 crate::components::source_panel::SourceNavigation::show_error_message(
2298 &mut self.state.source_panel,
2299 error.clone(),
2300 );
2301 }
2302 _ => {
2303 if matches!(self.state.loading_state, LoadingState::Initializing) {
2305 self.state
2306 .set_loading_state(LoadingState::ConnectingToRuntime);
2307 }
2308 }
2309 }
2310
2311 match status {
2312 RuntimeStatus::SourceCodeLoaded(source_info) => {
2313 let actions = crate::components::source_panel::SourceNavigation::load_source(
2315 &mut self.state.source_panel,
2316 source_info.file_path,
2317 source_info.current_line,
2318 );
2319 for action in actions {
2320 let _ = self.handle_action(action);
2321 }
2322
2323 tracing::debug!("Auto-requesting file list after source code loaded");
2325 if let Err(e) = self
2326 .state
2327 .event_registry
2328 .command_sender
2329 .send(crate::events::RuntimeCommand::InfoSource)
2330 {
2331 tracing::warn!("Failed to auto-request file list: {}", e);
2332 }
2333 }
2334 RuntimeStatus::FileInfo { groups } => {
2335 let mut files = Vec::new();
2337 for group in &groups {
2338 for file in &group.files {
2339 let full_path = if file.directory.is_empty() {
2341 file.path.clone()
2342 } else {
2343 format!("{}/{}", file.directory, file.path)
2344 };
2345 files.push(full_path);
2346 }
2347 }
2348
2349 self.sync_files_to_command_panel(files.clone());
2351
2352 if self.state.route_file_info_to_file_search {
2353 if let Some(ref mut cache) = self.state.command_panel.file_completion_cache {
2355 let actions =
2356 crate::components::source_panel::SourceSearch::set_file_search_files(
2357 &mut self.state.source_panel,
2358 cache,
2359 files.clone(),
2360 );
2361 for action in actions {
2362 let _ = self.handle_action(action);
2363 }
2364 }
2365
2366 self.state.route_file_info_to_file_search = false;
2368 } else {
2369 self.clear_waiting_state();
2371 let response =
2372 crate::components::command_panel::ResponseFormatter::format_file_info(
2373 &groups, false,
2374 );
2375 let styled_lines = crate::components::command_panel::ResponseFormatter::format_file_info_styled(
2376 &groups, false,
2377 );
2378 let action = Action::AddResponseWithStyle {
2379 content: response,
2380 styled_lines: Some(styled_lines),
2381 response_type: crate::action::ResponseType::Info,
2382 };
2383 let _ = self.handle_action(action);
2384 }
2385 }
2386 RuntimeStatus::FileInfoFailed { error } => {
2387 if self.state.route_file_info_to_file_search {
2388 let actions =
2389 crate::components::source_panel::SourceSearch::set_file_search_error(
2390 &mut self.state.source_panel,
2391 error,
2392 );
2393 for action in actions {
2394 let _ = self.handle_action(action);
2395 }
2396 self.state.route_file_info_to_file_search = false;
2397 } else {
2398 self.clear_waiting_state();
2399 let plain = format!("ā Failed to get file information: {error}");
2400 let styled = vec![
2401 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2402 .styled(plain.clone(), crate::components::command_panel::style_builder::StylePresets::ERROR)
2403 .build(),
2404 ];
2405 let action = Action::AddResponseWithStyle {
2406 content: plain,
2407 styled_lines: Some(styled),
2408 response_type: crate::action::ResponseType::Error,
2409 };
2410 let _ = self.handle_action(action);
2411 }
2412 }
2413 RuntimeStatus::InfoFunctionResult {
2414 target: _,
2415 info,
2416 verbose,
2417 } => {
2418 self.clear_waiting_state();
2420 let formatted_info = info.format_for_display(verbose);
2422 let styled_lines = info.format_for_display_styled(verbose);
2423 let action = Action::AddResponseWithStyle {
2424 content: formatted_info,
2425 styled_lines: Some(styled_lines),
2426 response_type: crate::action::ResponseType::Success,
2427 };
2428 let _ = self.handle_action(action);
2429 }
2430 RuntimeStatus::InfoFunctionFailed { target, error } => {
2431 self.clear_waiting_state();
2432 let text = format!("ā Failed to get debug info for function '{target}': {error}");
2433 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2434 let action = Action::AddResponseWithStyle {
2435 content: text,
2436 styled_lines: Some(styled),
2437 response_type: crate::action::ResponseType::Error,
2438 };
2439 let _ = self.handle_action(action);
2440 }
2441 RuntimeStatus::InfoLineResult {
2442 target: _,
2443 info,
2444 verbose,
2445 } => {
2446 self.clear_waiting_state();
2448 let formatted_info = info.format_for_display(verbose);
2450 let styled_lines = info.format_for_display_styled(verbose);
2451 let action = Action::AddResponseWithStyle {
2452 content: formatted_info,
2453 styled_lines: Some(styled_lines),
2454 response_type: crate::action::ResponseType::Success,
2455 };
2456 let _ = self.handle_action(action);
2457 }
2458 RuntimeStatus::InfoLineFailed { target, error } => {
2459 self.clear_waiting_state();
2460 let text = format!("ā Failed to get debug info for line '{target}': {error}");
2461 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2462 let action = Action::AddResponseWithStyle {
2463 content: text,
2464 styled_lines: Some(styled),
2465 response_type: crate::action::ResponseType::Error,
2466 };
2467 let _ = self.handle_action(action);
2468 }
2469 RuntimeStatus::InfoAddressResult {
2470 target: _,
2471 info,
2472 verbose,
2473 } => {
2474 self.clear_waiting_state();
2476 let formatted_info = info.format_for_display(verbose);
2478 let styled_lines = info.format_for_display_styled(verbose);
2479 let action = Action::AddResponseWithStyle {
2480 content: formatted_info,
2481 styled_lines: Some(styled_lines),
2482 response_type: crate::action::ResponseType::Success,
2483 };
2484 let _ = self.handle_action(action);
2485 }
2486 RuntimeStatus::InfoAddressFailed { target, error } => {
2487 self.clear_waiting_state();
2488 let text = format!("ā Failed to get debug info for address '{target}': {error}");
2489 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2490 let action = Action::AddResponseWithStyle {
2491 content: text,
2492 styled_lines: Some(styled),
2493 response_type: crate::action::ResponseType::Error,
2494 };
2495 let _ = self.handle_action(action);
2496 }
2497 RuntimeStatus::ShareInfo { libraries } => {
2498 let show_all = matches!(
2500 self.state.command_panel.input_state,
2501 crate::model::panel_state::InputState::WaitingResponse {
2502 command_type: crate::model::panel_state::CommandType::InfoShareAll,
2503 ..
2504 }
2505 );
2506
2507 self.clear_waiting_state();
2508
2509 let total = libraries.len();
2510 let display_libs: Vec<_> = if show_all {
2511 libraries
2512 } else {
2513 libraries
2514 .into_iter()
2515 .filter(|l| l.debug_info_available)
2516 .collect()
2517 };
2518
2519 if !show_all && display_libs.is_empty() && total > 0 {
2521 let content = format!(
2522 "š Shared Libraries ({total} total)\n\nā ļø No libraries with debug info found. Use 'info share all' to view all libraries."
2523 );
2524 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&content);
2525 let action = Action::AddResponseWithStyle {
2526 content,
2527 styled_lines: Some(styled),
2528 response_type: crate::action::ResponseType::Success,
2529 };
2530 let _ = self.handle_action(action);
2531 } else {
2532 let formatted_info =
2533 crate::components::command_panel::ResponseFormatter::format_shared_library_info(
2534 &display_libs, false,
2535 );
2536 let styled_lines =
2537 crate::components::command_panel::ResponseFormatter::format_shared_library_info_styled(
2538 &display_libs,
2539 false,
2540 );
2541 let action = Action::AddResponseWithStyle {
2542 content: formatted_info,
2543 styled_lines: Some(styled_lines),
2544 response_type: crate::action::ResponseType::Success,
2545 };
2546 let _ = self.handle_action(action);
2547 }
2548 }
2549 RuntimeStatus::ShareInfoFailed { error } => {
2550 self.clear_waiting_state();
2551 let text = format!("ā Failed to get shared library information: {error}");
2552 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2553 let action = Action::AddResponseWithStyle {
2554 content: text,
2555 styled_lines: Some(styled),
2556 response_type: crate::action::ResponseType::Error,
2557 };
2558 let _ = self.handle_action(action);
2559 }
2560 RuntimeStatus::ExecutableFileInfo {
2561 file_path,
2562 file_type,
2563 entry_point,
2564 has_symbols,
2565 has_debug_info,
2566 debug_file_path,
2567 text_section,
2568 data_section,
2569 mode_description,
2570 } => {
2571 self.clear_waiting_state();
2572 let info_display =
2573 crate::components::command_panel::response_formatter::ExecutableFileInfoDisplay {
2574 file_path: &file_path,
2575 file_type: &file_type,
2576 entry_point,
2577 has_symbols,
2578 has_debug_info,
2579 debug_file_path: &debug_file_path,
2580 text_section: &text_section,
2581 data_section: &data_section,
2582 mode_description: &mode_description,
2583 };
2584 let formatted_info =
2585 crate::components::command_panel::ResponseFormatter::format_executable_file_info(
2586 &info_display,
2587 );
2588 let styled_lines =
2589 crate::components::command_panel::ResponseFormatter::format_executable_file_info_styled(
2590 &info_display,
2591 );
2592 let action = Action::AddResponseWithStyle {
2593 content: formatted_info,
2594 styled_lines: Some(styled_lines),
2595 response_type: crate::action::ResponseType::Success,
2596 };
2597 let _ = self.handle_action(action);
2598 }
2599 RuntimeStatus::ExecutableFileInfoFailed { error } => {
2600 self.clear_waiting_state();
2601 let text = format!("ā Failed to get executable file information: {error}");
2602 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2603 let action = Action::AddResponseWithStyle {
2604 content: text,
2605 styled_lines: Some(styled),
2606 response_type: crate::action::ResponseType::Error,
2607 };
2608 let _ = self.handle_action(action);
2609 }
2610 RuntimeStatus::SrcPathInfo { info } => {
2611 self.clear_waiting_state();
2612 let formatted = info.format_for_display();
2613 let styled_lines = info.format_for_display_styled();
2614 let action = Action::AddResponseWithStyle {
2615 content: formatted,
2616 styled_lines: Some(styled_lines),
2617 response_type: crate::action::ResponseType::Info,
2618 };
2619 let _ = self.handle_action(action);
2620 }
2621 RuntimeStatus::SrcPathUpdated { message } => {
2622 self.clear_waiting_state();
2623
2624 self.state.route_file_info_to_file_search = true;
2627
2628 let plain = format!("ā
{message}\nš” Source code and file list reloading...");
2629 let styled = vec![
2630 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2631 .styled(
2632 format!("ā
{message}"),
2633 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
2634 )
2635 .build(),
2636 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2637 .styled(
2638 "š” Source code and file list reloading...",
2639 crate::components::command_panel::style_builder::StylePresets::TIP,
2640 )
2641 .build(),
2642 ];
2643 let action = Action::AddResponseWithStyle {
2644 content: plain,
2645 styled_lines: Some(styled),
2646 response_type: crate::action::ResponseType::Success,
2647 };
2648 let _ = self.handle_action(action);
2649 }
2650 RuntimeStatus::SrcPathFailed { error } => {
2651 self.clear_waiting_state();
2652 let text = format!(
2653 "ā {error}\n\nš No source available? You can hide the Source panel:\n ui source off # in UI command mode\n --no-source-panel # CLI flag\n [ui].show_source_panel=false # in config.toml"
2654 );
2655 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2656 let action = Action::AddResponseWithStyle {
2657 content: text,
2658 styled_lines: Some(styled),
2659 response_type: crate::action::ResponseType::Error,
2660 };
2661 let _ = self.handle_action(action);
2662 }
2663 RuntimeStatus::TraceInfo {
2664 trace_id,
2665 target,
2666 status,
2667 pid,
2668 binary,
2669 script_preview,
2670 pc,
2671 } => {
2672 self.clear_waiting_state();
2673
2674 if let Some(colon_pos) = target.rfind(':') {
2678 let file_part = &target[..colon_pos];
2679 if let Ok(line_num) = target[colon_pos + 1..].parse::<usize>() {
2680 self.state
2682 .source_panel
2683 .trace_locations
2684 .insert(trace_id, (file_part.to_string(), line_num));
2685
2686 if self.state.source_panel.file_path.as_ref()
2688 == Some(&file_part.to_string())
2689 {
2690 if self.state.source_panel.pending_trace_line == Some(line_num) {
2692 self.state.source_panel.pending_trace_line = None;
2693 }
2694
2695 match status {
2697 crate::events::TraceStatus::Active => {
2698 self.state.source_panel.disabled_lines.remove(&line_num);
2699 self.state.source_panel.traced_lines.insert(line_num);
2700 }
2701 crate::events::TraceStatus::Disabled => {
2702 self.state.source_panel.traced_lines.remove(&line_num);
2703 self.state.source_panel.disabled_lines.insert(line_num);
2704 }
2705 _ => {
2706 self.state.source_panel.traced_lines.remove(&line_num);
2708 self.state.source_panel.disabled_lines.remove(&line_num);
2709 }
2710 }
2711 }
2712 }
2713 }
2714
2715 let mut response = format!("š Trace {trace_id} Info:\n");
2717 response.push_str(&format!(" Target: {target}\n"));
2718 response.push_str(&format!(" Status: {status}\n"));
2719 response.push_str(&format!(" Binary: {binary}\n"));
2720 response.push_str(&format!(" PC: 0x{pc:x}\n"));
2721 if let Some(p) = pid {
2722 response.push_str(&format!(" PID: {p}\n"));
2723 }
2724 if let Some(ref preview) = script_preview {
2725 response.push_str(&format!(" Script:\n{preview}\n"));
2726 }
2727 let styled_lines = {
2729 let temp = crate::events::RuntimeStatus::TraceInfo {
2730 trace_id,
2731 target: target.clone(),
2732 status: status.clone(),
2733 pid,
2734 binary: binary.clone(),
2735 script_preview: None,
2736 pc,
2737 };
2738 if let Some(mut base) = temp.format_trace_info_styled() {
2739 if let Some(ref preview) = script_preview {
2740 use crate::components::command_panel::style_builder::StyledLineBuilder;
2741 use ratatui::text::Line;
2742 base.push(Line::from(""));
2743 base.push(StyledLineBuilder::new().key("š Script:").build());
2744 for line in preview.lines() {
2745 base.push(StyledLineBuilder::new().text(" ").value(line).build());
2746 }
2747 }
2748 Some(base)
2749 } else {
2750 None
2751 }
2752 };
2753
2754 let action = Action::AddResponseWithStyle {
2755 content: response,
2756 styled_lines,
2757 response_type: crate::action::ResponseType::Info,
2758 };
2759 let _ = self.handle_action(action);
2760 }
2761 RuntimeStatus::TraceInfoAll { summary, traces } => {
2762 self.clear_waiting_state();
2763
2764 for trace in &traces {
2766 if let Some(colon_pos) = trace.target_display.rfind(':') {
2769 let file_part = &trace.target_display[..colon_pos];
2770 if let Ok(line_num) = trace.target_display[colon_pos + 1..].parse::<usize>()
2771 {
2772 self.state
2774 .source_panel
2775 .trace_locations
2776 .insert(trace.trace_id, (file_part.to_string(), line_num));
2777
2778 if self.state.source_panel.file_path.as_ref()
2780 == Some(&file_part.to_string())
2781 {
2782 match trace.status {
2783 crate::events::TraceStatus::Active => {
2784 self.state.source_panel.disabled_lines.remove(&line_num);
2785 self.state.source_panel.traced_lines.insert(line_num);
2786 }
2787 crate::events::TraceStatus::Disabled => {
2788 self.state.source_panel.traced_lines.remove(&line_num);
2789 self.state.source_panel.disabled_lines.insert(line_num);
2790 }
2791 crate::events::TraceStatus::Failed => {
2792 self.state.source_panel.traced_lines.remove(&line_num);
2793 self.state.source_panel.disabled_lines.remove(&line_num);
2794 }
2795 }
2796 }
2797 }
2798 }
2799 }
2800
2801 let mut response = format!(
2802 "š All Traces ({} total, {} active):\n\n",
2803 summary.total, summary.active
2804 );
2805 for trace in &traces {
2806 response.push_str(&format!(" {}\n", trace.format_line()));
2808 }
2809 let styled_lines = (crate::events::RuntimeStatus::TraceInfoAll {
2811 summary: summary.clone(),
2812 traces: traces.clone(),
2813 })
2814 .format_trace_info_styled()
2815 .unwrap_or_default();
2816 let action = Action::AddResponseWithStyle {
2817 content: response,
2818 styled_lines: if styled_lines.is_empty() {
2819 None
2820 } else {
2821 Some(styled_lines)
2822 },
2823 response_type: crate::action::ResponseType::Info,
2824 };
2825 let _ = self.handle_action(action);
2826 }
2827 RuntimeStatus::TraceInfoFailed { trace_id, error } => {
2828 self.clear_waiting_state();
2829 let text = format!("ā Failed to get info for trace {trace_id}: {error}");
2830 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2831 let action = Action::AddResponseWithStyle {
2832 content: text,
2833 styled_lines: Some(styled),
2834 response_type: crate::action::ResponseType::Error,
2835 };
2836 let _ = self.handle_action(action);
2837 }
2838 RuntimeStatus::TraceEnabled { trace_id } => {
2839 self.clear_waiting_state();
2840
2841 if let Some((file_path, line_num)) =
2843 self.state.source_panel.trace_locations.get(&trace_id)
2844 {
2845 if self.state.source_panel.file_path.as_ref() == Some(file_path) {
2846 self.state.source_panel.disabled_lines.remove(line_num);
2847 self.state.source_panel.traced_lines.insert(*line_num);
2848 }
2849 }
2850
2851 let text = format!("ā
Trace {trace_id} enabled");
2852 let styled = vec![
2853 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2854 .styled(
2855 text.clone(),
2856 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
2857 )
2858 .build(),
2859 ];
2860 let action = Action::AddResponseWithStyle {
2861 content: text,
2862 styled_lines: Some(styled),
2863 response_type: crate::action::ResponseType::Success,
2864 };
2865 let _ = self.handle_action(action);
2866 }
2867 RuntimeStatus::TraceDisabled { trace_id } => {
2868 self.clear_waiting_state();
2869
2870 if let Some((file_path, line_num)) =
2872 self.state.source_panel.trace_locations.get(&trace_id)
2873 {
2874 if self.state.source_panel.file_path.as_ref() == Some(file_path) {
2875 self.state.source_panel.traced_lines.remove(line_num);
2876 self.state.source_panel.disabled_lines.insert(*line_num);
2877 }
2878 }
2879
2880 let text = format!("ā
Trace {trace_id} disabled");
2881 let styled = vec![
2882 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2883 .styled(
2884 text.clone(),
2885 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
2886 )
2887 .build(),
2888 ];
2889 let action = Action::AddResponseWithStyle {
2890 content: text,
2891 styled_lines: Some(styled),
2892 response_type: crate::action::ResponseType::Success,
2893 };
2894 let _ = self.handle_action(action);
2895 }
2896 RuntimeStatus::AllTracesEnabled { count, error } => {
2897 self.clear_waiting_state();
2898
2899 if error.is_none() {
2900 for (file_path, line_num) in self.state.source_panel.trace_locations.values() {
2902 if self.state.source_panel.file_path.as_ref() == Some(file_path) {
2903 self.state.source_panel.disabled_lines.remove(line_num);
2904 self.state.source_panel.traced_lines.insert(*line_num);
2905 }
2906 }
2907 }
2908
2909 let (plain, rtype, style) = if let Some(ref err) = error {
2910 (
2911 format!("ā Failed to enable traces: {err}"),
2912 crate::action::ResponseType::Error,
2913 crate::components::command_panel::style_builder::StylePresets::ERROR,
2914 )
2915 } else {
2916 (
2917 format!("ā
All traces enabled ({count} traces)"),
2918 crate::action::ResponseType::Success,
2919 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
2920 )
2921 };
2922 let styled = vec![
2923 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2924 .styled(plain.clone(), style)
2925 .build(),
2926 ];
2927 let action = Action::AddResponseWithStyle {
2928 content: plain,
2929 styled_lines: Some(styled),
2930 response_type: rtype,
2931 };
2932 let _ = self.handle_action(action);
2933 }
2934 RuntimeStatus::AllTracesDisabled { count, error } => {
2935 self.clear_waiting_state();
2936
2937 if error.is_none() {
2938 for (file_path, line_num) in self.state.source_panel.trace_locations.values() {
2940 if self.state.source_panel.file_path.as_ref() == Some(file_path) {
2941 self.state.source_panel.traced_lines.remove(line_num);
2942 self.state.source_panel.disabled_lines.insert(*line_num);
2943 }
2944 }
2945 }
2946
2947 let (plain, rtype, style) = if let Some(ref err) = error {
2948 (
2949 format!("ā Failed to disable traces: {err}"),
2950 crate::action::ResponseType::Error,
2951 crate::components::command_panel::style_builder::StylePresets::ERROR,
2952 )
2953 } else {
2954 (
2955 format!("ā
All traces disabled ({count} traces)"),
2956 crate::action::ResponseType::Success,
2957 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
2958 )
2959 };
2960 let styled = vec![
2961 crate::components::command_panel::style_builder::StyledLineBuilder::new()
2962 .styled(plain.clone(), style)
2963 .build(),
2964 ];
2965 let action = Action::AddResponseWithStyle {
2966 content: plain,
2967 styled_lines: Some(styled),
2968 response_type: rtype,
2969 };
2970 let _ = self.handle_action(action);
2971 }
2972 RuntimeStatus::TraceEnableFailed { trace_id, error } => {
2973 self.clear_waiting_state();
2974 let text = format!("ā Failed to enable trace {trace_id}: {error}");
2975 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2976 let action = Action::AddResponseWithStyle {
2977 content: text,
2978 styled_lines: Some(styled),
2979 response_type: crate::action::ResponseType::Error,
2980 };
2981 let _ = self.handle_action(action);
2982 }
2983 RuntimeStatus::TraceDisableFailed { trace_id, error } => {
2984 self.clear_waiting_state();
2985 let text = format!("ā Failed to disable trace {trace_id}: {error}");
2986 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
2987 let action = Action::AddResponseWithStyle {
2988 content: text,
2989 styled_lines: Some(styled),
2990 response_type: crate::action::ResponseType::Error,
2991 };
2992 let _ = self.handle_action(action);
2993 }
2994 RuntimeStatus::TraceDeleted { trace_id } => {
2995 self.clear_waiting_state();
2996
2997 if let Some((file_path, line_num)) =
2999 self.state.source_panel.trace_locations.remove(&trace_id)
3000 {
3001 if self.state.source_panel.file_path.as_ref() == Some(&file_path) {
3002 self.state.source_panel.traced_lines.remove(&line_num);
3003 self.state.source_panel.disabled_lines.remove(&line_num);
3004 }
3005 }
3006
3007 let text = format!("ā
Trace {trace_id} deleted");
3008 let styled = vec![
3009 crate::components::command_panel::style_builder::StyledLineBuilder::new()
3010 .styled(
3011 text.clone(),
3012 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
3013 )
3014 .build(),
3015 ];
3016 let action = Action::AddResponseWithStyle {
3017 content: text,
3018 styled_lines: Some(styled),
3019 response_type: crate::action::ResponseType::Success,
3020 };
3021 let _ = self.handle_action(action);
3022 }
3023 RuntimeStatus::AllTracesDeleted { count, error } => {
3024 self.clear_waiting_state();
3025
3026 if error.is_none() {
3027 self.state.source_panel.traced_lines.clear();
3029 self.state.source_panel.disabled_lines.clear();
3030 self.state.source_panel.trace_locations.clear();
3031 }
3032
3033 let (plain, rtype, style) = if let Some(ref err) = error {
3034 (
3035 format!("ā Failed to delete traces: {err}"),
3036 crate::action::ResponseType::Error,
3037 crate::components::command_panel::style_builder::StylePresets::ERROR,
3038 )
3039 } else {
3040 (
3041 format!("ā
All traces deleted ({count} traces)"),
3042 crate::action::ResponseType::Success,
3043 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
3044 )
3045 };
3046 let styled = vec![
3047 crate::components::command_panel::style_builder::StyledLineBuilder::new()
3048 .styled(plain.clone(), style)
3049 .build(),
3050 ];
3051 let action = Action::AddResponseWithStyle {
3052 content: plain,
3053 styled_lines: Some(styled),
3054 response_type: rtype,
3055 };
3056 let _ = self.handle_action(action);
3057 }
3058 RuntimeStatus::TraceDeleteFailed { trace_id, error } => {
3059 self.clear_waiting_state();
3060 let text = format!("ā Failed to delete trace {trace_id}: {error}");
3061 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
3062 let action = Action::AddResponseWithStyle {
3063 content: text,
3064 styled_lines: Some(styled),
3065 response_type: crate::action::ResponseType::Error,
3066 };
3067 let _ = self.handle_action(action);
3068 }
3069 RuntimeStatus::TracesSaved {
3070 filename,
3071 saved_count,
3072 total_count,
3073 } => {
3074 self.clear_waiting_state();
3075 let mut text =
3076 format!("ā
Saved {saved_count} of {total_count} traces to {filename}\n");
3077 text.push_str(" ⢠Selected indices are preserved in the save file\n");
3078
3079 use crate::components::command_panel::style_builder::{
3080 StylePresets, StyledLineBuilder,
3081 };
3082 let styled = vec![
3083 StyledLineBuilder::new()
3084 .styled(
3085 format!("ā
Saved {saved_count} of {total_count} traces to {filename}"),
3086 StylePresets::SUCCESS,
3087 )
3088 .build(),
3089 StyledLineBuilder::new()
3090 .text(" ⢠")
3091 .styled(
3092 "Selected indices are preserved in the save file",
3093 StylePresets::TIP,
3094 )
3095 .build(),
3096 ];
3097 let action = Action::AddResponseWithStyle {
3098 content: text,
3099 styled_lines: Some(styled),
3100 response_type: crate::action::ResponseType::Success,
3101 };
3102 let _ = self.handle_action(action);
3103 }
3104 RuntimeStatus::TracesSaveFailed { error } => {
3105 self.clear_waiting_state();
3106 let text = format!("ā Failed to save traces: {error}");
3107 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
3108 let action = Action::AddResponseWithStyle {
3109 content: text,
3110 styled_lines: Some(styled),
3111 response_type: crate::action::ResponseType::Error,
3112 };
3113 let _ = self.handle_action(action);
3114 }
3115 RuntimeStatus::TracesLoaded {
3116 filename,
3117 total_count,
3118 success_count,
3119 failed_count,
3120 disabled_count,
3121 details,
3122 } => {
3123 self.clear_waiting_state();
3124
3125 let mut response = String::new();
3127
3128 if failed_count == 0 {
3129 response.push_str(&format!(
3131 "ā Loaded {} traces from {} ({} enabled, {} disabled)",
3132 total_count,
3133 filename,
3134 success_count - disabled_count,
3135 disabled_count
3136 ));
3137 response.push('\n');
3138 response.push_str(" ⢠Selected indices from the file are restored\n");
3139 } else {
3140 response.push_str(&format!("ā ļø Partially loaded traces from {filename}\n"));
3142 response.push_str(&format!(
3143 " ā {} traces created ({} enabled, {} disabled)\n",
3144 success_count,
3145 success_count - disabled_count,
3146 disabled_count
3147 ));
3148 response
3149 .push_str(" ⢠Selected indices from the file are restored when present\n");
3150
3151 for detail in &details {
3153 if let crate::events::LoadStatus::Failed = detail.status {
3154 if let Some(ref error) = detail.error {
3155 response.push_str(&format!(" ā {} - {}\n", detail.target, error));
3156 }
3157 }
3158 }
3159 }
3160
3161 let mut styled = Vec::new();
3163 use crate::components::command_panel::style_builder::{
3164 StylePresets, StyledLineBuilder,
3165 };
3166 if failed_count == 0 {
3167 styled.push(
3168 StyledLineBuilder::new()
3169 .styled(
3170 format!(
3171 "ā
Loaded {} traces from {} ({} enabled, {} disabled)",
3172 total_count,
3173 filename,
3174 success_count - disabled_count,
3175 disabled_count
3176 ),
3177 StylePresets::SUCCESS,
3178 )
3179 .build(),
3180 );
3181 styled.push(
3182 StyledLineBuilder::new()
3183 .text(" ⢠")
3184 .styled(
3185 "Selected indices from the file are restored",
3186 StylePresets::TIP,
3187 )
3188 .build(),
3189 );
3190 } else {
3191 styled.push(
3192 StyledLineBuilder::new()
3193 .styled(
3194 format!("ā ļø Partially loaded traces from {filename}"),
3195 StylePresets::WARNING,
3196 )
3197 .build(),
3198 );
3199 styled.push(
3200 StyledLineBuilder::new()
3201 .text(" ")
3202 .styled(
3203 format!(
3204 "ā
{} traces created ({} enabled, {} disabled)",
3205 success_count,
3206 success_count - disabled_count,
3207 disabled_count
3208 ),
3209 StylePresets::SUCCESS,
3210 )
3211 .build(),
3212 );
3213 styled.push(
3214 StyledLineBuilder::new()
3215 .text(" ⢠")
3216 .styled(
3217 "Selected indices from the file are restored when present",
3218 StylePresets::TIP,
3219 )
3220 .build(),
3221 );
3222 for detail in &details {
3223 if let crate::events::LoadStatus::Failed = detail.status {
3224 if let Some(ref err) = detail.error {
3225 styled.push(
3226 StyledLineBuilder::new()
3227 .text(" ")
3228 .styled(
3229 format!("ā {} - {}", detail.target, err),
3230 StylePresets::ERROR,
3231 )
3232 .build(),
3233 );
3234 }
3235 }
3236 }
3237 }
3238
3239 let action = Action::AddResponseWithStyle {
3240 content: response,
3241 styled_lines: Some(styled),
3242 response_type: if failed_count == 0 {
3243 crate::action::ResponseType::Success
3244 } else {
3245 crate::action::ResponseType::Warning
3246 },
3247 };
3248 let _ = self.handle_action(action);
3249 }
3250 RuntimeStatus::TracesLoadFailed { filename, error } => {
3251 self.clear_waiting_state();
3252 let text = format!("ā Failed to load {filename}: {error}");
3253 let styled = crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&text);
3254 let action = Action::AddResponseWithStyle {
3255 content: text,
3256 styled_lines: Some(styled),
3257 response_type: crate::action::ResponseType::Error,
3258 };
3259 let _ = self.handle_action(action);
3260 }
3261 _ => {
3262 let should_clear_waiting = matches!(
3267 status,
3268 RuntimeStatus::AllTracesEnabled { .. }
3269 | RuntimeStatus::AllTracesDisabled { .. }
3270 | RuntimeStatus::AllTracesDeleted { .. }
3271 | RuntimeStatus::ScriptCompilationCompleted { .. }
3272 | RuntimeStatus::TraceInfoFailed { .. }
3273 | RuntimeStatus::FileInfoFailed { .. }
3274 | RuntimeStatus::ShareInfoFailed { .. }
3275 | RuntimeStatus::ExecutableFileInfoFailed { .. }
3276 | RuntimeStatus::SrcPathFailed { .. }
3277 );
3278
3279 if should_clear_waiting {
3280 self.clear_waiting_state();
3281 }
3282
3283 if let Some(content) = self.format_runtime_status_for_display(&status) {
3284 let styled_lines = if content.contains("\x1b[") {
3287 None
3288 } else {
3289 Some(crate::components::command_panel::ResponseFormatter::style_generic_message_lines(&content))
3290 };
3291 let action = Action::AddResponseWithStyle {
3292 content,
3293 styled_lines,
3294 response_type: self.get_response_type_for_status(&status),
3295 };
3296 let _ = self.handle_action(action);
3297 }
3298 }
3299 }
3300 }
3301
3302 async fn handle_trace_event(&mut self, trace_event: ghostscope_protocol::ParsedTraceEvent) {
3304 tracing::debug!("Trace event: {:?}", trace_event);
3305
3306 if self.state.realtime_output_logger.enabled {
3308 if let Err(e) = self.write_ebpf_event_to_output_log(&trace_event) {
3309 tracing::error!("Failed to write eBPF event to output log: {}", e);
3310 }
3311 }
3312
3313 self.state.ebpf_panel.add_trace_event(trace_event);
3314 }
3315
3316 fn format_runtime_status_for_display(
3318 &mut self,
3319 status: &crate::events::RuntimeStatus,
3320 ) -> Option<String> {
3321 use crate::events::RuntimeStatus;
3322
3323 match status {
3324 RuntimeStatus::ScriptCompilationCompleted { details } => {
3325 if let Some(ref mut batch) = self.state.command_panel.batch_loading {
3327 batch.completed_count += 1;
3329 if details.success_count > 0 {
3330 batch.success_count += details.success_count;
3331 for result in &details.results {
3333 if matches!(result.status, crate::events::ExecutionStatus::Success) {
3334 let trace_id = details.trace_ids.first().copied();
3335 batch.details.push(crate::events::TraceLoadDetail {
3336 target: result.target_name.clone(),
3337 trace_id,
3338 status: crate::events::LoadStatus::Created,
3339 error: None,
3340 });
3341 }
3342 }
3343 } else {
3344 batch.failed_count += 1;
3345 for result in &details.results {
3347 if let crate::events::ExecutionStatus::Failed(error) = &result.status {
3348 batch.details.push(crate::events::TraceLoadDetail {
3349 target: result.target_name.clone(),
3350 trace_id: None,
3351 status: crate::events::LoadStatus::Failed,
3352 error: Some(error.clone()),
3353 });
3354 }
3355 }
3356 }
3357
3358 if batch.completed_count >= batch.total_count {
3360 let filename = batch.filename.clone();
3362 let total_count = batch.total_count;
3363 let success_count = batch.success_count;
3364 let failed_count = batch.failed_count;
3365 let disabled_count = batch.disabled_count;
3366 let details = batch.details.clone();
3367
3368 self.state.command_panel.batch_loading = None;
3370
3371 self.clear_waiting_state();
3373
3374 let mut response = format!("š Loaded traces from {filename}\n");
3376 response.push_str(&format!(
3377 " Total: {total_count}, Success: {success_count}, Failed: {failed_count}"
3378 ));
3379 if disabled_count > 0 {
3380 response.push_str(&format!(", Disabled: {disabled_count}"));
3381 }
3382 response.push('\n');
3383
3384 if !details.is_empty() {
3386 response.push_str("\nš Details:\n");
3387 for detail in &details {
3388 match detail.status {
3389 crate::events::LoadStatus::Created => {
3390 if let Some(id) = detail.trace_id {
3391 response.push_str(&format!(
3392 " ā {} ā trace #{}\n",
3393 detail.target, id
3394 ));
3395 } else {
3396 response.push_str(&format!(" ā {}\n", detail.target));
3397 }
3398 }
3399 crate::events::LoadStatus::CreatedDisabled => {
3400 if let Some(id) = detail.trace_id {
3401 response.push_str(&format!(
3402 " ā {} ā trace #{} (disabled)\n",
3403 detail.target, id
3404 ));
3405 } else {
3406 response.push_str(&format!(
3407 " ā {} (disabled)\n",
3408 detail.target
3409 ));
3410 }
3411 }
3412 crate::events::LoadStatus::Failed => {
3413 if let Some(ref error) = detail.error {
3414 response.push_str(&format!(
3415 " ā {}: {}\n",
3416 detail.target, error
3417 ));
3418 } else {
3419 response.push_str(&format!(" ā {}\n", detail.target));
3420 }
3421 }
3422 _ => {}
3423 }
3424 }
3425 }
3426
3427 let styled_lines =
3429 crate::components::command_panel::ResponseFormatter::format_batch_load_summary_styled(
3430 &filename,
3431 total_count,
3432 success_count,
3433 failed_count,
3434 disabled_count,
3435 &details,
3436 );
3437
3438 let action = Action::AddResponseWithStyle {
3439 content: response,
3440 styled_lines: Some(styled_lines),
3441 response_type: if failed_count > 0 {
3442 crate::action::ResponseType::Warning
3443 } else {
3444 crate::action::ResponseType::Success
3445 },
3446 };
3447 let _ = self.handle_action(action);
3448
3449 return None;
3451 } else {
3452 return None;
3454 }
3455 }
3456
3457 self.clear_waiting_state();
3460
3461 if details.success_count > 0 || details.failed_count > 0 {
3463 let script_content = self
3465 .state
3466 .command_panel
3467 .script_cache
3468 .as_ref()
3469 .map(|cache| cache.lines.join("\n"));
3470
3471 Some(crate::components::command_panel::script_editor::ScriptEditor::format_compilation_results(
3473 details,
3474 script_content.as_deref(),
3475 &self.state.emoji_config,
3476 ))
3477 } else {
3478 let first_failed = details.results.first();
3480 if let Some(result) = first_failed {
3481 if let crate::events::ExecutionStatus::Failed(error) = &result.status {
3482 let error_details = crate::components::command_panel::script_editor::TraceErrorDetails {
3483 compilation_errors: None,
3484 uprobe_error: Some(error.clone()),
3485 suggestion: Some("Check function name and ensure binary has debug symbols".to_string()),
3486 };
3487
3488 let script_content = self
3490 .state
3491 .command_panel
3492 .script_cache
3493 .as_ref()
3494 .map(|cache| cache.lines.join("\n"));
3495
3496 Some(crate::components::command_panel::script_editor::ScriptEditor::format_trace_error_response_with_script(
3497 &result.target_name,
3498 error,
3499 Some(&error_details),
3500 script_content.as_deref(),
3501 &self.state.emoji_config,
3502 ))
3503 } else {
3504 None
3505 }
3506 } else {
3507 None
3508 }
3509 }
3510 }
3511 RuntimeStatus::AllTracesEnabled { count, error } => {
3512 if let Some(ref err) = error {
3513 let error_emoji = self
3514 .state
3515 .emoji_config
3516 .get_script_status(crate::ui::emoji::ScriptStatus::Error);
3517 Some(format!("{error_emoji} {err}"))
3518 } else if *count > 0 {
3519 let success_emoji = self
3520 .state
3521 .emoji_config
3522 .get_trace_status(crate::ui::emoji::TraceStatusType::Active);
3523 Some(format!("{success_emoji} Enabled {count} traces"))
3524 } else {
3525 None
3526 }
3527 }
3528 RuntimeStatus::AllTracesDisabled { count, error } => {
3529 if let Some(ref err) = error {
3530 let error_emoji = self
3531 .state
3532 .emoji_config
3533 .get_script_status(crate::ui::emoji::ScriptStatus::Error);
3534 Some(format!("{error_emoji} {err}"))
3535 } else if *count > 0 {
3536 let disabled_emoji = self
3537 .state
3538 .emoji_config
3539 .get_trace_status(crate::ui::emoji::TraceStatusType::Disabled);
3540 Some(format!("{disabled_emoji} Disabled {count} traces"))
3541 } else {
3542 None
3543 }
3544 }
3545 RuntimeStatus::AllTracesDeleted { count, error } => {
3546 if let Some(ref err) = error {
3547 let error_emoji = self
3548 .state
3549 .emoji_config
3550 .get_script_status(crate::ui::emoji::ScriptStatus::Error);
3551 Some(format!("{error_emoji} {err}"))
3552 } else if *count > 0 {
3553 Some(format!("ā Deleted {count} traces"))
3554 } else {
3555 None
3556 }
3557 }
3558 RuntimeStatus::TraceEnabled { trace_id } => {
3559 let success_emoji = self
3560 .state
3561 .emoji_config
3562 .get_trace_status(crate::ui::emoji::TraceStatusType::Active);
3563 Some(format!("{success_emoji} Trace {trace_id} enabled"))
3564 }
3565 RuntimeStatus::TraceDisabled { trace_id } => {
3566 let disabled_emoji = self
3567 .state
3568 .emoji_config
3569 .get_trace_status(crate::ui::emoji::TraceStatusType::Disabled);
3570 Some(format!("{disabled_emoji} Trace {trace_id} disabled"))
3571 }
3572 _ => None, }
3574 }
3575
3576 fn get_response_type_for_status(
3578 &self,
3579 status: &crate::events::RuntimeStatus,
3580 ) -> crate::action::ResponseType {
3581 use crate::events::RuntimeStatus;
3582
3583 match status {
3584 RuntimeStatus::ScriptCompilationCompleted { details } => {
3585 if details.success_count > 0 {
3587 crate::action::ResponseType::Success
3588 } else {
3589 crate::action::ResponseType::Error
3590 }
3591 }
3592 RuntimeStatus::AllTracesEnabled { error, .. }
3593 | RuntimeStatus::AllTracesDisabled { error, .. }
3594 | RuntimeStatus::AllTracesDeleted { error, .. } => {
3595 if error.is_some() {
3596 crate::action::ResponseType::Error
3597 } else {
3598 crate::action::ResponseType::Success
3599 }
3600 }
3601 _ => crate::action::ResponseType::Info,
3602 }
3603 }
3604
3605 fn clear_waiting_state(&mut self) {
3607 self.state.command_panel.input_state = crate::model::panel_state::InputState::Ready;
3608 }
3609
3610 fn validate_and_resolve_path(filename: &str) -> anyhow::Result<std::path::PathBuf> {
3613 use std::path::{Path, PathBuf};
3614
3615 if filename.contains("..") {
3617 return Err(anyhow::anyhow!(
3618 "Path traversal not allowed (contains '..')"
3619 ));
3620 }
3621
3622 let file_path = if Path::new(filename).is_relative() {
3624 let current_dir = std::env::current_dir()?;
3625 current_dir.join(filename)
3626 } else {
3627 PathBuf::from(filename)
3628 };
3629
3630 if Path::new(filename).is_relative() {
3633 let current_dir = std::env::current_dir()?;
3634 let canonical_current = current_dir
3635 .canonicalize()
3636 .unwrap_or_else(|_| current_dir.clone());
3637
3638 if let Some(parent) = file_path.parent() {
3640 if !parent.exists() {
3641 return Err(anyhow::anyhow!(
3642 "Directory does not exist: {}",
3643 parent.display()
3644 ));
3645 }
3646
3647 let canonical_parent = parent
3649 .canonicalize()
3650 .unwrap_or_else(|_| parent.to_path_buf());
3651 if !canonical_parent.starts_with(&canonical_current) {
3652 return Err(anyhow::anyhow!("Cannot save outside current directory"));
3653 }
3654 }
3655 }
3656
3657 Ok(file_path)
3658 }
3659
3660 fn start_realtime_output_logging(
3662 &mut self,
3663 filename: Option<String>,
3664 ) -> anyhow::Result<std::path::PathBuf> {
3665 use chrono::Local;
3666
3667 if self.state.realtime_output_logger.enabled {
3669 return Err(anyhow::anyhow!(
3670 "Realtime output logging already active to: {}",
3671 self.state
3672 .realtime_output_logger
3673 .file_path
3674 .as_ref()
3675 .map(|p| p.display().to_string())
3676 .unwrap_or_else(|| "unknown".to_string())
3677 ));
3678 }
3679
3680 let filename = filename.unwrap_or_else(|| {
3682 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
3683 format!("ebpf_output_{timestamp}.log")
3684 });
3685
3686 let file_path = Self::validate_and_resolve_path(&filename)?;
3688
3689 let is_new_file = !file_path.exists();
3691
3692 self.state.realtime_output_logger.start(file_path.clone())?;
3694
3695 if is_new_file {
3697 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
3698 self.state
3699 .realtime_output_logger
3700 .write_line("# GhostScope eBPF Output Log (Realtime)")?;
3701 self.state
3702 .realtime_output_logger
3703 .write_line(&format!("# Session: {timestamp}"))?;
3704 self.state
3705 .realtime_output_logger
3706 .write_line("# ========================================")?;
3707 self.state.realtime_output_logger.write_line("")?;
3708 } else {
3709 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
3711 self.state.realtime_output_logger.write_line("")?;
3712 self.state
3713 .realtime_output_logger
3714 .write_line("# ----------------------------------------")?;
3715 self.state
3716 .realtime_output_logger
3717 .write_line(&format!("# Resumed: {timestamp}"))?;
3718 self.state
3719 .realtime_output_logger
3720 .write_line("# ----------------------------------------")?;
3721 self.state.realtime_output_logger.write_line("")?;
3722 }
3723
3724 Ok(file_path)
3725 }
3726
3727 fn write_ebpf_event_to_output_log(
3729 &mut self,
3730 event: &ghostscope_protocol::ParsedTraceEvent,
3731 ) -> anyhow::Result<()> {
3732 if self.state.realtime_output_logger.enabled {
3733 let secs = event.timestamp / 1_000_000_000;
3735 let nanos = event.timestamp % 1_000_000_000;
3736 let formatted_ts = format!(
3737 "{:02}:{:02}:{:02}.{:06}",
3738 (secs / 3600) % 24,
3739 (secs / 60) % 60,
3740 secs % 60,
3741 nanos / 1000
3742 );
3743
3744 let formatted_output = event.to_formatted_output();
3746 let message = formatted_output.join(" ");
3747
3748 self.state.realtime_output_logger.write_line(&format!(
3750 "[{}] [PID {}/TID {}] Trace #{}: {}",
3751 formatted_ts, event.pid, event.tid, event.trace_id, message
3752 ))?;
3753 }
3754 Ok(())
3755 }
3756
3757 fn write_command_to_session_log(&mut self, command: &str) -> anyhow::Result<()> {
3759 if self.state.realtime_session_logger.enabled {
3760 self.state.realtime_session_logger.write_line("")?;
3761 self.state
3762 .realtime_session_logger
3763 .write_line(&format!(">>> {command}"))?;
3764 }
3765 Ok(())
3766 }
3767
3768 fn write_response_to_session_log(&mut self, response: &str) -> anyhow::Result<()> {
3770 if self.state.realtime_session_logger.enabled {
3771 for line in response.lines() {
3772 self.state
3773 .realtime_session_logger
3774 .write_line(&format!(" {line}"))?;
3775 }
3776 }
3777 Ok(())
3778 }
3779
3780 fn start_realtime_session_logging(
3782 &mut self,
3783 filename: Option<String>,
3784 ) -> anyhow::Result<std::path::PathBuf> {
3785 use chrono::Local;
3786
3787 if self.state.realtime_session_logger.enabled {
3789 return Err(anyhow::anyhow!(
3790 "Realtime session logging already active to: {}",
3791 self.state
3792 .realtime_session_logger
3793 .file_path
3794 .as_ref()
3795 .map(|p| p.display().to_string())
3796 .unwrap_or_else(|| "unknown".to_string())
3797 ));
3798 }
3799
3800 let filename = filename.unwrap_or_else(|| {
3802 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
3803 format!("command_session_{timestamp}.log")
3804 });
3805
3806 let file_path = Self::validate_and_resolve_path(&filename)?;
3808
3809 let is_new_file = !file_path.exists();
3811
3812 self.state
3814 .realtime_session_logger
3815 .start(file_path.clone())?;
3816
3817 if is_new_file {
3819 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
3820 self.state
3821 .realtime_session_logger
3822 .write_line("# GhostScope Command Session Log (Realtime)")?;
3823 self.state
3824 .realtime_session_logger
3825 .write_line(&format!("# Session: {timestamp}"))?;
3826 self.state
3827 .realtime_session_logger
3828 .write_line("# ========================================")?;
3829 self.state.realtime_session_logger.write_line("")?;
3830
3831 for static_line in &self.state.command_panel.static_lines {
3833 self.state
3834 .realtime_session_logger
3835 .write_line(&static_line.content)?;
3836 }
3837 self.state.realtime_session_logger.write_line("")?;
3838 } else {
3839 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
3841 self.state.realtime_session_logger.write_line("")?;
3842 self.state
3843 .realtime_session_logger
3844 .write_line("# ----------------------------------------")?;
3845 self.state
3846 .realtime_session_logger
3847 .write_line(&format!("# Resumed: {timestamp}"))?;
3848 self.state
3849 .realtime_session_logger
3850 .write_line("# ----------------------------------------")?;
3851 self.state.realtime_session_logger.write_line("")?;
3852 }
3853
3854 Ok(file_path)
3855 }
3856
3857 fn handle_ctrl_c(&mut self) -> Vec<Action> {
3859 if self.state.ui.focus.current_panel == crate::action::PanelType::EbpfInfo
3861 && self.state.ebpf_panel.is_expanded()
3862 {
3863 self.state.ebpf_panel.close_expanded();
3864 self.state.expecting_second_ctrl_c = false;
3866 return vec![];
3867 }
3868 let is_double_press = self.state.expecting_second_ctrl_c;
3870
3871 self.state.expecting_second_ctrl_c = true;
3873
3874 if is_double_press {
3876 tracing::info!("Double Ctrl+C detected, quitting application");
3877 return vec![Action::Quit];
3878 }
3879
3880 match self.state.ui.focus.current_panel {
3882 crate::action::PanelType::InteractiveCommand => {
3883 if self.state.command_panel.is_in_history_search() {
3885 self.state.command_panel.exit_history_search();
3887 self.state.command_panel.input_text.clear();
3888 self.state.command_panel.cursor_position = 0;
3889 vec![]
3891 } else {
3892 match self.state.command_panel.mode {
3893 crate::model::panel_state::InteractionMode::ScriptEditor => {
3894 vec![Action::ExitScriptMode]
3896 }
3897 crate::model::panel_state::InteractionMode::Input => {
3898 self.state.command_panel.input_text.clear();
3900 self.state.command_panel.cursor_position = 0;
3901 self.state.command_panel.input_text = "quit".to_string();
3902 self.state.command_panel.cursor_position = 4;
3903 self.state.command_panel.auto_suggestion.clear();
3905 vec![]
3908 }
3909 _ => {
3910 vec![]
3912 }
3913 }
3914 }
3915 }
3916 crate::action::PanelType::Source => {
3917 if self.state.source_panel.mode
3918 == crate::model::panel_state::SourcePanelMode::FileSearch
3919 {
3920 vec![Action::ExitFileSearch]
3922 } else {
3923 vec![]
3925 }
3926 }
3927 _ => {
3928 vec![]
3930 }
3931 }
3932 }
3933
3934 async fn cleanup(&mut self) -> Result<()> {
3936 disable_raw_mode()?;
3937 execute!(self.terminal.backend_mut(), DisableBracketedPaste)?;
3939 execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
3940 self.terminal.show_cursor()?;
3942 Ok(())
3943 }
3944}