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
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 async fn handle_input(&mut self, key: KeyEvent) -> Result<bool> {
86 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 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 if self.process_special_input(&input).await {
132 return Ok(false);
133 }
134
135 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 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 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 async fn process_special_input(&mut self, input: &str) -> bool {
167 if let Some(processed) = LanguageService::process_save_message(input).await {
169 self.message_display.add_message_instant(processed);
170 return true;
171 }
172
173 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 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 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 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 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 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 if Self::exceeds_bounds(&output_area, &input_area, size) {
295 return;
296 }
297
298 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 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 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 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 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 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}