rush_sync_server/output/
display.rs

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