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 add_simple_styled_response(
192 state: &mut CommandPanelState,
193 content: String,
194 style: ratatui::style::Style,
195 response_type: ResponseType,
196 ) {
197 let styled = vec![
198 crate::components::command_panel::style_builder::StyledLineBuilder::new()
199 .styled(&content, style)
200 .build(),
201 ];
202 Self::add_response_with_style(state, content, Some(styled), response_type);
203 }
204
205 pub fn format_batch_load_summary_styled(
208 filename: &str,
209 total_count: usize,
210 success_count: usize,
211 failed_count: usize,
212 disabled_count: usize,
213 details: &[crate::events::TraceLoadDetail],
214 ) -> Vec<Line<'static>> {
215 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
216 let mut lines = Vec::new();
217
218 lines.push(
220 StyledLineBuilder::new()
221 .styled(
222 format!("📂 Loaded traces from {filename}"),
223 StylePresets::TITLE,
224 )
225 .build(),
226 );
227
228 let summary = if disabled_count > 0 {
230 format!(
231 " Total: {total_count}, Success: {success_count}, Failed: {failed_count}, Disabled: {disabled_count}"
232 )
233 } else {
234 format!(" Total: {total_count}, Success: {success_count}, Failed: {failed_count}")
235 };
236 lines.push(StyledLineBuilder::new().value(&summary).build());
237 lines.push(
238 StyledLineBuilder::new()
239 .text(" • ")
240 .styled(
241 "Selected indices from the file are restored when present",
242 StylePresets::TIP,
243 )
244 .build(),
245 );
246
247 if !details.is_empty() {
249 lines.push(
250 StyledLineBuilder::new()
251 .styled("", StylePresets::VALUE)
252 .build(),
253 );
254 lines.push(
255 StyledLineBuilder::new()
256 .styled("📊 Details:", StylePresets::SECTION)
257 .build(),
258 );
259 for detail in details {
260 match detail.status {
261 crate::events::LoadStatus::Created => {
262 let text = if let Some(id) = detail.trace_id {
263 format!(" ✓ {} → trace #{}", detail.target, id)
264 } else {
265 format!(" ✓ {}", detail.target)
266 };
267 lines.push(
268 StyledLineBuilder::new()
269 .styled(text, StylePresets::SUCCESS)
270 .build(),
271 );
272 }
273 crate::events::LoadStatus::CreatedDisabled => {
274 let text = if let Some(id) = detail.trace_id {
275 format!(" ⊘ {} → trace #{} (disabled)", detail.target, id)
276 } else {
277 format!(" ⊘ {} (disabled)", detail.target)
278 };
279 lines.push(
280 StyledLineBuilder::new()
281 .styled(text, StylePresets::WARNING)
282 .build(),
283 );
284 }
285 crate::events::LoadStatus::Failed => {
286 let text = if let Some(ref error) = detail.error {
287 format!(" ✗ {}: {}", detail.target, error)
288 } else {
289 format!(" ✗ {}", detail.target)
290 };
291 lines.push(
292 StyledLineBuilder::new()
293 .styled(text, StylePresets::ERROR)
294 .build(),
295 );
296 }
297 _ => {}
298 }
299 }
300 }
301
302 lines
303 }
304
305 pub fn update_static_lines(state: &mut CommandPanelState) {
309 state
311 .static_lines
312 .retain(|line| line.line_type == LineType::Welcome);
313 state.styled_buffer = None;
314 state.styled_at_history_index = None;
315
316 for (index, item) in state.command_history.iter().enumerate() {
318 let command_line = format!(
320 "{prompt}{command}",
321 prompt = item.prompt,
322 command = item.command
323 );
324 state.static_lines.push(StaticTextLine {
325 content: command_line,
326 line_type: LineType::Command,
327 history_index: Some(index),
328 response_type: None,
329 styled_content: None,
330 });
331
332 if let Some(ref styled) = item.response_styled {
334 for line in styled.iter() {
336 let plain: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
337 state.static_lines.push(StaticTextLine {
338 content: plain,
339 line_type: LineType::Response,
340 history_index: Some(index),
341 response_type: item.response_type,
342 styled_content: Some(line.clone()),
343 });
344 }
345 } else if let Some(ref response) = item.response {
346 for response_line in Self::split_response_lines(response) {
348 state.static_lines.push(StaticTextLine {
349 content: response_line,
350 line_type: LineType::Response,
351 history_index: Some(index),
352 response_type: item.response_type,
353 styled_content: None,
354 });
355 }
356 }
357 }
358
359 }
362
363 fn split_response_lines(response: &str) -> Vec<String> {
365 response.lines().map(String::from).collect()
366 }
367
368 pub fn format_line_for_display(
372 state: &CommandPanelState,
373 line: &StaticTextLine,
374 is_current_input: bool,
375 width: usize,
376 ) -> Vec<Line<'static>> {
377 match line.line_type {
378 LineType::Command => Self::format_command_line(&line.content, width),
379 LineType::Response => Self::format_response_line(line, width),
380 LineType::Welcome => Self::format_response_line(line, width), LineType::CurrentInput => {
382 if is_current_input {
383 Self::format_current_input_line(state, &line.content, width)
384 } else {
385 Self::format_command_line(&line.content, width)
386 }
387 }
388 }
389 }
390
391 fn format_command_line(content: &str, width: usize) -> Vec<Line<'static>> {
393 let wrapped_lines = Self::wrap_text(content, width);
394 wrapped_lines
395 .into_iter()
396 .map(|line| Line::from(vec![Span::styled(line, Style::default().fg(Color::White))]))
397 .collect()
398 }
399
400 fn format_response_line(line: &StaticTextLine, width: usize) -> Vec<Line<'static>> {
402 let style = Self::get_response_style(&line.content, line.response_type);
403
404 if Self::is_script_display_line(&line.content) {
406 Self::format_script_display_line(&line.content, width)
407 } else {
408 let wrapped_lines = Self::wrap_text(&line.content, width);
409 wrapped_lines
410 .into_iter()
411 .map(|line_content| Line::from(vec![Span::styled(line_content, style)]))
412 .collect()
413 }
414 }
415
416 fn format_current_input_line(
418 _state: &CommandPanelState,
419 content: &str,
420 width: usize,
421 ) -> Vec<Line<'static>> {
422 let wrapped_lines = Self::wrap_text(content, width);
423
424 wrapped_lines
427 .into_iter()
428 .map(|line| Line::from(vec![Span::styled(line, UIThemes::input_mode())]))
429 .collect()
430 }
431
432 fn get_response_style(content: &str, response_type: Option<ResponseType>) -> Style {
434 if let Some(resp_type) = response_type {
436 return match resp_type {
437 ResponseType::Success => UIThemes::success_text(),
438 ResponseType::Error => UIThemes::error_text(),
439 ResponseType::Warning => UIThemes::warning_text(),
440 ResponseType::Info => UIThemes::info_text(),
441 ResponseType::Progress => UIThemes::progress_text(),
442 ResponseType::ScriptDisplay => UIThemes::script_mode(),
443 };
444 }
445
446 if content.starts_with(UIStrings::SUCCESS_PREFIX) || content.starts_with("✓") {
448 UIThemes::success_text()
449 } else if content.starts_with(UIStrings::ERROR_PREFIX) || content.starts_with("✗") {
450 UIThemes::error_text()
451 } else if content.starts_with(UIStrings::WARNING_PREFIX) || content.starts_with("⚠") {
452 UIThemes::warning_text()
453 } else if content.starts_with(UIStrings::PROGRESS_PREFIX) || content.starts_with("⏳") {
454 UIThemes::progress_text()
455 } else if content.starts_with("📝") {
456 UIThemes::script_mode()
457 } else {
458 Style::default()
459 }
460 }
461
462 fn is_script_display_line(content: &str) -> bool {
464 content.starts_with("📝")
465 || content.starts_with(UIStrings::SCRIPT_TARGET_PREFIX)
466 || content.chars().all(|c| c == '─' || c.is_whitespace())
467 || content.contains(" │ ")
468 }
469
470 fn format_script_display_line(content: &str, width: usize) -> Vec<Line<'static>> {
472 if content.starts_with("📝") || content.starts_with(UIStrings::SCRIPT_TARGET_PREFIX) {
473 vec![Line::from(vec![Span::styled(
475 content.to_string(),
476 Style::default()
477 .fg(Color::Green)
478 .add_modifier(Modifier::BOLD),
479 )])]
480 } else if content.chars().all(|c| c == '─' || c.is_whitespace()) {
481 vec![Line::from(vec![Span::styled(
483 content.to_string(),
484 Style::default().fg(Color::DarkGray),
485 )])]
486 } else if content.contains(" │ ") {
487 Self::format_script_code_line(content, width)
489 } else {
490 vec![Line::from(vec![Span::styled(
492 content.to_string(),
493 Style::default(),
494 )])]
495 }
496 }
497
498 fn format_script_code_line(content: &str, width: usize) -> Vec<Line<'static>> {
500 if let Some(separator_pos) = content.find(" │ ") {
501 let separator_str = " │ ";
502 let end_byte_pos = separator_pos + separator_str.len();
503
504 if end_byte_pos <= content.len() {
505 let line_number_part = &content[..end_byte_pos];
506 let code_part = &content[end_byte_pos..];
507
508 let wrapped_lines = Self::wrap_text(content, width);
509 wrapped_lines
510 .into_iter()
511 .enumerate()
512 .map(|(idx, line)| {
513 if idx == 0 {
514 let mut spans = vec![Span::styled(
516 line_number_part.to_string(),
517 Style::default().fg(Color::DarkGray),
518 )];
519
520 if !code_part.is_empty() {
521 let highlighted_spans =
523 syntax_highlighter::highlight_line(code_part);
524 spans.extend(highlighted_spans);
525 }
526
527 Line::from(spans)
528 } else {
529 let indent = " ".repeat(line_number_part.len());
531 let mut spans = vec![Span::styled(indent, Style::default())];
532
533 let highlighted_spans = syntax_highlighter::highlight_line(&line);
535 spans.extend(highlighted_spans);
536
537 Line::from(spans)
538 }
539 })
540 .collect()
541 } else {
542 vec![Line::from(vec![Span::styled(
543 content.to_string(),
544 Style::default(),
545 )])]
546 }
547 } else {
548 vec![Line::from(vec![Span::styled(
549 content.to_string(),
550 Style::default(),
551 )])]
552 }
553 }
554
555 fn wrap_text(text: &str, width: usize) -> Vec<String> {
557 if width == 0 || text.is_empty() {
558 return vec![text.to_string()];
559 }
560
561 let mut lines: Vec<String> = Vec::new();
562 let mut current_line = String::new();
563 let mut current_width: usize = 0;
564
565 for ch in text.chars() {
566 if ch == '\n' {
567 lines.push(current_line);
568 current_line = String::new();
569 current_width = 0;
570 continue;
571 }
572
573 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
574 if current_width + ch_width > width {
575 lines.push(current_line);
576 current_line = String::new();
577 current_width = 0;
578 }
579
580 current_line.push(ch);
581 current_width += ch_width;
582 }
583
584 if !current_line.is_empty() {
585 lines.push(current_line);
586 }
587
588 if lines.is_empty() {
589 vec![text.to_string()]
590 } else {
591 lines
592 }
593 }
594
595 pub fn format_file_info(groups: &[crate::events::SourceFileGroup], use_ascii: bool) -> String {
597 const MAX_FILES_DETAILED: usize = 1000;
598 const MAX_FILES_PER_MODULE: usize = 50;
599
600 let total_files: usize = groups.iter().map(|g| g.files.len()).sum();
601 let folder_icon = if use_ascii {
602 UISymbols::FILE_FOLDER_ASCII
603 } else {
604 UISymbols::FILE_FOLDER
605 };
606 let mut response = format!(
607 "{folder_icon} {} ({} modules, {total_files} files):\n\n",
608 UIStrings::SOURCE_FILES_HEADER,
609 groups.len()
610 );
611
612 if groups.is_empty() {
613 response.push_str(&format!(" {}\n", UIStrings::NO_SOURCE_FILES));
614 return response;
615 }
616
617 if total_files > MAX_FILES_DETAILED {
619 response.push_str(&format!(
620 "⚠️ Large dataset detected ({total_files} files). Showing summary view.\n\n"
621 ));
622 Self::format_file_summary(groups, use_ascii, &mut response);
623 } else {
624 for group in groups {
625 if group.files.len() > MAX_FILES_PER_MODULE {
627 Self::format_module_summary(group, use_ascii, &mut response);
628 } else {
629 Self::format_module_detailed(group, use_ascii, &mut response);
630 }
631 }
632 }
633
634 response
635 }
636
637 pub fn format_file_info_styled(
639 groups: &[crate::events::SourceFileGroup],
640 use_ascii: bool,
641 ) -> Vec<Line<'static>> {
642 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
643 use std::collections::BTreeMap;
644
645 let total_files: usize = groups.iter().map(|g| g.files.len()).sum();
646 let mut lines = Vec::new();
647
648 lines.push(
650 StyledLineBuilder::new()
651 .title(format!(
652 "{} ({} modules, {} files):",
653 UIStrings::SOURCE_FILES_HEADER,
654 groups.len(),
655 total_files
656 ))
657 .build(),
658 );
659 lines.push(Line::from(""));
660
661 if groups.is_empty() {
662 lines.push(
663 StyledLineBuilder::new()
664 .text(" ")
665 .value(UIStrings::NO_SOURCE_FILES)
666 .build(),
667 );
668 return lines;
669 }
670
671 for group in groups {
672 lines.push(
674 StyledLineBuilder::new()
675 .styled(format!("📦 {}", group.module_path), StylePresets::SECTION)
676 .build(),
677 );
678
679 if group.files.is_empty() {
680 lines.push(
681 StyledLineBuilder::new()
682 .styled(" └─", StylePresets::TREE)
683 .value("(no files)")
684 .build(),
685 );
686 lines.push(Line::from(""));
687 continue;
688 }
689
690 let mut dir_map: BTreeMap<String, Vec<&crate::events::SourceFileInfo>> =
692 BTreeMap::new();
693 for f in &group.files {
694 dir_map.entry(f.directory.clone()).or_default().push(f);
695 }
696
697 let dir_count = dir_map.len();
698 for (didx, (dir, files)) in dir_map.into_iter().enumerate() {
699 let last_dir = didx + 1 == dir_count;
700 let dir_prefix = if last_dir {
701 if use_ascii {
702 " └-"
703 } else {
704 " └─"
705 }
706 } else if use_ascii {
707 " |-"
708 } else {
709 " ├─"
710 };
711
712 lines.push(
713 StyledLineBuilder::new()
714 .styled(dir_prefix, StylePresets::TREE)
715 .text(" ")
716 .key(&dir)
717 .text(format!(" ({} files)", files.len()))
718 .build(),
719 );
720
721 for (fidx, file) in files.iter().enumerate() {
722 let last_file = fidx + 1 == files.len();
723 let file_prefix = if last_dir {
724 if last_file {
725 " └─"
726 } else {
727 " ├─"
728 }
729 } else if last_file {
730 " │ └─"
731 } else {
732 " │ ├─"
733 };
734 lines.push(
735 StyledLineBuilder::new()
736 .styled(file_prefix, StylePresets::TREE)
737 .text(" ")
738 .value(&file.path)
739 .build(),
740 );
741 }
742 }
743 lines.push(Line::from(""));
744 }
745
746 lines
747 }
748
749 fn format_file_summary(
751 groups: &[crate::events::SourceFileGroup],
752 use_ascii: bool,
753 response: &mut String,
754 ) {
755 let mut file_types = std::collections::HashMap::new();
757
758 for group in groups.iter().take(10) {
759 let module_icon = if use_ascii { "+" } else { "📦" };
761 response.push_str(&format!(
762 "{module_icon} {} ({} files)\n",
763 group.module_path,
764 group.files.len()
765 ));
766
767 for file in &group.files {
769 let ext = std::path::Path::new(&file.path)
770 .extension()
771 .and_then(|s| s.to_str())
772 .unwrap_or("(none)")
773 .to_ascii_lowercase();
774 *file_types.entry(ext).or_insert(0) += 1;
775 }
776 }
777
778 if groups.len() > 10 {
779 response.push_str(&format!("... and {} more modules\n", groups.len() - 10));
780 }
781
782 response.push_str("\n📊 File Type Summary:\n");
783 let mut sorted_types: Vec<_> = file_types.into_iter().collect();
784 sorted_types.sort_by(|a, b| b.1.cmp(&a.1));
785
786 for (ext, count) in sorted_types.into_iter().take(10) {
787 let icon = UISymbols::get_file_icon(&ext, use_ascii);
788 response.push_str(&format!(" {icon} .{ext}: {count} files\n"));
789 }
790
791 response.push_str("\n💡 Use 'o' key in source panel to search for specific files.\n");
792 }
793
794 fn format_module_summary(
796 group: &crate::events::SourceFileGroup,
797 use_ascii: bool,
798 response: &mut String,
799 ) {
800 let package_icon = if use_ascii {
801 UISymbols::FILE_PACKAGE_ASCII
802 } else {
803 UISymbols::FILE_PACKAGE
804 };
805 response.push_str(&format!(
806 "{package_icon} {} ({} files - showing summary)\n",
807 group.module_path,
808 group.files.len()
809 ));
810
811 let mut dir_map: std::collections::BTreeMap<String, usize> =
813 std::collections::BTreeMap::new();
814 for file in &group.files {
815 *dir_map.entry(file.directory.clone()).or_insert(0) += 1;
816 }
817
818 for (i, (dir, count)) in dir_map.iter().enumerate().take(5) {
819 let is_last = i == 4 || i == dir_map.len() - 1;
820 let prefix = if is_last { " └─" } else { " ├─" };
821 response.push_str(&format!("{prefix} {dir} ({count} files)\n"));
822 }
823
824 if dir_map.len() > 5 {
825 response.push_str(&format!(
826 " └─ ... and {} more directories\n",
827 dir_map.len() - 5
828 ));
829 }
830
831 response.push('\n');
832 }
833
834 fn format_module_detailed(
836 group: &crate::events::SourceFileGroup,
837 use_ascii: bool,
838 response: &mut String,
839 ) {
840 let group_file_count = group.files.len();
841 let package_icon = if use_ascii {
842 UISymbols::FILE_PACKAGE_ASCII
843 } else {
844 UISymbols::FILE_PACKAGE
845 };
846 response.push_str(&format!(
847 "{package_icon} {} ({group_file_count} files)\n",
848 group.module_path
849 ));
850
851 if group.files.is_empty() {
852 response.push_str(" └─ (no files)\n\n");
853 return;
854 }
855
856 let mut dir_map: std::collections::BTreeMap<String, Vec<&crate::events::SourceFileInfo>> =
857 std::collections::BTreeMap::new();
858 for f in &group.files {
859 dir_map.entry(f.directory.clone()).or_default().push(f);
860 }
861
862 let dir_count = dir_map.len();
863 for (didx, (dir, files)) in dir_map.into_iter().enumerate() {
864 let last_dir = didx + 1 == dir_count;
865 let dir_prefix = if last_dir {
866 if use_ascii {
867 UISymbols::NAV_TREE_LAST_ASCII
868 } else {
869 UISymbols::NAV_TREE_LAST
870 }
871 } else if use_ascii {
872 UISymbols::NAV_TREE_BRANCH_ASCII
873 } else {
874 UISymbols::NAV_TREE_BRANCH
875 };
876 response.push_str(&format!(" {dir_prefix} {dir} ({} files)\n", files.len()));
877
878 for (fidx, file) in files.iter().enumerate() {
879 let last_file = fidx + 1 == files.len();
880 let file_prefix = if last_dir {
881 if last_file {
882 " └─"
883 } else {
884 " ├─"
885 }
886 } else if last_file {
887 " │ └─"
888 } else {
889 " │ ├─"
890 };
891
892 let ext = std::path::Path::new(&file.path)
893 .extension()
894 .and_then(|s| s.to_str())
895 .unwrap_or("")
896 .to_ascii_lowercase();
897 let icon = UISymbols::get_file_icon(&ext, use_ascii);
898 let path = &file.path;
899 response.push_str(&format!("{file_prefix} {icon} {path}\n"));
900 }
901 }
902
903 response.push('\n');
904 }
905
906 pub fn format_shared_library_info(
908 libraries: &[crate::events::SharedLibraryInfo],
909 use_ascii: bool,
910 ) -> String {
911 let mut response = format!(
912 "{} {} ({}):\n\n",
913 if use_ascii {
914 UISymbols::LIBRARY_ICON_ASCII
915 } else {
916 UISymbols::LIBRARY_ICON
917 },
918 UIStrings::SHARED_LIBRARIES_HEADER,
919 libraries.len()
920 );
921
922 if !libraries.is_empty() {
923 response.push_str(UIStrings::SHARED_LIB_TABLE_HEADER);
924 response.push('\n');
925 response.push_str(&UIStrings::SCRIPT_SEPARATOR.repeat(90));
926 response.push('\n');
927
928 let mut debug_links = Vec::new();
930
931 for lib in libraries {
932 let from_str = format!("0x{:016x}", lib.from_address);
933 let to_str = format!("0x{:016x}", lib.to_address);
934
935 let syms_read = UISymbols::get_yes_no_icon(lib.symbols_read, use_ascii);
936 let debug_read = UISymbols::get_yes_no_icon(lib.debug_info_available, use_ascii);
937
938 response.push_str(&format!(
939 "{} {} {} {} {}\n",
940 from_str, to_str, syms_read, debug_read, lib.library_path
941 ));
942
943 if let Some(ref debug_path) = lib.debug_file_path {
945 debug_links.push((lib.library_path.clone(), debug_path.clone()));
946 }
947
948 if !lib.debug_info_available {
949 let library_name = lib
950 .library_path
951 .rsplit('/')
952 .next()
953 .unwrap_or(lib.library_path.as_str());
954 response.push_str(&format!(
955 "⚠️ Warning: {library_name} {}\n",
956 UIStrings::NO_DEBUG_INFO_WARNING
957 ));
958 }
959 }
960
961 if !debug_links.is_empty() {
963 response.push('\n');
964 response.push_str("Debug files (.gnu_debuglink):\n");
965 for (lib_path, debug_path) in debug_links {
966 response.push_str(&format!(" {lib_path} → {debug_path}\n"));
967 }
968 }
969 } else {
970 response.push_str(&format!(" {}\n", UIStrings::NO_SHARED_LIBRARIES));
971 }
972
973 response
974 }
975
976 pub fn format_executable_file_info(info: &ExecutableFileInfoDisplay) -> String {
978 let ExecutableFileInfoDisplay {
979 file_path,
980 file_type,
981 entry_point,
982 has_symbols,
983 has_debug_info,
984 debug_file_path,
985 text_section,
986 data_section,
987 mode_description,
988 } = info;
989 let mut response = String::new();
990
991 response.push_str("📄 Executable File Information:\n\n");
993
994 response.push_str(&format!(" File: {file_path}\n"));
996
997 response.push_str(&format!(" Type: {file_type}\n"));
999
1000 if let Some(entry) = entry_point {
1002 response.push_str(&format!(" Entry point: 0x{entry:x}\n"));
1003 }
1004
1005 response.push('\n');
1006
1007 response.push_str(" Symbols: ");
1009 if *has_symbols {
1010 response.push_str("✓ Available\n");
1011 } else {
1012 response.push_str("✗ Not available\n");
1013 }
1014
1015 response.push_str(" Debug info: ");
1016 if *has_debug_info {
1017 response.push_str("✓ Available");
1018 if let Some(ref debug_path) = debug_file_path {
1019 response.push_str(&format!(" (via debug link: {debug_path})"));
1020 }
1021 response.push('\n');
1022 } else {
1023 response.push_str("✗ Not available\n");
1024 }
1025
1026 response.push('\n');
1027
1028 let is_static_mode = mode_description.contains("Static analysis mode");
1030
1031 if is_static_mode {
1033 response.push_str(" Sections (ELF virtual addresses):\n");
1034 } else {
1035 response.push_str(" Sections (runtime loaded addresses):\n");
1036 }
1037
1038 if let Some(text) = text_section {
1039 response.push_str(&format!(
1040 " .text: 0x{:016x} - 0x{:016x} (size: {} bytes)\n",
1041 text.start_address, text.end_address, text.size
1042 ));
1043 }
1044
1045 if let Some(data) = data_section {
1046 response.push_str(&format!(
1047 " .data: 0x{:016x} - 0x{:016x} (size: {} bytes)\n",
1048 data.start_address, data.end_address, data.size
1049 ));
1050 }
1051
1052 response.push('\n');
1053
1054 response.push_str(&format!(" Mode: {mode_description}\n"));
1056
1057 response
1058 }
1059
1060 pub fn format_shared_library_info_styled(
1062 libraries: &[crate::events::SharedLibraryInfo],
1063 _use_ascii: bool,
1064 ) -> Vec<Line<'static>> {
1065 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1066 let mut lines = Vec::new();
1067 lines.push(
1068 StyledLineBuilder::new()
1069 .title(format!(
1070 "📚 {} ({})",
1071 UIStrings::SHARED_LIBRARIES_HEADER,
1072 libraries.len()
1073 ))
1074 .build(),
1075 );
1076 lines.push(Line::from(""));
1077
1078 if libraries.is_empty() {
1079 lines.push(
1080 StyledLineBuilder::new()
1081 .text(" ")
1082 .value(UIStrings::NO_SHARED_LIBRARIES)
1083 .build(),
1084 );
1085 return lines;
1086 }
1087
1088 for lib in libraries {
1089 let from_str = format!("0x{:016x}", lib.from_address);
1090 let to_str = format!("0x{:016x}", lib.to_address);
1091 let syms = if lib.symbols_read { "✅" } else { "❌" };
1092 let dbg = if lib.debug_info_available {
1093 "✅"
1094 } else {
1095 "❌"
1096 };
1097
1098 let mut b = StyledLineBuilder::new().text(" ");
1099 b = b
1100 .text(from_str)
1101 .text(" ")
1102 .text(to_str)
1103 .text(" ")
1104 .key("sym:")
1105 .text(" ")
1106 .styled(
1107 syms,
1108 if lib.symbols_read {
1109 StylePresets::SUCCESS
1110 } else {
1111 StylePresets::ERROR
1112 },
1113 )
1114 .text(" ")
1115 .key("dbg:")
1116 .text(" ")
1117 .styled(
1118 dbg,
1119 if lib.debug_info_available {
1120 StylePresets::SUCCESS
1121 } else {
1122 StylePresets::ERROR
1123 },
1124 )
1125 .text(" ")
1126 .value(&lib.library_path);
1127 lines.push(b.build());
1128
1129 if !lib.debug_info_available {
1130 let library_name = lib
1131 .library_path
1132 .rsplit('/')
1133 .next()
1134 .unwrap_or(lib.library_path.as_str());
1135 lines.push(
1136 StyledLineBuilder::new()
1137 .text(" ")
1138 .styled(
1139 format!(
1140 "⚠️ Warning: {} {}",
1141 library_name,
1142 UIStrings::NO_DEBUG_INFO_WARNING
1143 ),
1144 StylePresets::WARNING,
1145 )
1146 .build(),
1147 );
1148 }
1149
1150 if let Some(ref debug_path) = lib.debug_file_path {
1151 lines.push(
1152 StyledLineBuilder::new()
1153 .text(" ")
1154 .key("Debug file:")
1155 .text(" ")
1156 .value(debug_path)
1157 .build(),
1158 );
1159 }
1160 }
1161
1162 lines
1163 }
1164
1165 pub fn format_executable_file_info_styled(
1167 info: &ExecutableFileInfoDisplay,
1168 ) -> Vec<Line<'static>> {
1169 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
1170 let mut lines = vec![
1171 StyledLineBuilder::new()
1172 .title("📄 Executable File Information:")
1173 .build(),
1174 Line::from(""),
1175 ];
1176
1177 lines.push(
1178 StyledLineBuilder::new()
1179 .text(" ")
1180 .key("File:")
1181 .text(" ")
1182 .value(info.file_path)
1183 .build(),
1184 );
1185 lines.push(
1186 StyledLineBuilder::new()
1187 .text(" ")
1188 .key("Type:")
1189 .text(" ")
1190 .value(info.file_type)
1191 .build(),
1192 );
1193 if let Some(entry) = info.entry_point {
1194 lines.push(
1195 StyledLineBuilder::new()
1196 .text(" ")
1197 .key("Entry point:")
1198 .text(" ")
1199 .address(entry)
1200 .build(),
1201 );
1202 }
1203
1204 lines.push(Line::from(""));
1205
1206 lines.push(
1207 StyledLineBuilder::new()
1208 .text(" ")
1209 .key("Symbols:")
1210 .text(" ")
1211 .styled(
1212 if info.has_symbols {
1213 "✅ Available"
1214 } else {
1215 "❌ Not available"
1216 },
1217 if info.has_symbols {
1218 StylePresets::SUCCESS
1219 } else {
1220 StylePresets::ERROR
1221 },
1222 )
1223 .build(),
1224 );
1225
1226 let mut dbg_line = StyledLineBuilder::new()
1227 .text(" ")
1228 .key("Debug info:")
1229 .text(" ");
1230 if info.has_debug_info {
1231 dbg_line = dbg_line.styled("✅ Available", StylePresets::SUCCESS);
1232 if let Some(ref dbg_path) = info.debug_file_path {
1233 dbg_line = dbg_line
1234 .text(" (via debug link: ")
1235 .value(dbg_path)
1236 .text(")");
1237 }
1238 } else {
1239 dbg_line = dbg_line.styled("❌ Not available", StylePresets::ERROR);
1240 }
1241 lines.push(dbg_line.build());
1242
1243 lines.push(Line::from(""));
1244 let is_static_mode = info.mode_description.contains("Static analysis mode");
1245 lines.push(
1246 StyledLineBuilder::new()
1247 .text(" ")
1248 .styled(
1249 if is_static_mode {
1250 "Sections (ELF virtual addresses):"
1251 } else {
1252 "Sections (runtime loaded addresses):"
1253 },
1254 StylePresets::SECTION,
1255 )
1256 .build(),
1257 );
1258 if let Some(text) = info.text_section {
1259 lines.push(
1260 StyledLineBuilder::new()
1261 .text(" ")
1262 .key(".text:")
1263 .text(" ")
1264 .text(format!(
1265 "0x{:016x} - 0x{:016x} (size: {} bytes)",
1266 text.start_address, text.end_address, text.size
1267 ))
1268 .build(),
1269 );
1270 }
1271 if let Some(data) = info.data_section {
1272 lines.push(
1273 StyledLineBuilder::new()
1274 .text(" ")
1275 .key(".data:")
1276 .text(" ")
1277 .text(format!(
1278 "0x{:016x} - 0x{:016x} (size: {} bytes)",
1279 data.start_address, data.end_address, data.size
1280 ))
1281 .build(),
1282 );
1283 }
1284
1285 lines.push(Line::from(""));
1286 lines.push(
1287 StyledLineBuilder::new()
1288 .text(" ")
1289 .key("Mode:")
1290 .text(" ")
1291 .value(info.mode_description)
1292 .build(),
1293 );
1294
1295 lines
1296 }
1297
1298 pub fn render_panel(f: &mut Frame, area: Rect, state: &CommandPanelState) {
1300 let inner_area = Rect::new(
1302 area.x + 1,
1303 area.y + 1,
1304 area.width.saturating_sub(2),
1305 area.height.saturating_sub(2),
1306 );
1307
1308 let mut lines = Vec::new();
1310
1311 for item in state
1313 .command_history
1314 .iter()
1315 .rev()
1316 .take(inner_area.height as usize)
1317 {
1318 let command_line = Line::from(vec![
1320 Span::styled(&item.prompt, Style::default().fg(Color::DarkGray)),
1321 Span::raw(&item.command),
1322 ]);
1323 lines.push(command_line);
1324
1325 if let Some(ref response) = item.response {
1327 for line in response.lines().take(3) {
1328 lines.push(Line::from(Span::raw(line)));
1330 }
1331 }
1332 }
1333
1334 let current_prompt = "gs> "; let current_line = Line::from(vec![
1337 Span::styled(current_prompt, Style::default().fg(Color::Magenta)),
1338 Span::raw(&state.input_text),
1339 Span::styled("_", Style::default().fg(Color::White)), ]);
1341 lines.push(current_line);
1342
1343 let paragraph = Paragraph::new(lines);
1344 f.render_widget(paragraph, inner_area);
1345 }
1346}
1347
1348