rush_sync_server/ui/
screen.rs

1// ## FILE: src/ui/screen.rs - KOMPRIMIERTE VERSION
2use crate::commands::{history::HistoryKeyboardHandler, lang::LanguageService, theme::ThemeSystem};
3use crate::core::prelude::*;
4use crate::input::{
5    keyboard::{KeyAction, KeyboardManager},
6    state::InputState,
7    AppEvent, EventHandler,
8};
9use crate::output::display::MessageDisplay;
10use crate::ui::{
11    color::AppColor,
12    terminal::TerminalManager,
13    viewport::ScrollDirection,
14    widget::{AnimatedWidget, CursorWidget, StatefulWidget, Widget},
15};
16use crossterm::{event::KeyEvent, execute};
17use ratatui::{backend::CrosstermBackend, Terminal};
18use std::{
19    io::{self, Stdout},
20    sync::OnceLock,
21};
22
23pub type TerminalBackend = Terminal<CrosstermBackend<Stdout>>;
24
25pub struct ScreenManager {
26    terminal: TerminalBackend,
27    pub message_display: MessageDisplay,
28    input_state: InputState,
29    config: Config,
30    terminal_mgr: TerminalManager,
31    events: EventHandler,
32    keyboard_manager: KeyboardManager,
33    waiting_for_restart_confirmation: bool,
34}
35
36#[derive(Clone)]
37struct TerminalInfo {
38    term_program: String,
39    tmux: bool,
40}
41static TERMINAL_INFO: OnceLock<TerminalInfo> = OnceLock::new();
42
43impl ScreenManager {
44    pub async fn new(config: &Config) -> Result<Self> {
45        let mut terminal_mgr = TerminalManager::new().await?;
46        terminal_mgr.setup().await?;
47
48        let backend = CrosstermBackend::new(io::stdout());
49        let terminal = Terminal::new(backend)?;
50        let size = terminal.size()?;
51
52        let mut screen_manager = Self {
53            terminal,
54            terminal_mgr,
55            message_display: MessageDisplay::new(config, size.width, size.height),
56            input_state: InputState::new(config),
57            config: config.clone(),
58            events: EventHandler::new(config.poll_rate),
59            keyboard_manager: KeyboardManager::new(),
60            waiting_for_restart_confirmation: false,
61        };
62
63        let version = crate::core::constants::VERSION;
64        let startup_msg = get_command_translation("system.startup.version", &[version]);
65        screen_manager
66            .message_display
67            .add_message_instant(startup_msg);
68
69        // ✅ FERTIG!
70        Ok(screen_manager)
71    }
72
73    pub async fn run(&mut self) -> Result<()> {
74        let result = loop {
75            if let Some(event) = self.events.next().await {
76                match event {
77                    AppEvent::Input(key) => {
78                        if self.handle_input(key).await? {
79                            self.events.shutdown().await;
80                            break Ok(());
81                        }
82                    }
83                    AppEvent::Resize(w, h) => self.handle_resize(w, h).await?,
84                    AppEvent::Tick => self.handle_tick().await?,
85                }
86            }
87            self.render().await?;
88        };
89        self.terminal_mgr.cleanup().await?;
90        result
91    }
92
93    // ✅ KOMPRIMIERTES INPUT HANDLING
94    async fn handle_input(&mut self, key: KeyEvent) -> Result<bool> {
95        // History handling
96        if HistoryKeyboardHandler::get_history_action(&key).is_some() {
97            if let Some(input) = self.input_state.handle_input(key) {
98                self.process_special_input(&input).await;
99            }
100            return Ok(false);
101        }
102
103        // Scroll/Action handling
104        match self.keyboard_manager.get_action(&key) {
105            KeyAction::ScrollUp => {
106                self.message_display.handle_scroll(ScrollDirection::Up, 1);
107                Ok(false)
108            }
109            KeyAction::ScrollDown => {
110                self.message_display.handle_scroll(ScrollDirection::Down, 1);
111                Ok(false)
112            }
113            KeyAction::PageUp => {
114                self.message_display
115                    .handle_scroll(ScrollDirection::PageUp, 0);
116                Ok(false)
117            }
118            KeyAction::PageDown => {
119                self.message_display
120                    .handle_scroll(ScrollDirection::PageDown, 0);
121                Ok(false)
122            }
123            KeyAction::Submit => self.handle_submit(key).await,
124            KeyAction::Quit => Ok(true),
125            _ => {
126                if let Some(input) = self.input_state.handle_input(key) {
127                    self.process_special_input(&input).await;
128                }
129                Ok(false)
130            }
131        }
132    }
133
134    async fn handle_submit(&mut self, key: KeyEvent) -> Result<bool> {
135        let Some(input) = self.input_state.handle_input(key) else {
136            return Ok(false);
137        };
138
139        // 🎯 KRITISCHER FIX: SYSTEM-COMMANDS ZUERST PRÜFEN UND AUSFÜHREN!
140        //    (BEVOR wir sie als Text ausgeben!)
141
142        if input == "__CLEAR__" {
143            self.message_display.clear_messages();
144            return Ok(false);
145        }
146
147        if input == "__EXIT__" {
148            return Ok(true); // ✅ BEENDE PROGRAMM SOFORT!
149        }
150
151        if input.starts_with("__RESTART") {
152            self.handle_restart(&input).await;
153            return Ok(false);
154        }
155
156        // ✅ Process special messages (Theme, Language updates)
157        if self.process_special_input(&input).await {
158            return Ok(false);
159        }
160
161        // ✅ Add message to display (NUR wenn es KEIN System-Command war)
162        let cmd = input.trim().to_lowercase();
163        if input.starts_with("__")
164            || ["theme", "help", "lang"]
165                .iter()
166                .any(|&c| cmd.starts_with(c))
167        {
168            self.message_display.add_message_instant(input.clone());
169        } else {
170            self.message_display.add_message(input.clone());
171        }
172
173        Ok(false)
174    }
175
176    // ✅ VEREINFACHTE SPECIAL INPUT PROCESSING
177    async fn process_special_input(&mut self, input: &str) -> bool {
178        // Language updates
179        if let Some(processed) = LanguageService::process_save_message(input).await {
180            self.message_display.add_message_instant(processed);
181            return true;
182        }
183
184        // Theme updates
185        if let Some(processed) = self.process_theme_update(input).await {
186            self.message_display.add_message_instant(processed);
187            return true;
188        }
189
190        false
191    }
192
193    // ✅ KOMPRIMIERTES THEME UPDATE
194    async fn process_theme_update(&mut self, message: &str) -> Option<String> {
195        if !message.starts_with("__LIVE_THEME_UPDATE__") {
196            return None;
197        }
198
199        let parts: Vec<&str> = message.split("__MESSAGE__").collect();
200        if parts.len() != 2 {
201            return None;
202        }
203
204        let theme_name = parts[0].replace("__LIVE_THEME_UPDATE__", "");
205        let display_msg = parts[1];
206
207        // Load and apply theme
208        let theme_system = ThemeSystem::load().ok()?;
209        let theme_def = theme_system.get_theme(&theme_name)?;
210        let new_theme = self.create_theme(theme_def).ok()?;
211
212        // Backup state, update config, restore state
213        let backup = self.input_state.export_state();
214        self.config.theme = new_theme;
215        self.config.current_theme_name = theme_name;
216
217        self.message_display.clear_messages();
218        self.message_display.update_config(&self.config);
219
220        self.input_state = InputState::new(&self.config);
221        self.input_state.import_state(backup);
222
223        Some(display_msg.to_string())
224    }
225
226    fn create_theme(
227        &self,
228        def: &crate::commands::theme::ThemeDefinition,
229    ) -> Result<crate::core::config::Theme> {
230        Ok(crate::core::config::Theme {
231            input_text: AppColor::from_string(&def.input_text)?,
232            input_bg: AppColor::from_string(&def.input_bg)?,
233            output_text: AppColor::from_string(&def.output_text)?,
234            output_bg: AppColor::from_string(&def.output_bg)?,
235            input_cursor_prefix: def.input_cursor_prefix.clone(),
236            input_cursor_color: AppColor::from_string(&def.input_cursor_color)?,
237            input_cursor: def.input_cursor.clone(),
238            output_cursor: def.output_cursor.clone(),
239            output_cursor_color: AppColor::from_string(&def.output_cursor_color)?,
240        })
241    }
242
243    async fn handle_restart(&mut self, input: &str) {
244        if input.starts_with("__RESTART_WITH_MSG__") {
245            let msg = input.replace("__RESTART_WITH_MSG__", "").trim().to_string();
246            if !msg.is_empty() {
247                self.message_display.add_message_instant(msg);
248                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
249            }
250        }
251
252        if let Err(e) = self.perform_restart().await {
253            self.message_display
254                .add_message_instant(get_translation("screen.restart.failed", &[&e.to_string()]));
255        }
256    }
257
258    async fn handle_resize(&mut self, width: u16, height: u16) -> Result<()> {
259        self.message_display.handle_resize(width, height);
260        Ok(())
261    }
262
263    async fn handle_tick(&mut self) -> Result<()> {
264        self.message_display.update_typewriter();
265        self.input_state.tick();
266        Ok(())
267    }
268
269    // ✅ KOMPRIMIERTES RENDERING
270    async fn render(&mut self) -> Result<()> {
271        let (input_widget, cursor_pos) = self.input_state.render_with_cursor();
272
273        let viewport_ok = self.message_display.viewport().is_usable();
274        let output_area = self.message_display.viewport().output_area();
275        let input_area = self.message_display.viewport().input_area();
276
277        let (messages, config, layout, cursor_state) =
278            self.message_display.create_output_widget_for_rendering();
279
280        self.terminal.draw(|frame| {
281            let size = frame.size();
282
283            // Emergency cases with i18n
284            if size.width < 10 || size.height < 5 {
285                let widget = ratatui::widgets::Paragraph::new(get_translation(
286                    "screen.render.terminal_too_small",
287                    &[],
288                ))
289                .block(ratatui::widgets::Block::default());
290                frame.render_widget(widget, size);
291                return;
292            }
293
294            if !viewport_ok || !output_area.is_valid() || !input_area.is_valid() {
295                let widget = ratatui::widgets::Paragraph::new(get_translation(
296                    "screen.render.viewport_error",
297                    &[],
298                ))
299                .block(ratatui::widgets::Block::default());
300                frame.render_widget(widget, size);
301                return;
302            }
303
304            // Check bounds
305            if Self::exceeds_bounds(&output_area, &input_area, size) {
306                return;
307            }
308
309            // Render normally
310            let output_widget = crate::output::display::create_output_widget(
311                &messages,
312                layout,
313                &config,
314                cursor_state,
315            );
316
317            frame.render_widget(output_widget, output_area.as_rect());
318            frame.render_widget(input_widget, input_area.as_rect());
319
320            if let Some((x, y)) = cursor_pos {
321                frame.set_cursor(input_area.x + 3 + x, input_area.y + 1 + y);
322            }
323        })?;
324
325        // Cursor styling (unchanged)
326        if cursor_pos.is_some() {
327            self.apply_cursor_styling()?;
328        } else {
329            execute!(std::io::stdout(), crossterm::style::Print("\x1B[?25l"))?;
330        }
331        Ok(())
332    }
333
334    // ✅ UTILITY METHODS
335    fn exceeds_bounds(
336        output: &crate::ui::viewport::LayoutArea,
337        input: &crate::ui::viewport::LayoutArea,
338        size: ratatui::layout::Rect,
339    ) -> bool {
340        output.x + output.width > size.width
341            || output.y + output.height > size.height
342            || input.x + input.width > size.width
343            || input.y + input.height > size.height
344    }
345
346    // ✅ CURSOR STYLING (komprimiert)
347    fn apply_cursor_styling(&self) -> Result<()> {
348        let form = match self.config.theme.input_cursor.to_uppercase().as_str() {
349            "PIPE" => "\x1B[6 q",
350            "UNDERSCORE" => "\x1B[4 q",
351            "BLOCK" => "\x1B[2 q",
352            _ => "\x1B[6 q",
353        };
354
355        let color_cmds = self.get_cursor_colors(&self.config.theme.input_cursor_color);
356
357        execute!(std::io::stdout(), crossterm::style::Print(form))?;
358        for cmd in color_cmds {
359            execute!(std::io::stdout(), crossterm::style::Print(cmd))?;
360        }
361        execute!(std::io::stdout(), crossterm::style::Print("\x1B[?25h"))?;
362        Ok(())
363    }
364
365    fn get_cursor_colors(&self, color: &AppColor) -> Vec<String> {
366        let (r, g, b) = self.get_rgb(color);
367        let info = Self::terminal_info();
368
369        if info.tmux {
370            return vec![format!(
371                "\x1BPtmux;\x1B\x1B]12;#{:02x}{:02x}{:02x}\x07\x1B\\",
372                r, g, b
373            )];
374        }
375
376        let base = format!("\x1B]12;#{:02x}{:02x}{:02x}\x07", r, g, b);
377        match info.term_program.as_str() {
378            "Apple_Terminal" => vec![base],
379            p if p.starts_with("iTerm") => {
380                vec![format!("\x1B]Pl{:02x}{:02x}{:02x}\x1B\\", r, g, b), base]
381            }
382            _ => vec![base],
383        }
384    }
385
386    fn get_rgb(&self, color: &AppColor) -> (u8, u8, u8) {
387        match color.to_name() {
388            "black" => (0, 0, 0),
389            "red" => (255, 0, 0),
390            "green" => (0, 255, 0),
391            "yellow" => (255, 255, 0),
392            "blue" => (0, 0, 255),
393            "magenta" => (255, 0, 255),
394            "cyan" => (0, 255, 255),
395            "white" => (255, 255, 255),
396            "gray" => (128, 128, 128),
397            "darkgray" => (64, 64, 64),
398            _ => (255, 255, 255),
399        }
400    }
401
402    fn terminal_info() -> &'static TerminalInfo {
403        TERMINAL_INFO.get_or_init(|| TerminalInfo {
404            term_program: std::env::var("TERM_PROGRAM").unwrap_or_default(),
405            tmux: std::env::var("TMUX").is_ok(),
406        })
407    }
408
409    async fn perform_restart(&mut self) -> Result<()> {
410        execute!(
411            std::io::stdout(),
412            crossterm::style::Print("\x1B[0 q"),
413            crossterm::style::Print("\x1B[?25h")
414        )?;
415
416        self.terminal_mgr.cleanup().await?;
417        self.terminal_mgr = TerminalManager::new().await?;
418        self.terminal_mgr.setup().await?;
419
420        let backend = CrosstermBackend::new(io::stdout());
421        self.terminal = Terminal::new(backend)?;
422        let size = self.terminal.size()?;
423
424        self.message_display = MessageDisplay::new(&self.config, size.width, size.height);
425        self.input_state = InputState::new(&self.config);
426        self.waiting_for_restart_confirmation = false;
427
428        self.message_display
429            .add_message(get_translation("screen.restart.success", &[]));
430        Ok(())
431    }
432
433    // ✅ PUBLIC API METHODS
434    pub async fn switch_theme_safely(&mut self, theme_name: &str) -> Result<String> {
435        let system = ThemeSystem::load().map_err(|e| {
436            AppError::Validation(get_translation(
437                "screen.theme.load_failed",
438                &[&e.to_string()],
439            ))
440        })?;
441
442        let def = system.get_theme(theme_name).ok_or_else(|| {
443            AppError::Validation(get_translation("screen.theme.not_found", &[theme_name]))
444        })?;
445
446        let theme = self.create_theme(def)?;
447        let backup = self.input_state.export_state();
448
449        self.config.theme = theme;
450        self.config.current_theme_name = theme_name.to_string();
451        self.message_display.update_config(&self.config);
452
453        self.input_state = InputState::new(&self.config);
454        self.input_state.import_state(backup);
455
456        Ok(get_translation(
457            "screen.theme.switched_success",
458            &[&theme_name.to_uppercase()],
459        ))
460    }
461
462    // ✅ I18N VALIDATION (komprimiert)
463    pub fn validate_i18n_keys() -> Vec<String> {
464        [
465            "screen.theme.failed",
466            "screen.render.too_small.text",
467            "screen.render.viewport_error.text",
468            "system.commands.restart.success",
469        ]
470        .iter()
471        .filter(|&&key| !crate::i18n::has_translation(key))
472        .map(|&key| key.to_string())
473        .collect()
474    }
475}