1use super::syntax_highlighter;
2use crate::action::ResponseType;
3use crate::model::panel_state::{CommandPanelState, LineType, StaticTextLine};
4use crate::ui::{strings::UIStrings, symbols::UISymbols, themes::UIThemes};
5use ratatui::{
6 layout::Rect,
7 style::{Color, Modifier, Style},
8 text::{Line, Span},
9 widgets::Paragraph,
10 Frame,
11};
12use unicode_width::UnicodeWidthChar;
13
14pub struct ExecutableFileInfoDisplay<'a> {
21 pub file_path: &'a str,
22 pub file_type: &'a str,
23 pub entry_point: Option<u64>,
24 pub has_symbols: bool,
25 pub has_debug_info: bool,
26 pub debug_file_path: &'a Option<String>,
27 pub text_section: &'a Option<crate::events::SectionInfo>,
28 pub data_section: &'a Option<crate::events::SectionInfo>,
29 pub mode_description: &'a str,
30}
31
32pub struct ResponseFormatter;
34
35impl ResponseFormatter {
36 pub fn style_generic_message_lines(text: &str) -> Vec<Line<'static>> {
42 text.lines().map(Self::style_generic_message_line).collect()
43 }
44
45 fn style_generic_message_line(line: &str) -> Line<'static> {
46 use crate::components::command_panel::style_builder::StylePresets;
47 use ratatui::style::{Color, Style};
48 use ratatui::text::Span;
49
50 if line.trim().is_empty() {
51 return Line::from("");
52 }
53
54 let trimmed = line.trim_start();
56 let indent_len = line.len() - trimmed.len();
57 let indent = &line[..indent_len];
58
59 if trimmed.starts_with('✗') {
60 let mut without = &trimmed['✗'.len_utf8()..];
62 if without.starts_with('\u{FE0F}') {
64 let vs = '\u{FE0F}'.len_utf8();
65 without = &without[vs..];
66 }
67 let replaced = format!("{}{}{}", indent, "❌", without);
68 return Line::from(Span::styled(replaced, StylePresets::ERROR));
69 }
70
71 let mut spans: Vec<Span<'static>> = Vec::new();
73 let mut rest = line;
74
75 if let Some(first) = rest.chars().next() {
77 let mut style = Style::default();
78 let mut consumed: Option<usize> = None;
79 let mut sym: Option<&str> = None;
80 match first {
81 '✓' => {
82 style = StylePresets::SUCCESS;
83 consumed = Some('✓'.len_utf8());
84 sym = Some("✅");
85 }
86 '⚠' => {
87 style = StylePresets::WARNING;
88 consumed = Some('⚠'.len_utf8());
89 sym = Some("⚠️");
90 }
91 _ => {}
92 }
93 if let Some(mut n) = consumed {
94 if rest[n..].starts_with('\u{FE0F}') {
96 n += '\u{FE0F}'.len_utf8();
97 }
98 let rendered = sym.unwrap_or("");
99 let rendered = if rendered.is_empty() {
100 first.to_string()
101 } else {
102 rendered.to_string()
103 };
104 spans.push(Span::styled(rendered, style));
105 rest = &rest[n..];
106 }
107 }
108
109 let mut token = String::new();
111 for ch in rest.chars() {
112 let is_sep = ch.is_whitespace() || ",.:()[]{}".contains(ch);
113 if is_sep {
114 if !token.is_empty() {
115 spans.push(Self::style_token(&token));
116 token.clear();
117 }
118 if ch.is_whitespace() {
119 spans.push(Span::raw(ch.to_string()));
120 } else {
121 spans.push(Span::styled(
122 ch.to_string(),
123 Style::default().fg(Color::DarkGray),
124 ));
125 }
126 } else {
127 token.push(ch);
128 }
129 }
130 if !token.is_empty() {
131 spans.push(Self::style_token(&token));
132 }
133
134 Line::from(spans)
135 }
136
137 fn style_token(tok: &str) -> Span<'static> {
138 use ratatui::style::{Color, Style};
139 let lower = tok.to_ascii_lowercase();
140
141 if tok.starts_with("0x") || tok.chars().all(|c| c.is_ascii_hexdigit()) && tok.len() > 1 {
143 return Span::styled(tok.to_string(), Style::default().fg(Color::Yellow));
144 }
145
146 const KEYS: &[&str] = &[
148 "trace", "function", "line", "file", "pid", "pc", "saved", "loaded", "deleted",
149 "enabled", "disabled",
150 ];
151 if KEYS.iter().any(|k| lower == *k) {
152 return Span::styled(tok.to_string(), Style::default().fg(Color::Cyan));
153 }
154
155 const ERR: &[&str] = &[
157 "failed", "error", "unknown", "not", "found", "cannot", "missing",
158 ];
159 if ERR.iter().any(|k| lower == *k) {
160 return Span::styled(tok.to_string(), Style::default().fg(Color::Red));
161 }
162
163 Span::styled(tok.to_string(), Style::default().fg(Color::White))
165 }
166
167 pub fn add_response_with_style(
169 state: &mut CommandPanelState,
170 content: String,
171 styled_lines: Option<Vec<Line<'static>>>,
172 response_type: ResponseType,
173 ) {
174 if let Some(last_item) = state.command_history.last_mut() {
175 last_item.response = Some(content);
176 last_item.response_styled = styled_lines;
177 last_item.response_type = Some(response_type);
178 tracing::debug!(
179 "add_response_with_style: Added styled response to command '{}'",
180 last_item.command
181 );
182 } else {
183 tracing::warn!("add_response_with_style: No command in history to attach response to!");
184 }
185
186 Self::update_static_lines(state);
187 }
188
189 pub fn upsert_runtime_alert_with_style(
193 state: &mut CommandPanelState,
194 content: String,
195 styled_lines: Option<Vec<Line<'static>>>,
196 response_type: ResponseType,
197 ) {
198 state
199 .static_lines
200 .retain(|line| line.line_type != LineType::RuntimeAlert);
201
202 if let Some(styled) = styled_lines {
203 for styled_line in styled {
204 let plain: String = styled_line
205 .spans
206 .iter()
207 .map(|span| span.content.as_ref())
208 .collect();
209 state.static_lines.push(StaticTextLine {
210 content: plain,
211 line_type: LineType::RuntimeAlert,
212 history_index: None,
213 response_type: Some(response_type),
214 styled_content: Some(styled_line),
215 });
216 }
217 } else {
218 for line in Self::split_response_lines(&content) {
219 state.static_lines.push(StaticTextLine {
220 content: line,
221 line_type: LineType::RuntimeAlert,
222 history_index: None,
223 response_type: Some(response_type),
224 styled_content: None,
225 });
226 }
227 }
228
229 state.styled_buffer = None;
230 state.styled_at_history_index = None;
231 }
232
233 pub fn add_simple_styled_response(
236 state: &mut CommandPanelState,
237 content: String,
238 style: ratatui::style::Style,
239 response_type: ResponseType,
240 ) {
241 let styled = vec![
242 crate::components::command_panel::style_builder::StyledLineBuilder::new()
243 .styled(&content, style)
244 .build(),
245 ];
246 Self::add_response_with_style(state, content, Some(styled), response_type);
247 }
248
249 pub fn format_batch_load_summary_styled(
252 filename: &str,
253 total_count: usize,
254 success_count: usize,
255 failed_count: usize,
256 disabled_count: usize,
257 details: &[crate::events::TraceLoadDetail],
258 ) -> Vec<Line<'static>> {
259 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
260 let mut lines = Vec::new();
261
262 lines.push(
264 StyledLineBuilder::new()
265 .styled(
266 format!("📂 Loaded traces from {filename}"),
267 StylePresets::TITLE,
268 )
269 .build(),
270 );
271
272 let summary = if disabled_count > 0 {
274 format!(
275 " Total: {total_count}, Success: {success_count}, Failed: {failed_count}, Disabled: {disabled_count}"
276 )
277 } else {
278 format!(" Total: {total_count}, Success: {success_count}, Failed: {failed_count}")
279 };
280 lines.push(StyledLineBuilder::new().value(&summary).build());
281 lines.push(
282 StyledLineBuilder::new()
283 .text(" • ")
284 .styled(
285 "Selected indices from the file are restored when present",
286 StylePresets::TIP,
287 )
288 .build(),
289 );
290
291 if !details.is_empty() {
293 lines.push(
294 StyledLineBuilder::new()
295 .styled("", StylePresets::VALUE)
296 .build(),
297 );
298 lines.push(
299 StyledLineBuilder::new()
300 .styled("📊 Details:", StylePresets::SECTION)
301 .build(),
302 );
303 for detail in details {
304 match detail.status {
305 crate::events::LoadStatus::Created => {
306 let text = if let Some(id) = detail.trace_id {
307 format!(" ✓ {} → trace #{}", detail.target, id)
308 } else {
309 format!(" ✓ {}", detail.target)
310 };
311 lines.push(
312 StyledLineBuilder::new()
313 .styled(text, StylePresets::SUCCESS)
314 .build(),
315 );
316 }
317 crate::events::LoadStatus::CreatedDisabled => {
318 let text = if let Some(id) = detail.trace_id {
319 format!(" ⊘ {} → trace #{} (disabled)", detail.target, id)
320 } else {
321 format!(" ⊘ {} (disabled)", detail.target)
322 };
323 lines.push(
324 StyledLineBuilder::new()
325 .styled(text, StylePresets::WARNING)
326 .build(),
327 );
328 }
329 crate::events::LoadStatus::Failed => {
330 let text = if let Some(ref error) = detail.error {
331 format!(" ✗ {}: {}", detail.target, error)
332 } else {
333 format!(" ✗ {}", detail.target)
334 };
335 lines.push(
336 StyledLineBuilder::new()
337 .styled(text, StylePresets::ERROR)
338 .build(),
339 );
340 }
341 _ => {}
342 }
343 }
344 }
345
346 lines
347 }
348
349 pub fn update_static_lines(state: &mut CommandPanelState) {
353 state
355 .static_lines
356 .retain(|line| matches!(line.line_type, LineType::Welcome | LineType::RuntimeAlert));
357 state.styled_buffer = None;
358 state.styled_at_history_index = None;
359
360 for (index, item) in state.command_history.iter().enumerate() {
362 let command_line = format!(
364 "{prompt}{command}",
365 prompt = item.prompt,
366 command = item.command
367 );
368 state.static_lines.push(StaticTextLine {
369 content: command_line,
370 line_type: LineType::Command,
371 history_index: Some(index),
372 response_type: None,
373 styled_content: None,
374 });
375
376 if let Some(ref styled) = item.response_styled {
378 for line in styled.iter() {
380 let plain: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
381 state.static_lines.push(StaticTextLine {
382 content: plain,
383 line_type: LineType::Response,
384 history_index: Some(index),
385 response_type: item.response_type,
386 styled_content: Some(line.clone()),
387 });
388 }
389 } else if let Some(ref response) = item.response {
390 for response_line in Self::split_response_lines(response) {
392 state.static_lines.push(StaticTextLine {
393 content: response_line,
394 line_type: LineType::Response,
395 history_index: Some(index),
396 response_type: item.response_type,
397 styled_content: None,
398 });
399 }
400 }
401 }
402
403 }
406
407 fn split_response_lines(response: &str) -> Vec<String> {
409 response.lines().map(String::from).collect()
410 }
411
412 pub fn format_line_for_display(
416 state: &CommandPanelState,
417 line: &StaticTextLine,
418 is_current_input: bool,
419 width: usize,
420 ) -> Vec<Line<'static>> {
421 match line.line_type {
422 LineType::Command => Self::format_command_line(&line.content, width),
423 LineType::Response | LineType::RuntimeAlert => Self::format_response_line(line, width),
424 LineType::Welcome => Self::format_response_line(line, width), LineType::CurrentInput => {
426 if is_current_input {
427 Self::format_current_input_line(state, &line.content, width)
428 } else {
429 Self::format_command_line(&line.content, width)
430 }
431 }
432 }
433 }
434
435 fn format_command_line(content: &str, width: usize) -> Vec<Line<'static>> {
437 let wrapped_lines = Self::wrap_text(content, width);
438 wrapped_lines
439 .into_iter()
440 .map(|line| Line::from(vec![Span::styled(line, Style::default().fg(Color::White))]))
441 .collect()
442 }
443
444 fn format_response_line(line: &StaticTextLine, width: usize) -> Vec<Line<'static>> {
446 let style = Self::get_response_style(&line.content, line.response_type);
447
448 if Self::is_script_display_line(&line.content) {
450 Self::format_script_display_line(&line.content, width)
451 } else {
452 let wrapped_lines = Self::wrap_text(&line.content, width);
453 wrapped_lines
454 .into_iter()
455 .map(|line_content| Line::from(vec![Span::styled(line_content, style)]))
456 .collect()
457 }
458 }
459
460 fn format_current_input_line(
462 _state: &CommandPanelState,
463 content: &str,
464 width: usize,
465 ) -> Vec<Line<'static>> {
466 let wrapped_lines = Self::wrap_text(content, width);
467
468 wrapped_lines
471 .into_iter()
472 .map(|line| Line::from(vec![Span::styled(line, UIThemes::input_mode())]))
473 .collect()
474 }
475
476 fn get_response_style(content: &str, response_type: Option<ResponseType>) -> Style {
478 if let Some(resp_type) = response_type {
480 return match resp_type {
481 ResponseType::Success => UIThemes::success_text(),
482 ResponseType::Error => UIThemes::error_text(),
483 ResponseType::Warning => UIThemes::warning_text(),
484 ResponseType::Info => UIThemes::info_text(),
485 ResponseType::Progress => UIThemes::progress_text(),
486 ResponseType::ScriptDisplay => UIThemes::script_mode(),
487 };
488 }
489
490 if content.starts_with(UIStrings::SUCCESS_PREFIX) || content.starts_with("✓") {
492 UIThemes::success_text()
493 } else if content.starts_with(UIStrings::ERROR_PREFIX) || content.starts_with("✗") {
494 UIThemes::error_text()
495 } else if content.starts_with(UIStrings::WARNING_PREFIX) || content.starts_with("⚠") {
496 UIThemes::warning_text()
497 } else if content.starts_with(UIStrings::PROGRESS_PREFIX) || content.starts_with("⏳") {
498 UIThemes::progress_text()
499 } else if content.starts_with("📝") {
500 UIThemes::script_mode()
501 } else {
502 Style::default()
503 }
504 }
505
506 fn is_script_display_line(content: &str) -> bool {
508 content.starts_with("📝")
509 || content.starts_with(UIStrings::SCRIPT_TARGET_PREFIX)
510 || content.chars().all(|c| c == '─' || c.is_whitespace())
511 || content.contains(" │ ")
512 }
513
514 fn format_script_display_line(content: &str, width: usize) -> Vec<Line<'static>> {
516 if content.starts_with("📝") || content.starts_with(UIStrings::SCRIPT_TARGET_PREFIX) {
517 vec![Line::from(vec![Span::styled(
519 content.to_string(),
520 Style::default()
521 .fg(Color::Green)
522 .add_modifier(Modifier::BOLD),
523 )])]
524 } else if content.chars().all(|c| c == '─' || c.is_whitespace()) {
525 vec![Line::from(vec![Span::styled(
527 content.to_string(),
528 Style::default().fg(Color::DarkGray),
529 )])]
530 } else if content.contains(" │ ") {
531 Self::format_script_code_line(content, width)
533 } else {
534 vec![Line::from(vec![Span::styled(
536 content.to_string(),
537 Style::default(),
538 )])]
539 }
540 }
541
542 fn format_script_code_line(content: &str, width: usize) -> Vec<Line<'static>> {
544 if let Some(separator_pos) = content.find(" │ ") {
545 let separator_str = " │ ";
546 let end_byte_pos = separator_pos + separator_str.len();
547
548 if end_byte_pos <= content.len() {
549 let line_number_part = &content[..end_byte_pos];
550 let code_part = &content[end_byte_pos..];
551
552 let wrapped_lines = Self::wrap_text(content, width);
553 wrapped_lines
554 .into_iter()
555 .enumerate()
556 .map(|(idx, line)| {
557 if idx == 0 {
558 let mut spans = vec![Span::styled(
560 line_number_part.to_string(),
561 Style::default().fg(Color::DarkGray),
562 )];
563
564 if !code_part.is_empty() {
565 let highlighted_spans =
567 syntax_highlighter::highlight_line(code_part);
568 spans.extend(highlighted_spans);
569 }
570
571 Line::from(spans)
572 } else {
573 let indent = " ".repeat(line_number_part.len());
575 let mut spans = vec![Span::styled(indent, Style::default())];
576
577 let highlighted_spans = syntax_highlighter::highlight_line(&line);
579 spans.extend(highlighted_spans);
580
581 Line::from(spans)
582 }
583 })
584 .collect()
585 } else {
586 vec![Line::from(vec![Span::styled(
587 content.to_string(),
588 Style::default(),
589 )])]
590 }
591 } else {
592 vec![Line::from(vec![Span::styled(
593 content.to_string(),
594 Style::default(),
595 )])]
596 }
597 }
598
599 fn wrap_text(text: &str, width: usize) -> Vec<String> {
601 if width == 0 || text.is_empty() {
602 return vec![text.to_string()];
603 }
604
605 let mut lines: Vec<String> = Vec::new();
606 let mut current_line = String::new();
607 let mut current_width: usize = 0;
608
609 for ch in text.chars() {
610 if ch == '\n' {
611 lines.push(current_line);
612 current_line = String::new();
613 current_width = 0;
614 continue;
615 }
616
617 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
618 if current_width + ch_width > width {
619 lines.push(current_line);
620 current_line = String::new();
621 current_width = 0;
622 }
623
624 current_line.push(ch);
625 current_width += ch_width;
626 }
627
628 if !current_line.is_empty() {
629 lines.push(current_line);
630 }
631
632 if lines.is_empty() {
633 vec![text.to_string()]
634 } else {
635 lines
636 }
637 }
638
639 pub fn format_file_info(groups: &[crate::events::SourceFileGroup], use_ascii: bool) -> String {
641 const MAX_FILES_DETAILED: usize = 1000;
642 const MAX_FILES_PER_MODULE: usize = 50;
643
644 let total_files: usize = groups.iter().map(|g| g.files.len()).sum();
645 let folder_icon = if use_ascii {
646 UISymbols::FILE_FOLDER_ASCII
647 } else {
648 UISymbols::FILE_FOLDER
649 };
650 let mut response = format!(
651 "{folder_icon} {} ({} modules, {total_files} files):\n\n",
652 UIStrings::SOURCE_FILES_HEADER,
653 groups.len()
654 );
655
656 if groups.is_empty() {
657 response.push_str(&format!(" {}\n", UIStrings::NO_SOURCE_FILES));
658 return response;
659 }
660
661 if total_files > MAX_FILES_DETAILED {
663 response.push_str(&format!(
664 "⚠️ Large dataset detected ({total_files} files). Showing summary view.\n\n"
665 ));
666 Self::format_file_summary(groups, use_ascii, &mut response);
667 } else {
668 for group in groups {
669 if group.files.len() > MAX_FILES_PER_MODULE {
671 Self::format_module_summary(group, use_ascii, &mut response);
672 } else {
673 Self::format_module_detailed(group, use_ascii, &mut response);
674 }
675 }
676 }
677
678 response
679 }
680
681 pub fn format_file_info_styled(
683 groups: &[crate::events::SourceFileGroup],
684 use_ascii: bool,
685 ) -> Vec<Line<'static>> {
686 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
687 use std::collections::BTreeMap;
688
689 let total_files: usize = groups.iter().map(|g| g.files.len()).sum();
690 let mut lines = Vec::new();
691
692 lines.push(
694 StyledLineBuilder::new()
695 .title(format!(
696 "{} ({} modules, {} files):",
697 UIStrings::SOURCE_FILES_HEADER,
698 groups.len(),
699 total_files
700 ))
701 .build(),
702 );
703 lines.push(Line::from(""));
704
705 if groups.is_empty() {
706 lines.push(
707 StyledLineBuilder::new()
708 .text(" ")
709 .value(UIStrings::NO_SOURCE_FILES)
710 .build(),
711 );
712 return lines;
713 }
714
715 for group in groups {
716 lines.push(
718 StyledLineBuilder::new()
719 .styled(format!("📦 {}", group.module_path), StylePresets::SECTION)
720 .build(),
721 );
722
723 if group.files.is_empty() {
724 lines.push(
725 StyledLineBuilder::new()
726 .styled(" └─", StylePresets::TREE)
727 .value("(no files)")
728 .build(),
729 );
730 lines.push(Line::from(""));
731 continue;
732 }
733
734 let mut dir_map: BTreeMap<String, Vec<&crate::events::SourceFileInfo>> =
736 BTreeMap::new();
737 for f in &group.files {
738 dir_map.entry(f.directory.clone()).or_default().push(f);
739 }
740
741 let dir_count = dir_map.len();
742 for (didx, (dir, files)) in dir_map.into_iter().enumerate() {
743 let last_dir = didx + 1 == dir_count;
744 let dir_prefix = if last_dir {
745 if use_ascii {
746 " └-"
747 } else {
748 " └─"
749 }
750 } else if use_ascii {
751 " |-"
752 } else {
753 " ├─"
754 };
755
756 lines.push(
757 StyledLineBuilder::new()
758 .styled(dir_prefix, StylePresets::TREE)
759 .text(" ")
760 .key(&dir)
761 .text(format!(" ({} files)", files.len()))
762 .build(),
763 );
764
765 for (fidx, file) in files.iter().enumerate() {
766 let last_file = fidx + 1 == files.len();
767 let file_prefix = if last_dir {
768 if last_file {
769 " └─"
770 } else {
771 " ├─"
772 }
773 } else if last_file {
774 " │ └─"
775 } else {
776 " │ ├─"
777 };
778 lines.push(
779 StyledLineBuilder::new()
780 .styled(file_prefix, StylePresets::TREE)
781 .text(" ")
782 .value(&file.path)
783 .build(),
784 );
785 }
786 }
787 lines.push(Line::from(""));
788 }
789
790 lines
791 }
792
793 fn format_file_summary(
795 groups: &[crate::events::SourceFileGroup],
796 use_ascii: bool,
797 response: &mut String,
798 ) {
799 let mut file_types = std::collections::HashMap::new();
801
802 for group in groups.iter().take(10) {
803 let module_icon = if use_ascii { "+" } else { "📦" };
805 response.push_str(&format!(
806 "{module_icon} {} ({} files)\n",
807 group.module_path,
808 group.files.len()
809 ));
810
811 for file in &group.files {
813 let ext = std::path::Path::new(&file.path)
814 .extension()
815 .and_then(|s| s.to_str())
816 .unwrap_or("(none)")
817 .to_ascii_lowercase();
818 *file_types.entry(ext).or_insert(0) += 1;
819 }
820 }
821
822 if groups.len() > 10 {
823 response.push_str(&format!("... and {} more modules\n", groups.len() - 10));
824 }
825
826 response.push_str("\n📊 File Type Summary:\n");
827 let mut sorted_types: Vec<_> = file_types.into_iter().collect();
828 sorted_types.sort_by(|a, b| b.1.cmp(&a.1));
829
830 for (ext, count) in sorted_types.into_iter().take(10) {
831 let icon = UISymbols::get_file_icon(&ext, use_ascii);
832 response.push_str(&format!(" {icon} .{ext}: {count} files\n"));
833 }
834
835 response.push_str("\n💡 Use 'o' key in source panel to search for specific files.\n");
836 }
837
838 fn format_module_summary(
840 group: &crate::events::SourceFileGroup,
841 use_ascii: bool,
842 response: &mut String,
843 ) {
844 let package_icon = if use_ascii {
845 UISymbols::FILE_PACKAGE_ASCII
846 } else {
847 UISymbols::FILE_PACKAGE
848 };
849 response.push_str(&format!(
850 "{package_icon} {} ({} files - showing summary)\n",
851 group.module_path,
852 group.files.len()
853 ));
854
855 let mut dir_map: std::collections::BTreeMap<String, usize> =
857 std::collections::BTreeMap::new();
858 for file in &group.files {
859 *dir_map.entry(file.directory.clone()).or_insert(0) += 1;
860 }
861
862 for (i, (dir, count)) in dir_map.iter().enumerate().take(5) {
863 let is_last = i == 4 || i == dir_map.len() - 1;
864 let prefix = if is_last { " └─" } else { " ├─" };
865 response.push_str(&format!("{prefix} {dir} ({count} files)\n"));
866 }
867
868 if dir_map.len() > 5 {
869 response.push_str(&format!(
870 " └─ ... and {} more directories\n",
871 dir_map.len() - 5
872 ));
873 }
874
875 response.push('\n');
876 }
877
878 fn format_module_detailed(
880 group: &crate::events::SourceFileGroup,
881 use_ascii: bool,
882 response: &mut String,
883 ) {
884 let group_file_count = group.files.len();
885 let package_icon = if use_ascii {
886 UISymbols::FILE_PACKAGE_ASCII
887 } else {
888 UISymbols::FILE_PACKAGE
889 };
890 response.push_str(&format!(
891 "{package_icon} {} ({group_file_count} files)\n",
892 group.module_path
893 ));
894
895 if group.files.is_empty() {
896 response.push_str(" └─ (no files)\n\n");
897 return;
898 }
899
900 let mut dir_map: std::collections::BTreeMap<String, Vec<&crate::events::SourceFileInfo>> =
901 std::collections::BTreeMap::new();
902 for f in &group.files {
903 dir_map.entry(f.directory.clone()).or_default().push(f);
904 }
905
906 let dir_count = dir_map.len();
907 for (didx, (dir, files)) in dir_map.into_iter().enumerate() {
908 let last_dir = didx + 1 == dir_count;
909 let dir_prefix = if last_dir {
910 if use_ascii {
911 UISymbols::NAV_TREE_LAST_ASCII
912 } else {
913 UISymbols::NAV_TREE_LAST
914 }
915 } else if use_ascii {
916 UISymbols::NAV_TREE_BRANCH_ASCII
917 } else {
918 UISymbols::NAV_TREE_BRANCH
919 };
920 response.push_str(&format!(" {dir_prefix} {dir} ({} files)\n", files.len()));
921
922 for (fidx, file) in files.iter().enumerate() {
923 let last_file = fidx + 1 == files.len();
924 let file_prefix = if last_dir {
925 if last_file {
926 " └─"
927 } else {
928 " ├─"
929 }
930 } else if last_file {
931 " │ └─"
932 } else {
933 " │ ├─"
934 };
935
936 let ext = std::path::Path::new(&file.path)
937 .extension()
938 .and_then(|s| s.to_str())
939 .unwrap_or("")
940 .to_ascii_lowercase();
941 let icon = UISymbols::get_file_icon(&ext, use_ascii);
942 let path = &file.path;
943 response.push_str(&format!("{file_prefix} {icon} {path}\n"));
944 }
945 }
946
947 response.push('\n');
948 }
949
950 pub fn format_shared_library_info(
952 libraries: &[crate::events::SharedLibraryInfo],
953 use_ascii: bool,
954 ) -> String {
955 let mut response = format!(
956 "{} {} ({}):\n\n",
957 if use_ascii {
958 UISymbols::LIBRARY_ICON_ASCII
959 } else {
960 UISymbols::LIBRARY_ICON
961 },
962 UIStrings::SHARED_LIBRARIES_HEADER,
963 libraries.len()
964 );
965
966 if !libraries.is_empty() {
967 response.push_str(UIStrings::SHARED_LIB_TABLE_HEADER);
968 response.push('\n');
969 response.push_str(&UIStrings::SCRIPT_SEPARATOR.repeat(90));
970 response.push('\n');
971
972 let mut debug_links = Vec::new();
974
975 for lib in libraries {
976 let from_str = format!("0x{:016x}", lib.from_address);
977 let to_str = format!("0x{:016x}", lib.to_address);
978
979 let syms_read = UISymbols::get_yes_no_icon(lib.symbols_read, use_ascii);
980 let debug_read = UISymbols::get_yes_no_icon(lib.debug_info_available, use_ascii);
981
982 response.push_str(&format!(
983 "{} {} {} {} {}\n",
984 from_str, to_str, syms_read, debug_read, lib.library_path
985 ));
986
987 if let Some(ref debug_path) = lib.debug_file_path {
989 debug_links.push((lib.library_path.clone(), debug_path.clone()));
990 }
991
992 if !lib.debug_info_available {
993 let library_name = lib
994 .library_path
995 .rsplit('/')
996 .next()
997 .unwrap_or(lib.library_path.as_str());
998 response.push_str(&format!(
999 "⚠️ Warning: {library_name} {}\n",
1000 UIStrings::NO_DEBUG_INFO_WARNING
1001 ));
1002 }
1003 }
1004
1005 if !debug_links.is_empty() {
1007 response.push('\n');
1008 response.push_str("Debug files (.gnu_debuglink):\n");
1009 for (lib_path, debug_path) in debug_links {
1010 response.push_str(&format!(" {lib_path} → {debug_path}\n"));
1011 }
1012 }
1013 } else {
1014 response.push_str(&format!(" {}\n", UIStrings::NO_SHARED_LIBRARIES));
1015 }
1016
1017 response
1018 }
1019
1020 pub fn format_executable_file_info(info: &ExecutableFileInfoDisplay) -> String {
1022 let ExecutableFileInfoDisplay {
1023 file_path,
1024 file_type,
1025 entry_point,
1026 has_symbols,
1027 has_debug_info,
1028 debug_file_path,
1029 text_section,
1030 data_section,
1031 mode_description,
1032 } = info;
1033 let mut response = String::new();
1034
1035 response.push_str("📄 Executable File Information:\n\n");
1037
1038 response.push_str(&format!(" File: {file_path}\n"));
1040
1041 response.push_str(&format!(" Type: {file_type}\n"));
1043
1044 if let Some(entry) = entry_point {
1046 response.push_str(&format!(" Entry point: 0x{entry:x}\n"));
1047 }
1048
1049 response.push('\n');
1050
1051 response.push_str(" Symbols: ");
1053 if *has_symbols {
1054 response.push_str("✓ Available\n");
1055 } else {
1056 response.push_str("✗ Not available\n");
1057 }
1058
1059 response.push_str(" Debug info: ");
1060 if *has_debug_info {
1061 response.push_str("✓ Available");
1062 if let Some(ref debug_path) = debug_file_path {
1063 response.push_str(&format!(" (via debug link: {debug_path})"));
1064 }
1065 response.push('\n');
1066 } else {
1067 response.push_str("✗ Not available\n");
1068 }
1069
1070 response.push('\n');
1071
1072 let is_static_mode = mode_description.contains("Static analysis mode");
1074
1075 if is_static_mode {
1077 response.push_str(" Sections (ELF virtual addresses):\n");
1078 } else {
1079 response.push_str(" Sections (runtime loaded addresses):\n");
1080 }
1081
1082 if let Some(text) = text_section {
1083 response.push_str(&format!(
1084 " .text: 0x{:016x} - 0x{:016x} (size: {} bytes)\n",
1085 text.start_address, text.end_address, text.size
1086 ));
1087 }
1088
1089 if let Some(data) = data_section {
1090 response.push_str(&format!(
1091 " .data: 0x{:016x} - 0x{:016x} (size: {} bytes)\n",
1092 data.start_address, data.end_address, data.size
1093 ));
1094 }
1095
1096 response.push('\n');
1097
1098 response.push_str(&format!(" Mode: {mode_description}\n"));
1100
1101 response
1102 }
1103
1104 pub fn format_shared_library_info_styled(
1106 libraries: &[crate::events::SharedLibraryInfo],
1107 _use_ascii: bool,
1108 ) -> Vec<Line<'static>> {
1109 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1110 let mut lines = Vec::new();
1111 lines.push(
1112 StyledLineBuilder::new()
1113 .title(format!(
1114 "📚 {} ({})",
1115 UIStrings::SHARED_LIBRARIES_HEADER,
1116 libraries.len()
1117 ))
1118 .build(),
1119 );
1120 lines.push(Line::from(""));
1121
1122 if libraries.is_empty() {
1123 lines.push(
1124 StyledLineBuilder::new()
1125 .text(" ")
1126 .value(UIStrings::NO_SHARED_LIBRARIES)
1127 .build(),
1128 );
1129 return lines;
1130 }
1131
1132 for lib in libraries {
1133 let from_str = format!("0x{:016x}", lib.from_address);
1134 let to_str = format!("0x{:016x}", lib.to_address);
1135 let syms = if lib.symbols_read { "✅" } else { "❌" };
1136 let dbg = if lib.debug_info_available {
1137 "✅"
1138 } else {
1139 "❌"
1140 };
1141
1142 let mut b = StyledLineBuilder::new().text(" ");
1143 b = b
1144 .text(from_str)
1145 .text(" ")
1146 .text(to_str)
1147 .text(" ")
1148 .key("sym:")
1149 .text(" ")
1150 .styled(
1151 syms,
1152 if lib.symbols_read {
1153 StylePresets::SUCCESS
1154 } else {
1155 StylePresets::ERROR
1156 },
1157 )
1158 .text(" ")
1159 .key("dbg:")
1160 .text(" ")
1161 .styled(
1162 dbg,
1163 if lib.debug_info_available {
1164 StylePresets::SUCCESS
1165 } else {
1166 StylePresets::ERROR
1167 },
1168 )
1169 .text(" ")
1170 .value(&lib.library_path);
1171 lines.push(b.build());
1172
1173 if !lib.debug_info_available {
1174 let library_name = lib
1175 .library_path
1176 .rsplit('/')
1177 .next()
1178 .unwrap_or(lib.library_path.as_str());
1179 lines.push(
1180 StyledLineBuilder::new()
1181 .text(" ")
1182 .styled(
1183 format!(
1184 "⚠️ Warning: {} {}",
1185 library_name,
1186 UIStrings::NO_DEBUG_INFO_WARNING
1187 ),
1188 StylePresets::WARNING,
1189 )
1190 .build(),
1191 );
1192 }
1193
1194 if let Some(ref debug_path) = lib.debug_file_path {
1195 lines.push(
1196 StyledLineBuilder::new()
1197 .text(" ")
1198 .key("Debug file:")
1199 .text(" ")
1200 .value(debug_path)
1201 .build(),
1202 );
1203 }
1204 }
1205
1206 lines
1207 }
1208
1209 pub fn format_executable_file_info_styled(
1211 info: &ExecutableFileInfoDisplay,
1212 ) -> Vec<Line<'static>> {
1213 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1214 let mut lines = vec![
1215 StyledLineBuilder::new()
1216 .title("📄 Executable File Information:")
1217 .build(),
1218 Line::from(""),
1219 ];
1220
1221 lines.push(
1222 StyledLineBuilder::new()
1223 .text(" ")
1224 .key("File:")
1225 .text(" ")
1226 .value(info.file_path)
1227 .build(),
1228 );
1229 lines.push(
1230 StyledLineBuilder::new()
1231 .text(" ")
1232 .key("Type:")
1233 .text(" ")
1234 .value(info.file_type)
1235 .build(),
1236 );
1237 if let Some(entry) = info.entry_point {
1238 lines.push(
1239 StyledLineBuilder::new()
1240 .text(" ")
1241 .key("Entry point:")
1242 .text(" ")
1243 .address(entry)
1244 .build(),
1245 );
1246 }
1247
1248 lines.push(Line::from(""));
1249
1250 lines.push(
1251 StyledLineBuilder::new()
1252 .text(" ")
1253 .key("Symbols:")
1254 .text(" ")
1255 .styled(
1256 if info.has_symbols {
1257 "✅ Available"
1258 } else {
1259 "❌ Not available"
1260 },
1261 if info.has_symbols {
1262 StylePresets::SUCCESS
1263 } else {
1264 StylePresets::ERROR
1265 },
1266 )
1267 .build(),
1268 );
1269
1270 let mut dbg_line = StyledLineBuilder::new()
1271 .text(" ")
1272 .key("Debug info:")
1273 .text(" ");
1274 if info.has_debug_info {
1275 dbg_line = dbg_line.styled("✅ Available", StylePresets::SUCCESS);
1276 if let Some(ref dbg_path) = info.debug_file_path {
1277 dbg_line = dbg_line
1278 .text(" (via debug link: ")
1279 .value(dbg_path)
1280 .text(")");
1281 }
1282 } else {
1283 dbg_line = dbg_line.styled("❌ Not available", StylePresets::ERROR);
1284 }
1285 lines.push(dbg_line.build());
1286
1287 lines.push(Line::from(""));
1288 let is_static_mode = info.mode_description.contains("Static analysis mode");
1289 lines.push(
1290 StyledLineBuilder::new()
1291 .text(" ")
1292 .styled(
1293 if is_static_mode {
1294 "Sections (ELF virtual addresses):"
1295 } else {
1296 "Sections (runtime loaded addresses):"
1297 },
1298 StylePresets::SECTION,
1299 )
1300 .build(),
1301 );
1302 if let Some(text) = info.text_section {
1303 lines.push(
1304 StyledLineBuilder::new()
1305 .text(" ")
1306 .key(".text:")
1307 .text(" ")
1308 .text(format!(
1309 "0x{:016x} - 0x{:016x} (size: {} bytes)",
1310 text.start_address, text.end_address, text.size
1311 ))
1312 .build(),
1313 );
1314 }
1315 if let Some(data) = info.data_section {
1316 lines.push(
1317 StyledLineBuilder::new()
1318 .text(" ")
1319 .key(".data:")
1320 .text(" ")
1321 .text(format!(
1322 "0x{:016x} - 0x{:016x} (size: {} bytes)",
1323 data.start_address, data.end_address, data.size
1324 ))
1325 .build(),
1326 );
1327 }
1328
1329 lines.push(Line::from(""));
1330 lines.push(
1331 StyledLineBuilder::new()
1332 .text(" ")
1333 .key("Mode:")
1334 .text(" ")
1335 .value(info.mode_description)
1336 .build(),
1337 );
1338
1339 lines
1340 }
1341
1342 pub fn render_panel(f: &mut Frame, area: Rect, state: &CommandPanelState) {
1344 let inner_area = Rect::new(
1346 area.x + 1,
1347 area.y + 1,
1348 area.width.saturating_sub(2),
1349 area.height.saturating_sub(2),
1350 );
1351
1352 let mut lines = Vec::new();
1354
1355 for item in state
1357 .command_history
1358 .iter()
1359 .rev()
1360 .take(inner_area.height as usize)
1361 {
1362 let command_line = Line::from(vec![
1364 Span::styled(&item.prompt, Style::default().fg(Color::DarkGray)),
1365 Span::raw(&item.command),
1366 ]);
1367 lines.push(command_line);
1368
1369 if let Some(ref response) = item.response {
1371 for line in response.lines().take(3) {
1372 lines.push(Line::from(Span::raw(line)));
1374 }
1375 }
1376 }
1377
1378 let current_prompt = "gs> "; let current_line = Line::from(vec![
1381 Span::styled(current_prompt, Style::default().fg(Color::Magenta)),
1382 Span::raw(&state.input_text),
1383 Span::styled("_", Style::default().fg(Color::White)), ]);
1385 lines.push(current_line);
1386
1387 let paragraph = Paragraph::new(lines);
1388 f.render_widget(paragraph, inner_area);
1389 }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394 use super::*;
1395
1396 #[test]
1397 fn runtime_alert_visible_without_command_history() {
1398 let mut state = CommandPanelState::new();
1399 let content = "⚠ Trace queue saturated: dropped 10 events in last 1s".to_string();
1400 let styled = ResponseFormatter::style_generic_message_lines(&content);
1401
1402 ResponseFormatter::upsert_runtime_alert_with_style(
1403 &mut state,
1404 content.clone(),
1405 Some(styled),
1406 ResponseType::Warning,
1407 );
1408
1409 assert!(state.command_history.is_empty());
1410 assert!(state.static_lines.iter().any(|line| {
1411 line.line_type == LineType::RuntimeAlert
1412 && line.content.contains("Trace queue saturated")
1413 }));
1414 }
1415
1416 #[test]
1417 fn runtime_alert_is_upserted_and_survives_history_refresh() {
1418 let mut state = CommandPanelState::new();
1419
1420 ResponseFormatter::upsert_runtime_alert_with_style(
1421 &mut state,
1422 "⚠ old alert".to_string(),
1423 None,
1424 ResponseType::Warning,
1425 );
1426 ResponseFormatter::upsert_runtime_alert_with_style(
1427 &mut state,
1428 "⚠ new alert".to_string(),
1429 None,
1430 ResponseType::Warning,
1431 );
1432
1433 state.add_command_entry("info trace");
1435 ResponseFormatter::update_static_lines(&mut state);
1436
1437 let alert_lines: Vec<_> = state
1438 .static_lines
1439 .iter()
1440 .filter(|line| line.line_type == LineType::RuntimeAlert)
1441 .collect();
1442 assert_eq!(alert_lines.len(), 1);
1443 assert!(alert_lines[0].content.contains("new alert"));
1444 }
1445}
1446
1447