rush_sync_server/ui/
screen.rs

1// =====================================================
2// FILE: src/ui/screen.rs - KORRIGIERTE CONSTRUCTOR CALLS
3// =====================================================
4
5use crate::commands::history::HistoryKeyboardHandler;
6use crate::commands::lang::LanguageService;
7use crate::commands::theme::ThemeSystem;
8use crate::core::prelude::*;
9use crate::input::{
10    input::InputState,
11    keyboard::{KeyAction, KeyboardManager},
12};
13use crate::input::{AppEvent, EventHandler};
14use crate::output::{display::MessageDisplay, logging::AppLogger};
15use crate::ui::{color::AppColor, terminal::TerminalManager, widget::Widget};
16
17use crossterm::event::KeyEvent;
18use ratatui::{
19    backend::CrosstermBackend,
20    layout::{Constraint, Direction, Layout},
21    Terminal,
22};
23use std::io::{self, Stdout};
24
25pub type TerminalBackend = Terminal<CrosstermBackend<Stdout>>;
26
27pub struct ScreenManager {
28    terminal: TerminalBackend,
29    message_display: MessageDisplay,
30    input_state: Box<dyn Widget>,
31    terminal_size: (u16, u16),
32    config: Config,
33    terminal_mgr: TerminalManager,
34    events: EventHandler,
35    keyboard_manager: KeyboardManager,
36    waiting_for_restart_confirmation: bool,
37}
38
39impl ScreenManager {
40    pub async fn new(config: &Config) -> Result<Self> {
41        let mut terminal_mgr = TerminalManager::new().await?;
42        terminal_mgr.setup().await?;
43
44        let backend = CrosstermBackend::new(io::stdout());
45        let terminal = Terminal::new(backend)?;
46        let size = terminal.size()?;
47
48        let initial_height = size.height.saturating_sub(4) as usize;
49        let mut message_display = MessageDisplay::new(config);
50        message_display
51            .scroll_state
52            .update_dimensions(initial_height, 0);
53
54        let owned_config = config.clone();
55
56        Ok(Self {
57            terminal,
58            terminal_mgr,
59            message_display,
60            // ✅ KORRIGIERT: InputState::new nimmt nur &Config
61            input_state: Box::new(InputState::new(config)),
62            terminal_size: (size.width, size.height),
63            config: owned_config,
64            events: EventHandler::new(config.poll_rate),
65            keyboard_manager: KeyboardManager::new(),
66            waiting_for_restart_confirmation: false,
67        })
68    }
69
70    pub async fn run(&mut self) -> Result<()> {
71        let result = loop {
72            if let Some(event) = self.events.next().await {
73                match event {
74                    AppEvent::Input(key) => {
75                        if self.handle_input_event(key).await? {
76                            self.events.shutdown().await;
77                            break Ok(());
78                        }
79                    }
80                    AppEvent::Resize(width, height) => {
81                        self.handle_resize_event(width, height).await?;
82                    }
83                    AppEvent::Tick => {
84                        self.handle_tick_event().await?;
85                    }
86                }
87            }
88
89            self.process_pending_logs().await;
90            self.render().await?;
91        };
92
93        self.terminal_mgr.cleanup().await?;
94        result
95    }
96
97    async fn handle_input_event(&mut self, key: KeyEvent) -> Result<bool> {
98        if HistoryKeyboardHandler::get_history_action(&key).is_some() {
99            if let Some(new_input) = self.input_state.handle_input(key) {
100                if let Some(processed) = LanguageService::process_save_message(&new_input).await {
101                    self.message_display.add_message(processed);
102                    return Ok(false);
103                }
104
105                if let Some(processed) = self.process_live_theme_update(&new_input).await {
106                    self.message_display.add_message(processed);
107                    return Ok(false);
108                }
109
110                self.message_display.add_message(new_input.clone());
111
112                if new_input.starts_with("__CLEAR__") {
113                    self.message_display.clear_messages();
114                } else if new_input.starts_with("__EXIT__") {
115                    return Ok(true);
116                }
117            }
118            return Ok(false);
119        }
120
121        match self.keyboard_manager.get_action(&key) {
122            KeyAction::ScrollUp
123            | KeyAction::ScrollDown
124            | KeyAction::PageUp
125            | KeyAction::PageDown => {
126                let window_height = self.get_content_height();
127                self.message_display
128                    .handle_scroll(self.keyboard_manager.get_action(&key), window_height);
129            }
130            KeyAction::Submit => {
131                if let Some(new_input) = self.input_state.handle_input(key) {
132                    // ✅ DETECT Performance Commands BEFORE processing
133                    let input_command = new_input.trim().to_lowercase();
134                    let is_performance_command = input_command == "perf"
135                        || input_command == "performance"
136                        || input_command == "stats";
137
138                    if let Some(processed) = LanguageService::process_save_message(&new_input).await
139                    {
140                        self.message_display.add_message(processed);
141                        return Ok(false);
142                    }
143
144                    if let Some(processed) = self.process_live_theme_update(&new_input).await {
145                        self.message_display.add_message(processed);
146                        return Ok(false);
147                    }
148
149                    self.message_display.add_message(new_input.clone());
150
151                    // ✅ SANFTER FIX für Performance Commands (ohne terminal.clear)
152                    if is_performance_command {
153                        log::info!("🔧 Performance command '{}' detected", input_command);
154
155                        // Warten bis Message verarbeitet ist
156                        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
157
158                        // Scroll-System sanft reparieren
159                        self.message_display.scroll_state.force_auto_scroll();
160
161                        let window_height = self.get_content_height();
162                        let content_height = self.message_display.get_content_height();
163                        self.message_display
164                            .scroll_state
165                            .update_dimensions(window_height, content_height);
166
167                        log::info!("✅ Performance command: scroll reset applied");
168                    }
169
170                    // ✅ Standard Command-Verarbeitung
171                    if new_input.starts_with("__CLEAR__") {
172                        self.message_display.clear_messages();
173                    } else if new_input.starts_with("__EXIT__") {
174                        return Ok(true);
175                    } else if new_input.starts_with("__RESTART_WITH_MSG__") {
176                        let feedback_msg = new_input
177                            .replace("__RESTART_WITH_MSG__", "")
178                            .trim()
179                            .to_string();
180
181                        if !feedback_msg.is_empty() {
182                            self.message_display.add_message(feedback_msg);
183                            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
184                        }
185
186                        if let Err(e) = self.perform_restart().await {
187                            self.message_display
188                                .add_message(format!("Restart failed: {}", e));
189                        }
190                    } else if new_input.starts_with("__RESTART_FORCE__")
191                        || new_input == "__RESTART__"
192                    {
193                        if let Err(e) = self.perform_restart().await {
194                            self.message_display
195                                .add_message(format!("Restart failed: {}", e));
196                        }
197                    }
198                }
199            }
200            KeyAction::Quit => return Ok(true),
201            _ => {
202                if let Some(new_input) = self.input_state.handle_input(key) {
203                    if let Some(processed) = LanguageService::process_save_message(&new_input).await
204                    {
205                        self.message_display.add_message(processed);
206                        return Ok(false);
207                    }
208
209                    if let Some(processed) = self.process_live_theme_update(&new_input).await {
210                        self.message_display.add_message(processed);
211                        return Ok(false);
212                    }
213
214                    self.message_display.add_message(new_input);
215                }
216            }
217        }
218        Ok(false)
219    }
220
221    async fn process_live_theme_update(&mut self, message: &str) -> Option<String> {
222        if !message.starts_with("__LIVE_THEME_UPDATE__") {
223            return None;
224        }
225
226        let parts: Vec<&str> = message.split("__MESSAGE__").collect();
227        if parts.len() != 2 {
228            log::error!("Invalid live theme update format: {}", message);
229            return None;
230        }
231
232        let theme_part = parts[0].replace("__LIVE_THEME_UPDATE__", "");
233        let display_message = parts[1];
234
235        log::debug!("🎨 Processing live theme update: {}", theme_part);
236
237        let theme_system = match ThemeSystem::load() {
238            Ok(system) => system,
239            Err(e) => {
240                log::error!("Failed to load theme system: {}", e);
241                return Some(format!("❌ Theme update failed: {}", e));
242            }
243        };
244
245        if let Some(theme_def) = theme_system.get_theme(&theme_part) {
246            match self.create_theme_from_definition(theme_def) {
247                Ok(new_theme) => {
248                    let backup = self.input_state.get_backup_data().unwrap_or_default();
249
250                    self.config.theme = new_theme;
251                    self.config.current_theme_name = theme_part.clone();
252
253                    // ✅ KORRIGIERT: InputState::new nimmt nur &config
254                    self.input_state = Box::new(InputState::new(&self.config));
255                    self.input_state.restore_backup_data(backup.clone());
256
257                    self.message_display.update_config(&self.config);
258
259                    log::info!(
260                        "✅ Live theme '{}' applied with prompt '{}' - {} history entries preserved!",
261                        theme_part.to_uppercase(),
262                        self.config.theme.prompt_text,
263                        backup.history.len()
264                    );
265                    Some(display_message.to_string())
266                }
267                Err(e) => {
268                    log::error!("❌ Failed to create theme: {}", e);
269                    Some(format!("❌ Theme update failed: {}", e))
270                }
271            }
272        } else {
273            log::error!("❌ Theme '{}' not found in TOML", theme_part);
274            Some(format!("❌ Theme '{}' not found in config", theme_part))
275        }
276    }
277
278    fn create_theme_from_definition(
279        &self,
280        theme_def: &crate::commands::theme::ThemeDefinition,
281    ) -> Result<crate::core::config::Theme> {
282        use crate::ui::color::AppColor;
283
284        Ok(crate::core::config::Theme {
285            input_text: AppColor::from_string(&theme_def.input_text)?,
286            input_bg: AppColor::from_string(&theme_def.input_bg)?,
287            cursor: AppColor::from_string(&theme_def.cursor)?,
288            output_text: AppColor::from_string(&theme_def.output_text)?,
289            output_bg: AppColor::from_string(&theme_def.output_bg)?,
290            prompt_text: theme_def.prompt_text.clone(),
291            prompt_color: AppColor::from_string(&theme_def.prompt_color)?,
292        })
293    }
294
295    async fn handle_resize_event(&mut self, width: u16, height: u16) -> Result<()> {
296        eprintln!(
297            "🔄 RESIZE EVENT: {}x{} → {}x{}",
298            self.terminal_size.0, self.terminal_size.1, width, height
299        );
300
301        self.terminal_size = (width, height);
302        let window_height = self.get_content_height();
303        let content_height = self.message_display.get_content_height();
304
305        self.message_display
306            .scroll_state
307            .update_dimensions(window_height, content_height);
308
309        eprintln!(
310            "   Window height: {}, Content height: {}",
311            window_height, content_height
312        );
313        Ok(())
314    }
315
316    async fn handle_tick_event(&mut self) -> Result<()> {
317        self.message_display.update_typewriter();
318        if let Some(input_state) = self.input_state.as_input_state() {
319            input_state.update_cursor_blink();
320        }
321        Ok(())
322    }
323
324    fn get_content_height(&self) -> usize {
325        // Berechne verfügbare Höhe für Output-Bereich
326        let total_height = self.terminal_size.1 as usize;
327        let margin = 2; // top + bottom margin
328        let input_area = 3; // Input braucht 3 Zeilen
329
330        total_height
331            .saturating_sub(margin)
332            .saturating_sub(input_area)
333    }
334
335    async fn process_pending_logs(&mut self) {
336        match AppLogger::get_messages() {
337            Ok(messages) => {
338                for log_msg in messages {
339                    self.message_display.add_message(log_msg.formatted());
340                }
341            }
342            Err(e) => {
343                self.message_display.add_message(
344                    AppColor::from_any("error")
345                        .format_message("ERROR", &format!("Logging-Fehler: {:?}", e)),
346                );
347            }
348        }
349    }
350
351    async fn render(&mut self) -> Result<()> {
352        self.terminal.draw(|frame| {
353            let size = frame.size();
354
355            if size.width < 30 || size.height < 8 {
356                return;
357            }
358
359            // ✅ KORRIGIERT: Berücksichtige margin in Berechnung
360            let total_available = size.height.saturating_sub(2); // margin(1) = top+bottom = 2
361            let input_needs = 3; // Input braucht minimal 3 Zeilen
362            let output_gets = total_available.saturating_sub(input_needs);
363
364            // ✅ SICHERE LAYOUT-CONSTRAINTS - mathematisch korrekt
365            let chunks = Layout::default()
366                .direction(Direction::Vertical)
367                .margin(1)
368                .constraints([
369                    Constraint::Length(output_gets), // ✅ Output: Berechnet
370                    Constraint::Length(input_needs), // ✅ Input: Fest 3 Zeilen
371                ])
372                .split(size);
373
374            let output_chunk = chunks[0];
375            let input_chunk = chunks[1];
376
377            // ✅ DEBUG: Prüfe Layout-Math
378            let total_used = output_chunk.height + input_chunk.height + 2; // +2 für margin
379            if total_used != size.height {
380                // Verwende dein Log-System statt eprintln!
381                log::warn!(
382                    "⚠️ Layout-Math ERROR: terminal={}, used={}, output={}, input={}",
383                    size.height,
384                    total_used,
385                    output_chunk.height,
386                    input_chunk.height
387                );
388            }
389
390            // ✅ UPDATE Scroll-Dimensionen mit korrekter Output-Höhe
391            let total_lines = self.message_display.get_content_height();
392            self.message_display
393                .scroll_state
394                .update_dimensions(output_chunk.height as usize, total_lines);
395
396            // ✅ RENDER
397            let (messages_data, config) = self
398                .message_display
399                .create_output_widget_for_rendering(output_chunk.height);
400            let messages_refs: Vec<(&String, usize)> =
401                messages_data.iter().map(|(s, l)| (s, *l)).collect();
402
403            let output_widget = crate::output::display::create_output_widget(
404                &messages_refs,
405                output_chunk.height,
406                &config,
407            );
408            frame.render_widget(output_widget, output_chunk);
409
410            let input_widget = self.input_state.render();
411            frame.render_widget(input_widget, input_chunk);
412        })?;
413        Ok(())
414    }
415
416    async fn perform_restart(&mut self) -> Result<()> {
417        self.terminal_mgr.cleanup().await?;
418        self.terminal_mgr = TerminalManager::new().await?;
419        self.terminal_mgr.setup().await?;
420
421        let backend = CrosstermBackend::new(io::stdout());
422        self.terminal = Terminal::new(backend)?;
423
424        self.message_display.clear_messages();
425        // ✅ KORRIGIERT: InputState::new nimmt nur &config
426        self.input_state = Box::new(InputState::new(&self.config));
427        self.waiting_for_restart_confirmation = false;
428
429        self.message_display
430            .add_message(crate::i18n::get_command_translation(
431                "system.commands.restart.success",
432                &[],
433            ));
434        log::info!("Internal restart completed successfully");
435        Ok(())
436    }
437}