rush_sync_server/output/
display.rs

1use crate::core::prelude::*;
2use crate::ui::color::AppColor;
3use crate::ui::cursor::{CursorKind, UiCursor};
4use crate::ui::viewport::{ScrollDirection, Viewport, ViewportEvent};
5use ratatui::{
6    style::Style,
7    text::{Line, Span},
8    widgets::{Block, Borders, Paragraph, Wrap},
9};
10use strip_ansi_escapes::strip;
11use unicode_segmentation::UnicodeSegmentation;
12
13#[derive(Debug)]
14pub struct Message {
15    pub content: String,
16    pub current_length: usize,
17    pub timestamp: Instant,
18    pub line_count: usize,
19    pub typewriter_cursor: Option<UiCursor>,
20}
21
22type RenderData<'a> = (
23    Vec<(String, usize, bool, bool, bool)>,
24    Config,
25    crate::ui::viewport::LayoutArea,
26    &'a UiCursor,
27);
28
29impl Message {
30    pub fn new(content: String, typewriter_delay: Duration) -> Self {
31        let line_count = 1;
32
33        let initial_length = if typewriter_delay.as_millis() == 0 {
34            content.graphemes(true).count()
35        } else {
36            0
37        };
38
39        let typewriter_cursor = if typewriter_delay.as_millis() > 0 {
40            Some(UiCursor::for_typewriter())
41        } else {
42            None
43        };
44
45        Self {
46            content,
47            current_length: initial_length,
48            timestamp: Instant::now(),
49            line_count,
50            typewriter_cursor,
51        }
52    }
53
54    pub fn calculate_wrapped_line_count(&mut self, viewport: &Viewport) {
55        let clean_content = clean_message_for_display(&self.content);
56
57        if clean_content.is_empty() {
58            self.line_count = 1;
59            return;
60        }
61
62        let output_area = viewport.output_area();
63        let available_width = (output_area.width as usize).saturating_sub(2);
64        let effective_width = available_width.max(10);
65
66        let mut total_lines = 0;
67
68        for line in clean_content.lines() {
69            if line.is_empty() {
70                total_lines += 1;
71            } else {
72                let line_chars = line.graphemes(true).count();
73                if line_chars == 0 {
74                    total_lines += 1;
75                } else {
76                    let wrapped_lines = ((line_chars - 1) / effective_width) + 1;
77                    total_lines += wrapped_lines;
78                }
79            }
80        }
81
82        self.line_count = total_lines.max(1);
83    }
84
85    pub fn is_typing(&self) -> bool {
86        if self.typewriter_cursor.is_some() {
87            let total_length = self.content.graphemes(true).count();
88            self.current_length < total_length
89        } else {
90            false
91        }
92    }
93
94    pub fn is_cursor_visible(&self) -> bool {
95        if let Some(ref cursor) = self.typewriter_cursor {
96            cursor.is_visible()
97        } else {
98            false
99        }
100    }
101}
102
103static EMPTY_STRING: &str = "";
104
105pub struct MessageDisplay {
106    messages: Vec<Message>,
107    config: Config,
108    viewport: Viewport,
109    persistent_cursor: UiCursor,
110}
111
112impl MessageDisplay {
113    pub fn new(config: &Config, terminal_width: u16, terminal_height: u16) -> Self {
114        let viewport = Viewport::new(terminal_width, terminal_height);
115
116        // ✅ FIX: Output-Cursor mit richtiger CursorKind::Output erstellen
117        let persistent_cursor = UiCursor::from_config(config, CursorKind::Output);
118
119        Self {
120            messages: Vec::with_capacity(config.max_messages),
121            config: config.clone(),
122            viewport,
123            persistent_cursor, // ← Jetzt korrekt als Output-Cursor
124        }
125    }
126
127    pub fn update_config(&mut self, new_config: &Config) {
128        let old_cursor_config = self.config.theme.output_cursor.clone();
129        let old_cursor_color = self.config.theme.output_cursor_color;
130        let new_cursor_config = new_config.theme.output_cursor.clone();
131        let new_cursor_color = new_config.theme.output_cursor_color;
132        let old_theme = self.config.current_theme_name.clone();
133        let new_theme = new_config.current_theme_name.clone();
134
135        log::info!(
136            "📊 MessageDisplay CONFIG UPDATE START: '{}' → '{}' | output_cursor: '{}' ({}) → '{}' ({})",
137            old_theme,
138            new_theme,
139            old_cursor_config,
140            old_cursor_color.to_name(),
141            new_cursor_config,
142            new_cursor_color.to_name()
143        );
144
145        // ✅ STEP 1: Update internal config
146        self.config = new_config.clone();
147
148        // ✅ STEP 2: FORCE COMPLETE OUTPUT-CURSOR RECREATION
149        log::info!("🔄 RECREATING output cursor with new config...");
150        let old_cursor_debug = self.persistent_cursor.debug_info();
151        self.persistent_cursor = UiCursor::from_config(new_config, CursorKind::Output);
152        let new_cursor_debug = self.persistent_cursor.debug_info();
153
154        log::info!(
155            "🔄 OUTPUT-CURSOR TRANSITION:\n  OLD: {}\n  NEW: {}",
156            old_cursor_debug,
157            new_cursor_debug
158        );
159
160        // ✅ STEP 3: Handle message buffer
161        if self.messages.len() > self.config.max_messages {
162            let excess = self.messages.len() - self.config.max_messages;
163            self.messages.drain(0..excess);
164            self.recalculate_content_height();
165        }
166
167        // ✅ FINAL VERIFICATION
168        let final_symbol = self.persistent_cursor.get_symbol();
169        let final_color = self.persistent_cursor.color.to_name();
170        log::info!(
171            "✅ MessageDisplay CONFIG UPDATE COMPLETE: output_cursor_symbol='{}' ({}), expected='{}' ({})",
172            final_symbol,
173            final_color,
174            new_config.theme.output_cursor,
175            new_config.theme.output_cursor_color.to_name()
176        );
177    }
178
179    pub fn handle_viewport_event(&mut self, event: ViewportEvent) -> bool {
180        let changed = self.viewport.handle_event(event);
181        if changed {
182            self.recalculate_all_line_counts();
183            log::debug!("📐 Viewport updated: {}", self.viewport.debug_info());
184        }
185        changed
186    }
187
188    pub fn handle_resize(&mut self, width: u16, height: u16) -> bool {
189        let changed = self.handle_viewport_event(ViewportEvent::TerminalResized { width, height });
190
191        if changed {
192            self.viewport.force_auto_scroll();
193        }
194
195        changed
196    }
197
198    pub fn clear_messages(&mut self) {
199        self.messages.clear();
200        self.recalculate_content_height();
201        self.viewport.force_auto_scroll();
202        self.persistent_cursor.show_cursor();
203
204        log::debug!("🗑️ All messages cleared, persistent cursor remains active");
205    }
206
207    pub fn add_message(&mut self, content: String) {
208        if content.starts_with("[DEBUG]") || content.starts_with("[TRACE]") {
209            log::info!("STDERR: {}", content);
210            return;
211        }
212
213        if self.messages.len() >= self.config.max_messages {
214            self.messages.remove(0);
215        }
216
217        let mut message = Message::new(content, self.config.typewriter_delay);
218        message.calculate_wrapped_line_count(&self.viewport);
219
220        self.messages.push(message);
221        self.recalculate_content_height_silent();
222        self.scroll_to_bottom_direct_silent();
223    }
224
225    pub fn update_typewriter(&mut self) {
226        self.persistent_cursor.update_blink();
227
228        if self.config.typewriter_delay.as_millis() == 0 {
229            return;
230        }
231
232        if let Some(last_message) = self.messages.last_mut() {
233            let total_length = last_message.content.graphemes(true).count();
234
235            if let Some(ref mut cursor) = last_message.typewriter_cursor {
236                cursor.update_blink();
237            }
238
239            if last_message.current_length < total_length {
240                let elapsed = last_message.timestamp.elapsed();
241
242                if elapsed >= self.config.typewriter_delay {
243                    let chars_to_add = if self.config.typewriter_delay.as_millis() <= 5 {
244                        let ratio = elapsed.as_millis() as f64
245                            / self.config.typewriter_delay.as_millis() as f64;
246                        ratio.floor().max(1.0) as usize
247                    } else {
248                        1
249                    };
250
251                    let new_length = (last_message.current_length + chars_to_add).min(total_length);
252                    last_message.current_length = new_length;
253                    last_message.timestamp = Instant::now();
254
255                    if new_length == total_length {
256                        last_message.typewriter_cursor = None;
257                        self.viewport.force_auto_scroll();
258                        log::trace!("⌨️ Typewriter completed → message cursor removed, persistent cursor continues");
259                    }
260                }
261            }
262        }
263    }
264
265    fn recalculate_content_height_silent(&mut self) {
266        let total_lines = self
267            .messages
268            .iter()
269            .map(|msg| msg.line_count)
270            .sum::<usize>();
271
272        self.viewport.update_content_height_silent(total_lines);
273    }
274
275    fn scroll_to_bottom_direct_silent(&mut self) {
276        self.viewport.enable_auto_scroll_silent();
277
278        let content_height = self.viewport.content_height();
279        let window_height = self.viewport.window_height();
280
281        if content_height > window_height {
282            let max_offset = content_height - window_height;
283            self.viewport.set_scroll_offset_direct_silent(max_offset);
284        } else {
285            self.viewport.set_scroll_offset_direct_silent(0);
286        }
287    }
288
289    fn recalculate_all_line_counts(&mut self) {
290        for message in &mut self.messages {
291            message.calculate_wrapped_line_count(&self.viewport);
292        }
293
294        self.recalculate_content_height();
295
296        log::debug!(
297            "🔄 Recalculated all line counts for output_width: {}, total messages: {}",
298            self.viewport.output_area().width,
299            self.messages.len()
300        );
301    }
302
303    pub fn handle_scroll(&mut self, direction: ScrollDirection, amount: usize) {
304        let scroll_amount = match direction {
305            ScrollDirection::Up | ScrollDirection::Down => {
306                if amount == 0 {
307                    1
308                } else {
309                    amount
310                }
311            }
312            ScrollDirection::PageUp | ScrollDirection::PageDown => 0,
313            _ => amount,
314        };
315
316        log::trace!("📜 Manual scroll: {:?} by {}", direction, scroll_amount);
317
318        self.handle_viewport_event(ViewportEvent::ScrollRequest {
319            direction,
320            amount: scroll_amount,
321        });
322    }
323
324    fn recalculate_content_height(&mut self) {
325        let individual_line_counts: Vec<usize> =
326            self.messages.iter().map(|msg| msg.line_count).collect();
327
328        let total_lines = individual_line_counts.iter().sum::<usize>();
329
330        log::debug!(
331            "📊 Recalculating content height: {} messages → {} total lines",
332            self.messages.len(),
333            total_lines
334        );
335
336        let old_content_height = self.viewport.content_height();
337        self.viewport.update_content_height(total_lines);
338
339        let new_content_height = self.viewport.content_height();
340
341        log::debug!(
342            "📊 Content height updated: {} → {} (window: {})",
343            old_content_height,
344            new_content_height,
345            self.viewport.window_height()
346        );
347    }
348
349    pub fn get_content_height(&self) -> usize {
350        self.viewport.content_height()
351    }
352
353    pub fn get_window_height(&self) -> usize {
354        self.viewport.window_height()
355    }
356
357    pub fn get_visible_messages(&self) -> Vec<(String, usize, bool, bool, bool)> {
358        let window_height = self.viewport.window_height();
359        let content_height = self.viewport.content_height();
360
361        if self.messages.is_empty() {
362            return vec![(
363                EMPTY_STRING.to_string(),
364                0,
365                false,
366                false,
367                self.persistent_cursor.is_visible(),
368            )];
369        }
370
371        if content_height <= window_height {
372            let mut result: Vec<(String, usize, bool, bool, bool)> = self
373                .messages
374                .iter()
375                .enumerate()
376                .map(|(index, msg)| {
377                    let is_last = index == self.messages.len() - 1;
378                    (
379                        msg.content.clone(),
380                        msg.current_length,
381                        msg.is_typing(),
382                        msg.is_cursor_visible(),
383                        is_last && self.persistent_cursor.is_visible(),
384                    )
385                })
386                .collect();
387
388            if let Some(last_msg) = self.messages.last() {
389                if !last_msg.is_typing() {
390                    result.push((
391                        EMPTY_STRING.to_string(),
392                        0,
393                        false,
394                        false,
395                        self.persistent_cursor.is_visible(),
396                    ));
397                }
398            }
399
400            return result;
401        }
402
403        let mut visible = Vec::new();
404        let mut lines_used = 0;
405
406        for (index, message) in self.messages.iter().rev().enumerate() {
407            if lines_used + message.line_count <= window_height {
408                let is_last = index == 0;
409                visible.push((
410                    message.content.clone(),
411                    message.current_length,
412                    message.is_typing(),
413                    message.is_cursor_visible(),
414                    is_last && self.persistent_cursor.is_visible(),
415                ));
416                lines_used += message.line_count;
417            } else {
418                break;
419            }
420        }
421
422        visible.reverse();
423
424        if let Some((_, _, is_typing, _, _)) = visible.last() {
425            if !is_typing && lines_used < window_height {
426                visible.push((
427                    EMPTY_STRING.to_string(),
428                    0,
429                    false,
430                    false,
431                    self.persistent_cursor.is_visible(),
432                ));
433            }
434        }
435
436        visible
437    }
438
439    pub fn create_output_widget_for_rendering(&self) -> RenderData<'_> {
440        let messages = self.get_visible_messages();
441        (
442            messages,
443            self.config.clone(),
444            self.viewport.output_area(),
445            &self.persistent_cursor,
446        )
447    }
448
449    pub fn viewport(&self) -> &Viewport {
450        &self.viewport
451    }
452
453    pub fn viewport_mut(&mut self) -> &mut Viewport {
454        &mut self.viewport
455    }
456
457    pub fn debug_scroll_status(&self) -> String {
458        format!(
459            "Scroll: offset={}, content_height={}, window_height={}, auto_scroll={}, at_bottom={}",
460            self.viewport.scroll_offset(),
461            self.viewport.content_height(),
462            self.viewport.window_height(),
463            self.viewport.is_auto_scroll_enabled(),
464            self.viewport.scroll_offset()
465                >= self
466                    .viewport
467                    .content_height()
468                    .saturating_sub(self.viewport.window_height())
469        )
470    }
471
472    pub fn log(&mut self, level: &str, message: &str) {
473        let log_message = format!("[{}] {}", level, message);
474        self.add_message(log_message);
475    }
476
477    pub fn get_messages_count(&self) -> usize {
478        self.messages.len()
479    }
480}
481
482// UTILITY FUNCTIONS
483fn clean_ansi_codes(message: &str) -> String {
484    String::from_utf8_lossy(&strip(message.as_bytes()).unwrap_or_default()).into_owned()
485}
486
487fn clean_message_for_display(message: &str) -> String {
488    clean_ansi_codes(message)
489        .replace("__CONFIRM_EXIT__", "")
490        .replace("__CLEAR__", "")
491        .trim()
492        .to_string()
493}
494
495fn parse_message_parts(message: &str) -> Vec<(String, bool)> {
496    let mut parts = Vec::new();
497    let mut chars = message.char_indices().peekable();
498    let mut start = 0;
499
500    while let Some((i, c)) = chars.peek().cloned() {
501        if c == '[' {
502            if start < i {
503                let text = &message[start..i];
504                if !text.trim().is_empty() {
505                    parts.push((text.to_owned(), false));
506                }
507            }
508
509            if let Some(end_idx) = message[i..].find(']') {
510                let end = i + end_idx + 1;
511                parts.push((message[i..end].to_owned(), true));
512                start = end;
513                while let Some(&(ci, _)) = chars.peek() {
514                    if ci < end {
515                        chars.next();
516                    } else {
517                        break;
518                    }
519                }
520            } else {
521                parts.push((message[i..].to_owned(), false));
522                break;
523            }
524        } else {
525            chars.next();
526        }
527    }
528
529    if start < message.len() {
530        let remaining = &message[start..];
531        if !remaining.trim().is_empty() {
532            parts.push((remaining.to_owned(), false));
533        }
534    }
535
536    if parts.is_empty() {
537        parts.push((message.to_owned(), false));
538    }
539
540    parts
541}
542
543fn get_marker_color(marker: &str) -> AppColor {
544    let display_category = marker
545        .trim_start_matches('[')
546        .trim_end_matches(']')
547        .trim_start_matches("cat:")
548        .to_lowercase();
549
550    if AppColor::from_any(&display_category).to_name() != "gray" {
551        return AppColor::from_any(&display_category);
552    }
553
554    let mapped_category = crate::i18n::get_color_category_for_display(&display_category);
555    AppColor::from_any(mapped_category)
556}
557
558pub fn create_output_widget<'a>(
559    messages: &'a [(String, usize, bool, bool, bool)],
560    layout_area: crate::ui::viewport::LayoutArea,
561    config: &'a Config,
562    cursor_state: &'a UiCursor,
563) -> Paragraph<'a> {
564    let max_lines = layout_area.height as usize;
565    let mut lines = Vec::new();
566
567    if max_lines == 0 || layout_area.width == 0 {
568        log::warn!(
569            "🚨 Invalid layout area: {}x{}",
570            layout_area.width,
571            layout_area.height
572        );
573        return Paragraph::new(vec![Line::from(vec![Span::raw("⚠️ INVALID LAYOUT")])]).block(
574            Block::default()
575                .borders(Borders::NONE)
576                .style(Style::default().bg(config.theme.output_bg.into())),
577        );
578    }
579
580    let safe_max_lines = max_lines.min(1000);
581
582    if messages.is_empty() {
583        let empty_lines = vec![Line::from(vec![Span::raw("")]); safe_max_lines];
584        return Paragraph::new(empty_lines)
585            .block(
586                Block::default()
587                    .borders(Borders::NONE)
588                    .style(Style::default().bg(config.theme.output_bg.into())),
589            )
590            .wrap(Wrap { trim: true });
591    }
592
593    for (
594        message_idx,
595        (message, current_length, is_typing, msg_cursor_visible, persistent_cursor_visible),
596    ) in messages.iter().enumerate()
597    {
598        let is_last_message = message_idx == messages.len() - 1;
599
600        if message.is_empty() {
601            if *persistent_cursor_visible {
602                // ✅ FIX: OUTPUT-CURSOR als separates Symbol (NIEMALS invertiert!)
603                lines.push(Line::from(vec![cursor_state.create_cursor_span(config)]));
604            } else {
605                lines.push(Line::from(vec![Span::raw("")]));
606            }
607            continue;
608        }
609
610        let clean_message = clean_message_for_display(message);
611        let message_lines: Vec<&str> = clean_message.lines().collect();
612
613        if message_lines.is_empty() {
614            lines.push(Line::from(vec![Span::raw("")]));
615        } else {
616            for (line_idx, line_content) in message_lines.iter().enumerate() {
617                if lines.len() >= safe_max_lines {
618                    log::trace!("🛑 Reached safe line limit: {}", safe_max_lines);
619                    break;
620                }
621
622                let is_last_line = line_idx == message_lines.len() - 1;
623
624                let visible_chars = if is_last_message && is_last_line {
625                    let chars_before_this_line: usize = message_lines
626                        .iter()
627                        .take(line_idx)
628                        .map(|l| l.graphemes(true).count() + 1)
629                        .sum();
630
631                    let available_for_this_line =
632                        current_length.saturating_sub(chars_before_this_line);
633                    available_for_this_line.min(line_content.graphemes(true).count())
634                } else {
635                    line_content.graphemes(true).count()
636                };
637
638                let message_parts = parse_message_parts(line_content);
639                let mut spans = Vec::new();
640                let mut chars_used = 0;
641
642                for (part_text, is_marker) in message_parts {
643                    let part_chars = part_text.graphemes(true).count();
644                    let part_style = if is_marker {
645                        Style::default().fg(get_marker_color(&part_text).into())
646                    } else {
647                        Style::default().fg(config.theme.output_text.into())
648                    };
649
650                    if chars_used >= visible_chars {
651                        break;
652                    }
653
654                    let chars_needed = visible_chars - chars_used;
655
656                    if chars_needed >= part_chars {
657                        spans.push(Span::styled(part_text, part_style));
658                        chars_used += part_chars;
659                    } else {
660                        let graphemes: Vec<&str> = part_text.graphemes(true).collect();
661                        spans.push(Span::styled(
662                            graphemes
663                                .iter()
664                                .take(chars_needed)
665                                .copied()
666                                .collect::<String>(),
667                            part_style,
668                        ));
669                        break;
670                    }
671                }
672
673                // ✅ KRITISCHER FIX: OUTPUT-CURSOR RENDERING
674                if is_last_message && is_last_line {
675                    if *is_typing && *msg_cursor_visible {
676                        // ✅ TYPEWRITER-CURSOR: Immer als separates Symbol!
677                        spans.push(cursor_state.create_cursor_span(config));
678                    } else if !*is_typing && *persistent_cursor_visible {
679                        // ✅ PERSISTENT-CURSOR: Immer als separates Symbol!
680                        spans.push(cursor_state.create_cursor_span(config));
681                    }
682                }
683
684                if spans.is_empty() {
685                    spans.push(Span::raw(""));
686                }
687
688                lines.push(Line::from(spans));
689            }
690        }
691
692        if lines.len() >= safe_max_lines {
693            break;
694        }
695    }
696
697    while lines.len() < safe_max_lines {
698        lines.push(Line::from(vec![Span::raw("")]));
699    }
700
701    lines.truncate(safe_max_lines);
702
703    if lines.is_empty() {
704        log::error!("🚨 Empty lines vector created!");
705        lines.push(Line::from(vec![Span::raw("ERROR: Empty buffer")]));
706    }
707
708    log::trace!(
709        "✅ Output widget created: {} lines, output_cursor: {} ({})",
710        lines.len(),
711        cursor_state.get_symbol(),
712        cursor_state.color.to_name()
713    );
714
715    Paragraph::new(lines)
716        .block(
717            Block::default()
718                .borders(Borders::NONE)
719                .style(Style::default().bg(config.theme.output_bg.into())),
720        )
721        .wrap(Wrap { trim: true })
722}