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
22#[derive(Debug, Clone)]
23struct CachedLine {
24    content: String,
25    message_index: usize,
26    is_partial: bool,
27    visible_chars: usize,
28}
29
30type RenderData<'a> = (
31    Vec<(String, usize, bool, bool, bool)>,
32    Config,
33    crate::ui::viewport::LayoutArea,
34    &'a UiCursor,
35);
36
37impl Message {
38    pub fn new(content: String, typewriter_delay: Duration) -> Self {
39        let initial_length = if typewriter_delay.as_millis() == 0 {
40            content.graphemes(true).count() // Vollständig, ABER...
41        } else {
42            0
43        };
44
45        let typewriter_cursor = if typewriter_delay.as_millis() > 0 {
46            Some(UiCursor::for_typewriter()) // Nur bei aktiver Delay
47        } else {
48            None // ✅ Korrekt: Kein Cursor bei delay=0
49        };
50
51        Self {
52            content,
53            current_length: initial_length,
54            timestamp: Instant::now(),
55            line_count: 1,
56            typewriter_cursor,
57        }
58    }
59
60    pub fn calculate_wrapped_line_count(&mut self, viewport: &Viewport) {
61        let clean_content = clean_message_for_display(&self.content);
62
63        // WICHTIG: Leere Nachrichten = 1 Zeile
64        if clean_content.is_empty() {
65            self.line_count = 1;
66            return;
67        }
68
69        let output_area = viewport.output_area();
70        let effective_width = (output_area.width as usize).saturating_sub(2).max(10);
71
72        // KRITISCH: Zähle ALLE Zeilen korrekt!
73        let mut total_lines = 0;
74
75        // Split by newlines und behalte ALLE Zeilen (auch leere!)
76        let raw_lines: Vec<&str> = clean_content.lines().collect();
77
78        // Wenn Content mit Newline endet, füge leere Zeile hinzu
79        let lines_to_process = if clean_content.ends_with('\n') {
80            let mut lines = raw_lines;
81            lines.push("");
82            lines
83        } else if raw_lines.is_empty() {
84            vec![""]
85        } else {
86            raw_lines
87        };
88
89        // Berechne wrapped lines für JEDE Zeile
90        for line in lines_to_process {
91            if line.is_empty() {
92                total_lines += 1; // Leere Zeile = 1 Zeile
93            } else {
94                let line_chars = line.graphemes(true).count();
95                // Wrap-Berechnung: wie viele Terminal-Zeilen braucht diese Text-Zeile?
96                total_lines += ((line_chars.saturating_sub(1)) / effective_width) + 1;
97            }
98        }
99
100        self.line_count = total_lines.max(1);
101
102        log::trace!(
103            "📊 Message line count: {} lines (from {} chars, width {})",
104            self.line_count,
105            clean_content.len(),
106            effective_width
107        );
108    }
109
110    pub fn is_typing(&self) -> bool {
111        if self.typewriter_cursor.is_some() {
112            self.current_length < self.content.graphemes(true).count()
113        } else {
114            false
115        }
116    }
117
118    pub fn is_cursor_visible(&self) -> bool {
119        if let Some(ref cursor) = self.typewriter_cursor {
120            cursor.is_visible()
121        } else {
122            false
123        }
124    }
125}
126
127pub struct MessageDisplay {
128    messages: Vec<Message>,
129    line_cache: Vec<CachedLine>,
130    cache_dirty: bool,
131    config: Config,
132    viewport: Viewport,
133    persistent_cursor: UiCursor,
134    debug_enabled: bool,
135    debug_cycles: usize,
136}
137
138impl MessageDisplay {
139    pub fn new(config: &Config, terminal_width: u16, terminal_height: u16) -> Self {
140        let viewport = Viewport::new(terminal_width, terminal_height);
141        let persistent_cursor = UiCursor::from_config(config, CursorKind::Output);
142
143        Self::log_startup();
144
145        Self {
146            messages: Vec::with_capacity(config.max_messages),
147            line_cache: Vec::new(),
148            cache_dirty: true,
149            config: config.clone(),
150            viewport,
151            persistent_cursor,
152            debug_enabled: false,
153            debug_cycles: 0,
154        }
155    }
156
157    fn debug_log(&mut self, message: &str) {
158        if self.debug_enabled && self.debug_cycles > 0 {
159            log::info!("🔍 {}", message);
160            self.debug_cycles -= 1;
161            if self.debug_cycles == 0 {
162                self.debug_enabled = false;
163                log::info!("🔇 Debug disabled");
164            }
165        }
166    }
167
168    // ✅ OPTIMIZED: Cache-Rebuild ohne excessive Logs
169    fn rebuild_line_cache(&mut self) {
170        self.line_cache.clear();
171
172        let output_area = self.viewport.output_area();
173        let effective_width = (output_area.width as usize).saturating_sub(2).max(10);
174
175        for (msg_idx, message) in self.messages.iter().enumerate() {
176            let original_content = &message.content;
177
178            // Sichtbarer Content (bei Typewriter-Effekt)
179            let visible_content = if message.is_typing() {
180                let graphemes: Vec<&str> = original_content.graphemes(true).collect();
181                graphemes
182                    .iter()
183                    .take(message.current_length)
184                    .copied()
185                    .collect::<String>()
186            } else {
187                original_content.clone()
188            };
189
190            // Clean für Display
191            let clean_content = clean_message_for_display(&visible_content);
192
193            // KRITISCH: Korrekte Zeilen-Aufteilung
194            let raw_lines = if clean_content.is_empty() {
195                vec![String::new()]
196            } else {
197                let mut lines: Vec<String> = clean_content.lines().map(|s| s.to_string()).collect();
198
199                // Wenn mit Newline endet, füge leere Zeile hinzu
200                if clean_content.ends_with('\n') {
201                    lines.push(String::new());
202                }
203
204                if lines.is_empty() {
205                    lines.push(String::new());
206                }
207
208                lines
209            };
210
211            // WRAP JEDE ZEILE wenn zu lang
212            for (line_idx, raw_line) in raw_lines.iter().enumerate() {
213                if raw_line.is_empty() {
214                    // Leere Zeile direkt hinzufügen
215                    self.line_cache.push(CachedLine {
216                        content: String::new(),
217                        message_index: msg_idx,
218                        is_partial: false,
219                        visible_chars: 0,
220                    });
221                } else {
222                    // Wrap lange Zeilen
223                    let graphemes: Vec<&str> = raw_line.graphemes(true).collect();
224                    let mut start = 0;
225
226                    while start < graphemes.len() {
227                        let end = (start + effective_width).min(graphemes.len());
228                        let wrapped_line = graphemes[start..end].join("");
229
230                        let is_last_chunk = end == graphemes.len();
231                        let is_last_line = line_idx == raw_lines.len() - 1;
232
233                        self.line_cache.push(CachedLine {
234                            content: wrapped_line.clone(),
235                            message_index: msg_idx,
236                            is_partial: message.is_typing() && is_last_line && is_last_chunk,
237                            visible_chars: wrapped_line.graphemes(true).count(),
238                        });
239
240                        start = end;
241                    }
242                }
243            }
244        }
245
246        // Extra Cursor-Zeile am Ende
247        if let Some(last_msg) = self.messages.last() {
248            if !last_msg.is_typing() {
249                self.line_cache.push(CachedLine {
250                    content: String::new(),
251                    message_index: self.messages.len(),
252                    is_partial: false,
253                    visible_chars: 0,
254                });
255            }
256        }
257
258        self.cache_dirty = false;
259
260        // WICHTIG: Content-Höhe SOFORT updaten!
261        let new_height = self.line_cache.len();
262        self.viewport.update_content_height_silent(new_height);
263
264        log::info!(
265            "🔄 Cache rebuilt: {} lines from {} messages (viewport: {}x{})",
266            self.line_cache.len(),
267            self.messages.len(),
268            self.viewport.window_height(),
269            effective_width
270        );
271    }
272
273    // ✅ UNIFIED: Einzige get_visible_messages Funktion mit Smart-Fix
274    pub fn get_visible_messages(&mut self) -> Vec<(String, usize, bool, bool, bool)> {
275        if self.cache_dirty {
276            self.rebuild_line_cache();
277        }
278
279        let window_height = self.viewport.window_height();
280        let scroll_offset = self.viewport.scroll_offset();
281
282        // ✅ SMART FIX: Korrekte Berechnung für alle Fälle
283        let available_lines = self.line_cache.len().saturating_sub(scroll_offset);
284        let lines_to_show = available_lines.min(window_height);
285
286        let visible_start = scroll_offset;
287        let visible_end = scroll_offset + lines_to_show;
288
289        self.debug_log(&format!(
290            "Viewport: cache={}, offset={}, showing={}, range={}..{}",
291            self.line_cache.len(),
292            scroll_offset,
293            lines_to_show,
294            visible_start,
295            visible_end
296        ));
297
298        let mut result = Vec::new();
299
300        if self.line_cache.is_empty() {
301            result.push((
302                String::new(),
303                0,
304                false,
305                false,
306                self.persistent_cursor.is_visible(),
307            ));
308            return result;
309        }
310
311        // Process visible lines
312        for line_idx in visible_start..visible_end {
313            if let Some(cached_line) = self.line_cache.get(line_idx) {
314                let msg_idx = cached_line.message_index;
315                let is_last_line = line_idx == self.line_cache.len() - 1;
316
317                let (is_typing, cursor_visible) = if msg_idx < self.messages.len() {
318                    if let Some(msg) = self.messages.get(msg_idx) {
319                        (
320                            cached_line.is_partial && msg.is_typing(),
321                            msg.is_cursor_visible() && cached_line.is_partial,
322                        )
323                    } else {
324                        (false, false)
325                    }
326                } else {
327                    (false, false)
328                };
329
330                let persistent_cursor =
331                    is_last_line && !is_typing && self.persistent_cursor.is_visible();
332
333                result.push((
334                    cached_line.content.clone(),
335                    cached_line.visible_chars,
336                    is_typing,
337                    cursor_visible,
338                    persistent_cursor,
339                ));
340            }
341        }
342
343        // Padding to window height
344        while result.len() < window_height {
345            result.push((String::new(), 0, false, false, false));
346        }
347
348        self.debug_log(&format!("Result: {} lines generated", result.len()));
349        result
350    }
351
352    // ✅ SMART: Add message mit intelligenter Debug-Aktivierung
353    pub fn add_message(&mut self, content: String) {
354        self.add_message_with_typewriter(content, true);
355    }
356
357    pub fn add_message_instant(&mut self, content: String) {
358        self.add_message_with_typewriter(content, false);
359    }
360
361    fn add_message_with_typewriter(&mut self, content: String, use_typewriter: bool) {
362        let line_count = content.lines().count();
363
364        // PERFORMANCE: Große Nachrichten IMMER instant!
365        let force_instant = line_count > 5 || content.len() > 200;
366
367        if force_instant {
368            log::info!(
369                "📦 Large message ({} lines) - forcing instant display",
370                line_count
371            );
372        }
373        // Debug für große Nachrichten
374        if content.lines().count() > 3 {
375            log::info!("📦 Adding large message: {} lines", content.lines().count());
376        }
377
378        Self::log_to_file(&content);
379
380        // Entferne alte Nachrichten wenn Buffer voll
381        if self.messages.len() >= self.config.max_messages {
382            self.messages.remove(0);
383            self.cache_dirty = true;
384        }
385
386        let typewriter_delay = if use_typewriter && !force_instant {
387            self.config.typewriter_delay
388        } else {
389            Duration::from_millis(0) // Instant für große Nachrichten
390        };
391
392        let mut message = Message::new(content, typewriter_delay);
393
394        // KRITISCH: Berechne Line Count VOR dem Hinzufügen!
395        message.calculate_wrapped_line_count(&self.viewport);
396
397        log::info!(
398            "📝 New message: {} lines (typewriter: {})",
399            message.line_count,
400            use_typewriter
401        );
402
403        self.messages.push(message);
404        self.cache_dirty = true;
405
406        // FORCE CACHE REBUILD
407        self.rebuild_line_cache();
408
409        // AUTO-SCROLL wenn aktiviert
410        if self.viewport.is_auto_scroll_enabled() {
411            let content_height = self.line_cache.len();
412            let window_height = self.viewport.window_height();
413
414            if content_height > window_height {
415                let target_offset = content_height - window_height;
416                self.viewport.set_scroll_offset_direct_silent(target_offset);
417
418                log::info!(
419                    "📜 Auto-scroll: offset {} (content: {}, window: {})",
420                    target_offset,
421                    content_height,
422                    window_height
423                );
424            }
425        }
426    }
427
428    // ✅ OPTIMIZED: Typewriter ohne excessive Logs
429    pub fn update_typewriter(&mut self) {
430        self.persistent_cursor.update_blink();
431
432        if self.config.typewriter_delay.as_millis() == 0 {
433            return;
434        }
435
436        let mut needs_rebuild = false;
437
438        if let Some(last_message) = self.messages.last_mut() {
439            let total_length = last_message.content.graphemes(true).count();
440
441            if let Some(ref mut cursor) = last_message.typewriter_cursor {
442                cursor.update_blink();
443            }
444
445            if last_message.current_length < total_length {
446                let elapsed = last_message.timestamp.elapsed();
447
448                if elapsed >= self.config.typewriter_delay {
449                    let old_length = last_message.current_length;
450
451                    let chars_to_add = if self.config.typewriter_delay.as_millis() <= 5 {
452                        let ratio = elapsed.as_millis() as f64
453                            / self.config.typewriter_delay.as_millis() as f64;
454                        ratio.floor().max(1.0) as usize
455                    } else {
456                        1
457                    };
458
459                    let new_length = (last_message.current_length + chars_to_add).min(total_length);
460                    last_message.current_length = new_length;
461                    last_message.timestamp = Instant::now();
462
463                    let chars_since_last_rebuild = new_length - old_length;
464                    let next_chars = last_message
465                        .content
466                        .chars()
467                        .skip(old_length)
468                        .take(chars_to_add)
469                        .collect::<String>();
470
471                    // Rebuild NUR wenn ein '\n' dabei ist
472                    if next_chars.contains('\n') {
473                        needs_rebuild = true;
474                        log::trace!("🔄 Typewriter crossed newline boundary!");
475                    } else if chars_since_last_rebuild > 50 {
476                        // Oder alle 50 Zeichen für Safety
477                        needs_rebuild = true;
478                    }
479
480                    self.cache_dirty = true;
481
482                    if new_length == total_length {
483                        last_message.typewriter_cursor = None;
484                        needs_rebuild = true;
485
486                        // FORCE AUTO-SCROLL am Ende
487                        self.viewport.enable_auto_scroll_silent();
488                        self.viewport.scroll_to_bottom();
489                    }
490                }
491            }
492        }
493
494        // Rebuild wenn nötig
495        if needs_rebuild && self.cache_dirty {
496            self.rebuild_line_cache();
497        }
498    }
499
500    // ✅ SIMPLIFIED: Handle scroll ohne Debug-Spam
501    pub fn handle_scroll(&mut self, direction: ScrollDirection, amount: usize) {
502        match direction {
503            ScrollDirection::Up => self.viewport.scroll_up(amount.max(1)),
504            ScrollDirection::Down => self.viewport.scroll_down(amount.max(1)),
505            ScrollDirection::PageUp => self.viewport.page_up(),
506            ScrollDirection::PageDown => self.viewport.page_down(),
507            ScrollDirection::ToTop => self.viewport.scroll_to_top(),
508            ScrollDirection::ToBottom => self.viewport.scroll_to_bottom(),
509        }
510    }
511
512    pub fn handle_resize(&mut self, width: u16, height: u16) -> bool {
513        let changed = self.viewport.update_terminal_size(width, height);
514
515        if changed {
516            for message in &mut self.messages {
517                message.calculate_wrapped_line_count(&self.viewport);
518            }
519            self.cache_dirty = true;
520            self.viewport.force_auto_scroll();
521        }
522
523        changed
524    }
525
526    pub fn clear_messages(&mut self) {
527        self.messages.clear();
528        self.line_cache.clear();
529        self.cache_dirty = false;
530        self.viewport.update_content_height_silent(0);
531        self.viewport.force_auto_scroll();
532        self.persistent_cursor.show_cursor();
533    }
534
535    pub fn create_output_widget_for_rendering(&mut self) -> RenderData<'_> {
536        let messages = self.get_visible_messages();
537        (
538            messages,
539            self.config.clone(),
540            self.viewport.output_area(),
541            &self.persistent_cursor,
542        )
543    }
544
545    pub fn update_config(&mut self, new_config: &Config) {
546        self.config = new_config.clone();
547        self.persistent_cursor = UiCursor::from_config(new_config, CursorKind::Output);
548        self.cache_dirty = true;
549
550        if self.messages.len() > self.config.max_messages {
551            let excess = self.messages.len() - self.config.max_messages;
552            self.messages.drain(0..excess);
553            self.cache_dirty = true;
554        }
555    }
556
557    // ✅ GETTERS: Clean and simple
558    pub fn viewport(&self) -> &Viewport {
559        &self.viewport
560    }
561    pub fn viewport_mut(&mut self) -> &mut Viewport {
562        &mut self.viewport
563    }
564    pub fn get_messages_count(&self) -> usize {
565        self.messages.len()
566    }
567    pub fn get_line_count(&self) -> usize {
568        if self.cache_dirty {
569            self.messages.iter().map(|m| m.line_count).sum()
570        } else {
571            self.line_cache.len()
572        }
573    }
574
575    pub fn debug_scroll_status(&self) -> String {
576        format!(
577            "Scroll: offset={}, lines={}, window={}, auto={}, msgs={}, cache={}",
578            self.viewport.scroll_offset(),
579            self.viewport.content_height(),
580            self.viewport.window_height(),
581            self.viewport.is_auto_scroll_enabled(),
582            self.messages.len(),
583            self.line_cache.len()
584        )
585    }
586
587    // ✅ UNIFIED: Content height management
588    pub fn handle_viewport_event(&mut self, event: ViewportEvent) -> bool {
589        let changed = self.viewport.handle_event(event);
590        if changed {
591            for message in &mut self.messages {
592                message.calculate_wrapped_line_count(&self.viewport);
593            }
594            self.cache_dirty = true;
595        }
596        changed
597    }
598
599    pub fn get_content_height(&self) -> usize {
600        self.viewport.content_height()
601    }
602    pub fn get_window_height(&self) -> usize {
603        self.viewport.window_height()
604    }
605
606    pub fn log(&mut self, level: &str, message: &str) {
607        let log_message = format!("[{}] {}", level, message);
608        self.add_message(log_message);
609    }
610
611    // ✅ UTILITY: File logging (unchanged but cleaner)
612    fn log_to_file(content: &str) {
613        if content.starts_with("__") || content.trim().is_empty() {
614            return;
615        }
616
617        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
618        let log_line = format!("[{}] {}\n", timestamp, content);
619
620        if let Ok(exe_path) = std::env::current_exe() {
621            if let Some(base_dir) = exe_path.parent() {
622                let log_path = base_dir.join(".rss").join("rush.logs");
623                let _ = std::fs::create_dir_all(log_path.parent().unwrap());
624                let _ = std::fs::OpenOptions::new()
625                    .create(true)
626                    .append(true)
627                    .open(&log_path)
628                    .and_then(|mut file| {
629                        use std::io::Write;
630                        file.write_all(log_line.as_bytes())
631                    });
632            }
633        }
634    }
635
636    fn log_startup() {
637        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
638        let version = crate::core::constants::VERSION;
639        let startup_line = format!(
640            "[{}] === Rush Sync Server v{} Started ===\n",
641            timestamp, version
642        );
643
644        if let Ok(exe_path) = std::env::current_exe() {
645            if let Some(base_dir) = exe_path.parent() {
646                let log_path = base_dir.join(".rss").join("rush.logs");
647                let _ = std::fs::create_dir_all(log_path.parent().unwrap());
648                let _ = std::fs::OpenOptions::new()
649                    .create(true)
650                    .append(true)
651                    .open(&log_path)
652                    .and_then(|mut file| {
653                        use std::io::Write;
654                        file.write_all(startup_line.as_bytes())
655                    });
656            }
657        }
658    }
659}
660
661// ✅ UTILITY FUNCTIONS: Cleaner implementation
662fn clean_ansi_codes(message: &str) -> String {
663    String::from_utf8_lossy(&strip(message.as_bytes()).unwrap_or_default()).into_owned()
664}
665
666fn clean_message_for_display(message: &str) -> String {
667    clean_ansi_codes(message)
668        .replace("__CONFIRM_EXIT__", "")
669        .replace("__CLEAR__", "")
670        .trim()
671        .to_string()
672}
673
674fn parse_message_parts(message: &str) -> Vec<(String, bool)> {
675    let mut parts = Vec::new();
676    let mut chars = message.char_indices().peekable();
677    let mut start = 0;
678
679    while let Some((i, c)) = chars.peek().cloned() {
680        if c == '[' {
681            if start < i {
682                let text = &message[start..i];
683                if !text.trim().is_empty() {
684                    parts.push((text.to_owned(), false));
685                }
686            }
687
688            if let Some(end_idx) = message[i..].find(']') {
689                let end = i + end_idx + 1;
690                parts.push((message[i..end].to_owned(), true));
691                start = end;
692                while let Some(&(ci, _)) = chars.peek() {
693                    if ci < end {
694                        chars.next();
695                    } else {
696                        break;
697                    }
698                }
699            } else {
700                parts.push((message[i..].to_owned(), false));
701                break;
702            }
703        } else {
704            chars.next();
705        }
706    }
707
708    if start < message.len() {
709        let remaining = &message[start..];
710        if !remaining.trim().is_empty() {
711            parts.push((remaining.to_owned(), false));
712        }
713    }
714
715    if parts.is_empty() {
716        parts.push((message.to_owned(), false));
717    }
718
719    parts
720}
721
722fn get_marker_color(marker: &str) -> AppColor {
723    let display_category = marker
724        .trim_start_matches('[')
725        .trim_end_matches(']')
726        .trim_start_matches("cat:")
727        .to_lowercase();
728
729    if AppColor::from_any(&display_category).to_name() != "gray" {
730        return AppColor::from_any(&display_category);
731    }
732
733    let mapped_category = crate::i18n::get_color_category_for_display(&display_category);
734    AppColor::from_any(mapped_category)
735}
736
737// ✅ OPTIMIZED: Output widget creation
738pub fn create_output_widget<'a>(
739    messages: &'a [(String, usize, bool, bool, bool)],
740    layout_area: crate::ui::viewport::LayoutArea,
741    config: &'a Config,
742    cursor_state: &'a UiCursor,
743) -> Paragraph<'a> {
744    let max_lines = layout_area.height as usize;
745
746    if max_lines == 0 || layout_area.width == 0 {
747        return Paragraph::new(vec![Line::from(vec![Span::raw("⚠️ INVALID LAYOUT")])]).block(
748            Block::default()
749                .borders(Borders::NONE)
750                .style(Style::default().bg(config.theme.output_bg.into())),
751        );
752    }
753
754    let safe_max_lines = max_lines.min(1000);
755    let mut lines = Vec::new();
756
757    if messages.is_empty() {
758        let empty_lines = vec![Line::from(vec![Span::raw("")]); safe_max_lines];
759        return Paragraph::new(empty_lines)
760            .block(
761                Block::default()
762                    .borders(Borders::NONE)
763                    .style(Style::default().bg(config.theme.output_bg.into())),
764            )
765            .wrap(Wrap { trim: true });
766    }
767
768    for (
769        message_idx,
770        (message, current_length, is_typing, msg_cursor_visible, persistent_cursor_visible),
771    ) in messages.iter().enumerate()
772    {
773        let is_last_message = message_idx == messages.len() - 1;
774
775        if message.is_empty() {
776            if *persistent_cursor_visible {
777                lines.push(Line::from(vec![cursor_state.create_cursor_span(config)]));
778            } else {
779                lines.push(Line::from(vec![Span::raw("")]));
780            }
781            continue;
782        }
783
784        let clean_message = clean_message_for_display(message);
785        let message_lines: Vec<&str> = clean_message.lines().collect();
786
787        if message_lines.is_empty() {
788            lines.push(Line::from(vec![Span::raw("")]));
789        } else {
790            for (line_idx, line_content) in message_lines.iter().enumerate() {
791                if lines.len() >= safe_max_lines {
792                    break;
793                }
794
795                let is_last_line = line_idx == message_lines.len() - 1;
796                let visible_chars = if is_last_message && is_last_line {
797                    let chars_before_this_line: usize = message_lines
798                        .iter()
799                        .take(line_idx)
800                        .map(|l| l.graphemes(true).count() + 1)
801                        .sum();
802                    let available_for_this_line =
803                        current_length.saturating_sub(chars_before_this_line);
804                    available_for_this_line.min(line_content.graphemes(true).count())
805                } else {
806                    line_content.graphemes(true).count()
807                };
808
809                let message_parts = parse_message_parts(line_content);
810                let mut spans = Vec::new();
811                let mut chars_used = 0;
812
813                for (part_text, is_marker) in message_parts {
814                    let part_chars = part_text.graphemes(true).count();
815                    let part_style = if is_marker {
816                        Style::default().fg(get_marker_color(&part_text).into())
817                    } else {
818                        Style::default().fg(config.theme.output_text.into())
819                    };
820
821                    if chars_used >= visible_chars {
822                        break;
823                    }
824
825                    let chars_needed = visible_chars - chars_used;
826                    if chars_needed >= part_chars {
827                        spans.push(Span::styled(part_text, part_style));
828                        chars_used += part_chars;
829                    } else {
830                        let graphemes: Vec<&str> = part_text.graphemes(true).collect();
831                        spans.push(Span::styled(
832                            graphemes
833                                .iter()
834                                .take(chars_needed)
835                                .copied()
836                                .collect::<String>(),
837                            part_style,
838                        ));
839                        break;
840                    }
841                }
842
843                if is_last_message
844                    && is_last_line
845                    && ((*is_typing && *msg_cursor_visible)
846                        || (!*is_typing && *persistent_cursor_visible))
847                {
848                    spans.push(cursor_state.create_cursor_span(config));
849                }
850
851                if spans.is_empty() {
852                    spans.push(Span::raw(""));
853                }
854
855                lines.push(Line::from(spans));
856            }
857        }
858
859        if lines.len() >= safe_max_lines {
860            break;
861        }
862    }
863
864    while lines.len() < safe_max_lines {
865        lines.push(Line::from(vec![Span::raw("")]));
866    }
867
868    lines.truncate(safe_max_lines);
869
870    if lines.is_empty() {
871        lines.push(Line::from(vec![Span::raw("ERROR: Empty buffer")]));
872    }
873
874    Paragraph::new(lines)
875        .block(
876            Block::default()
877                .borders(Borders::NONE)
878                .style(Style::default().bg(config.theme.output_bg.into())),
879        )
880        .wrap(Wrap { trim: true })
881}