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