rush_sync_server/output/
output.rs

1// ## BEGIN ##
2use crate::core::prelude::*;
3use crate::ui::color::AppColor;
4use ratatui::{
5    style::Style,
6    text::{Line, Span},
7    widgets::{Block, Borders, Paragraph, Wrap},
8};
9use strip_ansi_escapes::strip;
10use unicode_segmentation::UnicodeSegmentation;
11
12/// ✅ Entfernt ANSI-Codes aus Logs (z.B. falls doch welche reinkommen)
13fn clean_ansi_codes(message: &str) -> String {
14    String::from_utf8_lossy(&strip(message.as_bytes()).unwrap_or_default()).into_owned()
15}
16
17/// ✅ Entfernt interne Steuerzeichen wie __CLEAR__
18fn clean_message_for_display(message: &str) -> String {
19    clean_ansi_codes(message)
20        .replace("__CONFIRM_EXIT__", "")
21        .replace("__CLEAR__", "")
22        .trim()
23        .to_string()
24}
25
26/// ✅ Teilt Message in Text + Marker ([INFO], [ERROR], etc.)
27// ## FILE: src/output/output.rs (Optimiert) ##
28fn parse_message_parts(message: &str) -> Vec<(String, bool)> {
29    let mut parts = Vec::new();
30    let mut chars = message.char_indices().peekable();
31    let mut start = 0;
32
33    while let Some((i, c)) = chars.peek().cloned() {
34        if c == '[' {
35            // Text vor dem Marker
36            if start < i {
37                let text = &message[start..i];
38                if !text.trim().is_empty() {
39                    parts.push((text.to_owned(), false));
40                }
41            }
42
43            // Marker finden
44            if let Some(end_idx) = message[i..].find(']') {
45                let end = i + end_idx + 1;
46                parts.push((message[i..end].to_owned(), true));
47                start = end;
48                while let Some(&(ci, _)) = chars.peek() {
49                    if ci < end {
50                        chars.next();
51                    } else {
52                        break;
53                    }
54                }
55            } else {
56                parts.push((message[i..].to_owned(), false));
57                break;
58            }
59        } else {
60            chars.next();
61        }
62    }
63
64    if start < message.len() {
65        let remaining = &message[start..];
66        if !remaining.trim().is_empty() {
67            parts.push((remaining.to_owned(), false));
68        }
69    }
70
71    if parts.is_empty() {
72        parts.push((message.to_owned(), false));
73    }
74
75    parts
76}
77
78/// ✅ Marker-Farben: nutzt i18n → z.B. "info" → AppColor
79fn get_marker_color(marker: &str) -> AppColor {
80    let display_category = marker
81        .trim_start_matches('[')
82        .trim_end_matches(']')
83        .trim_start_matches("cat:")
84        .to_lowercase();
85
86    // ✅ 1. Standard-Keys direkt
87    if AppColor::from_any(&display_category).to_name() != "gray" {
88        return AppColor::from_any(&display_category);
89    }
90
91    // ✅ 2. Übersetzte Marker → i18n-Mapping
92    let mapped_category = crate::i18n::get_color_category_for_display(&display_category);
93    AppColor::from_any(mapped_category)
94}
95
96/// ✅ Hauptfunktion: Baut den fertigen Paragraph
97pub fn create_output_widget<'a>(
98    messages: &'a [(&'a String, usize)],
99    available_height: u16,
100    config: &Config,
101) -> Paragraph<'a> {
102    let mut lines = Vec::new();
103    let max_visible_messages = (available_height as usize).saturating_sub(1);
104
105    if messages.is_empty() {
106        let empty_lines = vec![Line::from(vec![Span::raw("")]); max_visible_messages];
107        return Paragraph::new(empty_lines)
108            .block(
109                Block::default()
110                    .borders(Borders::NONE)
111                    .style(Style::default().bg(config.theme.output_bg.into())),
112            )
113            .wrap(Wrap { trim: true });
114    }
115
116    let start_idx = messages.len().saturating_sub(max_visible_messages);
117    let visible_messages = &messages[start_idx..];
118
119    for (idx, (message, current_length)) in visible_messages.iter().enumerate() {
120        let is_last_message = idx == visible_messages.len() - 1;
121        let clean_message = clean_message_for_display(message);
122        let message_parts = parse_message_parts(&clean_message);
123
124        let mut styled_parts = Vec::new();
125        let mut total_chars = 0;
126
127        for (part_text, is_marker) in message_parts {
128            let part_chars = part_text.graphemes(true).count();
129            let part_style = if is_marker {
130                Style::default().fg(get_marker_color(&part_text).into())
131            } else {
132                Style::default().fg(config.theme.output_text.into())
133            };
134
135            styled_parts.push((part_text, part_style, part_chars));
136            total_chars += part_chars;
137        }
138
139        let visible_chars = if is_last_message {
140            (*current_length).min(total_chars)
141        } else {
142            total_chars
143        };
144
145        let mut spans = Vec::new();
146        let mut chars_used = 0;
147
148        for (part_text, part_style, part_char_count) in styled_parts {
149            if chars_used >= visible_chars {
150                break;
151            }
152
153            let chars_needed = visible_chars - chars_used;
154
155            if chars_needed >= part_char_count {
156                spans.push(Span::styled(part_text, part_style));
157                chars_used += part_char_count;
158            } else {
159                let graphemes: Vec<&str> = part_text.graphemes(true).collect();
160                spans.push(Span::styled(
161                    graphemes
162                        .iter()
163                        .take(chars_needed)
164                        .copied()
165                        .collect::<String>(),
166                    part_style,
167                ));
168                break;
169            }
170        }
171
172        if spans.is_empty() {
173            spans.push(Span::raw(""));
174        }
175
176        lines.push(Line::from(spans));
177    }
178
179    let remaining_space = max_visible_messages.saturating_sub(lines.len());
180    for _ in 0..remaining_space {
181        lines.push(Line::from(vec![Span::raw("")]));
182    }
183
184    Paragraph::new(lines)
185        .block(
186            Block::default()
187                .borders(Borders::NONE)
188                .style(Style::default().bg(config.theme.output_bg.into())),
189        )
190        .wrap(Wrap { trim: true })
191}
192// ## END ##