ghostscope_ui/components/command_panel/
optimized_renderer.rs

1use super::syntax_highlighter;
2use crate::action::ResponseType;
3use crate::model::panel_state::{CommandPanelState, InteractionMode, LineType};
4use crate::ui::themes::UIThemes;
5use ratatui::{
6    layout::Rect,
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, BorderType, Borders, Paragraph},
10    Frame,
11};
12use unicode_width::UnicodeWidthChar;
13
14/// Direct renderer for command panel (no caching)
15#[derive(Debug)]
16pub struct OptimizedRenderer {
17    // Viewport management only
18    scroll_offset: usize,
19    visible_lines: usize,
20}
21
22impl OptimizedRenderer {
23    pub fn new() -> Self {
24        Self {
25            scroll_offset: 0,
26            visible_lines: 0,
27        }
28    }
29
30    /// Mark that updates are pending (placeholder for API compatibility)
31    pub fn mark_pending_updates(&mut self) {
32        // No-op: direct rendering doesn't need pending flags
33    }
34
35    /// Main render function with direct rendering
36    pub fn render(
37        &mut self,
38        f: &mut Frame,
39        area: Rect,
40        state: &CommandPanelState,
41        is_focused: bool,
42    ) {
43        // Update viewport
44        self.update_viewport(area);
45
46        // Always render directly - no caching
47        self.render_border(f, area, is_focused, state);
48        self.render_content(f, area, state);
49    }
50
51    /// Update viewport information
52    fn update_viewport(&mut self, area: Rect) {
53        let height = area.height.saturating_sub(2);
54        self.visible_lines = height as usize;
55    }
56
57    /// Render border with mode status
58    fn render_border(
59        &self,
60        f: &mut Frame,
61        area: Rect,
62        is_focused: bool,
63        state: &CommandPanelState,
64    ) {
65        let border_style = if !is_focused {
66            Style::default().fg(Color::White)
67        } else {
68            match state.input_state {
69                crate::model::panel_state::InputState::WaitingResponse { .. } => {
70                    Style::default().fg(Color::Yellow)
71                }
72                _ => match state.mode {
73                    InteractionMode::Input => Style::default().fg(Color::Green),
74                    InteractionMode::Command => Style::default().fg(Color::Cyan),
75                    InteractionMode::ScriptEditor => Style::default().fg(Color::Green),
76                },
77            }
78        };
79
80        let title = match state.input_state {
81            crate::model::panel_state::InputState::WaitingResponse { .. } => {
82                "Interactive Command (waiting for response...)".to_string()
83            }
84            _ => match state.mode {
85                InteractionMode::Input => "Interactive Command (input mode)".to_string(),
86                InteractionMode::Command => "Interactive Command (command mode)".to_string(),
87                InteractionMode::ScriptEditor => "Interactive Command (script mode)".to_string(),
88            },
89        };
90
91        let block = Block::default()
92            .title(title)
93            .borders(Borders::ALL)
94            .border_type(if is_focused {
95                BorderType::Thick
96            } else {
97                BorderType::Plain
98            })
99            .border_style(border_style);
100
101        f.render_widget(block, area);
102    }
103
104    /// Render content directly
105    fn render_content(&self, f: &mut Frame, area: Rect, state: &CommandPanelState) {
106        let inner_area = Rect::new(
107            area.x + 1,
108            area.y + 1,
109            area.width.saturating_sub(2),
110            area.height.saturating_sub(2),
111        );
112
113        let width = inner_area.width;
114        let mut lines = Vec::new();
115
116        // Render static lines (welcome messages, etc.)
117        for static_line in &state.static_lines {
118            match static_line.line_type {
119                LineType::Welcome => {
120                    if let Some(ref styled_content) = static_line.styled_content {
121                        let wrapped_lines = self.wrap_styled_line(styled_content, width as usize);
122                        lines.extend(wrapped_lines);
123                    } else {
124                        let wrapped_lines = self.wrap_text(&static_line.content, width);
125                        for wrapped_line in wrapped_lines {
126                            lines.push(self.create_fallback_welcome_line(&wrapped_line));
127                        }
128                    }
129                }
130                LineType::Response => {
131                    // Check if we have pre-styled content
132                    if let Some(ref styled_content) = static_line.styled_content {
133                        let wrapped_lines = self.wrap_styled_line(styled_content, width as usize);
134                        lines.extend(wrapped_lines);
135                    } else {
136                        // Fallback to old logic
137                        let wrapped_lines = self.wrap_text(&static_line.content, width);
138                        // If original content has ANSI codes, don't use response_type for wrapped lines
139                        let has_ansi = static_line.content.contains("\x1b[");
140                        for wrapped_line in wrapped_lines {
141                            let response_type = if has_ansi {
142                                None // Let ANSI parser handle colors
143                            } else {
144                                static_line.response_type
145                            };
146                            lines.push(self.create_response_line(&wrapped_line, response_type));
147                        }
148                    }
149                }
150                LineType::Command => {
151                    let wrapped_lines = self.wrap_text(&static_line.content, width);
152                    for wrapped_line in wrapped_lines {
153                        lines.push(self.create_command_line(&wrapped_line));
154                    }
155                }
156                LineType::CurrentInput => {
157                    let wrapped_lines = self.wrap_text(&static_line.content, width);
158                    for wrapped_line in wrapped_lines {
159                        lines.push(Line::from(Span::styled(
160                            wrapped_line,
161                            Style::default().fg(Color::White),
162                        )));
163                    }
164                }
165            }
166        }
167
168        // Note: command_history is now rendered via static_lines above
169        // (see ResponseFormatter::update_static_lines which is called in add_response)
170        // This eliminates redundant parsing and styling on every frame
171
172        // Render current input
173        match state.mode {
174            InteractionMode::ScriptEditor => {
175                if let Some(ref script_cache) = state.script_cache {
176                    self.render_script_editor(script_cache, width, &mut lines);
177                }
178            }
179            _ => {
180                if matches!(
181                    state.input_state,
182                    crate::model::panel_state::InputState::Ready
183                ) {
184                    if state.is_in_history_search() {
185                        self.render_history_search(state, width, &mut lines);
186                    } else {
187                        self.render_normal_input(state, width, &mut lines);
188                    }
189                }
190            }
191        }
192
193        // Apply viewport
194        let total_lines = lines.len();
195        let visible_count = inner_area.height as usize;
196
197        let (start_line, cursor_line_in_viewport) =
198            if matches!(state.mode, InteractionMode::Command) || state.is_in_history_search() {
199                let cursor_line = if state.is_in_history_search() {
200                    total_lines.saturating_sub(1)
201                } else {
202                    state.command_cursor_line
203                };
204
205                let mut start = total_lines.saturating_sub(visible_count);
206
207                if cursor_line < start {
208                    start = cursor_line;
209                } else if cursor_line >= start + visible_count {
210                    start = cursor_line.saturating_sub(visible_count - 1);
211                }
212
213                (start, Some(cursor_line.saturating_sub(start)))
214            } else {
215                let start = total_lines.saturating_sub(visible_count);
216                (start, None)
217            };
218
219        let mut visible_lines: Vec<Line> = lines
220            .into_iter()
221            .skip(start_line)
222            .take(visible_count)
223            .collect();
224
225        // Add command mode cursor if needed
226        if let Some(cursor_line_idx) = cursor_line_in_viewport {
227            if cursor_line_idx < visible_lines.len()
228                && matches!(state.mode, InteractionMode::Command)
229            {
230                self.add_command_cursor(
231                    &mut visible_lines[cursor_line_idx],
232                    state.command_cursor_column,
233                );
234            }
235        }
236
237        let paragraph = Paragraph::new(visible_lines);
238        f.render_widget(paragraph, inner_area);
239    }
240
241    /// Render script editor content
242    fn render_script_editor(
243        &self,
244        script_cache: &crate::model::panel_state::ScriptCache,
245        width: u16,
246        lines: &mut Vec<Line<'static>>,
247    ) {
248        // Header - make sure it doesn't exceed terminal width
249        let header = format!(
250            "🔨 Entering script mode for target: {}",
251            script_cache.target
252        );
253        // Truncate if too long
254        let header_display = if header.len() > width as usize {
255            format!("{}...", &header[..width as usize - 3])
256        } else {
257            header
258        };
259        lines.push(Line::from(Span::styled(
260            header_display,
261            Style::default().fg(Color::Cyan),
262        )));
263
264        // Separator - adapt to terminal width, but ensure minimum visibility
265        let separator_width = std::cmp::min(width as usize, 60).saturating_sub(4); // Leave some margin
266        let separator = "─".repeat(separator_width);
267        lines.push(Line::from(Span::styled(
268            separator,
269            Style::default().fg(Color::Cyan),
270        )));
271
272        // Prompt - wrap if necessary
273        let prompt_text = "Script Editor (Ctrl+s to submit, Esc to cancel):";
274        let wrapped_prompts = self.wrap_text(prompt_text, width);
275        for wrapped_prompt in wrapped_prompts {
276            lines.push(Line::from(Span::styled(
277                wrapped_prompt,
278                Style::default()
279                    .fg(Color::Cyan)
280                    .add_modifier(Modifier::BOLD),
281            )));
282        }
283
284        // Add empty line for better separation
285        lines.push(Line::from(""));
286
287        // Script lines
288        for (line_idx, line_content) in script_cache.lines.iter().enumerate() {
289            let line_number = format!("{:3} │ ", line_idx + 1);
290            let is_cursor_line = line_idx == script_cache.cursor_line;
291
292            // Check if line content needs wrapping
293            let line_number_width = line_number.chars().count();
294            let available_width = width as usize - line_number_width;
295
296            if line_content.chars().count() > available_width {
297                // Line needs wrapping
298                let wrapped_lines = self.wrap_text(line_content, width - line_number_width as u16);
299                for (wrap_idx, wrapped_content) in wrapped_lines.iter().enumerate() {
300                    if wrap_idx == 0 {
301                        // First line includes line number
302                        if is_cursor_line {
303                            lines.push(self.create_script_line_with_cursor(
304                                &line_number,
305                                wrapped_content,
306                                script_cache.cursor_col,
307                            ));
308                        } else {
309                            lines.push(
310                                self.create_highlighted_script_line(&line_number, wrapped_content),
311                            );
312                        }
313                    } else {
314                        // Continuation lines use spaces for alignment
315                        let continuation_prefix = " ".repeat(line_number_width);
316                        if is_cursor_line
317                            && script_cache.cursor_col >= wrapped_content.chars().count()
318                        {
319                            // Cursor might be on this continuation line
320                            let cursor_in_continuation =
321                                script_cache.cursor_col - wrapped_content.chars().count();
322                            lines.push(self.create_script_line_with_cursor(
323                                &continuation_prefix,
324                                wrapped_content,
325                                cursor_in_continuation,
326                            ));
327                        } else {
328                            lines.push(self.create_highlighted_script_line(
329                                &continuation_prefix,
330                                wrapped_content,
331                            ));
332                        }
333                    }
334                }
335            } else {
336                // Line fits within width
337                if is_cursor_line {
338                    lines.push(self.create_script_line_with_cursor(
339                        &line_number,
340                        line_content,
341                        script_cache.cursor_col,
342                    ));
343                } else {
344                    lines.push(self.create_highlighted_script_line(&line_number, line_content));
345                }
346            }
347        }
348    }
349
350    /// Render history search input
351    fn render_history_search(
352        &self,
353        state: &CommandPanelState,
354        width: u16,
355        lines: &mut Vec<Line<'static>>,
356    ) {
357        let search_query = state.get_history_search_query();
358
359        if let Some(matched_command) = state
360            .history_search
361            .current_match(&state.command_history_manager)
362        {
363            // Success case
364            let prompt_text = format!("(reverse-i-search)`{search_query}': ");
365            let full_content = format!("{prompt_text}{matched_command}");
366
367            if full_content.chars().count() > width as usize {
368                // Wrapped case
369                let wrapped_lines = self.wrap_text(&full_content, width);
370                let cursor_pos = prompt_text.chars().count() + search_query.len();
371
372                let mut char_count = 0;
373                for line in wrapped_lines.iter() {
374                    let line_char_count = line.chars().count();
375                    let line_end = char_count + line_char_count;
376
377                    if cursor_pos >= char_count && cursor_pos < line_end {
378                        // This line contains the cursor
379                        let cursor_in_line = cursor_pos - char_count;
380                        lines.push(self.create_history_search_line_with_cursor(
381                            line,
382                            &prompt_text,
383                            cursor_in_line,
384                        ));
385                    } else {
386                        lines.push(
387                            self.create_history_search_line_without_cursor(line, &prompt_text),
388                        );
389                    }
390                    char_count = line_end;
391                }
392            } else {
393                // Single line case
394                let cursor_pos = search_query.len();
395                lines.push(self.create_simple_history_search_line(
396                    &prompt_text,
397                    matched_command,
398                    cursor_pos,
399                    true,
400                ));
401            }
402        } else if search_query.is_empty() {
403            // Empty search
404            let prompt_text = "(reverse-i-search)`': ";
405
406            if prompt_text.chars().count() > width as usize {
407                // Wrapped empty search
408                let wrapped_lines = self.wrap_text(prompt_text, width);
409                for wrapped_line in wrapped_lines {
410                    lines.push(Line::from(Span::styled(
411                        wrapped_line,
412                        Style::default().fg(Color::Cyan),
413                    )));
414                }
415            } else {
416                // Single line empty search
417                lines.push(Line::from(Span::styled(
418                    prompt_text,
419                    Style::default().fg(Color::Cyan),
420                )));
421            }
422        } else {
423            // Failed search
424            let failed_prompt = format!("(failed reverse-i-search)`{search_query}': ");
425
426            if failed_prompt.chars().count() > width as usize {
427                // Wrapped failed search
428                let wrapped_lines = self.wrap_text(&failed_prompt, width);
429                for wrapped_line in wrapped_lines {
430                    lines.push(Line::from(Span::styled(
431                        wrapped_line,
432                        Style::default().fg(Color::Red),
433                    )));
434                }
435            } else {
436                // Single line failed search
437                lines.push(Line::from(Span::styled(
438                    failed_prompt,
439                    Style::default().fg(Color::Red),
440                )));
441            }
442        }
443    }
444
445    /// Render normal input
446    fn render_normal_input(
447        &self,
448        state: &CommandPanelState,
449        width: u16,
450        lines: &mut Vec<Line<'static>>,
451    ) {
452        let prompt = "(ghostscope) ";
453        let input_text = state.get_display_text();
454        let cursor_pos = state.get_display_cursor_position();
455
456        // Include auto-suggestion in wrapping calculation
457        let suggestion_text = state.get_suggestion_text().unwrap_or_default();
458        let full_content_with_suggestion = format!("{prompt}{input_text}{suggestion_text}");
459        let base_content = format!("{prompt}{input_text}");
460
461        if full_content_with_suggestion.chars().count() > width as usize {
462            // Handle wrapped input - wrap based on input only, but suggestion affects decision
463            tracing::debug!(
464                "Wrapping text: suggestion_len={}, input_len={}, total_len={}, width={}",
465                suggestion_text.len(),
466                input_text.len(),
467                full_content_with_suggestion.len(),
468                width
469            );
470            // Calculate how much space suggestion would take in the last line
471            let base_wrapped = self.wrap_text(&base_content, width);
472            let last_line_len = base_wrapped
473                .last()
474                .map(|line| line.chars().count())
475                .unwrap_or(0);
476            let suggestion_fits_in_last_line =
477                last_line_len + suggestion_text.chars().count() <= width as usize;
478
479            let wrapped_lines = if suggestion_fits_in_last_line {
480                // Suggestion fits, use base wrapping
481                base_wrapped
482            } else {
483                // Suggestion doesn't fit, wrap the full content but remove suggestion from lines
484                let full_wrapped = self.wrap_text(&full_content_with_suggestion, width);
485                full_wrapped
486                    .into_iter()
487                    .map(|line| {
488                        // Remove suggestion text from lines (it will be added back during rendering)
489                        if line.ends_with(&suggestion_text) {
490                            line[..line.len() - suggestion_text.len()].to_string()
491                        } else {
492                            line
493                        }
494                    })
495                    .collect()
496            };
497            tracing::debug!(
498                "Wrapped into {} lines: {:?}",
499                wrapped_lines.len(),
500                wrapped_lines
501            );
502            let cursor_pos_with_prompt = cursor_pos + prompt.chars().count();
503
504            let mut char_count = 0;
505            for (line_idx, line) in wrapped_lines.iter().enumerate() {
506                let line_char_count = line.chars().count();
507                let line_end = char_count + line_char_count;
508
509                let is_last_line = line_idx == wrapped_lines.len() - 1;
510                let cursor_in_range = cursor_pos_with_prompt >= char_count
511                    && (cursor_pos_with_prompt < line_end
512                        || (cursor_pos_with_prompt == line_end && is_last_line));
513
514                if cursor_in_range {
515                    let cursor_in_line = cursor_pos_with_prompt - char_count;
516                    lines.push(self.create_input_line_with_cursor_wrapped(
517                        line,
518                        prompt,
519                        cursor_in_line,
520                        line_idx == 0,
521                        is_last_line,
522                        state,
523                    ));
524                } else {
525                    lines.push(self.create_input_line_without_cursor(line, prompt, line_idx == 0));
526                }
527                char_count = line_end;
528            }
529        } else {
530            // Single line input
531            lines.push(self.create_input_line(prompt, input_text, cursor_pos, state));
532        }
533    }
534
535    /// Create simple history search line
536    fn create_simple_history_search_line(
537        &self,
538        prompt_text: &str,
539        matched_command: &str,
540        cursor_pos: usize,
541        show_cursor: bool,
542    ) -> Line<'static> {
543        let mut spans = vec![Span::styled(
544            prompt_text.to_string(),
545            Style::default().fg(Color::Cyan),
546        )];
547
548        if show_cursor {
549            self.add_text_with_cursor(&mut spans, matched_command, cursor_pos);
550        } else {
551            spans.push(Span::styled(matched_command.to_string(), Style::default()));
552        }
553
554        Line::from(spans)
555    }
556
557    /// Create history search line with cursor (wrapped)
558    fn create_history_search_line_with_cursor(
559        &self,
560        line: &str,
561        prompt_text: &str,
562        cursor_pos: usize,
563    ) -> Line<'static> {
564        let chars: Vec<char> = line.chars().collect();
565        let mut spans = Vec::new();
566        let prompt_len = prompt_text.chars().count();
567
568        if prompt_len > 0 && prompt_len <= chars.len() {
569            // Line contains prompt
570            let prompt_part: String = chars[..prompt_len].iter().collect();
571            spans.push(Span::styled(prompt_part, Style::default().fg(Color::Cyan)));
572
573            let text_part: String = chars[prompt_len..].iter().collect();
574            let cursor_in_text = cursor_pos.saturating_sub(prompt_len);
575            self.add_text_with_cursor(&mut spans, &text_part, cursor_in_text);
576        } else {
577            // Continuation line
578            self.add_text_with_cursor(&mut spans, line, cursor_pos);
579        }
580
581        Line::from(spans)
582    }
583
584    /// Create history search line without cursor (wrapped)
585    fn create_history_search_line_without_cursor(
586        &self,
587        line: &str,
588        prompt_text: &str,
589    ) -> Line<'static> {
590        let chars: Vec<char> = line.chars().collect();
591        let mut spans = Vec::new();
592        let prompt_len = prompt_text.chars().count();
593
594        if prompt_len > 0 && prompt_len <= chars.len() {
595            let prompt_part: String = chars[..prompt_len].iter().collect();
596            spans.push(Span::styled(prompt_part, Style::default().fg(Color::Cyan)));
597
598            let text_part: String = chars[prompt_len..].iter().collect();
599            spans.push(Span::styled(text_part, Style::default()));
600        } else {
601            spans.push(Span::styled(line.to_string(), Style::default()));
602        }
603
604        Line::from(spans)
605    }
606
607    /// Create input line with cursor for wrapped text environment
608    fn create_input_line_with_cursor_wrapped(
609        &self,
610        line: &str,
611        prompt: &str,
612        cursor_pos: usize,
613        is_first_line: bool,
614        is_last_line: bool,
615        state: &CommandPanelState,
616    ) -> Line<'static> {
617        let chars: Vec<char> = line.chars().collect();
618        let mut spans = Vec::new();
619        let prompt_len = if is_first_line {
620            prompt.chars().count()
621        } else {
622            0
623        };
624
625        if is_first_line && prompt_len <= chars.len() {
626            let prompt_part: String = chars[..prompt_len].iter().collect();
627            spans.push(Span::styled(
628                prompt_part,
629                Style::default().fg(Color::Magenta),
630            ));
631
632            let text_part: String = chars[prompt_len..].iter().collect();
633            let cursor_in_text = cursor_pos.saturating_sub(prompt_len);
634
635            // Only show auto-suggestion on last line when cursor is at end
636            if is_last_line && cursor_in_text >= text_part.chars().count() {
637                // Add text with suggestion
638                tracing::debug!(
639                    "Rendering with suggestion: is_last_line={}, cursor_in_text={}, text_part='{}'",
640                    is_last_line,
641                    cursor_in_text,
642                    text_part
643                );
644                self.add_text_with_cursor_and_suggestion(
645                    &mut spans,
646                    &text_part,
647                    cursor_in_text,
648                    state,
649                );
650            } else {
651                // Add text without suggestion
652                tracing::debug!("Rendering without suggestion: is_last_line={}, cursor_in_text={}, text_part='{}'",
653                               is_last_line, cursor_in_text, text_part);
654                self.add_text_with_cursor(&mut spans, &text_part, cursor_in_text);
655            }
656        } else {
657            // Only show auto-suggestion on last line when cursor is at end
658            if is_last_line && cursor_pos >= line.chars().count() {
659                // Add text with suggestion
660                self.add_text_with_cursor_and_suggestion(&mut spans, line, cursor_pos, state);
661            } else {
662                // Add text without suggestion
663                self.add_text_with_cursor(&mut spans, line, cursor_pos);
664            }
665        }
666
667        Line::from(spans)
668    }
669
670    /// Create input line without cursor
671    fn create_input_line_without_cursor(
672        &self,
673        line: &str,
674        prompt: &str,
675        is_first_line: bool,
676    ) -> Line<'static> {
677        let chars: Vec<char> = line.chars().collect();
678        let mut spans = Vec::new();
679        let prompt_len = if is_first_line {
680            prompt.chars().count()
681        } else {
682            0
683        };
684
685        if is_first_line && prompt_len <= chars.len() {
686            let prompt_part: String = chars[..prompt_len].iter().collect();
687            spans.push(Span::styled(
688                prompt_part,
689                Style::default().fg(Color::Magenta),
690            ));
691
692            let text_part: String = chars[prompt_len..].iter().collect();
693            spans.push(Span::styled(text_part, Style::default()));
694        } else {
695            spans.push(Span::styled(line.to_string(), Style::default()));
696        }
697
698        Line::from(spans)
699    }
700
701    /// Add command cursor to existing line
702    fn add_command_cursor(&self, line: &mut Line<'static>, cursor_col: usize) {
703        let mut new_spans = Vec::new();
704        let mut current_pos = 0;
705
706        for span in &line.spans {
707            let span_len = span.content.chars().count();
708            let span_end = current_pos + span_len;
709
710            if cursor_col >= current_pos && cursor_col < span_end {
711                let chars: Vec<char> = span.content.chars().collect();
712                let cursor_pos_in_span = cursor_col - current_pos;
713
714                if cursor_pos_in_span > 0 {
715                    let before: String = chars[..cursor_pos_in_span].iter().collect();
716                    new_spans.push(Span::styled(before, span.style));
717                }
718
719                if cursor_pos_in_span < chars.len() {
720                    let cursor_char = chars[cursor_pos_in_span];
721                    new_spans.push(Span::styled(
722                        cursor_char.to_string(),
723                        UIThemes::cursor_style(),
724                    ));
725
726                    if cursor_pos_in_span + 1 < chars.len() {
727                        let after: String = chars[cursor_pos_in_span + 1..].iter().collect();
728                        new_spans.push(Span::styled(after, span.style));
729                    }
730                } else {
731                    new_spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
732                }
733            } else {
734                new_spans.push(span.clone());
735            }
736
737            current_pos = span_end;
738        }
739
740        if cursor_col >= current_pos {
741            new_spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
742        }
743
744        line.spans = new_spans;
745    }
746
747    /// Create a styled input line with cursor
748    fn create_input_line(
749        &self,
750        prompt: &str,
751        input_text: &str,
752        cursor_pos: usize,
753        state: &CommandPanelState,
754    ) -> Line<'static> {
755        let chars: Vec<char> = input_text.chars().collect();
756        let mut spans = vec![Span::styled(
757            prompt.to_string(),
758            Style::default().fg(Color::Magenta),
759        )];
760
761        let show_cursor = matches!(state.mode, InteractionMode::Input);
762
763        if chars.is_empty() {
764            if let Some(suggestion_text) = state.get_suggestion_text() {
765                let suggestion_chars: Vec<char> = suggestion_text.chars().collect();
766                if !suggestion_chars.is_empty() {
767                    spans.push(Span::styled(
768                        suggestion_chars[0].to_string(),
769                        if show_cursor {
770                            UIThemes::cursor_style()
771                        } else {
772                            Style::default().fg(Color::DarkGray)
773                        },
774                    ));
775                    if suggestion_chars.len() > 1 {
776                        let remaining: String = suggestion_chars[1..].iter().collect();
777                        spans.push(Span::styled(
778                            remaining,
779                            Style::default().fg(Color::DarkGray),
780                        ));
781                    }
782                }
783            } else {
784                spans.push(Span::styled(
785                    " ".to_string(),
786                    if show_cursor {
787                        UIThemes::cursor_style()
788                    } else {
789                        Style::default()
790                    },
791                ));
792            }
793        } else if cursor_pos >= chars.len() {
794            // Cursor at end - check if we have auto-suggestion to merge
795            if let Some(suggestion_text) = state.get_suggestion_text() {
796                // We have auto-suggestion, show merged text with cursor at boundary
797                let full_text = format!("{input_text}{suggestion_text}");
798                let full_chars: Vec<char> = full_text.chars().collect();
799
800                // Show input part in normal color
801                if !input_text.is_empty() {
802                    spans.push(Span::styled(input_text.to_string(), Style::default()));
803                }
804
805                // Show the character at cursor position as block cursor
806                if cursor_pos < full_chars.len() {
807                    let cursor_char = full_chars[cursor_pos];
808                    spans.push(Span::styled(
809                        cursor_char.to_string(),
810                        if show_cursor {
811                            UIThemes::cursor_style()
812                        } else {
813                            Style::default().fg(Color::DarkGray)
814                        },
815                    ));
816
817                    // Show remaining characters in dark gray
818                    if cursor_pos + 1 < full_chars.len() {
819                        let remaining: String = full_chars[cursor_pos + 1..].iter().collect();
820                        spans.push(Span::styled(
821                            remaining,
822                            Style::default().fg(Color::DarkGray),
823                        ));
824                    }
825                } else {
826                    // Fallback - show space as block cursor
827                    spans.push(Span::styled(
828                        " ".to_string(),
829                        if show_cursor {
830                            UIThemes::cursor_style()
831                        } else {
832                            Style::default()
833                        },
834                    ));
835                }
836            } else {
837                // No auto-suggestion, show block cursor at end
838                spans.push(Span::styled(input_text.to_string(), Style::default()));
839                spans.push(Span::styled(
840                    " ".to_string(),
841                    if show_cursor {
842                        UIThemes::cursor_style()
843                    } else {
844                        Style::default()
845                    },
846                ));
847            }
848        } else {
849            // Cursor in middle of text - show character as block cursor
850            let before_cursor: String = chars[..cursor_pos].iter().collect();
851            let at_cursor = chars[cursor_pos];
852            let after_cursor: String = chars[cursor_pos + 1..].iter().collect();
853
854            // Text before cursor
855            if !before_cursor.is_empty() {
856                spans.push(Span::styled(before_cursor, Style::default()));
857            }
858
859            // Character at cursor position as block cursor
860            spans.push(Span::styled(
861                at_cursor.to_string(),
862                if show_cursor {
863                    UIThemes::cursor_style()
864                } else {
865                    Style::default()
866                },
867            ));
868
869            // Text after cursor
870            if !after_cursor.is_empty() {
871                spans.push(Span::styled(after_cursor, Style::default()));
872            }
873
874            // Add auto-suggestion at the end if cursor is at the end of meaningful text
875            if cursor_pos + 1 >= chars.len() {
876                if let Some(suggestion_text) = state.get_suggestion_text() {
877                    spans.push(Span::styled(
878                        suggestion_text.to_string(),
879                        Style::default().fg(Color::DarkGray),
880                    ));
881                }
882            }
883        }
884
885        Line::from(spans)
886    }
887
888    /// Create a script line with syntax highlighting
889    fn create_highlighted_script_line(&self, line_number: &str, content: &str) -> Line<'static> {
890        let mut spans = vec![Span::styled(
891            line_number.to_string(),
892            Style::default().fg(Color::DarkGray),
893        )];
894
895        // Get syntax highlighted spans for the content
896        let highlighted_spans = syntax_highlighter::highlight_line(content);
897        spans.extend(highlighted_spans);
898
899        Line::from(spans)
900    }
901
902    /// Create a script line with cursor and syntax highlighting
903    fn create_script_line_with_cursor(
904        &self,
905        line_number: &str,
906        content: &str,
907        cursor_pos: usize,
908    ) -> Line<'static> {
909        let chars: Vec<char> = content.chars().collect();
910        let mut spans = vec![Span::styled(
911            line_number.to_string(),
912            Style::default().fg(Color::DarkGray),
913        )];
914
915        if chars.is_empty() {
916            spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
917        } else if cursor_pos >= chars.len() {
918            // Add syntax highlighted content, then cursor at end
919            let highlighted_spans = syntax_highlighter::highlight_line(content);
920            spans.extend(highlighted_spans);
921            spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
922        } else {
923            // Split content at cursor position
924            let before_cursor: String = chars[..cursor_pos].iter().collect();
925            let at_cursor = chars[cursor_pos];
926            let after_cursor: String = chars[cursor_pos + 1..].iter().collect();
927
928            // Highlight before cursor part
929            if !before_cursor.is_empty() {
930                let before_spans = syntax_highlighter::highlight_line(&before_cursor);
931                spans.extend(before_spans);
932            }
933
934            // Character at cursor position as block cursor
935            spans.push(Span::styled(
936                at_cursor.to_string(),
937                UIThemes::cursor_style(),
938            ));
939
940            // Highlight after cursor part
941            if !after_cursor.is_empty() {
942                let after_spans = syntax_highlighter::highlight_line(&after_cursor);
943                spans.extend(after_spans);
944            }
945        }
946
947        Line::from(spans)
948    }
949
950    /// Create a styled command line
951    fn create_command_line(&self, content: &str) -> Line<'static> {
952        if content.starts_with("(ghostscope) ") {
953            let prompt_part = "(ghostscope) ";
954            let command_part = &content[prompt_part.len()..];
955
956            Line::from(vec![
957                Span::styled(
958                    prompt_part.to_string(),
959                    Style::default().fg(Color::DarkGray),
960                ),
961                Span::styled(command_part.to_string(), Style::default().fg(Color::Gray)),
962            ])
963        } else {
964            Line::from(Span::styled(
965                content.to_string(),
966                Style::default().fg(Color::Gray),
967            ))
968        }
969    }
970
971    /// Create a styled response line
972    fn create_response_line(
973        &self,
974        content: &str,
975        response_type: Option<ResponseType>,
976    ) -> Line<'static> {
977        // Check if content contains ANSI color codes
978        if content.contains("\x1b[") {
979            // Parse ANSI codes and create styled spans
980            Line::from(self.parse_ansi_colors(content))
981        } else {
982            // Use default response type styling
983            let style = match response_type {
984                Some(ResponseType::Success) => Style::default().fg(Color::Green),
985                Some(ResponseType::Error) => Style::default().fg(Color::Red),
986                Some(ResponseType::Warning) => Style::default().fg(Color::Yellow),
987                Some(ResponseType::Info) => Style::default().fg(Color::Cyan),
988                Some(ResponseType::Progress) => Style::default().fg(Color::Blue),
989                Some(ResponseType::ScriptDisplay) => Style::default().fg(Color::Magenta),
990                None => Style::default(), // No color for wrapped ANSI lines
991            };
992            Line::from(Span::styled(content.to_string(), style))
993        }
994    }
995
996    /// Create fallback welcome line
997    fn create_fallback_welcome_line(&self, content: &str) -> Line<'static> {
998        if content.contains("GhostScope") {
999            Line::from(Span::styled(
1000                content.to_string(),
1001                Style::default()
1002                    .fg(Color::Green)
1003                    .add_modifier(Modifier::BOLD),
1004            ))
1005        } else if content.starts_with("•") || content.starts_with("Loading completed in") {
1006            Line::from(Span::styled(
1007                content.to_string(),
1008                Style::default().fg(Color::Cyan),
1009            ))
1010        } else if content.starts_with("Attached to process") {
1011            Line::from(Span::styled(
1012                content.to_string(),
1013                Style::default().fg(Color::White),
1014            ))
1015        } else if content.trim().is_empty() {
1016            Line::from("")
1017        } else {
1018            Line::from(Span::styled(
1019                content.to_string(),
1020                Style::default().fg(Color::White),
1021            ))
1022        }
1023    }
1024
1025    /// Add text with cursor to spans vector
1026    fn add_text_with_cursor(&self, spans: &mut Vec<Span<'static>>, text: &str, cursor_pos: usize) {
1027        let chars: Vec<char> = text.chars().collect();
1028
1029        if chars.is_empty() {
1030            spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
1031        } else if cursor_pos == 0 {
1032            let first_char = chars[0];
1033            spans.push(Span::styled(
1034                first_char.to_string(),
1035                UIThemes::cursor_style(),
1036            ));
1037            if chars.len() > 1 {
1038                let remaining: String = chars[1..].iter().collect();
1039                spans.push(Span::styled(remaining, Style::default()));
1040            }
1041        } else if cursor_pos >= chars.len() {
1042            spans.push(Span::styled(text.to_string(), Style::default()));
1043            spans.push(Span::styled(" ".to_string(), UIThemes::cursor_style()));
1044        } else {
1045            let before_cursor: String = chars[..cursor_pos].iter().collect();
1046            let at_cursor = chars[cursor_pos];
1047            let after_cursor: String = chars[cursor_pos + 1..].iter().collect();
1048
1049            if !before_cursor.is_empty() {
1050                spans.push(Span::styled(before_cursor, Style::default()));
1051            }
1052
1053            spans.push(Span::styled(
1054                at_cursor.to_string(),
1055                UIThemes::cursor_style(),
1056            ));
1057
1058            if !after_cursor.is_empty() {
1059                spans.push(Span::styled(after_cursor, Style::default()));
1060            }
1061        }
1062    }
1063
1064    /// Add text with cursor and auto-suggestion support
1065    fn add_text_with_cursor_and_suggestion(
1066        &self,
1067        spans: &mut Vec<Span<'static>>,
1068        text: &str,
1069        cursor_pos: usize,
1070        state: &CommandPanelState,
1071    ) {
1072        let chars: Vec<char> = text.chars().collect();
1073        let show_cursor = matches!(state.mode, InteractionMode::Input);
1074
1075        if chars.is_empty() {
1076            if let Some(suggestion_text) = state.get_suggestion_text() {
1077                let suggestion_chars: Vec<char> = suggestion_text.chars().collect();
1078                if !suggestion_chars.is_empty() {
1079                    spans.push(Span::styled(
1080                        suggestion_chars[0].to_string(),
1081                        if show_cursor {
1082                            UIThemes::cursor_style()
1083                        } else {
1084                            Style::default().fg(Color::DarkGray)
1085                        },
1086                    ));
1087
1088                    if suggestion_chars.len() > 1 {
1089                        let remaining: String = suggestion_chars[1..].iter().collect();
1090                        spans.push(Span::styled(
1091                            remaining,
1092                            Style::default().fg(Color::DarkGray),
1093                        ));
1094                    }
1095                } else {
1096                    spans.push(Span::styled(
1097                        " ".to_string(),
1098                        if show_cursor {
1099                            UIThemes::cursor_style()
1100                        } else {
1101                            Style::default()
1102                        },
1103                    ));
1104                }
1105            } else {
1106                spans.push(Span::styled(
1107                    " ".to_string(),
1108                    if show_cursor {
1109                        UIThemes::cursor_style()
1110                    } else {
1111                        Style::default()
1112                    },
1113                ));
1114            }
1115        } else if cursor_pos >= chars.len() {
1116            // Cursor at end - check if we have auto-suggestion to merge
1117            if let Some(suggestion_text) = state.get_suggestion_text() {
1118                // We have auto-suggestion, show merged text with cursor at boundary
1119                let full_text = format!("{text}{suggestion_text}");
1120                let full_chars: Vec<char> = full_text.chars().collect();
1121
1122                // Show input part in normal color
1123                if !text.is_empty() {
1124                    spans.push(Span::styled(text.to_string(), Style::default()));
1125                }
1126
1127                // Show the character at cursor position as block cursor
1128                if cursor_pos < full_chars.len() {
1129                    let cursor_char = full_chars[cursor_pos];
1130                    spans.push(Span::styled(
1131                        cursor_char.to_string(),
1132                        if show_cursor {
1133                            UIThemes::cursor_style()
1134                        } else {
1135                            Style::default().fg(Color::DarkGray)
1136                        },
1137                    ));
1138
1139                    // Show remaining characters in dark gray
1140                    if cursor_pos + 1 < full_chars.len() {
1141                        let remaining: String = full_chars[cursor_pos + 1..].iter().collect();
1142                        spans.push(Span::styled(
1143                            remaining,
1144                            Style::default().fg(Color::DarkGray),
1145                        ));
1146                    }
1147                } else {
1148                    // Fallback - show space as block cursor
1149                    spans.push(Span::styled(
1150                        " ".to_string(),
1151                        if show_cursor {
1152                            UIThemes::cursor_style()
1153                        } else {
1154                            Style::default()
1155                        },
1156                    ));
1157                }
1158            } else {
1159                // No auto-suggestion, show block cursor at end
1160                spans.push(Span::styled(text.to_string(), Style::default()));
1161                spans.push(Span::styled(
1162                    " ".to_string(),
1163                    if show_cursor {
1164                        UIThemes::cursor_style()
1165                    } else {
1166                        Style::default()
1167                    },
1168                ));
1169            }
1170        } else {
1171            // Cursor in middle of text
1172            let before_cursor: String = chars[..cursor_pos].iter().collect();
1173            let at_cursor = chars[cursor_pos];
1174            let after_cursor: String = chars[cursor_pos + 1..].iter().collect();
1175
1176            if !before_cursor.is_empty() {
1177                spans.push(Span::styled(before_cursor, Style::default()));
1178            }
1179
1180            spans.push(Span::styled(
1181                at_cursor.to_string(),
1182                if show_cursor {
1183                    UIThemes::cursor_style()
1184                } else {
1185                    Style::default()
1186                },
1187            ));
1188
1189            if !after_cursor.is_empty() {
1190                spans.push(Span::styled(after_cursor, Style::default()));
1191            }
1192
1193            // Add auto-suggestion at the end if cursor is at the end of meaningful text
1194            if cursor_pos + 1 >= chars.len() {
1195                if let Some(suggestion_text) = state.get_suggestion_text() {
1196                    spans.push(Span::styled(
1197                        suggestion_text.to_string(),
1198                        Style::default().fg(Color::DarkGray),
1199                    ));
1200                }
1201            }
1202        }
1203    }
1204
1205    /// Wrap text to fit within the specified width (preserves ANSI escape sequences and color state)
1206    fn wrap_text(&self, text: &str, width: u16) -> Vec<String> {
1207        if width <= 2 {
1208            return vec![text.to_string()];
1209        }
1210
1211        let max_width = width as usize;
1212        let mut lines = Vec::new();
1213
1214        for line in text.lines() {
1215            // Calculate visible width (excluding ANSI codes)
1216            let line_width = self.visible_width(line);
1217
1218            if line_width <= max_width {
1219                lines.push(line.to_string());
1220            } else {
1221                let mut current_line = String::new();
1222                let mut current_width = 0;
1223                let mut chars = line.chars().peekable();
1224                let mut in_ansi_sequence = false;
1225                let mut active_color_code = String::new(); // Track active color
1226
1227                while let Some(ch) = chars.next() {
1228                    // Detect ANSI escape sequence start
1229                    if ch == '\x1b' && chars.peek() == Some(&'[') {
1230                        in_ansi_sequence = true;
1231                        current_line.push(ch);
1232                        active_color_code.clear();
1233                        active_color_code.push(ch);
1234                        continue;
1235                    }
1236
1237                    // If in ANSI sequence, add character without counting width
1238                    if in_ansi_sequence {
1239                        current_line.push(ch);
1240                        active_color_code.push(ch);
1241                        if ch == 'm' {
1242                            in_ansi_sequence = false;
1243                            // Check if this is a reset code
1244                            if active_color_code == "\x1b[0m" {
1245                                active_color_code.clear();
1246                            }
1247                        }
1248                        continue;
1249                    }
1250
1251                    // Normal character - count width
1252                    let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
1253
1254                    if current_width + char_width > max_width && !current_line.is_empty() {
1255                        // Add reset before line break if we have active color
1256                        if !active_color_code.is_empty() && !current_line.ends_with("\x1b[0m") {
1257                            current_line.push_str("\x1b[0m");
1258                        }
1259                        lines.push(current_line);
1260
1261                        // Start new line with active color if any
1262                        current_line = String::new();
1263                        if !active_color_code.is_empty() && active_color_code != "\x1b[0m" {
1264                            current_line.push_str(&active_color_code);
1265                        }
1266                        current_line.push(ch);
1267                        current_width = char_width;
1268                    } else {
1269                        current_line.push(ch);
1270                        current_width += char_width;
1271                    }
1272                }
1273
1274                if !current_line.is_empty() {
1275                    lines.push(current_line);
1276                }
1277            }
1278        }
1279
1280        if lines.is_empty() {
1281            lines.push(String::new());
1282        }
1283
1284        lines
1285    }
1286
1287    /// Calculate visible width of text (excluding ANSI escape sequences)
1288    fn visible_width(&self, text: &str) -> usize {
1289        let mut width = 0;
1290        let mut chars = text.chars().peekable();
1291        let mut in_ansi_sequence = false;
1292
1293        while let Some(ch) = chars.next() {
1294            if ch == '\x1b' && chars.peek() == Some(&'[') {
1295                in_ansi_sequence = true;
1296                continue;
1297            }
1298
1299            if in_ansi_sequence {
1300                if ch == 'm' {
1301                    in_ansi_sequence = false;
1302                }
1303                continue;
1304            }
1305
1306            width += UnicodeWidthChar::width(ch).unwrap_or(0);
1307        }
1308
1309        width
1310    }
1311
1312    /// Wrap styled line preserving spans - uses same logic as wrap_text() for consistency
1313    fn wrap_styled_line(&self, styled_line: &Line<'static>, width: usize) -> Vec<Line<'static>> {
1314        // Extract plain text from styled line
1315        let plain_text: String = styled_line
1316            .spans
1317            .iter()
1318            .map(|span| span.content.as_ref())
1319            .collect();
1320
1321        // Use wrap_text to get line breaks (same logic as cursor calculation)
1322        let wrapped_texts = self.wrap_text(&plain_text, width as u16);
1323
1324        if wrapped_texts.len() <= 1 {
1325            return vec![styled_line.clone()];
1326        }
1327
1328        // Now split styled_line according to wrapped_texts
1329        let mut result_lines = Vec::new();
1330        let mut span_index = 0;
1331        let mut span_char_offset = 0;
1332
1333        for wrapped_text in wrapped_texts {
1334            let mut current_line_spans = Vec::new();
1335            let mut chars_needed = wrapped_text.chars().count();
1336
1337            while chars_needed > 0 && span_index < styled_line.spans.len() {
1338                let span = &styled_line.spans[span_index];
1339                let span_text = span.content.as_ref();
1340                let span_chars: Vec<char> = span_text.chars().collect();
1341
1342                let available_chars = span_chars.len() - span_char_offset;
1343                let chars_to_take = chars_needed.min(available_chars);
1344
1345                let taken_text: String = span_chars
1346                    [span_char_offset..span_char_offset + chars_to_take]
1347                    .iter()
1348                    .collect();
1349
1350                if !taken_text.is_empty() {
1351                    current_line_spans.push(Span::styled(taken_text, span.style));
1352                }
1353
1354                span_char_offset += chars_to_take;
1355                chars_needed -= chars_to_take;
1356
1357                if span_char_offset >= span_chars.len() {
1358                    span_index += 1;
1359                    span_char_offset = 0;
1360                }
1361            }
1362
1363            if !current_line_spans.is_empty() {
1364                result_lines.push(Line::from(current_line_spans));
1365            }
1366        }
1367
1368        result_lines
1369    }
1370
1371    /// Scroll methods for API compatibility
1372    pub fn scroll_up(&mut self) {
1373        if self.scroll_offset > 0 {
1374            self.scroll_offset -= 1;
1375        }
1376    }
1377
1378    pub fn scroll_down(&mut self) {
1379        self.scroll_offset += 1;
1380    }
1381
1382    /// Parse ANSI color codes and create styled spans
1383    fn parse_ansi_colors(&self, text: &str) -> Vec<Span<'static>> {
1384        let mut spans = Vec::new();
1385        let mut current_text = String::new();
1386        let mut current_style = Style::default();
1387        let mut chars = text.chars().peekable();
1388
1389        while let Some(ch) = chars.next() {
1390            if ch == '\x1b' && chars.peek() == Some(&'[') {
1391                // Save current text if any
1392                if !current_text.is_empty() {
1393                    spans.push(Span::styled(current_text.clone(), current_style));
1394                    current_text.clear();
1395                }
1396
1397                // Skip '['
1398                chars.next();
1399
1400                // Parse color code
1401                let mut code = String::new();
1402                while let Some(&c) = chars.peek() {
1403                    if c == 'm' {
1404                        chars.next(); // consume 'm'
1405                        break;
1406                    }
1407                    code.push(c);
1408                    chars.next();
1409                }
1410
1411                // Apply color based on code
1412                current_style = match code.as_str() {
1413                    "0" => Style::default(),                     // Reset
1414                    "31" => Style::default().fg(Color::Red),     // Red
1415                    "32" => Style::default().fg(Color::Green),   // Green
1416                    "33" => Style::default().fg(Color::Yellow),  // Yellow
1417                    "34" => Style::default().fg(Color::Blue),    // Blue
1418                    "35" => Style::default().fg(Color::Magenta), // Magenta
1419                    "36" => Style::default().fg(Color::Cyan),    // Cyan
1420                    _ => current_style,                          // Unknown, keep current
1421                };
1422            } else {
1423                current_text.push(ch);
1424            }
1425        }
1426
1427        // Add remaining text
1428        if !current_text.is_empty() {
1429            spans.push(Span::styled(current_text, current_style));
1430        }
1431
1432        if spans.is_empty() {
1433            spans.push(Span::raw(text.to_string()));
1434        }
1435
1436        spans
1437    }
1438}
1439
1440impl Default for OptimizedRenderer {
1441    fn default() -> Self {
1442        Self::new()
1443    }
1444}