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}
37
38impl SystemCommandProcessor {
39 pub fn process_command(&mut self, input: &str) -> SystemCommandResult {
41 if let Some(result) = self.handle_system_commands(input) {
43 return result;
44 }
45
46 if let Some(result) = self.handle_confirmation_requests(input) {
48 return result;
49 }
50
51 if self.pending_confirmation.is_some() {
53 return self.handle_user_confirmation(input);
54 }
55
56 SystemCommandResult::NotSystemCommand
58 }
59
60 fn handle_system_commands(&mut self, input: &str) -> Option<SystemCommandResult> {
62 match input.trim() {
63 "__CLEAR__" => Some(SystemCommandResult::ClearScreen),
64 "__EXIT__" => Some(SystemCommandResult::Exit),
65 "__RESTART__" | "__RESTART_FORCE__" => Some(SystemCommandResult::Restart),
66 "__CLEAR_HISTORY__" => Some(SystemCommandResult::ClearHistory),
67 _ => None,
68 }
69 }
70
71 fn handle_confirmation_requests(&mut self, input: &str) -> Option<SystemCommandResult> {
73 if let Some(prompt) = input.strip_prefix("__CONFIRM:__EXIT__") {
75 self.pending_confirmation = Some(PendingConfirmation {
76 action: SystemAction::Exit,
77 });
78 return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
79 }
80
81 if let Some(prompt) = input.strip_prefix("__CONFIRM:__RESTART__") {
83 self.pending_confirmation = Some(PendingConfirmation {
84 action: SystemAction::Restart,
85 });
86 return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
87 }
88
89 if let Some(prompt) = input.strip_prefix("__CONFIRM:__CLEAR_HISTORY__") {
91 self.pending_confirmation = Some(PendingConfirmation {
92 action: SystemAction::ClearHistory,
93 });
94 return Some(SystemCommandResult::ShowPrompt(prompt.to_string()));
95 }
96
97 None
98 }
99
100 fn handle_user_confirmation(&mut self, input: &str) -> SystemCommandResult {
102 let confirm_key = t!("system.input.confirm.short").to_lowercase();
103 let user_input = input.trim().to_lowercase();
104
105 let result = if user_input == confirm_key {
106 match &self.pending_confirmation.as_ref().unwrap().action {
108 SystemAction::Exit => SystemCommandResult::Exit,
109 SystemAction::Restart => SystemCommandResult::Restart,
110 SystemAction::ClearHistory => SystemCommandResult::ClearHistory,
111 }
112 } else {
113 SystemCommandResult::Message(get_translation("system.input.cancelled", &[]))
115 };
116
117 self.pending_confirmation = None;
119 result
120 }
121
122 pub fn is_valid_confirmation_char(&self, c: char) -> bool {
124 if self.pending_confirmation.is_none() {
125 return false;
126 }
127
128 let confirm_char = t!("system.input.confirm.short").to_lowercase();
129 let cancel_char = t!("system.input.cancel.short").to_lowercase();
130 let char_str = c.to_lowercase().to_string();
131
132 [confirm_char, cancel_char].contains(&char_str)
133 }
134
135 pub fn is_waiting_for_confirmation(&self) -> bool {
137 self.pending_confirmation.is_some()
138 }
139
140 pub fn reset_for_language_change(&mut self) {
142 self.pending_confirmation = None;
143 }
144}
145
146#[derive(Debug, PartialEq)]
147pub enum SystemCommandResult {
148 NotSystemCommand,
149 ClearScreen,
150 Exit,
151 Restart,
152 ClearHistory,
153 ShowPrompt(String),
154 Message(String),
155}
156
157pub struct InputState {
162 content: String,
163 cursor: UiCursor,
164 prompt: String,
165 history_manager: HistoryManager,
166 config: Config,
167 command_handler: CommandHandler,
168 keyboard_manager: KeyboardManager,
169 system_processor: SystemCommandProcessor, }
171
172#[derive(Debug, Clone, Default)]
173pub struct InputStateBackup {
174 pub content: String,
175 pub history: Vec<String>,
176 pub cursor_pos: usize,
177}
178
179impl InputState {
180 pub fn new(config: &Config) -> Self {
181 let history_config = HistoryConfig::from_main_config(config);
182 Self {
183 content: String::with_capacity(100),
184 cursor: UiCursor::from_config(config, CursorKind::Input),
185 prompt: config.theme.input_cursor_prefix.clone(),
186 history_manager: HistoryManager::new(history_config.max_entries),
187 config: config.clone(),
188 command_handler: CommandHandler::new(),
189 keyboard_manager: KeyboardManager::new(),
190 system_processor: SystemCommandProcessor::default(),
191 }
192 }
193
194 pub fn update_from_config(&mut self, config: &Config) {
195 self.cursor.update_from_config(config);
196 self.prompt = config.theme.input_cursor_prefix.clone();
197 self.config = config.clone();
198 }
199
200 pub fn reset_for_language_change(&mut self) {
201 self.system_processor.reset_for_language_change(); self.clear_input();
203 }
204
205 pub fn clear_history(&mut self) {
207 self.history_manager.clear();
208 }
209
210 pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
215 if let Some(action) = HistoryKeyboardHandler::get_history_action(&key) {
217 return self.handle_history(action);
218 }
219
220 if key.code == KeyCode::Esc {
221 return None;
222 }
223
224 let action = self.keyboard_manager.get_action(&key);
225
226 if self.system_processor.is_waiting_for_confirmation() {
228 return self.handle_confirmation_input(action);
229 }
230
231 match action {
233 KeyAction::Submit => self.handle_submit(),
234 KeyAction::PasteBuffer => self.handle_paste(),
235 KeyAction::CopySelection => self.handle_copy(),
236 KeyAction::ClearLine => self.handle_clear_line(),
237 KeyAction::InsertChar(c) => {
238 self.insert_char(c);
239 None
240 }
241 KeyAction::MoveLeft => {
242 self.cursor.move_left();
243 None
244 }
245 KeyAction::MoveRight => {
246 self.cursor.move_right();
247 None
248 }
249 KeyAction::MoveToStart => {
250 self.cursor.move_to_start();
251 None
252 }
253 KeyAction::MoveToEnd => {
254 self.cursor.move_to_end();
255 None
256 }
257 KeyAction::Backspace => {
258 self.handle_backspace();
259 None
260 }
261 KeyAction::Delete => {
262 self.handle_delete();
263 None
264 }
265 _ => None,
266 }
267 }
268
269 fn handle_confirmation_input(&mut self, action: KeyAction) -> Option<String> {
271 match action {
272 KeyAction::Submit => {
273 let result = self.system_processor.process_command(&self.content);
274 self.clear_input();
275 self.convert_system_result(result)
276 }
277 KeyAction::InsertChar(c) => {
278 if self.system_processor.is_valid_confirmation_char(c) {
279 self.content.clear();
280 self.content.push(c);
281 self.cursor.update_text_length(&self.content);
282 self.cursor.move_to_end();
283 }
284 None
285 }
286 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
287 self.clear_input();
288 None
289 }
290 _ => None,
291 }
292 }
293
294 fn handle_submit(&mut self) -> Option<String> {
296 if self.content.is_empty() || self.content.trim().is_empty() {
297 return None;
298 }
299
300 if self.content.graphemes(true).count() > 1024 {
301 return Some(get_translation("system.input.too_long", &["1024"]));
302 }
303
304 let input = self.content.trim().to_string();
305
306 let system_result = self.system_processor.process_command(&input);
308 if system_result != SystemCommandResult::NotSystemCommand {
309 self.clear_input();
310 return self.convert_system_result(system_result);
311 }
312
313 let content = std::mem::take(&mut self.content);
315 self.cursor.reset_for_empty_text();
316 self.history_manager.add_entry(content.clone());
317
318 let result = self.command_handler.handle_input(&content);
319
320 if let Some(event) = HistoryEventHandler::handle_command_result(&result.message) {
322 return Some(self.handle_history_event(event));
323 }
324
325 let system_result = self.system_processor.process_command(&result.message);
327 if system_result != SystemCommandResult::NotSystemCommand {
328 return self.convert_system_result(system_result);
329 }
330
331 if result.should_exit {
333 Some(format!("__EXIT__{}", result.message))
334 } else {
335 Some(result.message)
336 }
337 }
338
339 fn convert_system_result(&mut self, result: SystemCommandResult) -> Option<String> {
341 match result {
342 SystemCommandResult::NotSystemCommand => None,
343 SystemCommandResult::ClearScreen => Some("__CLEAR__".to_string()),
344 SystemCommandResult::Exit => Some("__EXIT__".to_string()),
345 SystemCommandResult::Restart => Some("__RESTART__".to_string()),
346 SystemCommandResult::ClearHistory => {
347 self.clear_history(); Some(get_translation("system.input.history_cleared", &[]))
349 }
350 SystemCommandResult::ShowPrompt(prompt) => Some(prompt),
351 SystemCommandResult::Message(msg) => Some(msg),
352 }
353 }
354
355 fn handle_history(&mut self, action: HistoryAction) -> Option<String> {
360 let entry = match action {
361 HistoryAction::NavigatePrevious => self.history_manager.navigate_previous(),
362 HistoryAction::NavigateNext => self.history_manager.navigate_next(),
363 };
364
365 if let Some(entry) = entry {
366 self.content = entry;
367 self.cursor.update_text_length(&self.content);
368 self.cursor.move_to_end();
369 }
370 None
371 }
372
373 fn handle_history_event(&mut self, event: HistoryEvent) -> String {
374 match event {
375 HistoryEvent::Clear => {
376 self.clear_history(); HistoryEventHandler::create_clear_response()
378 }
379 HistoryEvent::Add(entry) => {
380 self.history_manager.add_entry(entry);
381 String::new()
382 }
383 _ => String::new(),
384 }
385 }
386
387 fn handle_paste(&mut self) -> Option<String> {
389 let text = self.read_clipboard()?;
390 let clean = text
391 .replace(['\n', '\r', '\t'], " ")
392 .chars()
393 .filter(|c| !c.is_control() || *c == ' ')
394 .collect::<String>();
395
396 if clean.is_empty() {
397 return Some(get_translation("system.input.clipboard.empty", &[]));
398 }
399
400 let current_len = self.content.graphemes(true).count();
401 let available = self.config.input_max_length.saturating_sub(current_len);
402 let paste_text = clean.graphemes(true).take(available).collect::<String>();
403
404 if !paste_text.is_empty() {
405 let byte_pos = self.cursor.get_byte_position(&self.content);
406 self.content.insert_str(byte_pos, &paste_text);
407 let chars_added = paste_text.graphemes(true).count();
408 self.cursor.update_text_length(&self.content);
409
410 for _ in 0..chars_added {
411 self.cursor.move_right();
412 }
413 Some(get_translation(
414 "system.input.clipboard.pasted",
415 &[&chars_added.to_string()],
416 ))
417 } else {
418 Some(get_translation(
419 "system.input.clipboard.nothing_to_paste",
420 &[],
421 ))
422 }
423 }
424
425 fn handle_copy(&self) -> Option<String> {
426 if self.content.is_empty() {
427 return Some(get_translation(
428 "system.input.clipboard.nothing_to_copy",
429 &[],
430 ));
431 }
432
433 if self.write_clipboard(&self.content) {
434 let preview = if self.content.chars().count() > 50 {
435 format!("{}...", self.content.chars().take(50).collect::<String>())
436 } else {
437 self.content.clone()
438 };
439 Some(get_translation(
440 "system.input.clipboard.copied",
441 &[&preview],
442 ))
443 } else {
444 Some(get_translation("system.input.clipboard.copy_failed", &[]))
445 }
446 }
447
448 fn handle_clear_line(&mut self) -> Option<String> {
449 if self.content.is_empty() {
450 return None;
451 }
452
453 let result = if self.write_clipboard(&self.content) {
454 let preview = if self.content.chars().count() > 50 {
455 format!("{}...", self.content.chars().take(50).collect::<String>())
456 } else {
457 self.content.clone()
458 };
459 get_translation("system.input.clipboard.cut", &[&preview])
460 } else {
461 get_translation("system.input.clipboard.cleared", &[])
462 };
463
464 self.clear_input();
465 Some(result)
466 }
467
468 fn read_clipboard(&self) -> Option<String> {
470 let output = self.get_clipboard_cmd("read")?.output().ok()?;
471 let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
472 if text.is_empty() {
473 None
474 } else {
475 Some(text)
476 }
477 }
478
479 fn write_clipboard(&self, text: &str) -> bool {
480 if text.is_empty() {
481 return false;
482 }
483
484 if let Some(mut cmd) = self.get_clipboard_cmd("write") {
485 if let Ok(mut child) = cmd.stdin(std::process::Stdio::piped()).spawn() {
486 if let Some(stdin) = child.stdin.as_mut() {
487 use std::io::Write;
488 let _ = stdin.write_all(text.as_bytes());
489 }
490 return child.wait().is_ok();
491 }
492 }
493 false
494 }
495
496 fn get_clipboard_cmd(&self, op: &str) -> Option<std::process::Command> {
497 #[cfg(target_os = "macos")]
498 {
499 Some(std::process::Command::new(if op == "read" {
500 "pbpaste"
501 } else {
502 "pbcopy"
503 }))
504 }
505
506 #[cfg(target_os = "linux")]
507 {
508 let mut cmd = std::process::Command::new("xclip");
509 if op == "read" {
510 cmd.args(["-selection", "clipboard", "-o"]);
511 } else {
512 cmd.args(["-selection", "clipboard"]);
513 }
514 Some(cmd)
515 }
516
517 #[cfg(target_os = "windows")]
518 {
519 if op == "read" {
520 let mut cmd = std::process::Command::new("powershell");
521 cmd.args(["-Command", "Get-Clipboard"]);
522 Some(cmd)
523 } else {
524 None
525 }
526 }
527
528 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
529 None
530 }
531
532 fn insert_char(&mut self, c: char) {
534 if self.content.graphemes(true).count() < self.config.input_max_length {
535 let byte_pos = self.cursor.get_byte_position(&self.content);
536 self.content.insert(byte_pos, c);
537 self.cursor.update_text_length(&self.content);
538 self.cursor.move_right();
539 }
540 }
541
542 fn handle_backspace(&mut self) {
543 if self.content.is_empty() || self.cursor.get_position() == 0 {
544 return;
545 }
546
547 let current = self.cursor.get_byte_position(&self.content);
548 let prev = self.cursor.get_prev_byte_position(&self.content);
549
550 if prev < current && current <= self.content.len() {
551 self.cursor.move_left();
552 self.content.replace_range(prev..current, "");
553 self.cursor.update_text_length(&self.content);
554
555 if self.content.is_empty() {
556 self.cursor.reset_for_empty_text();
557 }
558 }
559 }
560
561 fn handle_delete(&mut self) {
562 let text_len = self.content.graphemes(true).count();
563 if self.cursor.get_position() >= text_len || text_len == 0 {
564 return;
565 }
566
567 let current = self.cursor.get_byte_position(&self.content);
568 let next = self.cursor.get_next_byte_position(&self.content);
569
570 if current < next && next <= self.content.len() {
571 self.content.replace_range(current..next, "");
572 self.cursor.update_text_length(&self.content);
573
574 if self.content.is_empty() {
575 self.cursor.reset_for_empty_text();
576 }
577 }
578 }
579
580 fn clear_input(&mut self) {
581 self.content.clear();
582 self.history_manager.reset_position();
583 self.cursor.move_to_start();
584 }
585
586 pub fn get_content(&self) -> &str {
588 &self.content
589 }
590
591 pub fn get_history_count(&self) -> usize {
592 self.history_manager.entry_count()
593 }
594}
595
596impl 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}