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