1use crate::commands::handler::CommandHandler;
3use crate::commands::history::{
4 HistoryAction, HistoryConfig, HistoryEvent, HistoryEventHandler, HistoryKeyboardHandler,
5 HistoryManager,
6};
7use crate::core::prelude::*;
8use crate::input::keyboard::{KeyAction, KeyboardManager};
9use crate::ui::cursor::{CursorKind, UiCursor};
10use crate::ui::widget::{AnimatedWidget, CursorWidget, StatefulWidget, Widget};
11use ratatui::prelude::*;
12use ratatui::widgets::{Block, Borders, Padding, Paragraph};
13use unicode_segmentation::UnicodeSegmentation;
14use unicode_width::UnicodeWidthStr;
15
16pub struct InputState {
17 content: String,
18 cursor: UiCursor,
19 prompt: String,
20 history_manager: HistoryManager,
21 config: Config,
22 command_handler: CommandHandler,
23 keyboard_manager: KeyboardManager,
24 confirmation_state: ConfirmationState,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28enum ConfirmationState {
29 None,
30 Exit,
31 Restart,
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct InputStateBackup {
36 pub content: String,
37 pub history: Vec<String>,
38 pub cursor_pos: usize,
39}
40
41impl InputState {
42 pub fn new(config: &Config) -> Self {
43 let history_config = HistoryConfig::from_main_config(config);
44 Self {
45 content: String::with_capacity(100),
46 cursor: UiCursor::from_config(config, CursorKind::Input),
47 prompt: config.theme.input_cursor_prefix.clone(),
48 history_manager: HistoryManager::new(history_config.max_entries),
49 config: config.clone(),
50 command_handler: CommandHandler::new(),
51 keyboard_manager: KeyboardManager::new(),
52 confirmation_state: ConfirmationState::None,
53 }
54 }
55
56 pub fn update_from_config(&mut self, config: &Config) {
57 self.cursor.update_from_config(config);
58 self.prompt = config.theme.input_cursor_prefix.clone();
59 self.config = config.clone();
60 }
61
62 pub fn reset_for_language_change(&mut self) {
63 self.confirmation_state = ConfirmationState::None;
64 self.clear_input();
65 }
66
67 pub fn get_content(&self) -> &str {
68 &self.content
69 }
70 pub fn get_history_count(&self) -> usize {
71 self.history_manager.entry_count()
72 }
73
74 pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
76 if let Some(action) = HistoryKeyboardHandler::get_history_action(&key) {
78 return self.handle_history(action);
79 }
80
81 if key.code == KeyCode::Esc {
82 return None;
83 }
84
85 let action = self.keyboard_manager.get_action(&key);
86
87 if self.confirmation_state != ConfirmationState::None {
89 return self.handle_confirmation(action);
90 }
91
92 match action {
94 KeyAction::Submit => self.handle_submit(),
95 KeyAction::PasteBuffer => self.handle_paste(),
96 KeyAction::CopySelection => self.handle_copy(),
97 KeyAction::ClearLine => self.handle_clear_line(),
98 KeyAction::InsertChar(c) => {
99 self.insert_char(c);
100 None
101 }
102 KeyAction::MoveLeft => {
103 self.cursor.move_left();
104 None
105 }
106 KeyAction::MoveRight => {
107 self.cursor.move_right();
108 None
109 }
110 KeyAction::MoveToStart => {
111 self.cursor.move_to_start();
112 None
113 }
114 KeyAction::MoveToEnd => {
115 self.cursor.move_to_end();
116 None
117 }
118 KeyAction::Backspace => {
119 self.handle_backspace();
120 None
121 }
122 KeyAction::Delete => {
123 self.handle_delete();
124 None
125 }
126 _ => None,
127 }
128 }
129
130 fn handle_confirmation(&mut self, action: KeyAction) -> Option<String> {
132 match action {
133 KeyAction::Submit => {
134 let confirm = t!("system.input.confirm.short").to_lowercase();
135 let result = if self.content.trim().to_lowercase() == confirm {
136 match self.confirmation_state {
137 ConfirmationState::Exit => "__EXIT__".to_string(),
138 ConfirmationState::Restart => "__RESTART__".to_string(),
139 _ => get_translation("system.input.cancelled", &[]),
140 }
141 } else {
142 get_translation("system.input.cancelled", &[])
143 };
144
145 self.confirmation_state = ConfirmationState::None;
146 self.clear_input();
147 Some(result)
148 }
149 KeyAction::InsertChar(c) => {
150 let confirm_char = t!("system.input.confirm.short").to_lowercase();
151 let cancel_char = t!("system.input.cancel.short").to_lowercase();
152
153 if [confirm_char, cancel_char].contains(&c.to_lowercase().to_string()) {
154 self.content.clear();
155 self.content.push(c);
156 self.cursor.update_text_length(&self.content);
157 self.cursor.move_to_end();
158 }
159 None
160 }
161 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
162 self.clear_input();
163 None
164 }
165 _ => None,
166 }
167 }
168
169 fn handle_history(&mut self, action: HistoryAction) -> Option<String> {
170 let entry = match action {
171 HistoryAction::NavigatePrevious => self.history_manager.navigate_previous(),
172 HistoryAction::NavigateNext => self.history_manager.navigate_next(),
173 };
174
175 if let Some(entry) = entry {
176 self.content = entry;
177 self.cursor.update_text_length(&self.content);
178 self.cursor.move_to_end();
179 }
180 None
181 }
182
183 fn handle_submit(&mut self) -> Option<String> {
184 if self.content.is_empty() || self.content.trim().is_empty() {
185 return None;
186 }
187
188 if self.content.graphemes(true).count() > 1024 {
189 return Some(get_translation("system.input.too_long", &["1024"]));
190 }
191
192 let content = std::mem::take(&mut self.content);
193 self.cursor.reset_for_empty_text();
194 self.history_manager.add_entry(content.clone());
195
196 let result = self.command_handler.handle_input(&content);
197
198 if let Some(event) = HistoryEventHandler::handle_command_result(&result.message) {
200 return Some(self.handle_history_event(event));
201 }
202
203 if result.message.starts_with("__CONFIRM_EXIT__") {
205 self.confirmation_state = ConfirmationState::Exit;
206 return Some(result.message.replace("__CONFIRM_EXIT__", ""));
207 }
208 if result.message.starts_with("__CONFIRM_RESTART__") {
209 self.confirmation_state = ConfirmationState::Restart;
210 return Some(result.message.replace("__CONFIRM_RESTART__", ""));
211 }
212
213 if result.message.starts_with("__RESTART") {
215 let feedback = if result.message.starts_with("__RESTART_FORCE__") {
216 result
217 .message
218 .replace("__RESTART_FORCE__", "")
219 .trim()
220 .to_string()
221 } else {
222 result.message.replace("__RESTART__", "").trim().to_string()
223 };
224
225 return Some(if feedback.is_empty() {
226 "__RESTART__".to_string()
227 } else {
228 format!("__RESTART_WITH_MSG__{}", feedback)
229 });
230 }
231
232 if result.should_exit {
233 Some(format!("__EXIT__{}", result.message))
234 } else {
235 Some(result.message)
236 }
237 }
238
239 fn handle_history_event(&mut self, event: HistoryEvent) -> String {
240 match event {
241 HistoryEvent::Clear => {
242 self.history_manager.clear();
243 HistoryEventHandler::create_clear_response()
244 }
245 HistoryEvent::Add(entry) => {
246 self.history_manager.add_entry(entry);
247 String::new()
248 }
249 _ => String::new(),
250 }
251 }
252
253 fn handle_paste(&mut self) -> Option<String> {
255 let text = self.read_clipboard()?;
256 let clean = text
257 .replace(['\n', '\r', '\t'], " ")
258 .chars()
259 .filter(|c| !c.is_control() || *c == ' ')
260 .collect::<String>();
261
262 if clean.is_empty() {
263 return Some(get_translation("system.input.clipboard.empty", &[]));
264 }
265
266 let current_len = self.content.graphemes(true).count();
267 let available = self.config.input_max_length.saturating_sub(current_len);
268 let paste_text = clean.graphemes(true).take(available).collect::<String>();
269
270 if !paste_text.is_empty() {
271 let byte_pos = self.cursor.get_byte_position(&self.content);
272 self.content.insert_str(byte_pos, &paste_text);
273 let chars_added = paste_text.graphemes(true).count();
274 self.cursor.update_text_length(&self.content);
275
276 for _ in 0..chars_added {
277 self.cursor.move_right();
278 }
279 Some(get_translation(
280 "system.input.clipboard.pasted",
281 &[&chars_added.to_string()],
282 ))
283 } else {
284 Some(get_translation(
285 "system.input.clipboard.nothing_to_paste",
286 &[],
287 ))
288 }
289 }
290
291 fn handle_copy(&self) -> Option<String> {
292 if self.content.is_empty() {
293 return Some(get_translation(
294 "system.input.clipboard.nothing_to_copy",
295 &[],
296 ));
297 }
298
299 if self.write_clipboard(&self.content) {
300 let preview = if self.content.chars().count() > 50 {
301 format!("{}...", self.content.chars().take(50).collect::<String>())
302 } else {
303 self.content.clone()
304 };
305 Some(get_translation(
306 "system.input.clipboard.copied",
307 &[&preview],
308 ))
309 } else {
310 Some(get_translation("system.input.clipboard.copy_failed", &[]))
311 }
312 }
313
314 fn handle_clear_line(&mut self) -> Option<String> {
315 if self.content.is_empty() {
316 return None;
317 }
318
319 let result = if self.write_clipboard(&self.content) {
320 let preview = if self.content.chars().count() > 50 {
321 format!("{}...", self.content.chars().take(50).collect::<String>())
322 } else {
323 self.content.clone()
324 };
325 get_translation("system.input.clipboard.cut", &[&preview])
326 } else {
327 get_translation("system.input.clipboard.cleared", &[])
328 };
329
330 self.clear_input();
331 Some(result)
332 }
333
334 fn read_clipboard(&self) -> Option<String> {
336 let output = self.get_clipboard_cmd("read")?.output().ok()?;
337 let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
338 if text.is_empty() {
339 None
340 } else {
341 Some(text)
342 }
343 }
344
345 fn write_clipboard(&self, text: &str) -> bool {
346 if text.is_empty() {
347 return false;
348 }
349
350 if let Some(mut cmd) = self.get_clipboard_cmd("write") {
351 if let Ok(mut child) = cmd.stdin(std::process::Stdio::piped()).spawn() {
352 if let Some(stdin) = child.stdin.as_mut() {
353 use std::io::Write;
354 let _ = stdin.write_all(text.as_bytes());
355 }
356 return child.wait().is_ok();
357 }
358 }
359 false
360 }
361
362 fn get_clipboard_cmd(&self, op: &str) -> Option<std::process::Command> {
363 #[cfg(target_os = "macos")]
364 {
365 Some(std::process::Command::new(if op == "read" {
366 "pbpaste"
367 } else {
368 "pbcopy"
369 }))
370 }
371
372 #[cfg(target_os = "linux")]
373 {
374 let mut cmd = std::process::Command::new("xclip");
375 if op == "read" {
376 cmd.args(["-selection", "clipboard", "-o"]);
377 } else {
378 cmd.args(["-selection", "clipboard"]);
379 }
380 Some(cmd)
381 }
382
383 #[cfg(target_os = "windows")]
384 {
385 if op == "read" {
386 let mut cmd = std::process::Command::new("powershell");
387 cmd.args(["-Command", "Get-Clipboard"]);
388 Some(cmd)
389 } else {
390 None }
392 }
393
394 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
395 None
396 }
397
398 fn insert_char(&mut self, c: char) {
400 if self.content.graphemes(true).count() < self.config.input_max_length {
401 let byte_pos = self.cursor.get_byte_position(&self.content);
402 self.content.insert(byte_pos, c);
403 self.cursor.update_text_length(&self.content);
404 self.cursor.move_right();
405 }
406 }
407
408 fn handle_backspace(&mut self) {
409 if self.content.is_empty() || self.cursor.get_position() == 0 {
410 return;
411 }
412
413 let current = self.cursor.get_byte_position(&self.content);
414 let prev = self.cursor.get_prev_byte_position(&self.content);
415
416 if prev < current && current <= self.content.len() {
417 self.cursor.move_left();
418 self.content.replace_range(prev..current, "");
419 self.cursor.update_text_length(&self.content);
420
421 if self.content.is_empty() {
422 self.cursor.reset_for_empty_text();
423 }
424 }
425 }
426
427 fn handle_delete(&mut self) {
428 let text_len = self.content.graphemes(true).count();
429 if self.cursor.get_position() >= text_len || text_len == 0 {
430 return;
431 }
432
433 let current = self.cursor.get_byte_position(&self.content);
434 let next = self.cursor.get_next_byte_position(&self.content);
435
436 if current < next && next <= self.content.len() {
437 self.content.replace_range(current..next, "");
438 self.cursor.update_text_length(&self.content);
439
440 if self.content.is_empty() {
441 self.cursor.reset_for_empty_text();
442 }
443 }
444 }
445
446 fn clear_input(&mut self) {
447 self.content.clear();
448 self.history_manager.reset_position();
449 self.cursor.move_to_start();
450 }
451}
452
453impl Widget for InputState {
455 fn render(&self) -> Paragraph {
456 self.render_with_cursor().0
457 }
458
459 fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
460 self.handle_key_event(key)
461 }
462}
463
464impl CursorWidget for InputState {
465 fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
466 let graphemes: Vec<&str> = self.content.graphemes(true).collect();
467 let cursor_pos = self.cursor.get_position();
468 let prompt_width = self.prompt.width();
469 let available_width = self
470 .config
471 .input_max_length
472 .saturating_sub(prompt_width + 4);
473
474 let viewport_start = if cursor_pos > available_width {
476 cursor_pos - available_width + 10
477 } else {
478 0
479 };
480
481 let mut spans = vec![Span::styled(
483 &self.prompt,
484 Style::default().fg(self.config.theme.input_cursor_color.into()),
485 )];
486
487 let end_pos = (viewport_start + available_width).min(graphemes.len());
488 let visible = graphemes
489 .get(viewport_start..end_pos)
490 .unwrap_or(&[])
491 .join("");
492 spans.push(Span::styled(
493 visible,
494 Style::default().fg(self.config.theme.input_text.into()),
495 ));
496
497 let paragraph = Paragraph::new(Line::from(spans)).block(
498 Block::default()
499 .padding(Padding::new(3, 1, 1, 1))
500 .borders(Borders::NONE)
501 .style(Style::default().bg(self.config.theme.input_bg.into())),
502 );
503
504 let cursor_coord = if self.cursor.is_visible() && cursor_pos >= viewport_start {
506 let chars_before = graphemes.get(viewport_start..cursor_pos).unwrap_or(&[]);
507 let visible_width: usize = chars_before
508 .iter()
509 .map(|g| UnicodeWidthStr::width(*g))
510 .sum();
511 Some(((prompt_width + visible_width) as u16, 0u16))
512 } else {
513 None
514 };
515
516 (paragraph, cursor_coord)
517 }
518}
519
520impl StatefulWidget for InputState {
521 fn export_state(&self) -> InputStateBackup {
522 InputStateBackup {
523 content: self.content.clone(),
524 history: self.history_manager.get_all_entries(),
525 cursor_pos: self.cursor.get_current_position(),
526 }
527 }
528
529 fn import_state(&mut self, state: InputStateBackup) {
530 self.content = state.content;
531 self.history_manager.import_entries(state.history);
532 self.cursor.update_text_length(&self.content);
533 }
534}
535
536impl AnimatedWidget for InputState {
537 fn tick(&mut self) {
538 self.cursor.update_blink();
539 }
540}