rush_sync_server/ui/
screen.rs1use crate::commands::history::HistoryKeyboardHandler;
6use crate::commands::lang::LanguageManager;
7use crate::commands::theme::TomlThemeLoader;
8use crate::core::prelude::*;
9use crate::input::{
10 event::{AppEvent, EventHandler},
11 input::InputState,
12 keyboard::{KeyAction, KeyboardManager},
13};
14use crate::output::{logging::AppLogger, message::MessageManager, output::create_output_widget};
15use crate::ui::{color::AppColor, terminal::TerminalManager, widget::Widget};
16
17use crossterm::event::KeyEvent;
18use ratatui::{
19 backend::CrosstermBackend,
20 layout::{Constraint, Direction, Layout},
21 Terminal,
22};
23use std::io::{self, Stdout};
24
25pub type TerminalBackend = Terminal<CrosstermBackend<Stdout>>;
26
27pub struct ScreenManager {
28 terminal: TerminalBackend,
29 message_manager: MessageManager,
30 input_state: Box<dyn Widget>,
31 terminal_size: (u16, u16),
32 config: Config, terminal_mgr: TerminalManager,
34 events: EventHandler,
35 keyboard_manager: KeyboardManager,
36 waiting_for_restart_confirmation: bool,
37}
38
39impl ScreenManager {
40 pub async fn new(config: &Config) -> Result<Self> {
42 let mut terminal_mgr = TerminalManager::new().await?;
43 terminal_mgr.setup().await?;
44
45 let backend = CrosstermBackend::new(io::stdout());
46 let terminal = Terminal::new(backend)?;
47 let size = terminal.size()?;
48
49 let initial_height = size.height.saturating_sub(4) as usize;
50 let mut message_manager = MessageManager::new(config);
51 message_manager
52 .scroll_state
53 .update_dimensions(initial_height, 0);
54
55 let owned_config = config.clone();
57
58 Ok(Self {
59 terminal,
60 terminal_mgr,
61 message_manager,
62 input_state: Box::new(InputState::new(&config.prompt.text, config)),
63 terminal_size: (size.width, size.height),
64 config: owned_config, events: EventHandler::new(config.poll_rate),
66 keyboard_manager: KeyboardManager::new(),
67 waiting_for_restart_confirmation: false,
68 })
69 }
70
71 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_event(key).await? {
78 self.events.shutdown().await;
79 break Ok(());
80 }
81 }
82 AppEvent::Resize(width, height) => {
83 self.handle_resize_event(width, height).await?;
84 }
85 AppEvent::Tick => {
86 self.handle_tick_event().await?;
87 }
88 }
89 }
90
91 self.process_pending_logs().await;
92 self.render().await?;
93 };
94
95 self.terminal_mgr.cleanup().await?;
96 result
97 }
98
99 async fn handle_input_event(&mut self, key: KeyEvent) -> Result<bool> {
101 if HistoryKeyboardHandler::get_history_action(&key).is_some() {
103 if let Some(new_input) = self.input_state.handle_input(key) {
104 if let Some(processed) = LanguageManager::process_save_message(&new_input).await {
105 self.message_manager.add_message(processed);
106 return Ok(false);
107 }
108
109 if let Some(processed) = self.process_live_theme_update(&new_input).await {
111 self.message_manager.add_message(processed);
112 return Ok(false);
113 }
114
115 self.message_manager.add_message(new_input.clone());
116
117 if new_input.starts_with("__CLEAR__") {
118 self.message_manager.clear_messages();
119 } else if new_input.starts_with("__EXIT__") {
120 return Ok(true);
121 }
122 }
123 return Ok(false);
124 }
125
126 match self.keyboard_manager.get_action(&key) {
128 KeyAction::ScrollUp
129 | KeyAction::ScrollDown
130 | KeyAction::PageUp
131 | KeyAction::PageDown => {
132 let window_height = self.get_content_height();
133 self.message_manager
134 .handle_scroll(self.keyboard_manager.get_action(&key), window_height);
135 }
136 KeyAction::Submit => {
137 if let Some(new_input) = self.input_state.handle_input(key) {
138 if let Some(processed) = LanguageManager::process_save_message(&new_input).await
139 {
140 self.message_manager.add_message(processed);
141 return Ok(false);
142 }
143
144 if let Some(processed) = self.process_live_theme_update(&new_input).await {
146 self.message_manager.add_message(processed);
147 return Ok(false);
148 }
149
150 self.message_manager.add_message(new_input.clone());
151 if new_input.starts_with("__CLEAR__") {
152 self.message_manager.clear_messages();
153 } else if new_input.starts_with("__EXIT__") {
154 return Ok(true);
155 }
156 else if new_input.starts_with("__RESTART_WITH_MSG__") {
158 let feedback_msg = new_input
159 .replace("__RESTART_WITH_MSG__", "")
160 .trim()
161 .to_string();
162
163 if !feedback_msg.is_empty() {
164 self.message_manager.add_message(feedback_msg);
165 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
166 }
167
168 if let Err(e) = self.perform_restart().await {
169 self.message_manager
170 .add_message(format!("Restart failed: {}", e));
171 }
172 } else if new_input.starts_with("__RESTART_FORCE__")
173 || new_input == "__RESTART__"
174 {
175 if let Err(e) = self.perform_restart().await {
176 self.message_manager
177 .add_message(format!("Restart failed: {}", e));
178 }
179 }
180 }
181 }
182 KeyAction::Quit => return Ok(true),
183 _ => {
184 if let Some(new_input) = self.input_state.handle_input(key) {
185 if let Some(processed) = LanguageManager::process_save_message(&new_input).await
186 {
187 self.message_manager.add_message(processed);
188 return Ok(false);
189 }
190
191 if let Some(processed) = self.process_live_theme_update(&new_input).await {
193 self.message_manager.add_message(processed);
194 return Ok(false);
195 }
196
197 self.message_manager.add_message(new_input);
198 }
199 }
200 }
201 Ok(false)
202 }
203
204 async fn process_live_theme_update(&mut self, message: &str) -> Option<String> {
205 if !message.starts_with("__LIVE_THEME_UPDATE__") {
206 return None;
207 }
208
209 let parts: Vec<&str> = message.split("__MESSAGE__").collect();
210 if parts.len() != 2 {
211 log::error!("Invalid live theme update format: {}", message);
212 return None;
213 }
214
215 let theme_part = parts[0].replace("__LIVE_THEME_UPDATE__", "");
216 let display_message = parts[1];
217
218 log::debug!("🎨 Processing live theme update: {}", theme_part);
219
220 if let Some(theme_def) = TomlThemeLoader::load_theme_by_name_sync(&theme_part) {
222 match self.create_theme_from_definition(&theme_def) {
223 Ok(new_theme) => {
224 let backup = self.input_state.get_backup_data().unwrap_or_default();
226 log::debug!(
227 "📦 Backed up {} history entries, content: '{}'",
228 backup.history.len(),
229 backup.content
230 );
231
232 self.config.theme = new_theme;
234 self.config.current_theme_name = theme_part.clone();
235
236 self.input_state =
238 Box::new(InputState::new(&self.config.prompt.text, &self.config));
239
240 self.input_state.restore_backup_data(backup.clone());
242 log::debug!(
243 "🔄 Restored {} history entries to new InputState",
244 backup.history.len()
245 );
246
247 self.message_manager.update_config(&self.config);
249
250 log::info!(
251 "✅ Live theme '{}' applied from TOML - {} history entries preserved!",
252 theme_part.to_uppercase(),
253 backup.history.len()
254 );
255 Some(display_message.to_string())
256 }
257 Err(e) => {
258 log::error!("❌ Failed to create theme: {}", e);
259 Some(format!("❌ Theme update failed: {}", e))
260 }
261 }
262 } else {
263 log::error!("❌ Theme '{}' not found in TOML", theme_part);
264 Some(format!("❌ Theme '{}' not found in config", theme_part))
265 }
266 }
267
268 fn create_theme_from_definition(
270 &self,
271 theme_def: &crate::commands::theme::ThemeDefinition,
272 ) -> Result<crate::core::config::Theme> {
273 use crate::ui::color::AppColor;
274
275 Ok(crate::core::config::Theme {
276 input_text: AppColor::from_string(&theme_def.input_text)?,
277 input_bg: AppColor::from_string(&theme_def.input_bg)?,
278 cursor: AppColor::from_string(&theme_def.cursor)?,
279 output_text: AppColor::from_string(&theme_def.output_text)?,
280 output_bg: AppColor::from_string(&theme_def.output_bg)?,
281 })
282 }
283
284 async fn handle_resize_event(&mut self, width: u16, height: u16) -> Result<()> {
286 self.terminal_size = (width, height);
287 let window_height = self.get_content_height();
288 self.message_manager
289 .scroll_state
290 .update_dimensions(window_height, self.message_manager.get_content_height());
291 Ok(())
292 }
293
294 async fn handle_tick_event(&mut self) -> Result<()> {
296 self.message_manager.update_typewriter();
297 if let Some(input_state) = self.input_state.as_input_state() {
298 input_state.update_cursor_blink();
299 }
300 Ok(())
301 }
302
303 fn get_content_height(&self) -> usize {
304 self.terminal_size.1.saturating_sub(4) as usize
305 }
306
307 async fn process_pending_logs(&mut self) {
308 match AppLogger::get_messages() {
309 Ok(messages) => {
310 for log_msg in messages {
311 self.message_manager.add_message(log_msg.formatted());
312 }
313 }
314 Err(e) => {
315 self.message_manager.add_message(
316 AppColor::from_any("error")
317 .format_message("ERROR", &format!("Logging-Fehler: {:?}", e)),
318 );
319 }
320 }
321 }
322
323 async fn render(&mut self) -> Result<()> {
324 self.terminal.draw(|frame| {
325 let size = frame.size();
326 if size.width < 20 || size.height < 10 {
327 return;
328 }
329
330 let chunks = Layout::default()
331 .direction(Direction::Vertical)
332 .margin(1)
333 .constraints([Constraint::Min(3), Constraint::Length(3)])
334 .split(size);
335
336 let available_height = chunks[0].height as usize;
337 self.message_manager
338 .scroll_state
339 .update_dimensions(available_height, self.message_manager.get_content_height());
340
341 let messages = self.message_manager.get_messages();
342 let output_widget =
343 create_output_widget(&messages, available_height as u16, &self.config);
344 frame.render_widget(output_widget, chunks[0]);
345
346 let input_widget = self.input_state.render();
347 frame.render_widget(input_widget, chunks[1]);
348 })?;
349 Ok(())
350 }
351
352 async fn perform_restart(&mut self) -> Result<()> {
353 self.terminal_mgr.cleanup().await?;
355 self.terminal_mgr = TerminalManager::new().await?;
356 self.terminal_mgr.setup().await?;
357
358 let backend = CrosstermBackend::new(io::stdout());
359 self.terminal = Terminal::new(backend)?;
360
361 self.message_manager.clear_messages();
362 self.input_state = Box::new(InputState::new(&self.config.prompt.text, &self.config));
363 self.waiting_for_restart_confirmation = false;
364
365 self.message_manager
366 .add_message(crate::i18n::get_command_translation(
367 "system.commands.restart.success",
368 &[],
369 ));
370 log::info!("Internal restart completed successfully");
371 Ok(())
372 }
373}