rush_sync_server/output/
display.rs

1// =====================================================
2// FILE: src/output/display.rs - MESSAGE + OUTPUT KOMBINIERT
3// =====================================================
4
5use crate::core::prelude::*;
6use crate::input::keyboard::KeyAction;
7use crate::output::scroll::ScrollState;
8use crate::ui::color::AppColor;
9use ratatui::{
10    style::Style,
11    text::{Line, Span},
12    widgets::{Block, Borders, Paragraph, Wrap},
13};
14use strip_ansi_escapes::strip;
15use unicode_segmentation::UnicodeSegmentation;
16
17// ✅ MESSAGE STRUKTUR
18#[derive(Debug)]
19pub struct Message {
20    pub content: String,
21    pub current_length: usize,
22    pub timestamp: Instant,
23}
24
25// ✅ KOMBINIERTES DISPLAY-SYSTEM
26pub struct MessageDisplay {
27    messages: Vec<Message>,
28    config: Config,
29    pub scroll_state: ScrollState,
30}
31
32impl MessageDisplay {
33    /// Erstellt neues MessageDisplay
34    pub fn new(config: &Config) -> Self {
35        Self {
36            messages: Vec::with_capacity(config.max_messages),
37            config: config.clone(),
38            scroll_state: ScrollState::new(),
39        }
40    }
41
42    /// Updates Config (für Live-Updates)
43    pub fn update_config(&mut self, new_config: &Config) {
44        self.config = new_config.clone();
45
46        // Resize messages buffer falls max_messages geändert wurde
47        if self.messages.len() > self.config.max_messages {
48            let excess = self.messages.len() - self.config.max_messages;
49            self.messages.drain(0..excess);
50
51            if self.scroll_state.offset > 0 {
52                self.scroll_state.offset = self.scroll_state.offset.saturating_sub(excess);
53            }
54        } else {
55            self.messages
56                .reserve(self.config.max_messages.saturating_sub(self.messages.len()));
57        }
58
59        log::debug!(
60            "MessageDisplay config updated: max_messages = {}, typewriter_delay = {}ms",
61            self.config.max_messages,
62            self.config.typewriter_delay.as_millis()
63        );
64    }
65
66    /// Löscht alle Messages
67    pub fn clear_messages(&mut self) {
68        self.messages.clear();
69        self.scroll_state.force_auto_scroll();
70    }
71
72    /// Fügt neue Message hinzu
73    pub fn add_message(&mut self, content: String) {
74        // Buffer-Management
75        if self.messages.len() >= self.config.max_messages {
76            self.messages.remove(0);
77        }
78
79        // Typewriter
80        let initial_length = if self.config.typewriter_delay.as_millis() == 0 {
81            content.graphemes(true).count()
82        } else {
83            1
84        };
85
86        self.messages.push(Message {
87            content,
88            current_length: initial_length,
89            timestamp: Instant::now(),
90        });
91
92        // ✅ KORRIGIERT: Scroll-Update mit richtiger Berechnung
93        let total_lines = self.calculate_total_lines();
94        self.scroll_state
95            .update_dimensions(self.scroll_state.window_height, total_lines);
96        self.scroll_state.force_auto_scroll();
97
98        // log::trace!(
99        //     "📨 Message added, total_lines={}, window_height={}",
100        //     total_lines,
101        //     self.scroll_state.window_height
102        // );
103    }
104
105    /// Typewriter Update
106    pub fn update_typewriter(&mut self) {
107        if self.config.typewriter_delay.as_millis() == 0 {
108            return;
109        }
110
111        if let Some(last_message) = self.messages.last_mut() {
112            let total_length = last_message.content.graphemes(true).count();
113
114            if last_message.current_length < total_length {
115                let elapsed = last_message.timestamp.elapsed();
116
117                if elapsed >= self.config.typewriter_delay {
118                    let chars_to_add = if self.config.typewriter_delay.as_millis() <= 5 {
119                        let ratio = elapsed.as_millis() as f64
120                            / self.config.typewriter_delay.as_millis() as f64;
121                        ratio.floor().max(1.0) as usize
122                    } else {
123                        1
124                    };
125
126                    let new_length = (last_message.current_length + chars_to_add).min(total_length);
127                    last_message.current_length = new_length;
128                    last_message.timestamp = Instant::now();
129                }
130            }
131        }
132    }
133
134    /// Handle Scroll Events
135    pub fn handle_scroll(&mut self, action: KeyAction, window_height: usize) {
136        let total_lines = self.calculate_total_lines();
137        self.scroll_state
138            .update_dimensions(window_height, total_lines);
139
140        match action {
141            KeyAction::ScrollUp => self.scroll_state.scroll_up(1),
142            KeyAction::ScrollDown => self.scroll_state.scroll_down(1),
143            KeyAction::PageUp => {
144                let scroll_amount = window_height.saturating_sub(1);
145                self.scroll_state.scroll_up(scroll_amount);
146            }
147            KeyAction::PageDown => {
148                let scroll_amount = window_height.saturating_sub(1);
149                self.scroll_state.scroll_down(scroll_amount);
150            }
151            _ => {}
152        }
153    }
154
155    fn calculate_total_lines(&self) -> usize {
156        let total = self
157            .messages
158            .iter()
159            .map(|msg| {
160                let clean_msg = clean_message_for_display(&msg.content);
161                let line_count = if clean_msg.is_empty() {
162                    1
163                } else {
164                    clean_msg.lines().count()
165                };
166                line_count.max(1) // Mindestens 1 Zeile
167            })
168            .sum::<usize>();
169
170        //log::trace!("📊 Total lines calculated: {}", total);
171        total
172    }
173
174    /// Content Height für Scrolling
175    pub fn get_content_height(&self) -> usize {
176        self.calculate_total_lines()
177    }
178
179    /// Sichtbare Messages für Rendering
180    pub fn get_visible_messages(&self) -> Vec<(&String, usize)> {
181        let (start, end) = self.scroll_state.get_visible_range();
182        let start = start.min(self.messages.len());
183        let end = end.min(self.messages.len());
184
185        if start >= end {
186            return Vec::new();
187        }
188
189        self.messages[start..end]
190            .iter()
191            .map(|msg| (&msg.content, msg.current_length))
192            .collect()
193    }
194
195    /// Log Helper
196    pub fn log(&mut self, level: &str, message: &str) {
197        let log_message = format!("[{}] {}", level, message);
198        self.add_message(log_message);
199    }
200
201    /// Erstellt Output-Widget für Rendering
202    pub fn create_output_widget_for_rendering(
203        &self,
204        _available_height: u16,
205    ) -> (Vec<(String, usize)>, Config) {
206        let messages = self.get_visible_messages();
207        let messages_owned: Vec<(String, usize)> = messages
208            .into_iter()
209            .map(|(content, length)| (content.clone(), length))
210            .collect();
211        (messages_owned, self.config.clone())
212    }
213
214    /// ✅ NEU: Getter für messages (für Debug)
215    pub fn get_messages_count(&self) -> usize {
216        self.messages.len()
217    }
218}
219
220// ✅ OUTPUT WIDGET CREATION (aus output.rs übernommen)
221
222/// Entfernt ANSI-Codes aus Logs
223fn clean_ansi_codes(message: &str) -> String {
224    String::from_utf8_lossy(&strip(message.as_bytes()).unwrap_or_default()).into_owned()
225}
226
227/// Entfernt interne Steuerzeichen
228fn clean_message_for_display(message: &str) -> String {
229    clean_ansi_codes(message)
230        .replace("__CONFIRM_EXIT__", "")
231        .replace("__CLEAR__", "")
232        .trim()
233        .to_string()
234}
235
236/// Teilt Message in Text + Marker
237fn parse_message_parts(message: &str) -> Vec<(String, bool)> {
238    let mut parts = Vec::new();
239    let mut chars = message.char_indices().peekable();
240    let mut start = 0;
241
242    while let Some((i, c)) = chars.peek().cloned() {
243        if c == '[' {
244            if start < i {
245                let text = &message[start..i];
246                if !text.trim().is_empty() {
247                    parts.push((text.to_owned(), false));
248                }
249            }
250
251            if let Some(end_idx) = message[i..].find(']') {
252                let end = i + end_idx + 1;
253                parts.push((message[i..end].to_owned(), true));
254                start = end;
255                while let Some(&(ci, _)) = chars.peek() {
256                    if ci < end {
257                        chars.next();
258                    } else {
259                        break;
260                    }
261                }
262            } else {
263                parts.push((message[i..].to_owned(), false));
264                break;
265            }
266        } else {
267            chars.next();
268        }
269    }
270
271    if start < message.len() {
272        let remaining = &message[start..];
273        if !remaining.trim().is_empty() {
274            parts.push((remaining.to_owned(), false));
275        }
276    }
277
278    if parts.is_empty() {
279        parts.push((message.to_owned(), false));
280    }
281
282    parts
283}
284
285/// Marker-Farben
286fn get_marker_color(marker: &str) -> AppColor {
287    let display_category = marker
288        .trim_start_matches('[')
289        .trim_end_matches(']')
290        .trim_start_matches("cat:")
291        .to_lowercase();
292
293    if AppColor::from_any(&display_category).to_name() != "gray" {
294        return AppColor::from_any(&display_category);
295    }
296
297    let mapped_category = crate::i18n::get_color_category_for_display(&display_category);
298    AppColor::from_any(mapped_category)
299}
300
301/// Hauptfunktion: Baut den fertigen Paragraph
302pub fn create_output_widget<'a>(
303    messages: &'a [(&'a String, usize)],
304    available_height: u16,
305    config: &Config,
306) -> Paragraph<'a> {
307    let mut lines = Vec::new();
308    let max_lines = available_height as usize; // ✅ KEINE -1 mehr!
309
310    // ✅ CRITICAL CHECK
311    if max_lines == 0 {
312        return Paragraph::new(vec![Line::from(vec![Span::raw("⚠️ NO SPACE")])]).block(
313            Block::default()
314                .borders(Borders::NONE)
315                .style(Style::default().bg(config.theme.output_bg.into())),
316        );
317    }
318
319    if messages.is_empty() {
320        let empty_lines = vec![Line::from(vec![Span::raw("")]); max_lines];
321        return Paragraph::new(empty_lines)
322            .block(
323                Block::default()
324                    .borders(Borders::NONE)
325                    .style(Style::default().bg(config.theme.output_bg.into())),
326            )
327            .wrap(Wrap { trim: true });
328    }
329
330    // ✅ MULTILINE PROCESSING - sauber ohne Debug-Spam
331    for (message_idx, (message, current_length)) in messages.iter().enumerate() {
332        let is_last_message = message_idx == messages.len() - 1;
333        let clean_message = clean_message_for_display(message);
334        let message_lines: Vec<&str> = clean_message.lines().collect();
335
336        if message_lines.is_empty() {
337            lines.push(Line::from(vec![Span::raw("")]));
338        } else {
339            for (line_idx, line_content) in message_lines.iter().enumerate() {
340                if lines.len() >= max_lines {
341                    break; // ✅ Harte Grenze
342                }
343
344                let is_last_line = line_idx == message_lines.len() - 1;
345
346                let visible_chars = if is_last_message && is_last_line {
347                    let chars_before_this_line: usize = message_lines
348                        .iter()
349                        .take(line_idx)
350                        .map(|l| l.graphemes(true).count() + 1)
351                        .sum();
352
353                    let available_for_this_line =
354                        current_length.saturating_sub(chars_before_this_line);
355                    available_for_this_line.min(line_content.graphemes(true).count())
356                } else {
357                    line_content.graphemes(true).count()
358                };
359
360                let message_parts = parse_message_parts(line_content);
361                let mut spans = Vec::new();
362                let mut chars_used = 0;
363
364                for (part_text, is_marker) in message_parts {
365                    let part_chars = part_text.graphemes(true).count();
366                    let part_style = if is_marker {
367                        Style::default().fg(get_marker_color(&part_text).into())
368                    } else {
369                        Style::default().fg(config.theme.output_text.into())
370                    };
371
372                    if chars_used >= visible_chars {
373                        break;
374                    }
375
376                    let chars_needed = visible_chars - chars_used;
377
378                    if chars_needed >= part_chars {
379                        spans.push(Span::styled(part_text, part_style));
380                        chars_used += part_chars;
381                    } else {
382                        let graphemes: Vec<&str> = part_text.graphemes(true).collect();
383                        spans.push(Span::styled(
384                            graphemes
385                                .iter()
386                                .take(chars_needed)
387                                .copied()
388                                .collect::<String>(),
389                            part_style,
390                        ));
391                        break;
392                    }
393                }
394
395                if spans.is_empty() {
396                    spans.push(Span::raw(""));
397                }
398
399                lines.push(Line::from(spans));
400            }
401        }
402
403        if lines.len() >= max_lines {
404            break;
405        }
406    }
407
408    // ✅ FILL remaining lines nur wenn nötig
409    while lines.len() < max_lines {
410        lines.push(Line::from(vec![Span::raw("")]));
411    }
412
413    // ✅ FINAL SAFETY
414    lines.truncate(max_lines);
415
416    Paragraph::new(lines)
417        .block(
418            Block::default()
419                .borders(Borders::NONE)
420                .style(Style::default().bg(config.theme.output_bg.into())),
421        )
422        .wrap(Wrap { trim: true })
423}