1use crate::commands::history::HistoryKeyboardHandler;
2use crate::commands::lang::LanguageService;
3use crate::commands::theme::ThemeSystem;
4use crate::core::prelude::*;
5use crate::input::{
6 keyboard::{KeyAction, KeyboardManager},
7 state::InputState,
8};
9use crate::input::{AppEvent, EventHandler};
10use crate::output::display::MessageDisplay;
11use crate::ui::{
12 color::AppColor, terminal::TerminalManager, viewport::ScrollDirection, widget::Widget,
13};
14
15use crossterm::event::KeyEvent;
16use ratatui::{backend::CrosstermBackend, Terminal};
17use std::io::{self, Stdout};
18use std::sync::OnceLock;
19
20pub type TerminalBackend = Terminal<CrosstermBackend<Stdout>>;
21
22use crossterm::execute;
23
24pub struct ScreenManager {
25 terminal: TerminalBackend,
26 pub message_display: MessageDisplay,
27 input_state: Box<dyn Widget>,
28 config: Config,
29 terminal_mgr: TerminalManager,
30 events: EventHandler,
31 keyboard_manager: KeyboardManager,
32 waiting_for_restart_confirmation: bool,
33}
34
35#[derive(Clone)]
36struct TerminalInfo {
37 term_program: String,
38 tmux: bool,
39}
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 message_display = MessageDisplay::new(config, size.width, size.height);
53 let owned_config = config.clone();
54
55 Ok(Self {
56 terminal,
57 terminal_mgr,
58 message_display,
59 input_state: Box::new(InputState::new(config)),
60 config: owned_config,
61 events: EventHandler::new(config.poll_rate),
62 keyboard_manager: KeyboardManager::new(),
63 waiting_for_restart_confirmation: false,
64 })
65 }
66
67 pub async fn run(&mut self) -> Result<()> {
68 let result = loop {
69 if let Some(event) = self.events.next().await {
70 match event {
71 AppEvent::Input(key) => {
72 if self.handle_input_event(key).await? {
73 self.events.shutdown().await;
74 break Ok(());
75 }
76 }
77 AppEvent::Resize(width, height) => {
78 self.handle_resize_event(width, height).await?;
79 }
80 AppEvent::Tick => {
81 self.handle_tick_event().await?;
82 }
83 }
84 }
85
86 self.render().await?;
87 };
88
89 self.terminal_mgr.cleanup().await?;
90 result
91 }
92
93 async fn handle_input_event(&mut self, key: KeyEvent) -> Result<bool> {
94 if HistoryKeyboardHandler::get_history_action(&key).is_some() {
96 if let Some(new_input) = self.input_state.handle_input(key) {
97 if let Some(processed) = LanguageService::process_save_message(&new_input).await {
98 self.message_display.add_message(processed);
99 return Ok(false);
100 }
101
102 if let Some(processed) = self.process_live_theme_update(&new_input).await {
103 self.message_display.add_message_instant(processed); return Ok(false);
105 }
106
107 self.message_display.add_message_instant(new_input.clone());
108
109 if new_input.starts_with("__CLEAR__") {
110 self.message_display.clear_messages();
111 } else if new_input.starts_with("__EXIT__") {
112 return Ok(true);
113 }
114 }
115 return Ok(false);
116 }
117
118 match self.keyboard_manager.get_action(&key) {
120 KeyAction::ScrollUp => {
121 log::info!("🔼 SCROLL UP detected!");
122 log::info!(
123 " Before: offset={}, content={}, window={}",
124 self.message_display.viewport().scroll_offset(),
125 self.message_display.viewport().content_height(),
126 self.message_display.viewport().window_height()
127 );
128
129 self.message_display.handle_scroll(ScrollDirection::Up, 1);
130
131 log::info!(
132 " After: offset={}, auto_scroll={}",
133 self.message_display.viewport().scroll_offset(),
134 self.message_display.viewport().is_auto_scroll_enabled()
135 );
136 return Ok(false);
137 }
138 KeyAction::ScrollDown => {
139 log::info!("🔽 SCROLL DOWN detected!");
140 log::info!(
141 " Before: offset={}, content={}, window={}",
142 self.message_display.viewport().scroll_offset(),
143 self.message_display.viewport().content_height(),
144 self.message_display.viewport().window_height()
145 );
146
147 self.message_display.handle_scroll(ScrollDirection::Down, 1);
148
149 log::info!(
150 " After: offset={}, auto_scroll={}",
151 self.message_display.viewport().scroll_offset(),
152 self.message_display.viewport().is_auto_scroll_enabled()
153 );
154 return Ok(false);
155 }
156 KeyAction::PageUp => {
157 log::info!("📄 PAGE UP detected!");
158 self.message_display
159 .handle_scroll(ScrollDirection::PageUp, 0);
160 return Ok(false);
161 }
162 KeyAction::PageDown => {
163 log::info!("📄 PAGE DOWN detected!");
164 self.message_display
165 .handle_scroll(ScrollDirection::PageDown, 0);
166 return Ok(false);
167 }
168 KeyAction::Submit => {
169 log::info!("🖥️ SCREEN: About to call input_state.handle_input()");
170
171 if let Some(new_input) = self.input_state.handle_input(key) {
172 log::info!("🖥️ SCREEN: input_state returned {} chars", new_input.len());
173
174 let input_command = new_input.trim().to_lowercase();
175 let is_performance_command = input_command == "perf"
176 || input_command == "performance"
177 || input_command == "stats";
178
179 if let Some(processed) = LanguageService::process_save_message(&new_input).await
180 {
181 log::info!("🖥️ SCREEN: LanguageService processed message");
182 self.message_display.add_message_instant(processed); return Ok(false);
184 }
185
186 if let Some(processed) = self.process_live_theme_update(&new_input).await {
187 log::info!("🖥️ SCREEN: ThemeUpdate processed message");
188 self.message_display.add_message_instant(processed); return Ok(false);
190 }
191
192 log::info!("🖥️ SCREEN: Adding normal message to display");
193
194 if new_input.starts_with("__") {
196 self.message_display.add_message_instant(new_input.clone());
198 } else if input_command.starts_with("theme")
199 || input_command.starts_with("test")
200 || input_command.starts_with("help")
201 || input_command.starts_with("lang")
202 {
203 self.message_display.add_message_instant(new_input.clone());
205 } else {
206 self.message_display.add_message(new_input.clone());
208 }
209
210 if is_performance_command {
211 log::info!("🖥️ SCREEN: Performance command detected, forcing auto-scroll");
212 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
213 self.message_display.viewport_mut().force_auto_scroll();
214 }
215
216 if new_input.starts_with("__CLEAR__") {
218 log::info!("🖥️ SCREEN: Clearing messages");
219 self.message_display.clear_messages();
220 } else if new_input.starts_with("__EXIT__") {
221 log::info!("🖥️ SCREEN: Exit requested");
222 return Ok(true);
223 } else if new_input.starts_with("__RESTART_WITH_MSG__") {
224 log::info!("🖥️ SCREEN: Restart with message requested");
225 let feedback_msg = new_input
226 .replace("__RESTART_WITH_MSG__", "")
227 .trim()
228 .to_string();
229
230 if !feedback_msg.is_empty() {
231 self.message_display.add_message_instant(feedback_msg); tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
233 }
234
235 if let Err(e) = self.perform_restart().await {
236 self.message_display
237 .add_message_instant(format!("Restart failed: {}", e));
238 }
240 } else if new_input.starts_with("__RESTART_FORCE__")
241 || new_input == "__RESTART__"
242 {
243 log::info!("🖥️ SCREEN: Restart requested");
244 if let Err(e) = self.perform_restart().await {
245 self.message_display
246 .add_message_instant(format!("Restart failed: {}", e));
247 }
249 }
250 } else {
251 log::info!("🖥️ SCREEN: input_state.handle_input() returned None");
252 }
253 }
254 KeyAction::Quit => return Ok(true),
255 _ => {
256 if let Some(new_input) = self.input_state.handle_input(key) {
257 if let Some(processed) = LanguageService::process_save_message(&new_input).await
258 {
259 self.message_display.add_message_instant(processed); return Ok(false);
261 }
262
263 if let Some(processed) = self.process_live_theme_update(&new_input).await {
264 self.message_display.add_message_instant(processed); return Ok(false);
266 }
267
268 self.message_display.add_message_instant(new_input); }
270 }
271 }
272 Ok(false)
273 }
274
275 async fn process_live_theme_update(&mut self, message: &str) -> Option<String> {
277 if !message.starts_with("__LIVE_THEME_UPDATE__") {
278 return None;
279 }
280
281 let parts: Vec<&str> = message.split("__MESSAGE__").collect();
282 if parts.len() != 2 {
283 log::error!("{}", t!("screen.theme.invalid_format"));
284 return None;
285 }
286
287 let theme_part = parts[0].replace("__LIVE_THEME_UPDATE__", "");
288 let display_message = parts[1];
289
290 log::info!(
291 "🎨 LIVE THEME UPDATE STARTING: '{}' → '{}'",
292 self.config.current_theme_name,
293 theme_part
294 );
295
296 let theme_system = match ThemeSystem::load() {
297 Ok(system) => system,
298 Err(e) => {
299 log::error!("{} {}", t!("screen.theme.load_failed"), e);
300 return Some(tc!("screen.theme.failed", &e.to_string()));
301 }
302 };
303
304 if let Some(theme_def) = theme_system.get_theme(&theme_part) {
305 log::info!(
307 "📋 THEME DEFINITION LOADED:\n \
308 input_cursor_prefix: '{}'\n \
309 input_cursor_color: '{}'\n \
310 input_cursor: '{}'\n \
311 output_cursor: '{}'\n \
312 output_cursor_color: '{}'",
313 theme_def.input_cursor_prefix,
314 theme_def.input_cursor_color,
315 theme_def.input_cursor,
316 theme_def.output_cursor,
317 theme_def.output_cursor_color
318 );
319
320 match self.create_theme_from_definition(theme_def) {
321 Ok(new_theme) => {
322 let backup = self.input_state.get_backup_data().unwrap_or_default();
323
324 log::info!(
326 "🔄 THEME CONVERSION COMPLETE:\n \
327 OLD Config: input_cursor='{}', input_cursor_color='{}'\n \
328 NEW Config: input_cursor='{}', input_cursor_color='{}'",
329 self.config.theme.input_cursor,
330 self.config.theme.input_cursor_color.to_name(),
331 new_theme.input_cursor,
332 new_theme.input_cursor_color.to_name()
333 );
334
335 self.message_display.clear_messages();
337
338 self.config.theme = new_theme;
340 self.config.current_theme_name = theme_part.clone();
341
342 self.message_display.update_config(&self.config);
344
345 log::info!("🔄 RECREATING InputState with central cursor API...");
346 self.input_state = Box::new(InputState::new(&self.config));
347
348 if let Some(_input_widget) = self.input_state.as_input_state() {
350 log::info!(
351 "✅ INPUT-CURSOR CREATED:\n \
352 Expected: cursor='{}' (color: {})\n \
353 Theme config: prefix='{}' (color: {})",
354 self.config.theme.input_cursor,
355 self.config.theme.input_cursor_color.to_name(),
356 self.config.theme.input_cursor_prefix,
357 self.config.theme.input_cursor_color.to_name()
358 );
359 }
360
361 self.input_state.restore_backup_data(backup.clone());
362
363 log::info!(
365 "✅ LIVE THEME APPLIED SUCCESSFULLY:\n \
366 theme='{}'\n \
367 prefix='{}'\n \
368 input_cursor='{}'\n \
369 input_cursor_color='{}'\n \
370 output_cursor='{}'\n \
371 output_cursor_color='{}'\n \
372 history={} entries",
373 theme_part.to_uppercase(),
374 self.config.theme.input_cursor_prefix,
375 self.config.theme.input_cursor,
376 self.config.theme.input_cursor_color.to_name(),
377 self.config.theme.output_cursor,
378 self.config.theme.output_cursor_color.to_name(),
379 backup.history.len()
380 );
381
382 Some(display_message.to_string())
383 }
384 Err(e) => {
385 log::error!("{} {}", t!("screen.theme.load_failed"), e);
386 Some(tc!("screen.theme.failed", &e.to_string()))
387 }
388 }
389 } else {
390 log::error!("{} {}", t!("screen.theme.not_found"), theme_part);
391 Some(tc!("screen.theme.not_found_feedback", theme_part.as_str()))
392 }
393 }
394
395 fn create_theme_from_definition(
397 &self,
398 theme_def: &crate::commands::theme::ThemeDefinition,
399 ) -> Result<crate::core::config::Theme> {
400 use crate::ui::color::AppColor;
401
402 let input_cursor_color = AppColor::from_string(&theme_def.input_cursor_color)?;
403 let output_cursor_color = AppColor::from_string(&theme_def.output_cursor_color)?;
404
405 Ok(crate::core::config::Theme {
406 input_text: AppColor::from_string(&theme_def.input_text)?,
407 input_bg: AppColor::from_string(&theme_def.input_bg)?,
408 output_text: AppColor::from_string(&theme_def.output_text)?,
409 output_bg: AppColor::from_string(&theme_def.output_bg)?,
410
411 input_cursor_prefix: theme_def.input_cursor_prefix.clone(),
413 input_cursor_color,
414 input_cursor: theme_def.input_cursor.clone(),
415 output_cursor: theme_def.output_cursor.clone(),
416 output_cursor_color,
417 })
418 }
419
420 async fn handle_resize_event(&mut self, width: u16, height: u16) -> Result<()> {
421 log::info!(
422 "{}",
423 t!(
424 "screen.resize_event",
425 &self
426 .message_display
427 .viewport()
428 .terminal_size()
429 .0
430 .to_string(),
431 &self
432 .message_display
433 .viewport()
434 .terminal_size()
435 .1
436 .to_string(),
437 &width.to_string(),
438 &height.to_string()
439 )
440 );
441
442 let changed = self.message_display.handle_resize(width, height);
443
444 if changed {
445 log::info!(
446 "{}",
447 t!(
448 "screen.resize_completed",
449 &self.message_display.viewport().debug_info()
450 )
451 );
452 }
453
454 Ok(())
455 }
456
457 async fn handle_tick_event(&mut self) -> Result<()> {
458 self.message_display.update_typewriter();
460
461 if let Some(input_state) = self.input_state.as_input_state() {
463 input_state.update_cursor_blink();
464 }
465 Ok(())
466 }
467
468 async fn render(&mut self) -> Result<()> {
470 let (input_widget, cursor_pos) = self.input_state.render_with_cursor();
472
473 self.terminal.draw(|frame| {
474 let size = frame.size();
475
476 if size.width < 10 || size.height < 5 {
477 log::error!(
478 "{}",
479 t!(
480 "screen.render.too_small_log",
481 &size.width.to_string(),
482 &size.height.to_string()
483 )
484 );
485
486 let emergency_area = ratatui::layout::Rect {
487 x: 0,
488 y: 0,
489 width: size.width.max(1),
490 height: size.height.max(1),
491 };
492
493 let emergency_widget =
494 ratatui::widgets::Paragraph::new(t!("screen.render.too_small.text"))
495 .block(ratatui::widgets::Block::default());
496
497 frame.render_widget(emergency_widget, emergency_area);
498 return;
499 }
500
501 let viewport = self.message_display.viewport();
502
503 if !viewport.is_usable() {
504 log::error!("{}", t!("screen.render.viewport_not_usable_log"));
505
506 let error_area = ratatui::layout::Rect {
507 x: 0,
508 y: 0,
509 width: size.width,
510 height: size.height,
511 };
512
513 let error_widget =
514 ratatui::widgets::Paragraph::new(t!("screen.render.viewport_error.text"))
515 .block(ratatui::widgets::Block::default());
516
517 frame.render_widget(error_widget, error_area);
518 return;
519 }
520
521 let output_area = viewport.output_area();
522 let input_area = viewport.input_area();
523
524 if !output_area.is_valid() || !input_area.is_valid() {
525 log::error!(
526 "{}",
527 t!(
528 "screen.render.invalid_layout_log",
529 &output_area.width.to_string(),
530 &output_area.height.to_string(),
531 &output_area.x.to_string(),
532 &output_area.y.to_string(),
533 &input_area.width.to_string(),
534 &input_area.height.to_string(),
535 &input_area.x.to_string(),
536 &input_area.y.to_string()
537 )
538 );
539 return;
540 }
541
542 if output_area.x + output_area.width > size.width
543 || output_area.y + output_area.height > size.height
544 || input_area.x + input_area.width > size.width
545 || input_area.y + input_area.height > size.height
546 {
547 log::error!(
548 "{}",
549 t!(
550 "screen.render.exceed_bounds_log",
551 &size.width.to_string(),
552 &size.height.to_string()
553 )
554 );
555 return;
556 }
557
558 let (visible_messages, config, output_layout, cursor_state) =
560 self.message_display.create_output_widget_for_rendering();
561
562 let message_refs: Vec<(String, usize, bool, bool, bool)> = visible_messages;
563
564 let output_widget = crate::output::display::create_output_widget(
565 &message_refs,
566 output_layout,
567 &config,
568 cursor_state,
569 );
570
571 frame.render_widget(output_widget, output_area.as_rect());
573 frame.render_widget(input_widget, input_area.as_rect());
574
575 if let Some((rel_x, rel_y)) = cursor_pos {
577 let padding_left = 3u16;
579 let padding_top = 1u16;
580
581 let abs_x = input_area.x + padding_left + rel_x;
582 let abs_y = input_area.y + padding_top + rel_y;
583
584 frame.set_cursor(abs_x, abs_y);
586 }
587 })?;
588
589 if cursor_pos.is_some() {
591 let cursor_commands = self.get_terminal_cursor_commands();
593 for command in cursor_commands {
594 execute!(std::io::stdout(), crossterm::style::Print(command))?;
595 }
596 } else {
597 execute!(
599 std::io::stdout(),
600 crossterm::style::Print("\x1B[?25l") )?;
602 }
603
604 Ok(())
605 }
606
607 fn get_terminal_cursor_commands(&self) -> Vec<String> {
609 let mut commands = Vec::new();
610
611 let form_command = match self.config.theme.input_cursor.to_uppercase().as_str() {
613 "PIPE" => "\x1B[6 q", "UNDERSCORE" => "\x1B[4 q", "BLOCK" => "\x1B[2 q", _ => "\x1B[6 q", };
618 commands.push(form_command.to_string());
619
620 let cursor_color = &self.config.theme.input_cursor_color;
622 let color_commands = self.get_all_cursor_color_sequences(cursor_color);
623 commands.extend(color_commands);
624
625 commands.push("\x1B[?25h".to_string()); commands
629 }
630
631 fn detect_terminal_once() -> &'static TerminalInfo {
632 TERMINAL_INFO.get_or_init(|| {
633 let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default();
634 let term = std::env::var("TERM").unwrap_or_default();
635 let tmux = std::env::var("TMUX").is_ok();
636
637 log::info!("🖥️ TERMINAL DETECTION (ONE-TIME):");
638 log::info!(" TERM_PROGRAM: '{}'", term_program);
639 log::info!(" TERM: '{}'", term);
640 log::info!(" TMUX: {}", tmux);
641
642 TerminalInfo { term_program, tmux }
643 })
644 }
645
646 fn get_all_cursor_color_sequences(&self, color: &AppColor) -> Vec<String> {
648 let mut sequences = Vec::new();
649 let (r, g, b) = self.get_rgb_values(color);
650
651 let terminal_info = Self::detect_terminal_once();
653
654 if terminal_info.tmux {
656 sequences.push(format!(
657 "\x1BPtmux;\x1B\x1B]12;#{:02x}{:02x}{:02x}\x07\x1B\\",
658 r, g, b
659 ));
660 return sequences; }
662
663 if terminal_info.term_program == "Apple_Terminal" {
665 sequences.push(format!("\x1B]12;#{:02x}{:02x}{:02x}\x07", r, g, b));
666 return sequences;
667 }
668
669 if terminal_info.term_program.starts_with("iTerm") {
671 sequences.push(format!("\x1B]Pl{:02x}{:02x}{:02x}\x1B\\", r, g, b));
672 sequences.push(format!("\x1B]12;#{:02x}{:02x}{:02x}\x07", r, g, b));
673 return sequences;
674 }
675
676 if terminal_info.term_program == "vscode" || std::env::var("VSCODE_INJECTION").is_ok() {
678 sequences.push(format!("\x1B]12;#{:02x}{:02x}{:02x}\x07", r, g, b));
679 return sequences;
680 }
681
682 sequences.push(format!("\x1B]12;#{:02x}{:02x}{:02x}\x07", r, g, b));
684 sequences
685 }
686
687 fn get_rgb_values(&self, color: &AppColor) -> (u8, u8, u8) {
689 let rgb = match color.to_name() {
690 "black" => (0, 0, 0),
691 "red" => (255, 0, 0),
692 "green" => (0, 255, 0),
693 "yellow" => (255, 255, 0), "blue" => (0, 0, 255),
695 "magenta" => (255, 0, 255),
696 "cyan" => (0, 255, 255),
697 "white" => (255, 255, 255),
698 "gray" => (128, 128, 128),
699 "darkgray" => (64, 64, 64),
700 "lightred" => (255, 128, 128),
701 "lightgreen" => (128, 255, 128),
702 "lightyellow" => (255, 255, 128),
703 "lightblue" => (128, 128, 255),
704 "lightmagenta" => (255, 128, 255),
705 "lightcyan" => (128, 255, 255),
706 _ => (255, 255, 255), };
708
709 log::trace!(
710 "🎨 COLOR MAPPING: '{}' → RGB({}, {}, {})",
711 color.to_name(),
712 rgb.0,
713 rgb.1,
714 rgb.2
715 );
716 rgb
717 }
718
719 async fn perform_restart(&mut self) -> Result<()> {
720 log::info!("{}", t!("screen.restart.start"));
721
722 execute!(
724 std::io::stdout(),
725 crossterm::style::Print("\x1B[0 q"), crossterm::style::Print("\x1B[?25h") )?;
728
729 self.terminal_mgr.cleanup().await?;
730 self.terminal_mgr = TerminalManager::new().await?;
731 self.terminal_mgr.setup().await?;
732
733 let backend = CrosstermBackend::new(io::stdout());
734 self.terminal = Terminal::new(backend)?;
735 let size = self.terminal.size()?;
736
737 self.message_display = MessageDisplay::new(&self.config, size.width, size.height);
738 self.input_state = Box::new(InputState::new(&self.config));
739 self.waiting_for_restart_confirmation = false;
740
741 self.message_display
742 .add_message(tc!("system.commands.restart.success"));
743
744 log::info!("{}", t!("screen.restart.done"));
745 Ok(())
746 }
747}
748
749impl ScreenManager {
751 pub fn validate_i18n_keys() -> Vec<String> {
752 let required_keys = [
753 "screen.performance_command_detected",
754 "screen.performance_command_viewport_reset_applied",
755 "screen.theme.invalid_format",
756 "screen.theme.processing",
757 "screen.theme.load_failed",
758 "screen.theme.failed",
759 "screen.theme.applied",
760 "screen.theme.not_found",
761 "screen.theme.not_found_feedback",
762 "screen.render.too_small_log",
763 "screen.render.too_small.text",
764 "screen.render.viewport_not_usable_log",
765 "screen.render.viewport_error.text",
766 "screen.render.invalid_layout_log",
767 "screen.render.exceed_bounds_log",
768 "screen.restart.start",
769 "screen.restart.done",
770 "system.commands.restart.success",
771 ];
772
773 let mut missing = Vec::new();
774 for key in required_keys {
775 if !crate::i18n::has_translation(key) {
776 missing.push(key.to_string());
777 }
778 }
779 missing
780 }
781
782 pub fn get_i18n_debug_info() -> HashMap<String, usize> {
783 let mut info = HashMap::new();
784 let stats = crate::i18n::get_translation_stats();
785
786 info.insert("screen_manager_keys".to_string(), 18);
787 info.extend(stats);
788
789 info
790 }
791}