1use 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#[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 pub fn process_command(&mut self, input: &str) -> SystemCommandResult {
39 if let Some(result) = self.handle_system_commands(input) {
41 return result;
42 }
43
44 if let Some(result) = self.handle_confirmation_requests(input) {
46 return result;
47 }
48
49 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 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 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 if self.system_processor.is_waiting_for_confirmation() {
230 return self.handle_confirmation_input(action);
231 }
232
233 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 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 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 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 if let Some(event) = HistoryEventHandler::handle_command_result(&result.message) {
323 return Some(self.handle_history_event(event));
324 }
325
326 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 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 let viewport_start = if cursor_pos > available_width {
619 cursor_pos - available_width + 10
620 } else {
621 0
622 };
623
624 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 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}