ghostscope_ui/components/command_panel/
response_formatter.rs

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
14// Help message dynamic detection removed after pre-styled migration
15
16// Help styling handled upstream; no local emoji-based parsing
17// Info dynamic styling removed; titles are generated pre-styled upstream
18
19/// Parameter struct for format_executable_file_info to avoid too many function arguments
20pub 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
32/// Handles response formatting and display for the command panel
33pub struct ResponseFormatter;
34
35impl ResponseFormatter {
36    /// Create enhanced styled lines for generic messages.
37    /// - Leading symbols: ✓ (success), ✗ (error), ⚠ (warning)
38    /// - Numbers/hex addresses: Yellow
39    /// - Keywords (trace/function/line/file/pid/pc/enabled/disabled/deleted/saved/loaded): Cyan
40    /// - Error tokens (failed/error/unknown/not/found/cannot/missing): Red
41    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        // Detect leading status after indentation
55        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            // Replace leading ✗ with ❌ and paint whole line red
61            let mut without = &trimmed['✗'.len_utf8()..];
62            // Consume optional variation selector U+FE0F if present
63            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        // Otherwise, keep semantic token styling
72        let mut spans: Vec<Span<'static>> = Vec::new();
73        let mut rest = line;
74
75        // Leading symbol without trimming (✓/⚠)
76        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                // Consume optional variation selector U+FE0F if present right after the symbol
95                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        // Tokenize remaining by simple boundaries, preserving punctuation as separate tokens
110        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        // Hex or number
142        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        // Keywords for structure
147        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        // Error tokens
156        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        // Default
164        Span::styled(tok.to_string(), Style::default().fg(Color::White))
165    }
166
167    /// Add a response with pre-styled lines (preferred when available)
168    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    /// Helper method to create a simple single-line styled response
190    /// This reduces code duplication for common response patterns
191    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    /// Format styled lines for batch trace loading summary
206    /// This reduces code duplication in app.rs for source command responses
207    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        // Title line
219        lines.push(
220            StyledLineBuilder::new()
221                .styled(
222                    format!("📂 Loaded traces from {filename}"),
223                    StylePresets::TITLE,
224                )
225                .build(),
226        );
227
228        // Summary line
229        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        // Details
248        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    // Removed add_welcome_message - now using direct styled approach
306
307    /// Update the static lines display from command history
308    pub fn update_static_lines(state: &mut CommandPanelState) {
309        // Keep welcome messages but remove command/response lines
310        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        // Add history items
317        for (index, item) in state.command_history.iter().enumerate() {
318            // Add command line
319            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            // Add response lines if they exist
333            if let Some(ref styled) = item.response_styled {
334                // Preferred path: use pre-styled lines when available
335                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                // Fallback path: plain only (all help/info are pre-styled upstream now)
347                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        // Note: Current input line is rendered separately by the renderer (render_normal_input)
360        // Don't add it to static_lines to avoid duplication
361    }
362
363    /// Split response into individual lines for display
364    fn split_response_lines(response: &str) -> Vec<String> {
365        response.lines().map(String::from).collect()
366    }
367
368    // Removed: all dynamic help/info styling helpers (pre-styled upstream)
369
370    /// Format a line for display with proper styling
371    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), // Format welcome messages like responses
381            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    /// Format a command line
392    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    /// Format a response line with appropriate styling
401    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        // Check if this is a script display line
405        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    /// Format current input line with cursor indication
417    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        // For now, just return the styled line without cursor indication
425        // TODO: Add proper cursor rendering
426        wrapped_lines
427            .into_iter()
428            .map(|line| Line::from(vec![Span::styled(line, UIThemes::input_mode())]))
429            .collect()
430    }
431
432    /// Get appropriate style for response based on type and content
433    fn get_response_style(content: &str, response_type: Option<ResponseType>) -> Style {
434        // First check explicit response type
435        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        // Fallback to content-based detection
447        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    /// Check if a line is part of a script display
463    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    /// Format script display lines with syntax highlighting
471    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            // Header line - green and bold
474            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            // Separator line - dark gray
482            vec![Line::from(vec![Span::styled(
483                content.to_string(),
484                Style::default().fg(Color::DarkGray),
485            )])]
486        } else if content.contains(" │ ") {
487            // Script line with line number - apply syntax highlighting
488            Self::format_script_code_line(content, width)
489        } else {
490            // Regular line
491            vec![Line::from(vec![Span::styled(
492                content.to_string(),
493                Style::default(),
494            )])]
495        }
496    }
497
498    /// Format a script code line with line numbers
499    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                            // First line - format with line number and code parts
515                            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                                // Apply syntax highlighting to the code part
522                                let highlighted_spans =
523                                    syntax_highlighter::highlight_line(code_part);
524                                spans.extend(highlighted_spans);
525                            }
526
527                            Line::from(spans)
528                        } else {
529                            // Continuation lines - indent to align with code
530                            let indent = " ".repeat(line_number_part.len());
531                            let mut spans = vec![Span::styled(indent, Style::default())];
532
533                            // Apply syntax highlighting to continuation lines too
534                            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    /// Wrap text to fit within specified width (Unicode-aware)
556    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    /// Format file information display
596    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        // For large datasets, show summary mode
618        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                // For individual modules with many files, also use limited view
626                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    /// Styled: Format file information display
638    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        // Title
649        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            // Module path as section
673            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            // Group by directory (same logic as plain)
691            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    /// Format file information in summary mode for large datasets
750    fn format_file_summary(
751        groups: &[crate::events::SourceFileGroup],
752        use_ascii: bool,
753        response: &mut String,
754    ) {
755        // Show top N modules and file type statistics
756        let mut file_types = std::collections::HashMap::new();
757
758        for group in groups.iter().take(10) {
759            // Show top 10 modules
760            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            // Count file types in this module
768            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    /// Format a single module in summary mode
795    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        // Group by directory and show counts
812        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    /// Format a single module with full details
835    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    /// Format shared library information
907    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            // Collect libraries with debug links for later display
929            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                // Collect debug link info
944                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            // Show debug links section if any
962            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    /// Format executable file information for display
977    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        // Header
992        response.push_str("📄 Executable File Information:\n\n");
993
994        // File path
995        response.push_str(&format!("  File: {file_path}\n"));
996
997        // File type
998        response.push_str(&format!("  Type: {file_type}\n"));
999
1000        // Entry point
1001        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        // Symbol and debug information status
1008        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        // Determine if this is static analysis mode
1029        let is_static_mode = mode_description.contains("Static analysis mode");
1030
1031        // Sections
1032        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        // Mode description
1055        response.push_str(&format!("  Mode: {mode_description}\n"));
1056
1057        response
1058    }
1059
1060    /// Styled shared library information (new)
1061    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    /// Styled executable file information (new)
1166    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    /// Render the command panel content
1299    pub fn render_panel(f: &mut Frame, area: Rect, state: &CommandPanelState) {
1300        // Calculate inner area (excluding borders)
1301        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        // Create simple display content
1309        let mut lines = Vec::new();
1310
1311        // Show command history
1312        for item in state
1313            .command_history
1314            .iter()
1315            .rev()
1316            .take(inner_area.height as usize)
1317        {
1318            // Add command line
1319            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            // Add response if exists
1326            if let Some(ref response) = item.response {
1327                for line in response.lines().take(3) {
1328                    // Limit response lines
1329                    lines.push(Line::from(Span::raw(line)));
1330                }
1331            }
1332        }
1333
1334        // Always show current input line (with prompt)
1335        let current_prompt = "gs> "; // Use fixed prompt for now
1336        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)), // Simple cursor
1340        ]);
1341        lines.push(current_line);
1342
1343        let paragraph = Paragraph::new(lines);
1344        f.render_widget(paragraph, inner_area);
1345    }
1346}
1347
1348// Removed tests for dynamic help styling (now pre-styled upstream)