rush_sync_server/input/
state.rs

1// =====================================================
2// FILE: src/input/state.rs - CLEANED VERSION ohne Debug Commands
3// =====================================================
4
5use crate::commands::handler::CommandHandler;
6use crate::commands::history::{
7    HistoryAction, HistoryConfig, HistoryEvent, HistoryEventHandler, HistoryKeyboardHandler,
8    HistoryManager,
9};
10use crate::core::prelude::*;
11use crate::input::keyboard::{KeyAction, KeyboardManager};
12use crate::ui::cursor::{CursorKind, UiCursor};
13use crate::ui::widget::{InputWidget, Widget};
14use ratatui::prelude::*;
15use ratatui::widgets::{Block, Borders, Padding, Paragraph};
16use unicode_segmentation::UnicodeSegmentation;
17
18pub struct InputState {
19    content: String,
20    cursor: UiCursor,
21    prompt: String,
22    history_manager: HistoryManager,
23    config: Config,
24    command_handler: CommandHandler,
25    keyboard_manager: KeyboardManager,
26    waiting_for_exit_confirmation: bool,
27    waiting_for_restart_confirmation: bool,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct InputStateBackup {
32    pub content: String,
33    pub history: Vec<String>,
34    pub cursor_pos: usize,
35}
36
37impl InputState {
38    pub fn new(config: &Config) -> Self {
39        let history_config = HistoryConfig::from_main_config(config);
40
41        Self {
42            content: String::with_capacity(100),
43            cursor: UiCursor::from_config(config, CursorKind::Input),
44            prompt: config.theme.input_cursor_prefix.clone(),
45            history_manager: HistoryManager::new(history_config.max_entries),
46            config: config.clone(),
47            command_handler: CommandHandler::new(),
48            keyboard_manager: KeyboardManager::new(),
49            waiting_for_exit_confirmation: false,
50            waiting_for_restart_confirmation: false,
51        }
52    }
53
54    pub fn update_from_config(&mut self, config: &Config) {
55        self.cursor.update_from_config(config);
56        self.prompt = config.theme.input_cursor_prefix.clone();
57        self.config = config.clone();
58    }
59
60    pub fn validate_input(&self, input: &str) -> crate::core::error::Result<()> {
61        if input.trim().is_empty() {
62            return Err(AppError::Validation(t!("system.input.empty")));
63        }
64        let grapheme_count = input.graphemes(true).count();
65        let max_length = 1024;
66
67        if grapheme_count > max_length {
68            return Err(AppError::Validation(t!(
69                "system.input.too_long",
70                &max_length.to_string()
71            )));
72        }
73        Ok(())
74    }
75
76    pub fn reset_for_language_change(&mut self) {
77        self.waiting_for_exit_confirmation = false;
78        self.waiting_for_restart_confirmation = false;
79        self.content.clear();
80        self.history_manager.reset_position();
81        self.cursor.move_to_start();
82    }
83
84    fn handle_exit_confirmation(&mut self, action: KeyAction) -> Option<String> {
85        match action {
86            KeyAction::Submit => {
87                self.waiting_for_exit_confirmation = false;
88                let confirm_short = t!("system.input.confirm.short");
89                let cancel_short = t!("system.input.cancel.short");
90                match self.content.trim().to_lowercase().as_str() {
91                    input if input == confirm_short.to_lowercase() => {
92                        self.content.clear();
93                        Some("__EXIT__".to_string())
94                    }
95                    input if input == cancel_short.to_lowercase() => {
96                        self.clear_input();
97                        Some(t!("system.input.cancelled"))
98                    }
99                    _ => {
100                        self.clear_input();
101                        Some(t!("system.input.cancelled"))
102                    }
103                }
104            }
105            KeyAction::InsertChar(c) => {
106                let confirm_short = t!("system.input.confirm.short");
107                let cancel_short = t!("system.input.cancel.short");
108                if c.to_lowercase().to_string() == confirm_short.to_lowercase()
109                    || c.to_lowercase().to_string() == cancel_short.to_lowercase()
110                {
111                    self.content.clear();
112                    self.content.push(c);
113                    self.cursor.update_text_length(&self.content);
114                    self.cursor.move_to_end();
115                }
116                None
117            }
118            KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
119                self.clear_input();
120                None
121            }
122            _ => None,
123        }
124    }
125
126    fn handle_restart_confirmation(&mut self, action: KeyAction) -> Option<String> {
127        match action {
128            KeyAction::Submit => {
129                self.waiting_for_restart_confirmation = false;
130                let confirm_short = t!("system.input.confirm.short");
131                let cancel_short = t!("system.input.cancel.short");
132                match self.content.trim().to_lowercase().as_str() {
133                    input if input == confirm_short.to_lowercase() => {
134                        self.content.clear();
135                        Some("__RESTART__".to_string())
136                    }
137                    input if input == cancel_short.to_lowercase() => {
138                        self.clear_input();
139                        Some(t!("system.input.cancelled"))
140                    }
141                    _ => {
142                        self.clear_input();
143                        Some(t!("system.input.cancelled"))
144                    }
145                }
146            }
147            KeyAction::InsertChar(c) => {
148                let confirm_short = t!("system.input.confirm.short");
149                let cancel_short = t!("system.input.cancel.short");
150                if c.to_lowercase().to_string() == confirm_short.to_lowercase()
151                    || c.to_lowercase().to_string() == cancel_short.to_lowercase()
152                {
153                    self.content.clear();
154                    self.content.push(c);
155                    self.cursor.update_text_length(&self.content);
156                    self.cursor.move_to_end();
157                }
158                None
159            }
160            KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
161                self.clear_input();
162                None
163            }
164            _ => None,
165        }
166    }
167
168    fn clear_input(&mut self) {
169        self.content.clear();
170        self.history_manager.reset_position();
171        self.cursor.move_to_start();
172    }
173
174    fn handle_history_action(&mut self, action: HistoryAction) -> Option<String> {
175        match action {
176            HistoryAction::NavigatePrevious => {
177                if let Some(entry) = self.history_manager.navigate_previous() {
178                    self.content = entry;
179                    self.cursor.update_text_length(&self.content);
180                    self.cursor.move_to_end();
181                }
182            }
183            HistoryAction::NavigateNext => {
184                if let Some(entry) = self.history_manager.navigate_next() {
185                    self.content = entry;
186                    self.cursor.update_text_length(&self.content);
187                    self.cursor.move_to_end();
188                }
189            }
190        }
191        None
192    }
193
194    fn handle_history_event(&mut self, event: HistoryEvent) -> String {
195        match event {
196            HistoryEvent::Clear => {
197                self.history_manager.clear();
198                HistoryEventHandler::create_clear_response()
199            }
200            HistoryEvent::Add(entry) => {
201                self.history_manager.add_entry(entry);
202                String::new()
203            }
204            _ => String::new(),
205        }
206    }
207
208    pub fn execute(&self) -> crate::core::error::Result<String> {
209        Ok(format!(
210            "__CONFIRM_EXIT__{}",
211            t!("system.input.confirm_exit")
212        ))
213    }
214
215    fn read_clipboard(&self) -> Option<String> {
216        #[cfg(target_os = "macos")]
217        {
218            std::process::Command::new("pbpaste")
219                .output()
220                .ok()
221                .and_then(|output| {
222                    let text = String::from_utf8_lossy(&output.stdout).to_string();
223                    if text.trim().is_empty() {
224                        None
225                    } else {
226                        Some(text.trim().to_string())
227                    }
228                })
229        }
230
231        #[cfg(target_os = "linux")]
232        {
233            std::process::Command::new("xclip")
234                .args(["-selection", "clipboard", "-o"])
235                .output()
236                .or_else(|_| {
237                    std::process::Command::new("xsel")
238                        .args(["-b", "-o"])
239                        .output()
240                })
241                .ok()
242                .and_then(|output| {
243                    let text = String::from_utf8_lossy(&output.stdout).to_string();
244                    if text.trim().is_empty() {
245                        None
246                    } else {
247                        Some(text.trim().to_string())
248                    }
249                })
250        }
251
252        #[cfg(target_os = "windows")]
253        {
254            std::process::Command::new("powershell")
255                .args(["-Command", "Get-Clipboard"])
256                .output()
257                .ok()
258                .and_then(|output| {
259                    let text = String::from_utf8_lossy(&output.stdout).to_string();
260                    if text.trim().is_empty() {
261                        None
262                    } else {
263                        Some(text.trim().to_string())
264                    }
265                })
266        }
267
268        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
269        None
270    }
271
272    // ✅ EINFACHES CLIPBOARD SCHREIBEN
273    fn write_clipboard(&self, text: &str) -> bool {
274        if text.is_empty() {
275            return false;
276        }
277
278        #[cfg(target_os = "macos")]
279        {
280            std::process::Command::new("pbcopy")
281                .stdin(std::process::Stdio::piped())
282                .spawn()
283                .and_then(|mut child| {
284                    use std::io::Write;
285                    if let Some(stdin) = child.stdin.as_mut() {
286                        stdin.write_all(text.as_bytes())?;
287                    }
288                    child.wait()
289                })
290                .is_ok()
291        }
292
293        #[cfg(target_os = "linux")]
294        {
295            std::process::Command::new("xclip")
296                .args(["-selection", "clipboard"])
297                .stdin(std::process::Stdio::piped())
298                .spawn()
299                .and_then(|mut child| {
300                    use std::io::Write;
301                    if let Some(stdin) = child.stdin.as_mut() {
302                        stdin.write_all(text.as_bytes())?;
303                    }
304                    child.wait()
305                })
306                .is_ok()
307        }
308
309        #[cfg(target_os = "windows")]
310        {
311            std::process::Command::new("cmd")
312                .args(["/C", &format!("echo {}| clip", text)])
313                .output()
314                .is_ok()
315        }
316
317        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
318        false
319    }
320
321    // ✅ NEUE SELECTION HANDLING
322    pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
323        if let Some(history_action) = HistoryKeyboardHandler::get_history_action(&key) {
324            return self.handle_history_action(history_action);
325        }
326
327        if key.code == KeyCode::Esc {
328            return None;
329        }
330
331        let action = self.keyboard_manager.get_action(&key);
332
333        if self.waiting_for_exit_confirmation {
334            return self.handle_exit_confirmation(action);
335        }
336        if self.waiting_for_restart_confirmation {
337            return self.handle_restart_confirmation(action);
338        }
339
340        match action {
341            // ✅ FIXED: PASTE - Kompletten Text einfügen
342            KeyAction::PasteBuffer => {
343                if let Some(clipboard_text) = self.read_clipboard() {
344                    // Text säubern: Keine Newlines, nur druckbare Zeichen
345                    let clean_text = clipboard_text
346                        .replace('\n', " ")
347                        .replace('\r', "")
348                        .replace('\t', " ")
349                        .chars()
350                        .filter(|c| !c.is_control() || *c == ' ')
351                        .collect::<String>();
352
353                    if !clean_text.is_empty() {
354                        // Verfügbaren Platz berechnen
355                        let current_length = self.content.graphemes(true).count();
356                        let available_space =
357                            self.config.input_max_length.saturating_sub(current_length);
358
359                        // Text kürzen falls nötig
360                        let paste_text = if clean_text.graphemes(true).count() > available_space {
361                            clean_text
362                                .graphemes(true)
363                                .take(available_space)
364                                .collect::<String>()
365                        } else {
366                            clean_text
367                        };
368
369                        if !paste_text.is_empty() {
370                            // An Cursor-Position einfügen
371                            let byte_pos = self.cursor.get_byte_position(&self.content);
372                            self.content.insert_str(byte_pos, &paste_text);
373
374                            // Cursor nach eingefügtem Text positionieren
375                            let chars_added = paste_text.graphemes(true).count();
376                            self.cursor.update_text_length(&self.content);
377                            for _ in 0..chars_added {
378                                self.cursor.move_right();
379                            }
380
381                            return Some(format!("📋 Pasted {} chars", chars_added));
382                        }
383                    }
384                }
385                Some("❌ Clipboard empty or invalid".to_string())
386            }
387
388            // ✅ FIXED: COPY - Ganzen Input kopieren oder Selection
389            KeyAction::CopySelection => {
390                if !self.content.is_empty() {
391                    if self.write_clipboard(&self.content) {
392                        Some(format!(
393                            "📋 Copied: \"{}\"",
394                            if self.content.len() > 50 {
395                                format!("{}...", &self.content[..50])
396                            } else {
397                                self.content.clone()
398                            }
399                        ))
400                    } else {
401                        Some("❌ Copy failed".to_string())
402                    }
403                } else {
404                    Some("❌ Nothing to copy".to_string())
405                }
406            }
407
408            // Rest bleibt gleich...
409            KeyAction::Submit => {
410                if self.content.is_empty() {
411                    return None;
412                }
413                if self.validate_input(&self.content).is_ok() {
414                    let content = std::mem::take(&mut self.content);
415                    self.cursor.reset_for_empty_text();
416                    self.history_manager.add_entry(content.clone());
417                    let result = self.command_handler.handle_input(&content);
418
419                    if let Some(event) = HistoryEventHandler::handle_command_result(&result.message)
420                    {
421                        return Some(self.handle_history_event(event));
422                    }
423                    if result.message.starts_with("__CONFIRM_EXIT__") {
424                        self.waiting_for_exit_confirmation = true;
425                        return Some(result.message.replace("__CONFIRM_EXIT__", ""));
426                    }
427                    if result.message.starts_with("__CONFIRM_RESTART__") {
428                        self.waiting_for_restart_confirmation = true;
429                        return Some(result.message.replace("__CONFIRM_RESTART__", ""));
430                    }
431                    if result.message.starts_with("__RESTART_FORCE__")
432                        || result.message.starts_with("__RESTART__")
433                    {
434                        let feedback_text = if result.message.starts_with("__RESTART_FORCE__") {
435                            result
436                                .message
437                                .replace("__RESTART_FORCE__", "")
438                                .trim()
439                                .to_string()
440                        } else {
441                            result.message.replace("__RESTART__", "").trim().to_string()
442                        };
443                        if !feedback_text.is_empty() {
444                            return Some(format!("__RESTART_WITH_MSG__{}", feedback_text));
445                        } else {
446                            return Some("__RESTART__".to_string());
447                        }
448                    }
449                    if result.should_exit {
450                        return Some(format!("__EXIT__{}", result.message));
451                    }
452                    return Some(result.message);
453                }
454                None
455            }
456
457            KeyAction::InsertChar(c) => {
458                if self.content.graphemes(true).count() < self.config.input_max_length {
459                    let byte_pos = self.cursor.get_byte_position(&self.content);
460                    self.content.insert(byte_pos, c);
461                    self.cursor.update_text_length(&self.content);
462                    self.cursor.move_right();
463                }
464                None
465            }
466
467            KeyAction::ClearLine => {
468                if !self.content.is_empty() {
469                    // Vor dem Löschen in Clipboard kopieren
470                    if self.write_clipboard(&self.content) {
471                        let old_content = self.content.clone();
472                        self.content.clear();
473                        self.cursor.reset_for_empty_text();
474                        self.history_manager.reset_position();
475                        Some(format!(
476                            "📋 Cut: \"{}\"",
477                            if old_content.len() > 50 {
478                                format!("{}...", &old_content[..50])
479                            } else {
480                                old_content
481                            }
482                        ))
483                    } else {
484                        self.content.clear();
485                        self.cursor.reset_for_empty_text();
486                        self.history_manager.reset_position();
487                        Some("Input cleared".to_string())
488                    }
489                } else {
490                    None
491                }
492            }
493
494            // Cursor movement...
495            KeyAction::MoveLeft => {
496                self.cursor.move_left();
497                None
498            }
499            KeyAction::MoveRight => {
500                self.cursor.move_right();
501                None
502            }
503            KeyAction::MoveToStart => {
504                self.cursor.move_to_start();
505                None
506            }
507            KeyAction::MoveToEnd => {
508                self.cursor.move_to_end();
509                None
510            }
511
512            KeyAction::Backspace => {
513                if self.content.is_empty() || self.cursor.get_position() == 0 {
514                    return None;
515                }
516                let current_byte_pos = self.cursor.get_byte_position(&self.content);
517                let prev_byte_pos = self.cursor.get_prev_byte_position(&self.content);
518                if prev_byte_pos >= current_byte_pos || current_byte_pos > self.content.len() {
519                    self.cursor.update_text_length(&self.content);
520                    return None;
521                }
522                self.cursor.move_left();
523                self.content
524                    .replace_range(prev_byte_pos..current_byte_pos, "");
525                self.cursor.update_text_length(&self.content);
526                if self.content.is_empty() {
527                    self.cursor.reset_for_empty_text();
528                }
529                None
530            }
531
532            KeyAction::Delete => {
533                let text_length = self.content.graphemes(true).count();
534                if self.cursor.get_position() >= text_length || text_length == 0 {
535                    return None;
536                }
537                let current_byte_pos = self.cursor.get_byte_position(&self.content);
538                let next_byte_pos = self.cursor.get_next_byte_position(&self.content);
539                if current_byte_pos >= next_byte_pos || next_byte_pos > self.content.len() {
540                    self.cursor.update_text_length(&self.content);
541                    return None;
542                }
543                self.content
544                    .replace_range(current_byte_pos..next_byte_pos, "");
545                self.cursor.update_text_length(&self.content);
546                if self.content.is_empty() {
547                    self.cursor.reset_for_empty_text();
548                }
549                None
550            }
551
552            KeyAction::ScrollUp
553            | KeyAction::ScrollDown
554            | KeyAction::PageUp
555            | KeyAction::PageDown
556            | KeyAction::Cancel
557            | KeyAction::Quit
558            | KeyAction::NoAction => None,
559        }
560    }
561
562    pub fn export_state(&self) -> InputStateBackup {
563        InputStateBackup {
564            content: self.content.clone(),
565            history: self.history_manager.get_all_entries(),
566            cursor_pos: self.cursor.get_current_position(),
567        }
568    }
569
570    pub fn import_state(&mut self, backup: InputStateBackup) {
571        self.content = backup.content;
572        self.history_manager.import_entries(backup.history);
573        self.cursor.update_text_length(&self.content);
574    }
575
576    pub fn get_content(&self) -> &str {
577        &self.content
578    }
579
580    pub fn get_history_count(&self) -> usize {
581        self.history_manager.entry_count()
582    }
583}
584
585impl Widget for InputState {
586    fn render(&self) -> Paragraph {
587        self.render_with_cursor().0
588    }
589
590    fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
591        use unicode_width::UnicodeWidthStr;
592
593        let graphemes: Vec<&str> = self.content.graphemes(true).collect();
594        let cursor_pos = self.cursor.get_position();
595
596        let prompt_display = self.config.theme.input_cursor_prefix.clone();
597        let prompt_width = prompt_display.width();
598
599        let available_width = self
600            .config
601            .input_max_length
602            .saturating_sub(prompt_width + 4);
603
604        let viewport_start = if cursor_pos > available_width {
605            cursor_pos - available_width + 10
606        } else {
607            0
608        };
609
610        let mut spans = Vec::new();
611        spans.push(Span::styled(
612            prompt_display,
613            Style::default().fg(self.config.theme.input_cursor_color.into()),
614        ));
615
616        let end_pos = (viewport_start + available_width).min(graphemes.len());
617        let visible = graphemes
618            .get(viewport_start..end_pos)
619            .unwrap_or(&[])
620            .join("");
621        spans.push(Span::styled(
622            visible,
623            Style::default().fg(self.config.theme.input_text.into()),
624        ));
625
626        let paragraph = Paragraph::new(Line::from(spans)).block(
627            Block::default()
628                .padding(Padding::new(3, 1, 1, 1))
629                .borders(Borders::NONE)
630                .style(Style::default().bg(self.config.theme.input_bg.into())),
631        );
632
633        let cursor_coord = if self.cursor.is_visible() {
634            let visible_chars_before_cursor = if cursor_pos > viewport_start {
635                let chars_before = graphemes.get(viewport_start..cursor_pos).unwrap_or(&[]);
636                chars_before
637                    .iter()
638                    .map(|g| UnicodeWidthStr::width(*g))
639                    .sum::<usize>()
640            } else {
641                0
642            };
643
644            let rel_x = (prompt_width + visible_chars_before_cursor) as u16;
645            let rel_y = 0u16;
646
647            Some((rel_x, rel_y))
648        } else {
649            None
650        };
651
652        (paragraph, cursor_coord)
653    }
654
655    fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
656        self.handle_key_event(key)
657    }
658
659    fn as_input_state(&mut self) -> Option<&mut dyn InputWidget> {
660        Some(self)
661    }
662
663    fn get_backup_data(&self) -> Option<InputStateBackup> {
664        Some(self.export_state())
665    }
666
667    fn restore_backup_data(&mut self, backup: InputStateBackup) {
668        self.import_state(backup);
669    }
670}
671
672impl InputWidget for InputState {
673    fn update_cursor_blink(&mut self) {
674        self.cursor.update_blink();
675    }
676}