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 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 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 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 if self.process_special_input(&input).await {
154 return Ok(false);
155 }
156
157 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 if let Some(processed) = LanguageService::process_save_message(input).await {
175 self.message_display.add_message_instant(processed);
176 return true;
177 }
178
179 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 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 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 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 if Self::exceeds_bounds(&output_area, &input_area, size) {
301 return;
302 }
303
304 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 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 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}