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