Skip to main content

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    /// Upsert a runtime alert line that is independent from command history.
190    /// This is used for periodic/system warnings (e.g., backpressure) and must
191    /// remain visible even when no command has been entered yet.
192    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    /// Helper method to create a simple single-line styled response
234    /// This reduces code duplication for common response patterns
235    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    /// Format styled lines for batch trace loading summary
250    /// This reduces code duplication in app.rs for source command responses
251    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        // Title line
263        lines.push(
264            StyledLineBuilder::new()
265                .styled(
266                    format!("📂 Loaded traces from {filename}"),
267                    StylePresets::TITLE,
268                )
269                .build(),
270        );
271
272        // Summary line
273        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        // Details
292        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    // Removed add_welcome_message - now using direct styled approach
350
351    /// Update the static lines display from command history
352    pub fn update_static_lines(state: &mut CommandPanelState) {
353        // Keep welcome/runtime alert messages but remove command/response lines
354        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        // Add history items
361        for (index, item) in state.command_history.iter().enumerate() {
362            // Add command line
363            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            // Add response lines if they exist
377            if let Some(ref styled) = item.response_styled {
378                // Preferred path: use pre-styled lines when available
379                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                // Fallback path: plain only (all help/info are pre-styled upstream now)
391                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        // Note: Current input line is rendered separately by the renderer (render_normal_input)
404        // Don't add it to static_lines to avoid duplication
405    }
406
407    /// Split response into individual lines for display
408    fn split_response_lines(response: &str) -> Vec<String> {
409        response.lines().map(String::from).collect()
410    }
411
412    // Removed: all dynamic help/info styling helpers (pre-styled upstream)
413
414    /// Format a line for display with proper styling
415    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), // Format welcome messages like responses
425            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    /// Format a command line
436    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    /// Format a response line with appropriate styling
445    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        // Check if this is a script display line
449        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    /// Format current input line with cursor indication
461    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        // For now, just return the styled line without cursor indication
469        // TODO: Add proper cursor rendering
470        wrapped_lines
471            .into_iter()
472            .map(|line| Line::from(vec![Span::styled(line, UIThemes::input_mode())]))
473            .collect()
474    }
475
476    /// Get appropriate style for response based on type and content
477    fn get_response_style(content: &str, response_type: Option<ResponseType>) -> Style {
478        // First check explicit response type
479        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        // Fallback to content-based detection
491        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    /// Check if a line is part of a script display
507    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    /// Format script display lines with syntax highlighting
515    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            // Header line - green and bold
518            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            // Separator line - dark gray
526            vec![Line::from(vec![Span::styled(
527                content.to_string(),
528                Style::default().fg(Color::DarkGray),
529            )])]
530        } else if content.contains(" │ ") {
531            // Script line with line number - apply syntax highlighting
532            Self::format_script_code_line(content, width)
533        } else {
534            // Regular line
535            vec![Line::from(vec![Span::styled(
536                content.to_string(),
537                Style::default(),
538            )])]
539        }
540    }
541
542    /// Format a script code line with line numbers
543    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                            // First line - format with line number and code parts
559                            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                                // Apply syntax highlighting to the code part
566                                let highlighted_spans =
567                                    syntax_highlighter::highlight_line(code_part);
568                                spans.extend(highlighted_spans);
569                            }
570
571                            Line::from(spans)
572                        } else {
573                            // Continuation lines - indent to align with code
574                            let indent = " ".repeat(line_number_part.len());
575                            let mut spans = vec![Span::styled(indent, Style::default())];
576
577                            // Apply syntax highlighting to continuation lines too
578                            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    /// Wrap text to fit within specified width (Unicode-aware)
600    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    /// Format file information display
640    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        // For large datasets, show summary mode
662        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                // For individual modules with many files, also use limited view
670                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    /// Styled: Format file information display
682    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        // Title
693        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            // Module path as section
717            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            // Group by directory (same logic as plain)
735            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    /// Format file information in summary mode for large datasets
794    fn format_file_summary(
795        groups: &[crate::events::SourceFileGroup],
796        use_ascii: bool,
797        response: &mut String,
798    ) {
799        // Show top N modules and file type statistics
800        let mut file_types = std::collections::HashMap::new();
801
802        for group in groups.iter().take(10) {
803            // Show top 10 modules
804            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            // Count file types in this module
812            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    /// Format a single module in summary mode
839    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        // Group by directory and show counts
856        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    /// Format a single module with full details
879    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    /// Format shared library information
951    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            // Collect libraries with debug links for later display
973            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                // Collect debug link info
988                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            // Show debug links section if any
1006            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    /// Format executable file information for display
1021    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        // Header
1036        response.push_str("📄 Executable File Information:\n\n");
1037
1038        // File path
1039        response.push_str(&format!("  File: {file_path}\n"));
1040
1041        // File type
1042        response.push_str(&format!("  Type: {file_type}\n"));
1043
1044        // Entry point
1045        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        // Symbol and debug information status
1052        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        // Determine if this is static analysis mode
1073        let is_static_mode = mode_description.contains("Static analysis mode");
1074
1075        // Sections
1076        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        // Mode description
1099        response.push_str(&format!("  Mode: {mode_description}\n"));
1100
1101        response
1102    }
1103
1104    /// Styled shared library information (new)
1105    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    /// Styled executable file information (new)
1210    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    /// Render the command panel content
1343    pub fn render_panel(f: &mut Frame, area: Rect, state: &CommandPanelState) {
1344        // Calculate inner area (excluding borders)
1345        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        // Create simple display content
1353        let mut lines = Vec::new();
1354
1355        // Show command history
1356        for item in state
1357            .command_history
1358            .iter()
1359            .rev()
1360            .take(inner_area.height as usize)
1361        {
1362            // Add command line
1363            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            // Add response if exists
1370            if let Some(ref response) = item.response {
1371                for line in response.lines().take(3) {
1372                    // Limit response lines
1373                    lines.push(Line::from(Span::raw(line)));
1374                }
1375            }
1376        }
1377
1378        // Always show current input line (with prompt)
1379        let current_prompt = "gs> "; // Use fixed prompt for now
1380        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)), // Simple cursor
1384        ]);
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        // Add one command and refresh static lines to simulate normal command flow.
1434        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// Removed tests for dynamic help styling (now pre-styled upstream)