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