1use crate::action::ResponseType;
2use crate::events::UiTraceEvent;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::time::Instant;
5
6#[derive(Debug, Clone)]
8pub struct CachedTraceEvent {
9 pub event: UiTraceEvent,
10 pub formatted_timestamp: String,
11}
12
13#[derive(Debug, Clone)]
15pub struct SourcePanelState {
16 pub content: Vec<String>,
17 pub current_line: Option<usize>,
18 pub cursor_line: usize,
19 pub cursor_col: usize,
20 pub scroll_offset: usize,
21 pub horizontal_scroll_offset: usize,
22 pub file_path: Option<String>,
23 pub language: String,
24 pub area_height: u16,
25 pub area_width: u16,
26
27 pub search_query: String,
29 pub search_matches: Vec<(usize, usize, usize)>, pub current_match: Option<usize>,
31 pub is_searching: bool,
32
33 pub file_search_query: String,
37 pub file_search_cursor_pos: usize, pub file_search_filtered_indices: Vec<usize>,
39 pub file_search_selected: usize,
40 pub file_search_scroll: usize,
41 pub file_search_message: Option<String>,
42 pub is_file_searching: bool,
43
44 pub number_buffer: String,
46 pub expecting_g: bool,
47 pub g_pressed: bool,
48
49 pub mode: SourcePanelMode,
51
52 pub traced_lines: HashSet<usize>, pub disabled_lines: HashSet<usize>, pub pending_trace_line: Option<usize>, pub trace_locations: HashMap<u32, (String, usize)>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum SourcePanelMode {
62 Normal,
63 TextSearch,
64 FileSearch,
65}
66
67impl SourcePanelState {
68 pub fn new() -> Self {
69 Self {
70 content: vec!["// No source code loaded".to_string()],
71 current_line: None,
72 cursor_line: 0,
73 cursor_col: 0,
74 scroll_offset: 0,
75 horizontal_scroll_offset: 0,
76 file_path: None,
77 language: "c".to_string(),
78 area_height: 10,
79 area_width: 80,
80 search_query: String::new(),
81 search_matches: Vec::new(),
82 current_match: None,
83 is_searching: false,
84 file_search_query: String::new(),
85 file_search_cursor_pos: 0,
86 file_search_filtered_indices: Vec::new(),
87 file_search_selected: 0,
88 file_search_scroll: 0,
89 file_search_message: None,
90 is_file_searching: false,
91 number_buffer: String::new(),
92 expecting_g: false,
93 g_pressed: false,
94 mode: SourcePanelMode::Normal,
95 traced_lines: HashSet::new(),
96 disabled_lines: HashSet::new(),
97 pending_trace_line: None,
98 trace_locations: HashMap::new(),
99 }
100 }
101}
102
103impl Default for SourcePanelState {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[derive(Debug)]
111pub struct EbpfPanelState {
112 pub trace_events: VecDeque<CachedTraceEvent>,
113 pub scroll_offset: usize,
114 pub max_messages: usize,
115 pub auto_scroll: bool,
116 pub cursor_trace_index: usize, pub show_cursor: bool, pub display_mode: DisplayMode, pub next_message_id: u64, pub numeric_prefix: Option<String>,
122 pub g_pressed: bool, pub view_mode: EbpfViewMode,
125 pub expanded_scroll: usize, pub last_inner_height: usize, }
128
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub enum DisplayMode {
131 AutoRefresh, Scroll, }
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum EbpfViewMode {
137 List,
138 Expanded { index: usize, scroll: usize },
139}
140
141impl EbpfPanelState {
142 pub fn new() -> Self {
143 Self::new_with_max_messages(2000)
144 }
145
146 pub fn new_with_max_messages(max_messages: usize) -> Self {
147 Self {
148 trace_events: VecDeque::new(),
149 scroll_offset: 0,
150 max_messages,
151 auto_scroll: true,
152 cursor_trace_index: 0,
153 show_cursor: false,
154 display_mode: DisplayMode::AutoRefresh,
155 next_message_id: 1,
156 numeric_prefix: None,
157 g_pressed: false,
158 view_mode: EbpfViewMode::List,
159 expanded_scroll: 0,
160 last_inner_height: 0,
161 }
162 }
163
164 pub fn add_trace_event(&mut self, trace_event: UiTraceEvent) {
165 let formatted_timestamp = crate::utils::format_timestamp_ns(trace_event.timestamp);
167 let cached_event = CachedTraceEvent {
168 event: trace_event,
169 formatted_timestamp,
170 };
171
172 self.trace_events.push_back(cached_event);
173 if self.trace_events.len() > self.max_messages {
174 self.trace_events.pop_front();
175 }
176
177 if self.display_mode == DisplayMode::AutoRefresh {
179 self.scroll_to_bottom();
180 }
181 }
182
183 pub fn runtime_warning_event(message: String, timestamp: u64) -> UiTraceEvent {
184 UiTraceEvent::text_event(0, timestamp, 0, 0, message, Some(1))
185 }
186
187 pub fn add_runtime_warning_message(&mut self, message: String, timestamp: u64) {
188 self.add_trace_event(Self::runtime_warning_event(message, timestamp));
189 }
190
191 pub fn scroll_up(&mut self) {
192 if self.scroll_offset > 0 {
193 self.scroll_offset -= 1;
194 self.auto_scroll = false;
195 }
196 }
197
198 pub fn scroll_down(&mut self) {
199 let total_lines = self.trace_events.len();
200 if self.scroll_offset + 1 < total_lines {
201 self.scroll_offset += 1;
202 } else {
203 self.auto_scroll = true;
204 }
205 }
206
207 pub fn scroll_to_bottom(&mut self) {
208 self.scroll_offset = 0;
210 self.auto_scroll = true;
211 self.show_cursor = false;
212 }
213
214 pub fn move_cursor_up(&mut self) {
215 self.enter_scroll_mode();
216 if self.cursor_trace_index > 0 {
217 self.cursor_trace_index -= 1;
218 }
219 }
220
221 pub fn move_cursor_down(&mut self) {
222 self.enter_scroll_mode();
223 if self.cursor_trace_index + 1 < self.trace_events.len() {
224 self.cursor_trace_index += 1;
225 }
226 }
227
228 pub fn move_cursor_up_10(&mut self) {
229 self.enter_scroll_mode();
230 self.cursor_trace_index = self.cursor_trace_index.saturating_sub(10);
231 }
232
233 pub fn move_cursor_down_10(&mut self) {
234 self.enter_scroll_mode();
235 let max_index = self.trace_events.len().saturating_sub(1);
236 self.cursor_trace_index = (self.cursor_trace_index + 10).min(max_index);
237 }
238
239 pub fn jump_to_first(&mut self) {
241 self.enter_scroll_mode();
242 self.cursor_trace_index = 0;
243 }
244
245 pub fn jump_to_last(&mut self) {
247 self.enter_scroll_mode();
248 self.cursor_trace_index = self.trace_events.len().saturating_sub(1);
249 }
250
251 pub fn push_numeric_digit(&mut self, ch: char) {
253 if ch.is_ascii_digit() {
254 self.enter_scroll_mode();
255 let s = self.numeric_prefix.get_or_insert_with(String::new);
256 if s.len() < 9 {
257 s.push(ch);
258 }
259 self.g_pressed = false;
261 }
262 }
263
264 pub fn confirm_goto(&mut self) {
266 if let Some(s) = self.numeric_prefix.take() {
267 if let Ok(num) = s.parse::<u64>() {
268 self.jump_to_message_number(num);
269 return;
270 }
271 }
272 self.jump_to_last();
273 }
274
275 pub fn jump_to_message_number(&mut self, message_number: u64) {
277 self.enter_scroll_mode();
278 if self.trace_events.is_empty() {
279 self.cursor_trace_index = 0;
280 return;
281 }
282
283 if message_number == 0 {
286 self.cursor_trace_index = 0;
287 return;
288 }
289
290 let target_index = (message_number - 1) as usize;
291 self.cursor_trace_index = target_index.min(self.trace_events.len().saturating_sub(1));
292 }
293
294 pub fn exit_to_auto_refresh(&mut self) {
296 self.numeric_prefix = None;
297 self.g_pressed = false;
298 self.hide_cursor();
299 self.scroll_to_bottom();
300 }
301
302 pub fn handle_g_key(&mut self) {
304 self.enter_scroll_mode();
305 if self.g_pressed {
306 self.g_pressed = false;
307 self.jump_to_first();
308 } else {
309 self.g_pressed = true;
310 }
311 }
312
313 fn enter_scroll_mode(&mut self) {
315 if self.display_mode != DisplayMode::Scroll {
316 self.display_mode = DisplayMode::Scroll;
317 self.show_cursor = true;
318 self.auto_scroll = false;
319 self.cursor_trace_index = self.trace_events.len().saturating_sub(1);
321 }
322 }
323
324 pub fn hide_cursor(&mut self) {
325 self.display_mode = DisplayMode::AutoRefresh;
326 self.show_cursor = false;
327 self.auto_scroll = true;
328 }
329
330 pub fn open_expanded_current(&mut self) {
332 if self.cursor_trace_index < self.trace_events.len() {
333 self.view_mode = EbpfViewMode::Expanded {
334 index: self.cursor_trace_index,
335 scroll: 0,
336 };
337 self.expanded_scroll = 0;
338 }
339 }
340
341 pub fn close_expanded(&mut self) {
342 self.view_mode = EbpfViewMode::List;
343 self.expanded_scroll = 0;
344 }
345
346 pub fn is_expanded(&self) -> bool {
347 matches!(self.view_mode, EbpfViewMode::Expanded { .. })
348 }
349
350 pub fn expanded_index(&self) -> Option<usize> {
351 if let EbpfViewMode::Expanded { index, .. } = self.view_mode {
352 Some(index)
353 } else {
354 None
355 }
356 }
357
358 pub fn set_expanded_scroll(&mut self, value: usize) {
359 self.expanded_scroll = value;
360 if let EbpfViewMode::Expanded { index, .. } = self.view_mode {
361 self.view_mode = EbpfViewMode::Expanded {
362 index,
363 scroll: self.expanded_scroll,
364 };
365 }
366 }
367
368 pub fn scroll_expanded_up(&mut self, lines: usize) {
369 let new_off = self.expanded_scroll.saturating_sub(lines);
370 self.set_expanded_scroll(new_off);
371 }
372
373 pub fn scroll_expanded_down(&mut self, lines: usize, max_lines: usize) {
374 let new_off = (self.expanded_scroll + lines).min(max_lines);
375 self.set_expanded_scroll(new_off);
376 }
377}
378
379impl Default for EbpfPanelState {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[derive(Debug)]
387pub struct CommandPanelState {
388 pub input_text: String,
390 pub cursor_position: usize,
391
392 pub mode: InteractionMode,
394 pub input_state: InputState,
395
396 pub command_history: Vec<CommandHistoryItem>,
398 pub history_index: Option<usize>,
399 pub unsent_input_backup: Option<String>,
400
401 pub script_cache: Option<ScriptCache>,
403
404 pub command_cursor_line: usize,
406 pub command_cursor_column: usize,
407 pub cached_panel_width: u16, pub file_completion_cache:
411 Option<crate::components::command_panel::file_completion::FileCompletionCache>,
412
413 pub saved_input_cursor: usize, pub saved_script_cursor: Option<(usize, usize)>, pub previous_mode: Option<InteractionMode>, pub static_lines: Vec<StaticTextLine>,
420 pub scroll_offset: usize,
421 pub styled_buffer: Option<Vec<ratatui::text::Line<'static>>>,
422 pub styled_at_history_index: Option<usize>,
423
424 pub jk_escape_state: JkEscapeState,
426 pub jk_timer: Option<Instant>,
427
428 pub max_history_items: usize,
430
431 pub command_history_manager: crate::components::command_panel::CommandHistory,
433 pub history_search: crate::components::command_panel::HistorySearchState,
434 pub auto_suggestion: crate::components::command_panel::AutoSuggestionState,
435
436 pub batch_loading: Option<BatchLoadingState>,
438}
439
440#[derive(Debug, Clone)]
442pub struct BatchLoadingState {
443 pub filename: String,
444 pub total_count: usize,
445 pub completed_count: usize,
446 pub success_count: usize,
447 pub failed_count: usize,
448 pub disabled_count: usize,
449 pub details: Vec<crate::events::TraceLoadDetail>,
450 requested_enabled: VecDeque<bool>,
451}
452
453impl BatchLoadingState {
454 pub fn new(filename: String, traces: &[crate::events::TraceDefinition]) -> Self {
455 Self {
456 filename,
457 total_count: traces.len(),
458 completed_count: 0,
459 success_count: 0,
460 failed_count: 0,
461 disabled_count: 0,
462 details: Vec::new(),
463 requested_enabled: traces.iter().map(|trace| trace.enabled).collect(),
464 }
465 }
466
467 pub fn record_script_compilation(&mut self, details: &crate::events::ScriptCompilationDetails) {
468 self.completed_count += 1;
469
470 let requested_enabled = self.requested_enabled.pop_front().unwrap_or(true);
471 self.success_count += details.success_count;
472 self.failed_count += details.failed_count;
473
474 let mut trace_ids = details.trace_ids.iter().copied();
475 for result in &details.results {
476 match &result.status {
477 crate::events::ExecutionStatus::Success => {
478 let trace_id = trace_ids.next();
479 let status = if requested_enabled {
480 crate::events::LoadStatus::Created
481 } else {
482 self.disabled_count += 1;
483 crate::events::LoadStatus::CreatedDisabled
484 };
485
486 self.details.push(crate::events::TraceLoadDetail {
487 target: result.target_name.clone(),
488 trace_id,
489 status,
490 error: None,
491 });
492 }
493 crate::events::ExecutionStatus::Failed(error) => {
494 self.details.push(crate::events::TraceLoadDetail {
495 target: result.target_name.clone(),
496 trace_id: None,
497 status: crate::events::LoadStatus::Failed,
498 error: Some(error.clone()),
499 });
500 }
501 crate::events::ExecutionStatus::Skipped(_) => {}
502 }
503 }
504 }
505}
506
507#[derive(Debug, Clone, Copy, PartialEq)]
508pub enum InteractionMode {
509 Input, Command, ScriptEditor, }
513
514#[derive(Debug, Clone, PartialEq)]
515pub enum JkEscapeState {
516 None, J, }
519
520#[derive(Debug, Clone, PartialEq)]
521pub enum InputState {
522 Ready, WaitingResponse {
524 command: String,
526 sent_time: Instant,
527 command_type: CommandType,
528 },
529 ScriptEditor, }
531
532#[derive(Debug, Clone, PartialEq)]
533pub enum CommandType {
534 Script,
535 Enable { trace_id: u32 },
536 Disable { trace_id: u32 },
537 Delete { trace_id: u32 },
538 EnableAll,
539 DisableAll,
540 DeleteAll,
541 InfoFunction { target: String, verbose: bool },
542 InfoLine { target: String, verbose: bool },
543 InfoAddress { target: String, verbose: bool },
544 InfoTrace { trace_id: Option<u32> },
545 InfoTraceAll,
546 InfoSource,
547 InfoShare,
548 InfoShareAll,
549 InfoFile,
550 SaveTraces,
551 LoadTraces,
552 SrcPath,
553 SrcPathAdd,
554 SrcPathMap,
555 SrcPathRemove,
556 SrcPathClear,
557 SrcPathReset,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq)]
561pub enum ScriptStatus {
562 Draft, Submitted, Error, }
566
567#[derive(Debug, Clone)]
568pub struct SavedScript {
569 pub content: String, pub cursor_line: usize, pub cursor_col: usize, }
573
574#[derive(Debug, Clone)]
575pub struct ScriptCache {
576 pub target: String, pub original_command: String, pub selected_index: Option<usize>, pub lines: Vec<String>, pub cursor_line: usize, pub cursor_col: usize, pub status: ScriptStatus, pub saved_scripts: HashMap<String, SavedScript>, }
585
586#[derive(Debug, Clone)]
587pub struct CommandHistoryItem {
588 pub command: String,
589 pub response: Option<String>,
590 pub response_styled: Option<Vec<ratatui::text::Line<'static>>>,
591 pub timestamp: std::time::Instant,
592 pub prompt: String,
593 pub response_type: Option<ResponseType>,
594}
595
596#[derive(Debug, Clone)]
597pub struct StaticTextLine {
598 pub content: String,
599 pub line_type: LineType,
600 pub history_index: Option<usize>,
601 pub response_type: Option<ResponseType>,
602 pub styled_content: Option<ratatui::text::Line<'static>>,
604}
605
606#[derive(Debug, Clone, Copy, PartialEq)]
607pub enum LineType {
608 Command,
609 Response,
610 RuntimeAlert,
611 CurrentInput,
612 Welcome,
613}
614
615impl CommandPanelState {
616 pub fn new() -> Self {
617 Self::new_with_config(&crate::model::ui_state::HistoryConfig::default())
618 }
619
620 pub fn new_with_config(history_config: &crate::model::ui_state::HistoryConfig) -> Self {
621 Self {
622 input_text: String::new(),
623 cursor_position: 0,
624 mode: InteractionMode::Input,
625 input_state: InputState::Ready,
626 command_history: Vec::new(),
627 history_index: None,
628 unsent_input_backup: None,
629 script_cache: None,
630 command_cursor_line: 0,
631 command_cursor_column: 0,
632 cached_panel_width: 80, file_completion_cache: None,
634 saved_input_cursor: 0,
635 saved_script_cursor: None,
636 previous_mode: None,
637 static_lines: Vec::new(),
638 scroll_offset: 0,
639 styled_buffer: None,
640 styled_at_history_index: None,
641 jk_escape_state: JkEscapeState::None,
642 jk_timer: None,
643 max_history_items: history_config.max_entries,
644
645 command_history_manager:
647 crate::components::command_panel::CommandHistory::new_with_config(history_config),
648 history_search: crate::components::command_panel::HistorySearchState::new(),
649 auto_suggestion: crate::components::command_panel::AutoSuggestionState::new(),
650 batch_loading: None,
651 }
652 }
653
654 pub fn update_panel_width(&mut self, width: u16) {
656 self.cached_panel_width = width;
657 }
658
659 pub fn reflow_command_cursor_after_width_change(&mut self) {
661 if self.mode != InteractionMode::Command {
662 return;
663 }
664
665 let wrapped = self.get_command_mode_wrapped_lines(self.cached_panel_width);
666 if wrapped.is_empty() {
667 self.command_cursor_line = 0;
668 self.command_cursor_column = 0;
669 return;
670 }
671
672 if self.command_cursor_line >= wrapped.len() {
673 self.command_cursor_line = wrapped.len().saturating_sub(1);
674 }
675
676 let line_len = wrapped[self.command_cursor_line].chars().count();
678 if self.command_cursor_column > line_len {
679 self.command_cursor_column = line_len;
680 }
681 }
682
683 pub fn remap_command_cursor_on_width_change(&mut self, old_width: u16, new_width: u16) {
685 if self.mode != InteractionMode::Command {
686 return;
687 }
688
689 let logical_lines = self.get_command_mode_lines();
691 if logical_lines.is_empty() {
692 self.command_cursor_line = 0;
693 self.command_cursor_column = 0;
694 return;
695 }
696
697 let mut acc_old = 0usize;
699 let mut target_logical_idx = 0usize;
700 let mut logical_char_offset = 0usize;
701 let mut found = false;
702
703 for (i, line) in logical_lines.iter().enumerate() {
704 let wraps_old = self.wrap_text_unicode(line, old_width);
705 let wrap_count = wraps_old.len();
706 if self.command_cursor_line < acc_old + wrap_count {
707 let within = self.command_cursor_line - acc_old;
708 let offset: usize = wraps_old
710 .iter()
711 .take(within)
712 .map(|s| s.chars().count())
713 .sum();
714 let this_len = wraps_old[within].chars().count();
715 let col = self.command_cursor_column.min(this_len);
716 logical_char_offset = offset + col;
717 target_logical_idx = i;
718 found = true;
719 break;
720 }
721 acc_old += wrap_count;
722 }
723
724 if !found {
725 self.cached_panel_width = new_width;
727 self.reflow_command_cursor_after_width_change();
728 return;
729 }
730
731 let mut acc_new = 0usize;
733 for (i, line) in logical_lines.iter().enumerate() {
734 let wraps_new = self.wrap_text_unicode(line, new_width);
735 if i == target_logical_idx {
736 let mut remaining = logical_char_offset;
737 let mut widx = 0usize;
738 while widx < wraps_new.len() {
739 let seg_len = wraps_new[widx].chars().count();
740 if remaining <= seg_len {
741 self.command_cursor_line = acc_new + widx;
742 self.command_cursor_column = remaining.min(seg_len);
743 break;
744 }
745 remaining -= seg_len;
746 widx += 1;
747 }
748
749 if widx >= wraps_new.len() {
750 self.command_cursor_line = acc_new + wraps_new.len().saturating_sub(1);
752 self.command_cursor_column =
753 wraps_new.last().map(|s| s.chars().count()).unwrap_or(0);
754 }
755 break;
756 } else {
757 acc_new += wraps_new.len();
758 }
759 }
760 }
761
762 pub fn cleanup_file_completion_cache(&mut self) {
764 if let Some(cache) = &self.file_completion_cache {
765 if cache.should_cleanup() {
766 tracing::debug!("Cleaning up unused file completion cache");
767 self.file_completion_cache = None;
768 }
769 }
770 }
771
772 pub fn enter_command_mode(&mut self, panel_width: u16) {
774 self.cached_panel_width = panel_width;
775 self.previous_mode = Some(self.mode);
776
777 match self.mode {
778 InteractionMode::Input => {
779 self.saved_input_cursor = self.cursor_position;
780 let wrapped_lines = self.get_command_mode_wrapped_lines(panel_width);
783
784 self.command_cursor_line = wrapped_lines.len().saturating_sub(1);
785 let prompt = crate::ui::strings::UIStrings::GHOSTSCOPE_PROMPT;
787 self.command_cursor_column = prompt.chars().count() + self.cursor_position;
788 }
789 InteractionMode::ScriptEditor => {
790 if let Some(ref script_cache) = self.script_cache {
791 self.saved_script_cursor =
792 Some((script_cache.cursor_line, script_cache.cursor_col));
793 let lines = self.get_command_mode_lines();
795 let script_start_line = self.get_script_start_line();
796 self.command_cursor_line = (script_start_line + 3 + script_cache.cursor_line)
798 .min(lines.len().saturating_sub(1));
799 self.command_cursor_column = script_cache.cursor_col;
800 }
801 }
802 InteractionMode::Command => {
803 return;
805 }
806 }
807
808 self.mode = InteractionMode::Command;
809 }
810
811 pub fn exit_command_mode(&mut self) {
813 if let Some(previous_mode) = self.previous_mode {
814 match previous_mode {
815 InteractionMode::Input => {
816 self.cursor_position = self.saved_input_cursor;
817 self.mode = InteractionMode::Input;
818 }
819 InteractionMode::ScriptEditor => {
820 if let Some((line, col)) = self.saved_script_cursor {
821 if let Some(ref mut script_cache) = self.script_cache {
822 script_cache.cursor_line = line;
823 script_cache.cursor_col = col;
824 }
825 }
826 self.mode = InteractionMode::ScriptEditor;
827 }
828 InteractionMode::Command => {
829 self.mode = InteractionMode::Input;
831 }
832 }
833 self.previous_mode = None;
834 } else {
835 self.mode = InteractionMode::Input;
837 }
838 }
839
840 pub fn get_total_lines(&self) -> usize {
842 let wrapped_lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
844 wrapped_lines.len()
845 }
846
847 fn get_script_start_line(&self) -> usize {
849 let mut offset = self.static_lines.len();
851
852 offset += 3; offset
854 }
855
856 pub fn get_command_mode_lines(&self) -> Vec<String> {
858 let mut lines = Vec::new();
859
860 for static_line in &self.static_lines {
862 lines.push(static_line.content.clone());
863 }
864
865 let display_mode = if self.mode == InteractionMode::Command {
868 self.previous_mode.unwrap_or(InteractionMode::Input)
869 } else {
870 self.mode
871 };
872
873 match display_mode {
874 InteractionMode::Input => {
875 if matches!(self.input_state, InputState::Ready) {
876 lines.push(format!("(ghostscope) {}", self.input_text));
877 }
878 }
879 InteractionMode::ScriptEditor => {
880 if let Some(ref script_cache) = self.script_cache {
881 lines.push(format!(
882 "🔨 Entering script mode for target: {}",
883 script_cache.target
884 ));
885 lines.push("─".repeat(50));
886 lines.push("Script Editor (Ctrl+s to submit, Esc to cancel):".to_string());
887
888 for (idx, line) in script_cache.lines.iter().enumerate() {
889 lines.push(format!("{:3} │ {}", idx + 1, line));
890 }
891 }
892 }
893 InteractionMode::Command => {
894 if matches!(self.input_state, InputState::Ready) {
896 lines.push(format!("(ghostscope) {}", self.input_text));
897 }
898 }
899 }
900
901 lines
902 }
903
904 pub fn get_command_mode_wrapped_lines(&self, available_width: u16) -> Vec<String> {
906 let logical_lines = self.get_command_mode_lines();
907 let mut wrapped_lines = Vec::new();
908
909 for logical_line in logical_lines {
910 let wrapped = self.wrap_text_unicode(&logical_line, available_width);
911 wrapped_lines.extend(wrapped);
912 }
913
914 if matches!(self.previous_mode, Some(InteractionMode::Input)) {
916 let current_input_line = format!("(ghostscope) {}", self.input_text);
917
918 let should_add = if wrapped_lines.is_empty() {
920 true
921 } else {
922 !wrapped_lines.iter().any(|line| line == ¤t_input_line)
923 };
924
925 if should_add {
926 let wrapped = self.wrap_text_unicode(¤t_input_line, available_width);
927 wrapped_lines.extend(wrapped);
928 }
929 }
930
931 wrapped_lines
932 }
933
934 fn wrap_text_unicode(&self, text: &str, width: u16) -> Vec<String> {
936 use unicode_width::UnicodeWidthChar;
937
938 if width <= 2 {
939 return vec![text.to_string()];
940 }
941
942 let max_width = width as usize;
943 let mut lines = Vec::new();
944
945 for line in text.lines() {
946 let line_width: usize = line
948 .chars()
949 .map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
950 .sum();
951
952 if line_width <= max_width {
953 lines.push(line.to_string());
954 } else {
955 let mut current_line = String::new();
957 let mut current_width = 0;
958
959 for ch in line.chars() {
960 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
961
962 if current_width + char_width > max_width && !current_line.is_empty() {
963 lines.push(current_line);
965 current_line = ch.to_string();
966 current_width = char_width;
967 } else {
968 current_line.push(ch);
969 current_width += char_width;
970 }
971 }
972
973 if !current_line.is_empty() {
974 lines.push(current_line);
975 }
976 }
977 }
978
979 if lines.is_empty() {
980 lines.push(String::new());
981 }
982
983 lines
984 }
985
986 pub fn move_command_cursor_up(&mut self) {
988 if self.command_cursor_line > 0 {
989 self.command_cursor_line -= 1;
990 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
992 if self.command_cursor_line < lines.len() {
993 let line_len = lines[self.command_cursor_line].chars().count();
994 self.command_cursor_column = self.command_cursor_column.min(line_len);
995 }
996 }
997 }
998
999 pub fn move_command_cursor_down(&mut self) {
1001 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
1003 if self.command_cursor_line + 1 < lines.len() {
1004 self.command_cursor_line += 1;
1005 if self.command_cursor_line < lines.len() {
1007 let line_len = lines[self.command_cursor_line].chars().count();
1008 self.command_cursor_column = self.command_cursor_column.min(line_len);
1009 }
1010 }
1011 }
1012
1013 pub fn move_command_cursor_left(&mut self) {
1015 if self.command_cursor_column > 0 {
1016 self.command_cursor_column -= 1;
1018 } else if self.command_cursor_line > 0 {
1019 self.command_cursor_line -= 1;
1021 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
1022 if self.command_cursor_line < lines.len() {
1023 self.command_cursor_column = lines[self.command_cursor_line].chars().count();
1025 }
1026 }
1027 }
1028
1029 pub fn move_command_cursor_right(&mut self) {
1031 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
1032 if self.command_cursor_line < lines.len() {
1033 let line_len = lines[self.command_cursor_line].chars().count();
1035 if self.command_cursor_column < line_len {
1036 self.command_cursor_column += 1;
1038 } else if self.command_cursor_line + 1 < lines.len() {
1039 self.command_cursor_line += 1;
1041 self.command_cursor_column = 0;
1042 }
1043 }
1044 }
1045
1046 pub fn history_previous(&mut self) {
1048 if self.command_history.is_empty() {
1049 return;
1050 }
1051
1052 match self.history_index {
1053 None => {
1054 self.unsent_input_backup = Some(self.input_text.clone());
1056 self.history_index = Some(self.command_history.len() - 1);
1057 self.input_text = self.command_history[self.command_history.len() - 1]
1058 .command
1059 .clone();
1060 self.cursor_position = self.input_text.len();
1061 }
1062 Some(current_idx) => {
1063 if current_idx > 0 {
1064 self.history_index = Some(current_idx - 1);
1066 self.input_text = self.command_history[current_idx - 1].command.clone();
1067 self.cursor_position = self.input_text.len();
1068 }
1069 }
1071 }
1072 }
1073
1074 pub fn history_next(&mut self) {
1076 if self.command_history.is_empty() {
1077 return;
1078 }
1079
1080 match self.history_index {
1081 None => {
1082 }
1084 Some(current_idx) => {
1085 if current_idx + 1 < self.command_history.len() {
1086 self.history_index = Some(current_idx + 1);
1088 self.input_text = self.command_history[current_idx + 1].command.clone();
1089 self.cursor_position = self.input_text.len();
1090 } else {
1091 self.history_index = None;
1093 if let Some(backup) = self.unsent_input_backup.take() {
1094 self.input_text = backup;
1095 } else {
1096 self.input_text.clear();
1097 }
1098 self.cursor_position = self.input_text.len();
1099 }
1100 }
1101 }
1102 }
1103
1104 pub fn move_command_half_page_up(&mut self) {
1106 let jump_size = 10; if self.command_cursor_line >= jump_size {
1108 self.command_cursor_line -= jump_size;
1109 } else {
1110 self.command_cursor_line = 0;
1111 }
1112
1113 let lines = self.get_command_mode_lines();
1115 if self.command_cursor_line < lines.len() {
1116 let line_len = lines[self.command_cursor_line].chars().count();
1117 self.command_cursor_column = self.command_cursor_column.min(line_len);
1118 }
1119 }
1120
1121 pub fn move_command_half_page_down(&mut self) {
1123 let jump_size = 10; let total_lines = self.get_total_lines();
1125
1126 if self.command_cursor_line + jump_size < total_lines {
1127 self.command_cursor_line += jump_size;
1128 } else {
1129 self.command_cursor_line = total_lines.saturating_sub(1);
1130 }
1131
1132 let lines = self.get_command_mode_lines();
1134 if self.command_cursor_line < lines.len() {
1135 let line_len = lines[self.command_cursor_line].chars().count();
1136 self.command_cursor_column = self.command_cursor_column.min(line_len);
1137 }
1138 }
1139
1140 pub fn update_auto_suggestion(&mut self) {
1144 tracing::debug!("update_auto_suggestion: input_text='{}'", self.input_text);
1145
1146 self.auto_suggestion
1147 .update(&self.input_text, &self.command_history_manager);
1148
1149 if let Some(suggestion) = self.auto_suggestion.get_full_suggestion() {
1150 tracing::debug!("update_auto_suggestion: found suggestion='{}'", suggestion);
1151 } else {
1152 tracing::debug!("update_auto_suggestion: no suggestion found");
1153 }
1154 }
1155
1156 pub fn accept_auto_suggestion(&mut self) {
1159 if let Some(suggestion) = self.auto_suggestion.get_full_suggestion() {
1160 tracing::debug!(
1161 "accept_auto_suggestion: before - input_text='{}', cursor_position={}",
1162 self.input_text,
1163 self.cursor_position
1164 );
1165 tracing::debug!(
1166 "accept_auto_suggestion: accepting suggestion='{}'",
1167 suggestion
1168 );
1169
1170 self.input_text = suggestion.to_string();
1171 self.cursor_position = self.input_text.len();
1172 self.auto_suggestion.clear();
1173
1174 self.history_search.clear();
1176
1177 tracing::debug!(
1178 "accept_auto_suggestion: after - input_text='{}', cursor_position={}",
1179 self.input_text,
1180 self.cursor_position
1181 );
1182 } else {
1183 tracing::debug!("accept_auto_suggestion: no suggestion available");
1184 }
1185 }
1186
1187 pub fn start_history_search(&mut self) {
1189 tracing::debug!(
1190 "start_history_search: command_history.len()={}",
1191 self.command_history.len()
1192 );
1193 self.history_search.start_search();
1194 self.input_text.clear();
1196 self.cursor_position = 0;
1197 tracing::debug!(
1198 "start_history_search: after clear - command_history.len()={}",
1199 self.command_history.len()
1200 );
1201 }
1202
1203 pub fn update_history_search(&mut self, query: String) {
1205 self.history_search
1206 .update_query(query, &self.command_history_manager);
1207 }
1210
1211 pub fn next_history_match(&mut self) {
1213 self.history_search
1216 .next_match(&self.command_history_manager);
1217 }
1218
1219 pub fn exit_history_search(&mut self) {
1221 self.history_search.clear();
1222 self.auto_suggestion.clear();
1223 }
1224
1225 pub fn exit_history_search_with_selection(&mut self, selected_command: &str) {
1227 self.input_text = selected_command.to_string();
1228 self.cursor_position = self.input_text.len();
1229 self.history_search.clear();
1230 self.auto_suggestion.clear();
1231 }
1232
1233 pub fn add_command_to_history(&mut self, command: &str) {
1235 self.command_history_manager.add_command(command);
1236 }
1237
1238 pub fn add_command_entry(&mut self, command: &str) {
1241 use std::time::Instant;
1242
1243 self.command_history_manager.add_command(command);
1245
1246 let item = CommandHistoryItem {
1248 command: command.to_string(),
1249 response: None, response_styled: None,
1251 timestamp: Instant::now(),
1252 prompt: "(ghostscope) ".to_string(),
1253 response_type: None,
1254 };
1255 self.command_history.push(item);
1256
1257 tracing::debug!(
1258 "add_command_entry: Added command '{}', history length: {}",
1259 command,
1260 self.command_history.len()
1261 );
1262
1263 const MAX_HISTORY: usize = 1000;
1265 if self.command_history.len() > MAX_HISTORY {
1266 self.command_history.remove(0);
1267 }
1268
1269 crate::components::command_panel::ResponseFormatter::update_static_lines(self);
1271 }
1272
1273 pub fn is_in_history_search(&self) -> bool {
1275 self.history_search.is_active
1276 }
1277
1278 pub fn get_history_search_query(&self) -> &str {
1280 &self.history_search.query
1281 }
1282
1283 pub fn add_styled_welcome_lines(
1287 &mut self,
1288 styled_lines: Vec<ratatui::text::Line<'static>>,
1289 response_type: ResponseType,
1290 ) {
1291 self.static_lines
1293 .retain(|line| !matches!(line.line_type, LineType::Welcome));
1294
1295 for styled_line in styled_lines {
1296 let content: String = styled_line
1298 .spans
1299 .iter()
1300 .map(|span| span.content.as_ref())
1301 .collect();
1302
1303 self.static_lines.push(StaticTextLine {
1304 content,
1305 line_type: LineType::Welcome,
1306 history_index: None,
1307 response_type: Some(response_type),
1308 styled_content: Some(styled_line),
1309 });
1310 }
1311 self.styled_buffer = None;
1313 self.styled_at_history_index = None;
1314 }
1315
1316 pub fn get_suggestion_text(&self) -> Option<&str> {
1318 self.auto_suggestion.get_suggestion_text()
1319 }
1320
1321 pub fn get_display_text(&self) -> &str {
1325 if self.is_in_history_search() {
1326 if let Some(matched_command) = self
1328 .history_search
1329 .current_match(&self.command_history_manager)
1330 {
1331 return matched_command;
1332 }
1333 }
1334 &self.input_text
1337 }
1338
1339 pub fn get_display_cursor_position(&self) -> usize {
1342 let result = if self.is_in_history_search() {
1343 self.history_search.query.len()
1345 } else {
1346 self.cursor_position
1348 };
1349
1350 tracing::debug!("get_display_cursor_position: is_in_history_search={}, input_text='{}', cursor_position={}, result={}",
1351 self.is_in_history_search(), self.input_text, self.cursor_position, result);
1352 result
1353 }
1354}
1355
1356impl Default for CommandPanelState {
1357 fn default() -> Self {
1358 Self::new()
1359 }
1360}
1361
1362#[cfg(test)]
1363mod tests {
1364 use super::*;
1365 use crate::events::{
1366 ExecutionStatus, LoadStatus, ScriptCompilationDetails, ScriptExecutionResult,
1367 TraceDefinition,
1368 };
1369
1370 #[test]
1371 fn runtime_warning_message_is_added_to_ebpf_panel() {
1372 let mut state = EbpfPanelState::new_with_max_messages(10);
1373
1374 state.add_runtime_warning_message(
1375 "Warning: TUI trace queue saturated; dropped 7 events before display".to_string(),
1376 123_456,
1377 );
1378
1379 let cached = state.trace_events.back().expect("warning event");
1380 assert_eq!(cached.event.trace_id, 0);
1381 assert_eq!(cached.event.timestamp, 123_456);
1382 assert_eq!(cached.event.pid, 0);
1383 assert_eq!(cached.event.tid, 0);
1384 assert_eq!(
1385 cached.event.to_formatted_output(),
1386 vec!["Warning: TUI trace queue saturated; dropped 7 events before display".to_string()]
1387 );
1388 }
1389
1390 fn trace_definition(target: &str, enabled: bool) -> TraceDefinition {
1391 TraceDefinition {
1392 target: target.to_string(),
1393 script: "print value".to_string(),
1394 enabled,
1395 selected_index: None,
1396 }
1397 }
1398
1399 fn successful_compilation(trace_id: u32, target: &str) -> ScriptCompilationDetails {
1400 ScriptCompilationDetails {
1401 trace_ids: vec![trace_id],
1402 results: vec![ScriptExecutionResult {
1403 pc_address: 0x1234,
1404 target_name: target.to_string(),
1405 binary_path: "/tmp/app".to_string(),
1406 status: ExecutionStatus::Success,
1407 source_file: None,
1408 source_line: None,
1409 is_inline: None,
1410 }],
1411 total_count: 1,
1412 success_count: 1,
1413 failed_count: 0,
1414 }
1415 }
1416
1417 fn failed_compilation(target: &str, error: &str) -> ScriptCompilationDetails {
1418 ScriptCompilationDetails {
1419 trace_ids: vec![],
1420 results: vec![ScriptExecutionResult {
1421 pc_address: 0x1234,
1422 target_name: target.to_string(),
1423 binary_path: "/tmp/app".to_string(),
1424 status: ExecutionStatus::Failed(error.to_string()),
1425 source_file: None,
1426 source_line: None,
1427 is_inline: None,
1428 }],
1429 total_count: 1,
1430 success_count: 0,
1431 failed_count: 1,
1432 }
1433 }
1434
1435 #[test]
1436 fn batch_loading_records_disabled_trace_requests() {
1437 let traces = vec![
1438 trace_definition("disabled_target", false),
1439 trace_definition("enabled_target", true),
1440 ];
1441 let mut batch = BatchLoadingState::new("saved.gs".to_string(), &traces);
1442
1443 batch.record_script_compilation(&successful_compilation(42, "disabled_target"));
1444 batch.record_script_compilation(&successful_compilation(43, "enabled_target"));
1445
1446 assert_eq!(batch.completed_count, 2);
1447 assert_eq!(batch.success_count, 2);
1448 assert_eq!(batch.failed_count, 0);
1449 assert_eq!(batch.disabled_count, 1);
1450 assert_eq!(batch.details.len(), 2);
1451 assert!(matches!(
1452 batch.details[0].status,
1453 LoadStatus::CreatedDisabled
1454 ));
1455 assert_eq!(batch.details[0].trace_id, Some(42));
1456 assert!(matches!(batch.details[1].status, LoadStatus::Created));
1457 assert_eq!(batch.details[1].trace_id, Some(43));
1458 }
1459
1460 #[test]
1461 fn batch_loading_does_not_report_failed_disabled_restore_as_disabled() {
1462 let traces = vec![trace_definition("disabled_target", false)];
1463 let mut batch = BatchLoadingState::new("saved.gs".to_string(), &traces);
1464
1465 batch.record_script_compilation(&failed_compilation(
1466 "disabled_target",
1467 "Failed to restore disabled trace #42; trace remains active",
1468 ));
1469
1470 assert_eq!(batch.completed_count, 1);
1471 assert_eq!(batch.success_count, 0);
1472 assert_eq!(batch.failed_count, 1);
1473 assert_eq!(batch.disabled_count, 0);
1474 assert_eq!(batch.details.len(), 1);
1475 assert!(matches!(batch.details[0].status, LoadStatus::Failed));
1476 assert_eq!(batch.details[0].trace_id, None);
1477 }
1478}