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