Skip to main content

rush_sync_server/input/
state.rs

1// src/input/state.rs
2
3use crate::commands::handler::CommandHandler;
4use crate::commands::history::{
5    HistoryAction, HistoryConfig, HistoryEvent, HistoryEventHandler, HistoryKeyboardHandler,
6    HistoryManager,
7};
8use crate::core::prelude::*;
9use crate::input::keyboard::{KeyAction, KeyboardManager};
10use crate::ui::cursor::{CursorKind, UiCursor};
11use crate::ui::widget::{AnimatedWidget, CursorWidget, StatefulWidget, Widget};
12use ratatui::prelude::*;
13use ratatui::widgets::{Block, Borders, Padding, Paragraph};
14use unicode_segmentation::UnicodeSegmentation;
15use unicode_width::UnicodeWidthStr;
16
17// Central system command processor
18#[derive(Default)]
19pub struct SystemCommandProcessor {
20    pending_confirmation: Option<PendingConfirmation>,
21}
22
23#[derive(Debug, Clone)]
24struct PendingConfirmation {
25    action: SystemAction,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29enum SystemAction {
30    Exit,
31    Restart,
32    ClearHistory,
33    CleanupExecute(String),
34}
35
36impl SystemCommandProcessor {
37    /// Process system commands and cleanup confirmations.
38    pub fn process_command(&mut self, input: &str) -> SystemCommandResult {
39        // Direct system commands
40        if let Some(result) = self.handle_system_commands(input) {
41            return result;
42        }
43
44        // Confirmation requests (including cleanup)
45        if let Some(result) = self.handle_confirmation_requests(input) {
46            return result;
47        }
48
49        // User confirmations
50        if self.pending_confirmation.is_some() {
51            return self.handle_user_confirmation(input);
52        }
53
54        SystemCommandResult::NotSystemCommand
55    }
56
57    fn handle_system_commands(&mut self, input: &str) -> Option<SystemCommandResult> {
58        use crate::core::constants::*;
59        match input.trim() {
60            s if s == SIG_CLEAR => Some(SystemCommandResult::ClearScreen),
61            s if s == SIG_EXIT => Some(SystemCommandResult::Exit),
62            s if s == SIG_RESTART || s == SIG_RESTART_FORCE => Some(SystemCommandResult::Restart),
63            s if s == SIG_CLEAR_HISTORY => Some(SystemCommandResult::ClearHistory),
64            _ => None,
65        }
66    }
67
68    fn handle_confirmation_requests(&mut self, input: &str) -> Option<SystemCommandResult> {
69        use crate::core::constants::*;
70
71        let confirm_exit = format!("{}{}", SIG_CONFIRM_PREFIX, SIG_EXIT);
72        let confirm_restart = format!("{}{}", SIG_CONFIRM_PREFIX, SIG_RESTART);
73        let confirm_history = format!("{}{}", SIG_CONFIRM_PREFIX, SIG_CLEAR_HISTORY);
74        let confirm_cleanup = format!("{}{}", SIG_CONFIRM_PREFIX, SIG_CONFIRM_CLEANUP);
75
76        if let Some(prompt) = input.strip_prefix(&confirm_exit) {
77            self.pending_confirmation = Some(PendingConfirmation {
78                action: SystemAction::Exit,
79            });
80            return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
81        }
82
83        if let Some(prompt) = input.strip_prefix(&confirm_restart) {
84            self.pending_confirmation = Some(PendingConfirmation {
85                action: SystemAction::Restart,
86            });
87            return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
88        }
89
90        if let Some(prompt) = input.strip_prefix(&confirm_history) {
91            self.pending_confirmation = Some(PendingConfirmation {
92                action: SystemAction::ClearHistory,
93            });
94            return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
95        }
96
97        if let Some(rest) = input.strip_prefix(&confirm_cleanup) {
98            if let Some((force_command, prompt)) = rest.split_once("__") {
99                self.pending_confirmation = Some(PendingConfirmation {
100                    action: SystemAction::CleanupExecute(force_command.to_string()),
101                });
102                return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
103            }
104        }
105
106        None
107    }
108
109    /// Handle user confirmation input (y/n).
110    fn handle_user_confirmation(&mut self, input: &str) -> SystemCommandResult {
111        let confirm_key = t!("system.input.confirm.short").to_lowercase();
112        let user_input = input.trim().to_lowercase();
113
114        let Some(pending) = self.pending_confirmation.as_ref() else {
115            return SystemCommandResult::NotSystemCommand;
116        };
117
118        let result = if user_input == confirm_key {
119            match &pending.action {
120                SystemAction::Exit => SystemCommandResult::Exit,
121                SystemAction::Restart => SystemCommandResult::Restart,
122                SystemAction::ClearHistory => SystemCommandResult::ClearHistory,
123                SystemAction::CleanupExecute(force_command) => {
124                    SystemCommandResult::CleanupExecute(force_command.clone())
125                }
126            }
127        } else {
128            SystemCommandResult::Message(get_translation("system.input.cancelled", &[]))
129        };
130
131        self.pending_confirmation = None;
132        result
133    }
134
135    pub fn is_valid_confirmation_char(&self, c: char) -> bool {
136        if self.pending_confirmation.is_none() {
137            return false;
138        }
139
140        let confirm_char = t!("system.input.confirm.short").to_lowercase();
141        let cancel_char = t!("system.input.cancel.short").to_lowercase();
142        let char_str = c.to_lowercase().to_string();
143
144        [confirm_char, cancel_char].contains(&char_str)
145    }
146
147    pub fn is_waiting_for_confirmation(&self) -> bool {
148        self.pending_confirmation.is_some()
149    }
150
151    pub fn reset_for_language_change(&mut self) {
152        self.pending_confirmation = None;
153    }
154}
155
156#[derive(Debug, PartialEq)]
157pub enum SystemCommandResult {
158    NotSystemCommand,
159    ClearScreen,
160    Exit,
161    Restart,
162    ClearHistory,
163    CleanupExecute(String),
164    ShowPrompt(String),
165    Message(String),
166}
167
168pub struct InputState {
169    content: String,
170    cursor: UiCursor,
171    prompt: String,
172    history_manager: HistoryManager,
173    config: Config,
174    command_handler: CommandHandler,
175    keyboard_manager: KeyboardManager,
176    system_processor: SystemCommandProcessor,
177}
178
179#[derive(Debug, Clone, Default)]
180pub struct InputStateBackup {
181    pub content: String,
182    pub history: Vec<String>,
183    pub cursor_pos: usize,
184}
185
186impl InputState {
187    pub fn new(config: &Config) -> Self {
188        let history_config = HistoryConfig::from_main_config(config);
189        Self {
190            content: String::with_capacity(100),
191            cursor: UiCursor::from_config(config, CursorKind::Input),
192            prompt: config.theme.input_cursor_prefix.clone(),
193            history_manager: HistoryManager::new(history_config.max_entries),
194            config: config.clone(),
195            command_handler: CommandHandler::new(),
196            keyboard_manager: KeyboardManager::new(),
197            system_processor: SystemCommandProcessor::default(),
198        }
199    }
200
201    pub fn update_from_config(&mut self, config: &Config) {
202        self.cursor.update_from_config(config);
203        self.prompt = config.theme.input_cursor_prefix.clone();
204        self.config = config.clone();
205    }
206
207    pub fn reset_for_language_change(&mut self) {
208        self.system_processor.reset_for_language_change();
209        self.clear_input();
210    }
211
212    pub fn clear_history(&mut self) {
213        self.history_manager.clear();
214    }
215
216    pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
217        // History navigation
218        if let Some(action) = HistoryKeyboardHandler::get_history_action(&key) {
219            return self.handle_history(action);
220        }
221
222        if key.code == KeyCode::Esc {
223            return None;
224        }
225
226        let action = self.keyboard_manager.get_action(&key);
227
228        // Confirmation mode: only allow confirmation characters
229        if self.system_processor.is_waiting_for_confirmation() {
230            return self.handle_confirmation_input(action);
231        }
232
233        // Normal mode
234        match action {
235            KeyAction::Submit => self.handle_submit(),
236            KeyAction::PasteBuffer => self.handle_paste(),
237            KeyAction::CopySelection => self.handle_copy(),
238            KeyAction::ClearLine => self.handle_clear_line(),
239            KeyAction::InsertChar(c) => {
240                self.insert_char(c);
241                None
242            }
243            KeyAction::MoveLeft => {
244                self.cursor.move_left();
245                None
246            }
247            KeyAction::MoveRight => {
248                self.cursor.move_right();
249                None
250            }
251            KeyAction::MoveToStart => {
252                self.cursor.move_to_start();
253                None
254            }
255            KeyAction::MoveToEnd => {
256                self.cursor.move_to_end();
257                None
258            }
259            KeyAction::Backspace => {
260                self.handle_backspace();
261                None
262            }
263            KeyAction::Delete => {
264                self.handle_delete();
265                None
266            }
267            _ => None,
268        }
269    }
270
271    /// Handle input while waiting for confirmation (only y/n allowed).
272    fn handle_confirmation_input(&mut self, action: KeyAction) -> Option<String> {
273        match action {
274            KeyAction::Submit => {
275                let result = self.system_processor.process_command(&self.content);
276                self.clear_input();
277                self.convert_system_result(result)
278            }
279            KeyAction::InsertChar(c) => {
280                if self.system_processor.is_valid_confirmation_char(c) {
281                    self.content.clear();
282                    self.content.push(c);
283                    self.cursor.update_text_length(&self.content);
284                    self.cursor.move_to_end();
285                }
286                None
287            }
288            KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
289                self.clear_input();
290                None
291            }
292            _ => None,
293        }
294    }
295
296    fn handle_submit(&mut self) -> Option<String> {
297        if self.content.is_empty() || self.content.trim().is_empty() {
298            return None;
299        }
300
301        if self.content.graphemes(true).count() > 1024 {
302            return Some(get_translation("system.input.too_long", &["1024"]));
303        }
304
305        let input = self.content.trim().to_string();
306
307        // System command processing
308        let system_result = self.system_processor.process_command(&input);
309        if system_result != SystemCommandResult::NotSystemCommand {
310            self.clear_input();
311            return self.convert_system_result(system_result);
312        }
313
314        // Normal command processing
315        let content = std::mem::take(&mut self.content);
316        self.cursor.reset_for_empty_text();
317        self.history_manager.add_entry(content.clone());
318
319        let result = self.command_handler.handle_input(&content);
320
321        // Handle special responses (e.g. history events)
322        if let Some(event) = HistoryEventHandler::handle_command_result(&result.message) {
323            return Some(self.handle_history_event(event));
324        }
325
326        // Check for system responses
327        let system_result = self.system_processor.process_command(&result.message);
328        if system_result != SystemCommandResult::NotSystemCommand {
329            return self.convert_system_result(system_result);
330        }
331
332        // Standard response
333        if result.should_exit {
334            Some(format!(
335                "{}{}",
336                crate::core::constants::SIG_EXIT,
337                result.message
338            ))
339        } else {
340            Some(result.message)
341        }
342    }
343
344    fn convert_system_result(&mut self, result: SystemCommandResult) -> Option<String> {
345        use crate::core::constants::*;
346        match result {
347            SystemCommandResult::NotSystemCommand => None,
348            SystemCommandResult::ClearScreen => Some(SIG_CLEAR.to_string()),
349            SystemCommandResult::Exit => Some(SIG_EXIT.to_string()),
350            SystemCommandResult::Restart => Some(SIG_RESTART.to_string()),
351            SystemCommandResult::ClearHistory => {
352                self.clear_history();
353                Some(get_translation("system.input.history_cleared", &[]))
354            }
355            SystemCommandResult::CleanupExecute(force_command) => {
356                let result = self.command_handler.handle_input(&force_command);
357                Some(result.message)
358            }
359            SystemCommandResult::ShowPrompt(prompt) => Some(prompt),
360            SystemCommandResult::Message(msg) => Some(msg),
361        }
362    }
363
364    fn handle_history(&mut self, action: HistoryAction) -> Option<String> {
365        let entry = match action {
366            HistoryAction::NavigatePrevious => self.history_manager.navigate_previous(),
367            HistoryAction::NavigateNext => self.history_manager.navigate_next(),
368        };
369
370        if let Some(entry) = entry {
371            self.content = entry;
372            self.cursor.update_text_length(&self.content);
373            self.cursor.move_to_end();
374        }
375        None
376    }
377
378    fn handle_history_event(&mut self, event: HistoryEvent) -> String {
379        match event {
380            HistoryEvent::Clear => {
381                self.clear_history();
382                HistoryEventHandler::create_clear_response()
383            }
384            HistoryEvent::Add(entry) => {
385                self.history_manager.add_entry(entry);
386                String::new()
387            }
388            _ => String::new(),
389        }
390    }
391
392    fn handle_paste(&mut self) -> Option<String> {
393        let text = self.read_clipboard()?;
394        let clean = text
395            .replace(['\n', '\r', '\t'], " ")
396            .chars()
397            .filter(|c| !c.is_control() || *c == ' ')
398            .collect::<String>();
399
400        if clean.is_empty() {
401            return Some(get_translation("system.input.clipboard.empty", &[]));
402        }
403
404        let current_len = self.content.graphemes(true).count();
405        let available = self.config.input_max_length.saturating_sub(current_len);
406        let paste_text = clean.graphemes(true).take(available).collect::<String>();
407
408        if !paste_text.is_empty() {
409            let byte_pos = self.cursor.get_byte_position(&self.content);
410            self.content.insert_str(byte_pos, &paste_text);
411            let chars_added = paste_text.graphemes(true).count();
412            self.cursor.update_text_length(&self.content);
413
414            for _ in 0..chars_added {
415                self.cursor.move_right();
416            }
417            Some(get_translation(
418                "system.input.clipboard.pasted",
419                &[&chars_added.to_string()],
420            ))
421        } else {
422            Some(get_translation(
423                "system.input.clipboard.nothing_to_paste",
424                &[],
425            ))
426        }
427    }
428
429    fn handle_copy(&self) -> Option<String> {
430        if self.content.is_empty() {
431            return Some(get_translation(
432                "system.input.clipboard.nothing_to_copy",
433                &[],
434            ));
435        }
436
437        if self.write_clipboard(&self.content) {
438            let preview = if self.content.chars().count() > 50 {
439                format!("{}...", self.content.chars().take(50).collect::<String>())
440            } else {
441                self.content.clone()
442            };
443            Some(get_translation(
444                "system.input.clipboard.copied",
445                &[&preview],
446            ))
447        } else {
448            Some(get_translation("system.input.clipboard.copy_failed", &[]))
449        }
450    }
451
452    fn handle_clear_line(&mut self) -> Option<String> {
453        if self.content.is_empty() {
454            return None;
455        }
456
457        let result = if self.write_clipboard(&self.content) {
458            let preview = if self.content.chars().count() > 50 {
459                format!("{}...", self.content.chars().take(50).collect::<String>())
460            } else {
461                self.content.clone()
462            };
463            get_translation("system.input.clipboard.cut", &[&preview])
464        } else {
465            get_translation("system.input.clipboard.cleared", &[])
466        };
467
468        self.clear_input();
469        Some(result)
470    }
471
472    fn read_clipboard(&self) -> Option<String> {
473        let output = self.get_clipboard_cmd("read")?.output().ok()?;
474        let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
475        if text.is_empty() {
476            None
477        } else {
478            Some(text)
479        }
480    }
481
482    fn write_clipboard(&self, text: &str) -> bool {
483        if text.is_empty() {
484            return false;
485        }
486
487        if let Some(mut cmd) = self.get_clipboard_cmd("write") {
488            if let Ok(mut child) = cmd.stdin(std::process::Stdio::piped()).spawn() {
489                if let Some(stdin) = child.stdin.as_mut() {
490                    use std::io::Write;
491                    let _ = stdin.write_all(text.as_bytes());
492                }
493                return child.wait().is_ok();
494            }
495        }
496        false
497    }
498
499    fn get_clipboard_cmd(&self, op: &str) -> Option<std::process::Command> {
500        #[cfg(target_os = "macos")]
501        {
502            Some(std::process::Command::new(if op == "read" {
503                "pbpaste"
504            } else {
505                "pbcopy"
506            }))
507        }
508
509        #[cfg(target_os = "linux")]
510        {
511            let mut cmd = std::process::Command::new("xclip");
512            if op == "read" {
513                cmd.args(["-selection", "clipboard", "-o"]);
514            } else {
515                cmd.args(["-selection", "clipboard"]);
516            }
517            Some(cmd)
518        }
519
520        #[cfg(target_os = "windows")]
521        {
522            if op == "read" {
523                let mut cmd = std::process::Command::new("powershell");
524                cmd.args(["-Command", "Get-Clipboard"]);
525                Some(cmd)
526            } else {
527                None
528            }
529        }
530
531        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
532        None
533    }
534
535    fn insert_char(&mut self, c: char) {
536        if self.content.graphemes(true).count() < self.config.input_max_length {
537            let byte_pos = self.cursor.get_byte_position(&self.content);
538            self.content.insert(byte_pos, c);
539            self.cursor.update_text_length(&self.content);
540            self.cursor.move_right();
541        }
542    }
543
544    fn handle_backspace(&mut self) {
545        if self.content.is_empty() || self.cursor.get_position() == 0 {
546            return;
547        }
548
549        let current = self.cursor.get_byte_position(&self.content);
550        let prev = self.cursor.get_prev_byte_position(&self.content);
551
552        if prev < current && current <= self.content.len() {
553            self.cursor.move_left();
554            self.content.replace_range(prev..current, "");
555            self.cursor.update_text_length(&self.content);
556
557            if self.content.is_empty() {
558                self.cursor.reset_for_empty_text();
559            }
560        }
561    }
562
563    fn handle_delete(&mut self) {
564        let text_len = self.content.graphemes(true).count();
565        if self.cursor.get_position() >= text_len || text_len == 0 {
566            return;
567        }
568
569        let current = self.cursor.get_byte_position(&self.content);
570        let next = self.cursor.get_next_byte_position(&self.content);
571
572        if current < next && next <= self.content.len() {
573            self.content.replace_range(current..next, "");
574            self.cursor.update_text_length(&self.content);
575
576            if self.content.is_empty() {
577                self.cursor.reset_for_empty_text();
578            }
579        }
580    }
581
582    fn clear_input(&mut self) {
583        self.content.clear();
584        self.history_manager.reset_position();
585        self.cursor.move_to_start();
586    }
587
588    pub fn get_content(&self) -> &str {
589        &self.content
590    }
591
592    pub fn get_history_count(&self) -> usize {
593        self.history_manager.entry_count()
594    }
595}
596
597impl Widget for InputState {
598    fn render(&self) -> Paragraph<'_> {
599        self.render_with_cursor().0
600    }
601
602    fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
603        self.handle_key_event(key)
604    }
605}
606
607impl CursorWidget for InputState {
608    fn render_with_cursor(&self) -> (Paragraph<'_>, Option<(u16, u16)>) {
609        let graphemes: Vec<&str> = self.content.graphemes(true).collect();
610        let cursor_pos = self.cursor.get_position();
611        let prompt_width = self.prompt.width();
612        let available_width = self
613            .config
614            .input_max_length
615            .saturating_sub(prompt_width + 4);
616
617        // Viewport calculation
618        let viewport_start = if cursor_pos > available_width {
619            cursor_pos - available_width + 10
620        } else {
621            0
622        };
623
624        // Create spans
625        let mut spans = vec![Span::styled(
626            &self.prompt,
627            Style::default().fg(self.config.theme.input_cursor_color.into()),
628        )];
629
630        let end_pos = (viewport_start + available_width).min(graphemes.len());
631        let visible = graphemes
632            .get(viewport_start..end_pos)
633            .unwrap_or(&[])
634            .join("");
635        spans.push(Span::styled(
636            visible,
637            Style::default().fg(self.config.theme.input_text.into()),
638        ));
639
640        let paragraph = Paragraph::new(Line::from(spans)).block(
641            Block::default()
642                .padding(Padding::new(3, 1, 1, 1))
643                .borders(Borders::NONE)
644                .style(Style::default().bg(self.config.theme.input_bg.into())),
645        );
646
647        // Cursor coordinates
648        let cursor_coord = if self.cursor.is_visible() && cursor_pos >= viewport_start {
649            let chars_before = graphemes.get(viewport_start..cursor_pos).unwrap_or(&[]);
650            let visible_width: usize = chars_before
651                .iter()
652                .map(|g| UnicodeWidthStr::width(*g))
653                .sum();
654            Some(((prompt_width + visible_width) as u16, 0u16))
655        } else {
656            None
657        };
658
659        (paragraph, cursor_coord)
660    }
661}
662
663impl StatefulWidget for InputState {
664    fn export_state(&self) -> InputStateBackup {
665        InputStateBackup {
666            content: self.content.clone(),
667            history: self.history_manager.get_all_entries(),
668            cursor_pos: self.cursor.get_current_position(),
669        }
670    }
671
672    fn import_state(&mut self, state: InputStateBackup) {
673        self.content = state.content;
674        self.history_manager.import_entries(state.history);
675        self.cursor.update_text_length(&self.content);
676    }
677}
678
679impl AnimatedWidget for InputState {
680    fn tick(&mut self) {
681        self.cursor.update_blink();
682    }
683}