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