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