Skip to main content

rush_sync_server/ui/
screen.rs

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