1use crossterm::event::{KeyEvent, MouseEvent};
2use ghostscope_protocol::ParsedTraceEvent;
3use tokio::sync::mpsc;
4use unicode_width::UnicodeWidthStr;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum TraceStatus {
9 Active,
10 Disabled,
11 Failed,
12}
13
14impl std::fmt::Display for TraceStatus {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 TraceStatus::Active => write!(f, "Active"),
18 TraceStatus::Disabled => write!(f, "Disabled"),
19 TraceStatus::Failed => write!(f, "Failed"),
20 }
21 }
22}
23
24impl TraceStatus {
25 pub fn to_emoji(&self) -> String {
27 match self {
28 TraceStatus::Active => "β
".to_string(),
29 TraceStatus::Disabled => "βΈοΈ".to_string(),
30 TraceStatus::Failed => "β".to_string(),
31 }
32 }
33
34 pub fn from_string(s: &str) -> Self {
36 match s {
37 "Active" => TraceStatus::Active,
38 "Disabled" => TraceStatus::Disabled,
39 "Failed" => TraceStatus::Failed,
40 _ => TraceStatus::Failed, }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub enum TuiEvent {
48 Key(KeyEvent),
49 Mouse(MouseEvent),
50 Resize(u16, u16),
51 Quit,
52}
53
54#[derive(Debug)]
56pub struct EventRegistry {
57 pub command_sender: mpsc::UnboundedSender<RuntimeCommand>,
59
60 pub trace_receiver: mpsc::Receiver<ParsedTraceEvent>,
62 pub status_receiver: mpsc::UnboundedReceiver<RuntimeStatus>,
63}
64
65#[derive(Debug, Clone)]
67pub struct SourceCodeInfo {
68 pub file_path: String,
69 pub current_line: Option<usize>,
70}
71
72#[derive(Debug, Clone)]
74pub struct TargetDebugInfo {
75 pub target: String,
76 pub target_type: TargetType,
77 pub file_path: Option<String>,
78 pub line_number: Option<u32>,
79 pub function_name: Option<String>,
80 pub modules: Vec<ModuleDebugInfo>, }
82
83impl TargetDebugInfo {
84 pub fn format_for_display(&self, verbose: bool) -> String {
86 let mut result = String::new();
87
88 let module_count = self.modules.len();
90 let total_addresses: usize = self
91 .modules
92 .iter()
93 .map(|module| module.address_mappings.len())
94 .sum();
95
96 let header_prefix = match self.target_type {
98 TargetType::Function => "π§ Function Debug Info",
99 TargetType::SourceLocation => "π Line Debug Info",
100 TargetType::Address => "π Address Debug Info",
101 };
102 result.push_str(&format!(
103 "{header_prefix}: {} ({} modules, {} traceable addresses)\n\n",
104 self.target, module_count, total_addresses
105 ));
106
107 for (module_idx, module) in self.modules.iter().enumerate() {
109 let is_last_module = module_idx == self.modules.len() - 1;
110 result.push_str(&module.format_for_display(
111 is_last_module,
112 &self.file_path,
113 self.line_number,
114 verbose,
115 ));
116 }
117
118 if let TargetType::Address = self.target_type {
120 let example_addr = self
122 .modules
123 .iter()
124 .flat_map(|m| m.address_mappings.iter())
125 .map(|m| m.address)
126 .next();
127 if let Some(addr) = example_addr {
128 result.push_str("\nπ‘ Tips:\n");
129 result.push_str(&format!(
130 " - In '-t <module>' mode: use `trace 0x{addr:x} {{ ... }}` (defaults to that module)\n"
131 ));
132 result.push_str(&format!(
133 " - In '-p <pid>' mode: default module is the main executable; for library addresses, start GhostScope with '-t <that .so>' then use `trace 0x{addr:x} {{ ... }}`\n"
134 ));
135 }
136 }
137
138 result
139 }
140
141 pub fn format_for_display_styled(&self, verbose: bool) -> Vec<ratatui::text::Line<'static>> {
143 use crate::components::command_panel::style_builder::StyledLineBuilder;
144 use ratatui::text::Line;
145
146 let mut lines = Vec::new();
147
148 let total_addresses: usize = self.modules.iter().map(|m| m.address_mappings.len()).sum();
150 let header_prefix = match self.target_type {
151 TargetType::Function => "π§ Function Debug Info",
152 TargetType::SourceLocation => "π Line Debug Info",
153 TargetType::Address => "π Address Debug Info",
154 };
155 lines.push(
156 StyledLineBuilder::new()
157 .title(format!(
158 "{header_prefix}: {} ({} modules, {} addresses)",
159 self.target,
160 self.modules.len(),
161 total_addresses
162 ))
163 .build(),
164 );
165 lines.push(Line::from(""));
166
167 for (idx, module) in self.modules.iter().enumerate() {
168 let is_last = idx + 1 == self.modules.len();
169 lines.extend(module.format_for_display_styled(
170 is_last,
171 &self.file_path,
172 self.line_number,
173 verbose,
174 ));
175 }
176
177 if let TargetType::Address = self.target_type {
179 if let Some(addr) = self
181 .modules
182 .iter()
183 .flat_map(|m| m.address_mappings.iter())
184 .map(|m| m.address)
185 .next()
186 {
187 lines.push(Line::from(""));
188 lines.push(
189 StyledLineBuilder::new()
190 .styled(
191 "π‘ Tips:",
192 crate::components::command_panel::style_builder::StylePresets::SECTION,
193 )
194 .build(),
195 );
196 lines.push(
197 StyledLineBuilder::new()
198 .text(" - In '-t <module>' mode: use ")
199 .value(format!("trace 0x{addr:x} {{ ... }}"))
200 .text(" (defaults to that module)")
201 .build(),
202 );
203 lines.push(
204 StyledLineBuilder::new()
205 .text(" - In '-p <pid>' mode: default module is main executable; for library addresses, start with '-t <that .so>' then use ")
206 .value(format!("trace 0x{addr:x} {{ ... }}"))
207 .build(),
208 );
209 }
210 }
211
212 lines
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct ModuleDebugInfo {
219 pub binary_path: String,
220 pub address_mappings: Vec<AddressMapping>,
221}
222
223impl ModuleDebugInfo {
224 pub fn format_for_display(
226 &self,
227 is_last_module: bool,
228 source_file: &Option<String>,
229 source_line: Option<u32>,
230 verbose: bool,
231 ) -> String {
232 let mut result = String::new();
233
234 result.push_str(&format!("π¦ {}", &self.binary_path));
236
237 if let Some(ref file) = source_file {
239 if let Some(line) = source_line {
240 result.push_str(&format!(" @ {file}:{line}\n"));
241 } else {
242 result.push_str(&format!(" @ {file}\n"));
243 }
244 } else {
245 result.push('\n');
246 }
247
248 for (addr_idx, mapping) in self.address_mappings.iter().enumerate() {
249 let is_last_addr = addr_idx == self.address_mappings.len() - 1;
250 let addr_prefix = match (is_last_module, is_last_addr) {
251 (true, true) => " ββ",
252 (true, false) => " ββ",
253 (false, true) => "β ββ",
254 (false, false) => "β ββ",
255 };
256
257 let mut pc_description = if let Some(i) = mapping.index {
259 format!("[{}] π― 0x{:x}", i, mapping.address)
260 } else {
261 format!("π― 0x{:x}", mapping.address)
262 };
263 if let Some(is_inline) = mapping.is_inline {
264 pc_description
265 .push_str(&format!(" β {}", if is_inline { "inline" } else { "call" }));
266 }
267 if let (Some(ref file), Some(line)) = (&mapping.source_file, mapping.source_line) {
268 pc_description.push_str(&format!(" @ {file}:{line}"));
269 }
270
271 result.push_str(&format!("{addr_prefix} {pc_description}\n"));
272
273 if !mapping.parameters.is_empty() {
275 let param_prefix = match (is_last_module, is_last_addr) {
276 (true, true) => " ββ",
277 (true, false) => " β ββ",
278 (false, true) => "β ββ",
279 (false, false) => "β β ββ",
280 };
281
282 result.push_str(&format!("{param_prefix} π₯ Parameters\n"));
283
284 for (param_idx, param) in mapping.parameters.iter().enumerate() {
285 let is_last_param =
286 param_idx == mapping.parameters.len() - 1 && mapping.variables.is_empty();
287 let item_prefix = match (is_last_module, is_last_addr, is_last_param) {
288 (true, true, true) => " β ββ",
289 (true, true, false) => " β ββ",
290 (true, false, true) => " β β ββ",
291 (true, false, false) => " β β ββ",
292 (false, true, true) => "β β ββ",
293 (false, true, false) => "β β ββ",
294 (false, false, true) => "β β β ββ",
295 (false, false, false) => "β β β ββ",
296 };
297
298 let param_line = Self::format_variable_line(param, verbose);
299
300 result.push_str(&Self::wrap_long_line(
301 &format!("{item_prefix} {param_line}"),
302 80,
303 item_prefix,
304 ));
305 }
306 }
307
308 if !mapping.variables.is_empty() {
310 let var_prefix = match (is_last_module, is_last_addr) {
311 (true, true) => " ββ",
312 (true, false) => " β ββ",
313 (false, true) => "β ββ",
314 (false, false) => "β β ββ",
315 };
316
317 result.push_str(&format!("{var_prefix} π¦ Variables\n"));
318
319 for (var_idx, var) in mapping.variables.iter().enumerate() {
320 let is_last_var = var_idx == mapping.variables.len() - 1;
321 let item_prefix = match (is_last_module, is_last_addr, is_last_var) {
322 (true, true, true) => " ββ",
323 (true, true, false) => " ββ",
324 (true, false, true) => " β ββ",
325 (true, false, false) => " β ββ",
326 (false, true, true) => "β ββ",
327 (false, true, false) => "β ββ",
328 (false, false, true) => "β β ββ",
329 (false, false, false) => "β β ββ",
330 };
331
332 let var_line = Self::format_variable_line(var, verbose);
333
334 result.push_str(&Self::wrap_long_line(
335 &format!("{item_prefix} {var_line}"),
336 80,
337 item_prefix,
338 ));
339 }
340 }
341 }
342
343 result
344 }
345
346 pub fn format_variable_line(var: &VariableDebugInfo, verbose: bool) -> String {
348 let type_display = var
350 .type_pretty
351 .as_ref()
352 .filter(|pretty| !pretty.is_empty())
353 .cloned()
354 .unwrap_or_else(|| "unknown".to_string());
355
356 let name = &var.name;
357 if !verbose || var.location_description.is_empty() || var.location_description == "None" {
358 format!("{name} ({type_display})")
359 } else {
360 let location = &var.location_description;
361 format!("{name} ({type_display}) = {location}")
362 }
363 }
364
365 fn wrap_long_line(text: &str, max_width: usize, indent: &str) -> String {
367 if text.len() <= max_width {
368 format!("{text}\n")
369 } else {
370 let mut result = String::new();
371 let mut current_line = text.to_string();
372
373 while current_line.len() > max_width {
374 let break_point = current_line
375 .rfind(' ')
376 .unwrap_or(max_width.saturating_sub(10));
377 let (first_part, rest) = current_line.split_at(break_point);
378 result.push_str(&format!("{first_part}\n"));
379
380 let continuation_indent =
382 format!("{} ", indent.replace("ββ", "β ").replace("ββ", " "));
383 let trimmed_rest = rest.trim();
384 current_line = format!("{continuation_indent}{trimmed_rest}");
385 }
386
387 if !current_line.trim().is_empty() {
388 result.push_str(&format!("{current_line}\n"));
389 }
390
391 result
392 }
393 }
394}
395
396impl ModuleDebugInfo {
397 pub fn format_for_display_styled(
399 &self,
400 is_last_module: bool,
401 source_file: &Option<String>,
402 source_line: Option<u32>,
403 verbose: bool,
404 ) -> Vec<ratatui::text::Line<'static>> {
405 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
406
407 let mut lines = Vec::new();
408
409 let mut builder = StyledLineBuilder::new()
410 .styled("π¦ ", StylePresets::SECTION)
411 .styled(&self.binary_path, StylePresets::SECTION);
412
413 if let Some(ref file) = source_file {
414 builder = builder.text(" @ ").styled(
415 if let Some(line) = source_line {
416 format!("{file}:{line}")
417 } else {
418 file.clone()
419 },
420 StylePresets::LOCATION,
421 );
422 }
423
424 lines.push(builder.build());
425
426 for (addr_idx, mapping) in self.address_mappings.iter().enumerate() {
427 let is_last_addr = addr_idx + 1 == self.address_mappings.len();
428 lines.extend(mapping.format_for_display_styled(is_last_module, is_last_addr, verbose));
429 }
430
431 lines
432 }
433}
434
435#[derive(Debug, Clone)]
437pub struct AddressMapping {
438 pub address: u64,
439 pub binary_path: String, pub function_name: Option<String>,
441 pub variables: Vec<VariableDebugInfo>,
442 pub parameters: Vec<VariableDebugInfo>,
443 pub source_file: Option<String>,
444 pub source_line: Option<u32>,
445 pub is_inline: Option<bool>,
446 pub index: Option<usize>, }
448
449impl AddressMapping {
450 pub fn format_for_display_styled(
452 &self,
453 is_last_module: bool,
454 is_last_addr: bool,
455 verbose: bool,
456 ) -> Vec<ratatui::text::Line<'static>> {
457 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
458
459 let mut lines = Vec::new();
460
461 let prefix = match (is_last_module, is_last_addr) {
462 (true, true) => " ββ",
463 (true, false) => " ββ",
464 (false, true) => "β ββ",
465 (false, false) => "β ββ",
466 };
467
468 let mut header = StyledLineBuilder::new().styled(prefix, StylePresets::TREE);
470 if let Some(i) = self.index {
471 header = header
472 .text(" ")
473 .styled(format!("[{i}]"), StylePresets::ADDRESS);
474 }
475 header = header.text(" π― ").address(self.address);
476
477 if let Some(is_inline) = self.is_inline {
478 header = header
479 .text(" ")
480 .key("β")
481 .text(" ")
482 .styled(if is_inline { "inline" } else { "call" }, StylePresets::KEY);
483 }
484 if let (Some(ref file), Some(line)) = (&self.source_file, self.source_line) {
485 header = header
486 .text(" ")
487 .key("@")
488 .text(" ")
489 .value(format!("{file}:{line}"));
490 }
491
492 lines.push(header.build());
493
494 if !self.parameters.is_empty() {
495 let param_prefix = match (is_last_module, is_last_addr) {
496 (true, true) => " ββ",
497 (true, false) => " β ββ",
498 (false, true) => "β ββ",
499 (false, false) => "β β ββ",
500 };
501
502 lines.push(
503 StyledLineBuilder::new()
504 .styled(param_prefix, StylePresets::TREE)
505 .styled(" π₯ Parameters", StylePresets::SECTION)
506 .build(),
507 );
508
509 for (param_idx, param) in self.parameters.iter().enumerate() {
510 let is_last_param =
511 param_idx + 1 == self.parameters.len() && self.variables.is_empty();
512 let item_prefix = match (is_last_module, is_last_addr, is_last_param) {
513 (true, true, true) => " β ββ",
514 (true, true, false) => " β ββ",
515 (true, false, true) => " β β ββ",
516 (true, false, false) => " β β ββ",
517 (false, true, true) => "β β ββ",
518 (false, true, false) => "β β ββ",
519 (false, false, true) => "β β β ββ",
520 (false, false, false) => "β β β ββ",
521 };
522
523 lines.push(Self::format_variable_styled(item_prefix, param, verbose));
524 }
525 }
526
527 if !self.variables.is_empty() {
528 let var_prefix = match (is_last_module, is_last_addr) {
529 (true, true) => " ββ",
530 (true, false) => " β ββ",
531 (false, true) => "β ββ",
532 (false, false) => "β β ββ",
533 };
534
535 lines.push(
536 StyledLineBuilder::new()
537 .styled(var_prefix, StylePresets::TREE)
538 .styled(" π¦ Variables", StylePresets::SECTION)
539 .build(),
540 );
541
542 for (var_idx, var) in self.variables.iter().enumerate() {
543 let is_last_var = var_idx + 1 == self.variables.len();
544 let item_prefix = match (is_last_module, is_last_addr, is_last_var) {
545 (true, true, true) => " ββ",
546 (true, true, false) => " ββ",
547 (true, false, true) => " β ββ",
548 (true, false, false) => " β ββ",
549 (false, true, true) => "β ββ",
550 (false, true, false) => "β ββ",
551 (false, false, true) => "β β ββ",
552 (false, false, false) => "β β ββ",
553 };
554
555 lines.push(Self::format_variable_styled(item_prefix, var, verbose));
556 }
557 }
558
559 lines
560 }
561
562 fn format_variable_styled(
563 indent_prefix: &str,
564 var: &VariableDebugInfo,
565 verbose: bool,
566 ) -> ratatui::text::Line<'static> {
567 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
568
569 let type_display = var
570 .type_pretty
571 .as_ref()
572 .filter(|s| !s.is_empty())
573 .map(|s| s.as_str())
574 .unwrap_or("unknown");
575
576 let mut builder = StyledLineBuilder::new()
577 .styled(indent_prefix, StylePresets::TREE)
578 .text(" ")
579 .value(&var.name)
580 .key(": ")
581 .styled(type_display, StylePresets::TYPE);
582
583 if let Some(size) = var.size {
584 builder = builder.text(" ").text(format!("({size} bytes)"));
585 }
586
587 if verbose && !var.location_description.is_empty() && var.location_description != "None" {
588 builder = builder
589 .text(" ")
590 .key("@")
591 .text(" ")
592 .styled(&var.location_description, StylePresets::LOCATION);
593 }
594
595 builder.build()
596 }
597}
598
599#[derive(Debug, Clone)]
601pub enum TargetType {
602 Function,
603 SourceLocation,
604 Address,
605}
606
607#[derive(Debug, Clone)]
609pub struct VariableDebugInfo {
610 pub name: String,
611 pub type_name: String,
612 pub type_pretty: Option<String>,
613 pub location_description: String,
614 pub size: Option<u64>,
615 pub scope_start: Option<u64>,
616 pub scope_end: Option<u64>,
617}
618
619#[derive(Debug, Clone)]
621pub enum RuntimeCommand {
622 ExecuteScript {
623 command: String,
624 selected_index: Option<usize>,
625 },
626 RequestSourceCode, DisableTrace(u32), EnableTrace(u32), DisableAllTraces, EnableAllTraces, DeleteTrace(u32), DeleteAllTraces, InfoFunction {
634 target: String,
635 verbose: bool,
636 }, InfoLine {
638 target: String,
639 verbose: bool,
640 }, InfoAddress {
642 target: String,
643 verbose: bool,
644 }, InfoTrace {
646 trace_id: Option<u32>,
647 }, InfoTraceAll,
649 InfoSource, InfoShare, InfoFile, SaveTraces {
653 filename: Option<String>,
654 filter: crate::components::command_panel::trace_persistence::SaveFilter,
655 }, LoadTraces {
657 filename: String,
658 traces: Vec<TraceDefinition>,
659 }, SrcPathList,
661 SrcPathAddDir {
662 dir: String,
663 },
664 SrcPathAddMap {
665 from: String,
666 to: String,
667 },
668 SrcPathRemove {
669 pattern: String,
670 },
671 SrcPathClear,
672 SrcPathReset,
673 Shutdown,
674}
675
676#[derive(Debug, Clone)]
678pub struct TraceDefinition {
679 pub target: String,
680 pub script: String,
681 pub enabled: bool,
682 pub selected_index: Option<usize>,
683}
684
685#[derive(Debug, Clone)]
687pub struct TraceLoadDetail {
688 pub target: String,
689 pub trace_id: Option<u32>,
690 pub status: LoadStatus,
691 pub error: Option<String>,
692}
693
694#[derive(Debug, Clone)]
696pub enum LoadStatus {
697 Created, CreatedDisabled, Failed, Skipped, }
702
703#[derive(Debug, Clone)]
705pub enum ExecutionStatus {
706 Success,
707 Failed(String), Skipped(String), }
710
711#[derive(Debug, Clone)]
713pub struct ScriptExecutionResult {
714 pub pc_address: u64,
715 pub target_name: String,
716 pub binary_path: String, pub status: ExecutionStatus,
718 pub source_file: Option<String>,
719 pub source_line: Option<u32>,
720 pub is_inline: Option<bool>,
721}
722
723#[derive(Debug, Clone)]
725pub struct ScriptCompilationDetails {
726 pub trace_ids: Vec<u32>, pub results: Vec<ScriptExecutionResult>,
728 pub total_count: usize,
729 pub success_count: usize,
730 pub failed_count: usize,
731}
732
733#[derive(Debug, Clone)]
734pub enum RuntimeStatus {
735 DwarfLoadingStarted,
736 DwarfLoadingCompleted {
737 symbols_count: usize,
738 },
739 DwarfLoadingFailed(String),
740 ScriptCompilationCompleted {
741 details: ScriptCompilationDetails, },
743 UprobeAttached {
744 function: String,
745 address: u64,
746 },
747 UprobeDetached {
748 function: String,
749 },
750 SourceCodeLoaded(SourceCodeInfo),
751 SourceCodeLoadFailed(String),
752 TraceEnabled {
753 trace_id: u32,
754 },
755 TraceDisabled {
756 trace_id: u32,
757 },
758 AllTracesEnabled {
759 count: usize,
760 error: Option<String>, },
762 AllTracesDisabled {
763 count: usize,
764 error: Option<String>, },
766 TraceEnableFailed {
767 trace_id: u32,
768 error: String,
769 },
770 TraceDisableFailed {
771 trace_id: u32,
772 error: String,
773 },
774 TraceDeleted {
775 trace_id: u32,
776 },
777 AllTracesDeleted {
778 count: usize,
779 error: Option<String>, },
781 TraceDeleteFailed {
782 trace_id: u32,
783 error: String,
784 },
785 InfoFunctionResult {
786 target: String,
787 info: TargetDebugInfo,
788 verbose: bool,
789 },
790 InfoFunctionFailed {
791 target: String,
792 error: String,
793 },
794 InfoLineResult {
795 target: String,
796 info: TargetDebugInfo,
797 verbose: bool,
798 },
799 InfoLineFailed {
800 target: String,
801 error: String,
802 },
803 InfoAddressResult {
804 target: String,
805 info: TargetDebugInfo,
806 verbose: bool,
807 },
808 InfoAddressFailed {
809 target: String,
810 error: String,
811 },
812 TraceInfo {
814 trace_id: u32,
815 target: String,
816 status: TraceStatus,
817 pid: Option<u32>,
818 host_pid: Option<u32>,
819 binary: String,
820 script_preview: Option<String>,
821 pc: u64,
822 },
823 TraceInfoAll {
825 summary: TraceSummaryInfo,
826 traces: Vec<TraceDetailInfo>,
827 },
828 TraceInfoFailed {
830 trace_id: u32,
831 error: String,
832 },
833 FileInfo {
835 groups: Vec<SourceFileGroup>,
836 },
837 FileInfoFailed {
839 error: String,
840 },
841 TracesSaved {
843 filename: String,
844 saved_count: usize,
845 total_count: usize,
846 },
847 TracesSaveFailed {
849 error: String,
850 },
851 TracesLoaded {
853 filename: String,
854 total_count: usize,
855 success_count: usize,
856 failed_count: usize,
857 disabled_count: usize,
858 details: Vec<TraceLoadDetail>,
859 },
860 TracesLoadFailed {
862 filename: String,
863 error: String,
864 },
865 ShareInfo {
867 libraries: Vec<SharedLibraryInfo>,
868 },
869 ShareInfoFailed {
871 error: String,
872 },
873 ExecutableFileInfo {
875 file_path: String,
876 file_type: String,
877 entry_point: Option<u64>,
878 has_symbols: bool,
879 has_debug_info: bool,
880 debug_file_path: Option<String>,
881 text_section: Option<SectionInfo>,
882 data_section: Option<SectionInfo>,
883 mode_description: String,
884 },
885 ExecutableFileInfoFailed {
887 error: String,
888 },
889 DwarfModuleDiscovered {
891 module_path: String,
892 total_modules: usize,
893 },
894 DwarfModuleLoadingStarted {
895 module_path: String,
896 current: usize,
897 total: usize,
898 },
899 DwarfModuleLoadingCompleted {
900 module_path: String,
901 stats: ModuleLoadingStats,
902 current: usize,
903 total: usize,
904 },
905 DwarfModuleLoadingFailed {
906 module_path: String,
907 error: String,
908 current: usize,
909 total: usize,
910 },
911 SrcPathInfo {
912 info: SourcePathInfo,
913 },
914 SrcPathUpdated {
915 message: String,
916 },
917 SrcPathFailed {
918 error: String,
919 },
920 TraceBackpressure {
922 dropped_since_last: u64,
923 dropped_total: u64,
924 queue_capacity: usize,
925 },
926}
927
928#[derive(Debug, Clone)]
930pub struct ModuleLoadingStats {
931 pub functions: usize,
932 pub variables: usize,
933 pub types: usize,
934 pub load_time_ms: u64,
935}
936
937#[derive(Debug, Clone)]
939pub struct TraceSummaryInfo {
940 pub total: usize,
941 pub active: usize,
942 pub disabled: usize,
943}
944
945#[derive(Debug, Clone)]
947pub struct TraceDetailInfo {
948 pub trace_id: u32,
949 pub target_display: String,
950 pub binary_path: String,
951 pub pc: u64,
952 pub status: TraceStatus,
953 pub duration: String, }
955
956impl TraceDetailInfo {
957 pub fn format_line(&self) -> String {
959 let binary_name = std::path::Path::new(&self.binary_path)
961 .file_name()
962 .and_then(|name| name.to_str())
963 .unwrap_or(&self.binary_path);
964
965 format!(
966 "#{} | {}+0x{:x} | {} ({}) ",
967 self.trace_id, binary_name, self.pc, self.target_display, self.status
968 )
969 }
970}
971
972#[derive(Debug, Clone)]
974pub struct SourceFileInfo {
975 pub path: String,
976 pub directory: String,
977}
978
979#[derive(Debug, Clone)]
981pub struct SourceFileGroup {
982 pub module_path: String,
983 pub files: Vec<SourceFileInfo>,
984}
985
986#[derive(Debug, Clone)]
988pub struct SharedLibraryInfo {
989 pub from_address: u64, pub to_address: u64, pub symbols_read: bool, pub debug_info_available: bool, pub library_path: String, pub size: u64, pub debug_file_path: Option<String>, }
997
998#[derive(Debug, Clone)]
1000pub struct SectionInfo {
1001 pub start_address: u64, pub end_address: u64, pub size: u64, }
1005
1006impl EventRegistry {
1007 pub fn new() -> (Self, RuntimeChannels) {
1008 Self::new_with_trace_capacity(DEFAULT_TRACE_CHANNEL_CAPACITY)
1009 }
1010
1011 pub fn new_with_trace_capacity(trace_capacity: usize) -> (Self, RuntimeChannels) {
1012 let trace_capacity = trace_capacity.max(1);
1013 let (command_tx, command_rx) = mpsc::unbounded_channel();
1014 let (trace_tx, trace_rx) = mpsc::channel::<ParsedTraceEvent>(trace_capacity);
1015 let (status_tx, status_rx) = mpsc::unbounded_channel();
1016
1017 let registry = EventRegistry {
1018 command_sender: command_tx,
1019 trace_receiver: trace_rx,
1020 status_receiver: status_rx,
1021 };
1022
1023 let channels = RuntimeChannels {
1024 command_receiver: command_rx,
1025 trace_sender: trace_tx.clone(),
1026 status_sender: status_tx.clone(),
1027 trace_channel_capacity: trace_capacity,
1028 };
1029
1030 (registry, channels)
1031 }
1032}
1033
1034pub const DEFAULT_TRACE_CHANNEL_CAPACITY: usize = 4096;
1036
1037#[derive(Debug)]
1039pub struct RuntimeChannels {
1040 pub command_receiver: mpsc::UnboundedReceiver<RuntimeCommand>,
1041 pub trace_sender: mpsc::Sender<ParsedTraceEvent>,
1042 pub status_sender: mpsc::UnboundedSender<RuntimeStatus>,
1043 pub trace_channel_capacity: usize,
1044}
1045
1046impl RuntimeChannels {
1047 pub fn create_status_sender(&self) -> mpsc::UnboundedSender<RuntimeStatus> {
1049 self.status_sender.clone()
1050 }
1051
1052 pub fn create_trace_sender(&self) -> mpsc::Sender<ParsedTraceEvent> {
1054 self.trace_sender.clone()
1055 }
1056}
1057
1058impl RuntimeStatus {
1059 pub fn format_trace_info(&self) -> Option<String> {
1061 match self {
1062 RuntimeStatus::TraceInfo {
1063 trace_id,
1064 target,
1065 status,
1066 pid,
1067 host_pid,
1068 binary,
1069 script_preview,
1070 pc,
1071 } => {
1072 let mut result =
1074 format!("π Trace [{}] {} {}\n", trace_id, status.to_emoji(), status);
1075
1076 let binary_name = std::path::Path::new(binary)
1078 .file_name()
1079 .and_then(|name| name.to_str())
1080 .unwrap_or(binary);
1081
1082 let mut fields: Vec<(&str, String)> = Vec::new();
1083 fields.push(("π― Target", target.clone()));
1084 fields.push(("π¦ Binary", binary.clone()));
1085 fields.push(("π Address", format!("{binary_name}+0x{pc:x}")));
1086 match (pid, host_pid) {
1087 (Some(proc_pid), Some(host_pid_val)) if proc_pid != host_pid_val => {
1088 fields.push(("π·οΈ PID(proc)", proc_pid.to_string()));
1089 fields.push(("π·οΈ PID(host)", host_pid_val.to_string()));
1090 }
1091 (Some(proc_pid), _) => {
1092 fields.push(("π·οΈ PID", proc_pid.to_string()));
1093 }
1094 (None, Some(host_pid_val)) => {
1095 fields.push(("π·οΈ PID(host)", host_pid_val.to_string()));
1096 }
1097 (None, None) => {}
1098 }
1099 if let Some(ref script) = script_preview {
1100 fields.push(("π Script", script.clone()));
1101 }
1102
1103 let max_key_width = fields.iter().map(|(k, _)| k.width()).max().unwrap_or(0);
1105
1106 for (key, value) in fields {
1107 let key_width = key.width();
1108 let pad = max_key_width.saturating_sub(key_width);
1109 let spaces = " ".repeat(pad);
1110 result.push_str(&format!(" {key}{spaces}: {value}\n"));
1111 }
1112
1113 Some(result)
1114 }
1115 _ => None,
1116 }
1117 }
1118
1119 pub fn format_trace_info_styled(&self) -> Option<Vec<ratatui::text::Line<'static>>> {
1121 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1122 use ratatui::text::Line;
1123
1124 match self {
1125 RuntimeStatus::TraceInfo {
1126 trace_id,
1127 target,
1128 status,
1129 pid,
1130 host_pid,
1131 binary,
1132 script_preview: _,
1133 pc,
1134 } => {
1135 let mut lines = Vec::new();
1136
1137 lines.push(
1139 StyledLineBuilder::new()
1140 .title(format!(
1141 "π Trace [{}] {} {}",
1142 trace_id,
1143 status.to_emoji(),
1144 status
1145 ))
1146 .build(),
1147 );
1148
1149 let binary_name = std::path::Path::new(binary)
1150 .file_name()
1151 .and_then(|name| name.to_str())
1152 .unwrap_or(binary)
1153 .to_string();
1154
1155 lines.push(
1156 StyledLineBuilder::new()
1157 .text(" ")
1158 .key("π― Target:")
1159 .text(" ")
1160 .value(target)
1161 .build(),
1162 );
1163 lines.push(
1164 StyledLineBuilder::new()
1165 .text(" ")
1166 .key("π¦ Binary:")
1167 .text(" ")
1168 .value(binary)
1169 .build(),
1170 );
1171 lines.push(
1172 StyledLineBuilder::new()
1173 .text(" ")
1174 .key("π Address:")
1175 .text(" ")
1176 .value(format!("{binary_name}+0x{pc:x}"))
1177 .build(),
1178 );
1179
1180 match (pid, host_pid) {
1181 (Some(proc_pid), Some(host_pid_val)) if proc_pid != host_pid_val => {
1182 lines.push(
1183 StyledLineBuilder::new()
1184 .text(" ")
1185 .key("π·οΈ PID(proc):")
1186 .text(" ")
1187 .value(proc_pid.to_string())
1188 .build(),
1189 );
1190 lines.push(
1191 StyledLineBuilder::new()
1192 .text(" ")
1193 .key("π·οΈ PID(host):")
1194 .text(" ")
1195 .value(host_pid_val.to_string())
1196 .build(),
1197 );
1198 }
1199 (Some(proc_pid), _) => {
1200 lines.push(
1201 StyledLineBuilder::new()
1202 .text(" ")
1203 .key("π·οΈ PID:")
1204 .text(" ")
1205 .value(proc_pid.to_string())
1206 .build(),
1207 );
1208 }
1209 (None, Some(host_pid_val)) => {
1210 lines.push(
1211 StyledLineBuilder::new()
1212 .text(" ")
1213 .key("π·οΈ PID(host):")
1214 .text(" ")
1215 .value(host_pid_val.to_string())
1216 .build(),
1217 );
1218 }
1219 (None, None) => {}
1220 }
1221
1222 Some(lines)
1223 }
1224 RuntimeStatus::TraceInfoAll { summary, traces } => {
1225 let mut lines = Vec::new();
1226 lines.push(
1228 StyledLineBuilder::new()
1229 .title(format!(
1230 "π All Traces ({} total, {} active):",
1231 summary.total, summary.active
1232 ))
1233 .build(),
1234 );
1235 lines.push(Line::from(""));
1236
1237 for t in traces {
1238 let binary_name = std::path::Path::new(&t.binary_path)
1239 .file_name()
1240 .and_then(|name| name.to_str())
1241 .unwrap_or(&t.binary_path)
1242 .to_string();
1243 let status_style = match t.status {
1244 TraceStatus::Active => StylePresets::SUCCESS,
1245 TraceStatus::Disabled => StylePresets::LOCATION,
1246 TraceStatus::Failed => StylePresets::ERROR,
1247 };
1248 let line = StyledLineBuilder::new()
1249 .text(" ")
1250 .styled(format!("#{}", t.trace_id), StylePresets::ADDRESS)
1251 .text(" | ")
1252 .styled(format!("{}+0x{:x}", binary_name, t.pc), StylePresets::KEY)
1253 .text(" | ")
1254 .value(&t.target_display)
1255 .text(" (")
1256 .styled(t.status.to_string(), status_style)
1257 .text(")")
1258 .build();
1259 lines.push(line);
1260 }
1261
1262 Some(lines)
1263 }
1264 _ => None,
1265 }
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272
1273 fn sample_event(trace_id: u64) -> ParsedTraceEvent {
1274 ParsedTraceEvent {
1275 timestamp: 0,
1276 trace_id,
1277 pid: 42,
1278 tid: 42,
1279 instructions: vec![],
1280 }
1281 }
1282
1283 #[test]
1284 fn event_registry_uses_bounded_trace_channel_capacity() {
1285 let (_registry, channels) = EventRegistry::new_with_trace_capacity(1);
1286 assert_eq!(channels.trace_channel_capacity, 1);
1287
1288 channels.trace_sender.try_send(sample_event(1)).unwrap();
1289 assert!(channels.trace_sender.try_send(sample_event(2)).is_err());
1290 }
1291}
1292
1293#[derive(Debug, Clone)]
1295pub struct SourcePathInfo {
1296 pub substitutions: Vec<PathSubstitution>,
1297 pub search_dirs: Vec<String>,
1298 pub runtime_substitution_count: usize,
1299 pub runtime_search_dir_count: usize,
1300 pub config_substitution_count: usize,
1301 pub config_search_dir_count: usize,
1302}
1303
1304impl SourcePathInfo {
1305 pub fn format_for_display(&self) -> String {
1307 let mut output = String::new();
1308
1309 output.push_str("ποΈ Source Path Configuration:\n\n");
1310
1311 if self.substitutions.is_empty() {
1313 output.push_str("Path Substitutions: (none)\n");
1314 } else {
1315 output.push_str(&format!(
1316 "Path Substitutions ({}):\n",
1317 self.substitutions.len()
1318 ));
1319 for (i, sub) in self.substitutions.iter().enumerate() {
1320 let marker = if i < self.runtime_substitution_count {
1321 "[runtime]"
1322 } else {
1323 "[config] "
1324 };
1325 output.push_str(&format!(" {} {} -> {}\n", marker, sub.from, sub.to));
1326 }
1327 }
1328
1329 output.push('\n');
1330
1331 if self.search_dirs.is_empty() {
1333 output.push_str("Search Directories: (none)\n");
1334 } else {
1335 output.push_str(&format!(
1336 "Search Directories ({}):\n",
1337 self.search_dirs.len()
1338 ));
1339 for (i, dir) in self.search_dirs.iter().enumerate() {
1340 let marker = if i < self.runtime_search_dir_count {
1341 "[runtime]"
1342 } else {
1343 "[config] "
1344 };
1345 output.push_str(&format!(" {marker} {dir}\n"));
1346 }
1347 }
1348
1349 output.push_str("\nπ‘ Runtime rules take precedence over config file rules.\n");
1350 output.push_str(
1351 "π‘ Use 'srcpath clear' to remove runtime rules, 'srcpath reset' to reset to config.\n",
1352 );
1353
1354 output
1355 }
1356
1357 pub fn format_for_display_styled(&self) -> Vec<ratatui::text::Line<'static>> {
1359 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1360 use ratatui::text::Line;
1361
1362 let mut lines = Vec::new();
1363
1364 lines.push(
1366 StyledLineBuilder::new()
1367 .title("ποΈ Source Path Configuration:")
1368 .build(),
1369 );
1370 lines.push(Line::from(""));
1371
1372 if self.substitutions.is_empty() {
1374 lines.push(
1375 StyledLineBuilder::new()
1376 .key("Path Substitutions:")
1377 .text(" (none)")
1378 .build(),
1379 );
1380 } else {
1381 lines.push(
1382 StyledLineBuilder::new()
1383 .key(format!(
1384 "Path Substitutions ({}):",
1385 self.substitutions.len()
1386 ))
1387 .build(),
1388 );
1389 for (i, sub) in self.substitutions.iter().enumerate() {
1390 let marker = if i < self.runtime_substitution_count {
1391 "[runtime]"
1392 } else {
1393 "[config] "
1394 };
1395 lines.push(
1396 StyledLineBuilder::new()
1397 .text(" ")
1398 .styled(marker, StylePresets::MARKER)
1399 .text(" ")
1400 .value(&sub.from)
1401 .styled(" -> ", StylePresets::TREE)
1402 .styled(&sub.to, StylePresets::KEY)
1403 .build(),
1404 );
1405 }
1406 }
1407
1408 lines.push(Line::from(""));
1409
1410 if self.search_dirs.is_empty() {
1412 lines.push(
1413 StyledLineBuilder::new()
1414 .key("Search Directories:")
1415 .text(" (none)")
1416 .build(),
1417 );
1418 } else {
1419 lines.push(
1420 StyledLineBuilder::new()
1421 .key(format!("Search Directories ({}):", self.search_dirs.len()))
1422 .build(),
1423 );
1424 for (i, dir) in self.search_dirs.iter().enumerate() {
1425 let marker = if i < self.runtime_search_dir_count {
1426 "[runtime]"
1427 } else {
1428 "[config] "
1429 };
1430 lines.push(
1431 StyledLineBuilder::new()
1432 .text(" ")
1433 .styled(marker, StylePresets::MARKER)
1434 .text(" ")
1435 .value(dir)
1436 .build(),
1437 );
1438 }
1439 }
1440
1441 lines.push(Line::from(""));
1442 lines.push(
1443 StyledLineBuilder::new()
1444 .styled(
1445 "π‘ Runtime rules take precedence over config file rules.",
1446 StylePresets::TIP,
1447 )
1448 .build(),
1449 );
1450 lines.push(
1451 StyledLineBuilder::new()
1452 .styled(
1453 "π‘ Use 'srcpath clear' to remove runtime rules, 'srcpath reset' to reset to config.",
1454 StylePresets::TIP,
1455 )
1456 .build(),
1457 );
1458
1459 lines
1460 }
1461}
1462
1463#[derive(Debug, Clone, PartialEq, Eq)]
1465pub struct PathSubstitution {
1466 pub from: String,
1467 pub to: String,
1468}