rush_sync_server/input/
state.rs

1// ## FILE: src/input/state.rs - KOMPRIMIERTE VERSION
2use crate::commands::handler::CommandHandler;
3use crate::commands::history::{
4    HistoryAction, HistoryConfig, HistoryEvent, HistoryEventHandler, HistoryKeyboardHandler,
5    HistoryManager,
6};
7use crate::core::prelude::*;
8use crate::input::keyboard::{KeyAction, KeyboardManager};
9use crate::ui::cursor::{CursorKind, UiCursor};
10use crate::ui::widget::{AnimatedWidget, CursorWidget, StatefulWidget, Widget};
11use ratatui::prelude::*;
12use ratatui::widgets::{Block, Borders, Padding, Paragraph};
13use unicode_segmentation::UnicodeSegmentation;
14use unicode_width::UnicodeWidthStr;
15
16pub struct InputState {
17    content: String,
18    cursor: UiCursor,
19    prompt: String,
20    history_manager: HistoryManager,
21    config: Config,
22    command_handler: CommandHandler,
23    keyboard_manager: KeyboardManager,
24    confirmation_state: ConfirmationState,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28enum ConfirmationState {
29    None,
30    Exit,
31    Restart,
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct InputStateBackup {
36    pub content: String,
37    pub history: Vec<String>,
38    pub cursor_pos: usize,
39}
40
41impl InputState {
42    pub fn new(config: &Config) -> Self {
43        let history_config = HistoryConfig::from_main_config(config);
44        Self {
45            content: String::with_capacity(100),
46            cursor: UiCursor::from_config(config, CursorKind::Input),
47            prompt: config.theme.input_cursor_prefix.clone(),
48            history_manager: HistoryManager::new(history_config.max_entries),
49            config: config.clone(),
50            command_handler: CommandHandler::new(),
51            keyboard_manager: KeyboardManager::new(),
52            confirmation_state: ConfirmationState::None,
53        }
54    }
55
56    pub fn update_from_config(&mut self, config: &Config) {
57        self.cursor.update_from_config(config);
58        self.prompt = config.theme.input_cursor_prefix.clone();
59        self.config = config.clone();
60    }
61
62    pub fn reset_for_language_change(&mut self) {
63        self.confirmation_state = ConfirmationState::None;
64        self.clear_input();
65    }
66
67    pub fn get_content(&self) -> &str {
68        &self.content
69    }
70    pub fn get_history_count(&self) -> usize {
71        self.history_manager.entry_count()
72    }
73
74    // ✅ KOMPRIMIERTE INPUT HANDLING
75    pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
76        // History navigation
77        if let Some(action) = HistoryKeyboardHandler::get_history_action(&key) {
78            return self.handle_history(action);
79        }
80
81        if key.code == KeyCode::Esc {
82            return None;
83        }
84
85        let action = self.keyboard_manager.get_action(&key);
86
87        // Confirmation handling
88        if self.confirmation_state != ConfirmationState::None {
89            return self.handle_confirmation(action);
90        }
91
92        // Regular input handling
93        match action {
94            KeyAction::Submit => self.handle_submit(),
95            KeyAction::PasteBuffer => self.handle_paste(),
96            KeyAction::CopySelection => self.handle_copy(),
97            KeyAction::ClearLine => self.handle_clear_line(),
98            KeyAction::InsertChar(c) => {
99                self.insert_char(c);
100                None
101            }
102            KeyAction::MoveLeft => {
103                self.cursor.move_left();
104                None
105            }
106            KeyAction::MoveRight => {
107                self.cursor.move_right();
108                None
109            }
110            KeyAction::MoveToStart => {
111                self.cursor.move_to_start();
112                None
113            }
114            KeyAction::MoveToEnd => {
115                self.cursor.move_to_end();
116                None
117            }
118            KeyAction::Backspace => {
119                self.handle_backspace();
120                None
121            }
122            KeyAction::Delete => {
123                self.handle_delete();
124                None
125            }
126            _ => None,
127        }
128    }
129
130    // ✅ KOMPRIMIERTE CONFIRMATION LOGIC
131    fn handle_confirmation(&mut self, action: KeyAction) -> Option<String> {
132        match action {
133            KeyAction::Submit => {
134                let confirm = t!("system.input.confirm.short").to_lowercase();
135                let result = if self.content.trim().to_lowercase() == confirm {
136                    match self.confirmation_state {
137                        ConfirmationState::Exit => "__EXIT__".to_string(),
138                        ConfirmationState::Restart => "__RESTART__".to_string(),
139                        _ => get_translation("system.input.cancelled", &[]),
140                    }
141                } else {
142                    get_translation("system.input.cancelled", &[])
143                };
144
145                self.confirmation_state = ConfirmationState::None;
146                self.clear_input();
147                Some(result)
148            }
149            KeyAction::InsertChar(c) => {
150                let confirm_char = t!("system.input.confirm.short").to_lowercase();
151                let cancel_char = t!("system.input.cancel.short").to_lowercase();
152
153                if [confirm_char, cancel_char].contains(&c.to_lowercase().to_string()) {
154                    self.content.clear();
155                    self.content.push(c);
156                    self.cursor.update_text_length(&self.content);
157                    self.cursor.move_to_end();
158                }
159                None
160            }
161            KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
162                self.clear_input();
163                None
164            }
165            _ => None,
166        }
167    }
168
169    fn handle_history(&mut self, action: HistoryAction) -> Option<String> {
170        let entry = match action {
171            HistoryAction::NavigatePrevious => self.history_manager.navigate_previous(),
172            HistoryAction::NavigateNext => self.history_manager.navigate_next(),
173        };
174
175        if let Some(entry) = entry {
176            self.content = entry;
177            self.cursor.update_text_length(&self.content);
178            self.cursor.move_to_end();
179        }
180        None
181    }
182
183    fn handle_submit(&mut self) -> Option<String> {
184        if self.content.is_empty() || self.content.trim().is_empty() {
185            return None;
186        }
187
188        if self.content.graphemes(true).count() > 1024 {
189            return Some(get_translation("system.input.too_long", &["1024"]));
190        }
191
192        let content = std::mem::take(&mut self.content);
193        self.cursor.reset_for_empty_text();
194        self.history_manager.add_entry(content.clone());
195
196        let result = self.command_handler.handle_input(&content);
197
198        // Handle special responses (unchanged)
199        if let Some(event) = HistoryEventHandler::handle_command_result(&result.message) {
200            return Some(self.handle_history_event(event));
201        }
202
203        // Handle confirmations with i18n
204        if result.message.starts_with("__CONFIRM_EXIT__") {
205            self.confirmation_state = ConfirmationState::Exit;
206            return Some(result.message.replace("__CONFIRM_EXIT__", ""));
207        }
208        if result.message.starts_with("__CONFIRM_RESTART__") {
209            self.confirmation_state = ConfirmationState::Restart;
210            return Some(result.message.replace("__CONFIRM_RESTART__", ""));
211        }
212
213        // Handle restart commands (unchanged)
214        if result.message.starts_with("__RESTART") {
215            let feedback = if result.message.starts_with("__RESTART_FORCE__") {
216                result
217                    .message
218                    .replace("__RESTART_FORCE__", "")
219                    .trim()
220                    .to_string()
221            } else {
222                result.message.replace("__RESTART__", "").trim().to_string()
223            };
224
225            return Some(if feedback.is_empty() {
226                "__RESTART__".to_string()
227            } else {
228                format!("__RESTART_WITH_MSG__{}", feedback)
229            });
230        }
231
232        if result.should_exit {
233            Some(format!("__EXIT__{}", result.message))
234        } else {
235            Some(result.message)
236        }
237    }
238
239    fn handle_history_event(&mut self, event: HistoryEvent) -> String {
240        match event {
241            HistoryEvent::Clear => {
242                self.history_manager.clear();
243                HistoryEventHandler::create_clear_response()
244            }
245            HistoryEvent::Add(entry) => {
246                self.history_manager.add_entry(entry);
247                String::new()
248            }
249            _ => String::new(),
250        }
251    }
252
253    // ✅ KOMPRIMIERTE CLIPBOARD OPERATIONS
254    fn handle_paste(&mut self) -> Option<String> {
255        let text = self.read_clipboard()?;
256        let clean = text
257            .replace(['\n', '\r', '\t'], " ")
258            .chars()
259            .filter(|c| !c.is_control() || *c == ' ')
260            .collect::<String>();
261
262        if clean.is_empty() {
263            return Some(get_translation("system.input.clipboard.empty", &[]));
264        }
265
266        let current_len = self.content.graphemes(true).count();
267        let available = self.config.input_max_length.saturating_sub(current_len);
268        let paste_text = clean.graphemes(true).take(available).collect::<String>();
269
270        if !paste_text.is_empty() {
271            let byte_pos = self.cursor.get_byte_position(&self.content);
272            self.content.insert_str(byte_pos, &paste_text);
273            let chars_added = paste_text.graphemes(true).count();
274            self.cursor.update_text_length(&self.content);
275
276            for _ in 0..chars_added {
277                self.cursor.move_right();
278            }
279            Some(get_translation(
280                "system.input.clipboard.pasted",
281                &[&chars_added.to_string()],
282            ))
283        } else {
284            Some(get_translation(
285                "system.input.clipboard.nothing_to_paste",
286                &[],
287            ))
288        }
289    }
290
291    fn handle_copy(&self) -> Option<String> {
292        if self.content.is_empty() {
293            return Some(get_translation(
294                "system.input.clipboard.nothing_to_copy",
295                &[],
296            ));
297        }
298
299        if self.write_clipboard(&self.content) {
300            let preview = if self.content.chars().count() > 50 {
301                format!("{}...", self.content.chars().take(50).collect::<String>())
302            } else {
303                self.content.clone()
304            };
305            Some(get_translation(
306                "system.input.clipboard.copied",
307                &[&preview],
308            ))
309        } else {
310            Some(get_translation("system.input.clipboard.copy_failed", &[]))
311        }
312    }
313
314    fn handle_clear_line(&mut self) -> Option<String> {
315        if self.content.is_empty() {
316            return None;
317        }
318
319        let result = if self.write_clipboard(&self.content) {
320            let preview = if self.content.chars().count() > 50 {
321                format!("{}...", self.content.chars().take(50).collect::<String>())
322            } else {
323                self.content.clone()
324            };
325            get_translation("system.input.clipboard.cut", &[&preview])
326        } else {
327            get_translation("system.input.clipboard.cleared", &[])
328        };
329
330        self.clear_input();
331        Some(result)
332    }
333
334    // ✅ KOMPRIMIERTE CLIPBOARD SYSTEM
335    fn read_clipboard(&self) -> Option<String> {
336        let output = self.get_clipboard_cmd("read")?.output().ok()?;
337        let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
338        if text.is_empty() {
339            None
340        } else {
341            Some(text)
342        }
343    }
344
345    fn write_clipboard(&self, text: &str) -> bool {
346        if text.is_empty() {
347            return false;
348        }
349
350        if let Some(mut cmd) = self.get_clipboard_cmd("write") {
351            if let Ok(mut child) = cmd.stdin(std::process::Stdio::piped()).spawn() {
352                if let Some(stdin) = child.stdin.as_mut() {
353                    use std::io::Write;
354                    let _ = stdin.write_all(text.as_bytes());
355                }
356                return child.wait().is_ok();
357            }
358        }
359        false
360    }
361
362    fn get_clipboard_cmd(&self, op: &str) -> Option<std::process::Command> {
363        #[cfg(target_os = "macos")]
364        {
365            Some(std::process::Command::new(if op == "read" {
366                "pbpaste"
367            } else {
368                "pbcopy"
369            }))
370        }
371
372        #[cfg(target_os = "linux")]
373        {
374            let mut cmd = std::process::Command::new("xclip");
375            if op == "read" {
376                cmd.args(["-selection", "clipboard", "-o"]);
377            } else {
378                cmd.args(["-selection", "clipboard"]);
379            }
380            Some(cmd)
381        }
382
383        #[cfg(target_os = "windows")]
384        {
385            if op == "read" {
386                let mut cmd = std::process::Command::new("powershell");
387                cmd.args(["-Command", "Get-Clipboard"]);
388                Some(cmd)
389            } else {
390                None // Windows write handling unterschiedlich
391            }
392        }
393
394        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
395        None
396    }
397
398    // ✅ KOMPRIMIERTE TEXT EDITING
399    fn insert_char(&mut self, c: char) {
400        if self.content.graphemes(true).count() < self.config.input_max_length {
401            let byte_pos = self.cursor.get_byte_position(&self.content);
402            self.content.insert(byte_pos, c);
403            self.cursor.update_text_length(&self.content);
404            self.cursor.move_right();
405        }
406    }
407
408    fn handle_backspace(&mut self) {
409        if self.content.is_empty() || self.cursor.get_position() == 0 {
410            return;
411        }
412
413        let current = self.cursor.get_byte_position(&self.content);
414        let prev = self.cursor.get_prev_byte_position(&self.content);
415
416        if prev < current && current <= self.content.len() {
417            self.cursor.move_left();
418            self.content.replace_range(prev..current, "");
419            self.cursor.update_text_length(&self.content);
420
421            if self.content.is_empty() {
422                self.cursor.reset_for_empty_text();
423            }
424        }
425    }
426
427    fn handle_delete(&mut self) {
428        let text_len = self.content.graphemes(true).count();
429        if self.cursor.get_position() >= text_len || text_len == 0 {
430            return;
431        }
432
433        let current = self.cursor.get_byte_position(&self.content);
434        let next = self.cursor.get_next_byte_position(&self.content);
435
436        if current < next && next <= self.content.len() {
437            self.content.replace_range(current..next, "");
438            self.cursor.update_text_length(&self.content);
439
440            if self.content.is_empty() {
441                self.cursor.reset_for_empty_text();
442            }
443        }
444    }
445
446    fn clear_input(&mut self) {
447        self.content.clear();
448        self.history_manager.reset_position();
449        self.cursor.move_to_start();
450    }
451}
452
453// ✅ WIDGET TRAIT IMPLEMENTATIONS (angepasst an neue Namen)
454impl Widget for InputState {
455    fn render(&self) -> Paragraph {
456        self.render_with_cursor().0
457    }
458
459    fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
460        self.handle_key_event(key)
461    }
462}
463
464impl CursorWidget for InputState {
465    fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
466        let graphemes: Vec<&str> = self.content.graphemes(true).collect();
467        let cursor_pos = self.cursor.get_position();
468        let prompt_width = self.prompt.width();
469        let available_width = self
470            .config
471            .input_max_length
472            .saturating_sub(prompt_width + 4);
473
474        // Viewport calculation
475        let viewport_start = if cursor_pos > available_width {
476            cursor_pos - available_width + 10
477        } else {
478            0
479        };
480
481        // Create spans
482        let mut spans = vec![Span::styled(
483            &self.prompt,
484            Style::default().fg(self.config.theme.input_cursor_color.into()),
485        )];
486
487        let end_pos = (viewport_start + available_width).min(graphemes.len());
488        let visible = graphemes
489            .get(viewport_start..end_pos)
490            .unwrap_or(&[])
491            .join("");
492        spans.push(Span::styled(
493            visible,
494            Style::default().fg(self.config.theme.input_text.into()),
495        ));
496
497        let paragraph = Paragraph::new(Line::from(spans)).block(
498            Block::default()
499                .padding(Padding::new(3, 1, 1, 1))
500                .borders(Borders::NONE)
501                .style(Style::default().bg(self.config.theme.input_bg.into())),
502        );
503
504        // Cursor coordinates
505        let cursor_coord = if self.cursor.is_visible() && cursor_pos >= viewport_start {
506            let chars_before = graphemes.get(viewport_start..cursor_pos).unwrap_or(&[]);
507            let visible_width: usize = chars_before
508                .iter()
509                .map(|g| UnicodeWidthStr::width(*g))
510                .sum();
511            Some(((prompt_width + visible_width) as u16, 0u16))
512        } else {
513            None
514        };
515
516        (paragraph, cursor_coord)
517    }
518}
519
520impl StatefulWidget for InputState {
521    fn export_state(&self) -> InputStateBackup {
522        InputStateBackup {
523            content: self.content.clone(),
524            history: self.history_manager.get_all_entries(),
525            cursor_pos: self.cursor.get_current_position(),
526        }
527    }
528
529    fn import_state(&mut self, state: InputStateBackup) {
530        self.content = state.content;
531        self.history_manager.import_entries(state.history);
532        self.cursor.update_text_length(&self.content);
533    }
534}
535
536impl AnimatedWidget for InputState {
537    fn tick(&mut self) {
538        self.cursor.update_blink();
539    }
540}