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, CursorType, 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 log::debug!(
60 "ā
InputState cursor updated via central API: {}",
61 self.cursor.debug_info()
62 );
63 }
64
65 pub fn validate_input(&self, input: &str) -> crate::core::error::Result<()> {
66 if input.trim().is_empty() {
67 return Err(AppError::Validation(get_translation(
68 "system.input.empty",
69 &[],
70 )));
71 }
72 let grapheme_count = input.graphemes(true).count();
73 let max_length = 1024;
74
75 if grapheme_count > max_length {
76 return Err(AppError::Validation(get_translation(
77 "system.input.too_long",
78 &[&max_length.to_string()],
79 )));
80 }
81 Ok(())
82 }
83
84 pub fn reset_for_language_change(&mut self) {
85 self.waiting_for_exit_confirmation = false;
86 self.waiting_for_restart_confirmation = false;
87 self.content.clear();
88 self.history_manager.reset_position();
89 self.cursor.move_to_start();
90 log::debug!("InputState reset for language change");
91 }
92
93 fn handle_exit_confirmation(&mut self, action: KeyAction) -> Option<String> {
94 match action {
95 KeyAction::Submit => {
96 self.waiting_for_exit_confirmation = false;
97 let confirm_short = crate::i18n::get_translation("system.input.confirm.short", &[]);
98 let cancel_short = crate::i18n::get_translation("system.input.cancel.short", &[]);
99 match self.content.trim().to_lowercase().as_str() {
100 input if input == confirm_short.to_lowercase() => {
101 self.content.clear();
102 Some("__EXIT__".to_string())
103 }
104 input if input == cancel_short.to_lowercase() => {
105 self.clear_input();
106 Some(crate::i18n::get_translation("system.input.cancelled", &[]))
107 }
108 _ => {
109 self.clear_input();
110 Some(crate::i18n::get_translation("system.input.cancelled", &[]))
111 }
112 }
113 }
114 KeyAction::InsertChar(c) => {
115 let confirm_short = crate::i18n::get_translation("system.input.confirm.short", &[]);
116 let cancel_short = crate::i18n::get_translation("system.input.cancel.short", &[]);
117 if c.to_lowercase().to_string() == confirm_short.to_lowercase()
118 || c.to_lowercase().to_string() == cancel_short.to_lowercase()
119 {
120 self.content.clear();
121 self.content.push(c);
122 self.cursor.update_text_length(&self.content);
123 self.cursor.move_to_end();
124 }
125 None
126 }
127 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
128 self.clear_input();
129 None
130 }
131 _ => None,
132 }
133 }
134
135 fn handle_restart_confirmation(&mut self, action: KeyAction) -> Option<String> {
136 match action {
137 KeyAction::Submit => {
138 self.waiting_for_restart_confirmation = false;
139 let confirm_short = crate::i18n::get_translation("system.input.confirm.short", &[]);
140 let cancel_short = crate::i18n::get_translation("system.input.cancel.short", &[]);
141 match self.content.trim().to_lowercase().as_str() {
142 input if input == confirm_short.to_lowercase() => {
143 self.content.clear();
144 Some("__RESTART__".to_string())
145 }
146 input if input == cancel_short.to_lowercase() => {
147 self.clear_input();
148 Some(crate::i18n::get_translation("system.input.cancelled", &[]))
149 }
150 _ => {
151 self.clear_input();
152 Some(crate::i18n::get_translation("system.input.cancelled", &[]))
153 }
154 }
155 }
156 KeyAction::InsertChar(c) => {
157 let confirm_short = crate::i18n::get_translation("system.input.confirm.short", &[]);
158 let cancel_short = crate::i18n::get_translation("system.input.cancel.short", &[]);
159 if c.to_lowercase().to_string() == confirm_short.to_lowercase()
160 || c.to_lowercase().to_string() == cancel_short.to_lowercase()
161 {
162 self.content.clear();
163 self.content.push(c);
164 self.cursor.update_text_length(&self.content);
165 self.cursor.move_to_end();
166 }
167 None
168 }
169 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
170 self.clear_input();
171 None
172 }
173 _ => None,
174 }
175 }
176
177 fn clear_input(&mut self) {
178 self.content.clear();
179 self.history_manager.reset_position();
180 self.cursor.move_to_start();
181 }
182
183 fn handle_history_action(&mut self, action: HistoryAction) -> Option<String> {
184 match action {
185 HistoryAction::NavigatePrevious => {
186 if let Some(entry) = self.history_manager.navigate_previous() {
187 self.content = entry;
188 self.cursor.update_text_length(&self.content);
189 self.cursor.move_to_end();
190 }
191 }
192 HistoryAction::NavigateNext => {
193 if let Some(entry) = self.history_manager.navigate_next() {
194 self.content = entry;
195 self.cursor.update_text_length(&self.content);
196 self.cursor.move_to_end();
197 }
198 }
199 }
200 None
201 }
202
203 fn handle_history_event(&mut self, event: HistoryEvent) -> String {
204 match event {
205 HistoryEvent::Clear => {
206 self.history_manager.clear();
207 HistoryEventHandler::create_clear_response()
208 }
209 HistoryEvent::Add(entry) => {
210 self.history_manager.add_entry(entry);
211 String::new()
212 }
213 _ => String::new(),
214 }
215 }
216
217 pub fn execute(&self) -> crate::core::error::Result<String> {
218 Ok(format!(
219 "__CONFIRM_EXIT__{}",
220 get_translation("system.input.confirm_exit", &[])
221 ))
222 }
223
224 pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
225 if let Some(history_action) = HistoryKeyboardHandler::get_history_action(&key) {
226 return self.handle_history_action(history_action);
227 }
228
229 if key.code == KeyCode::Esc {
230 return None;
231 }
232
233 let action = self.keyboard_manager.get_action(&key);
234
235 if self.waiting_for_exit_confirmation {
236 return self.handle_exit_confirmation(action);
237 }
238 if self.waiting_for_restart_confirmation {
239 return self.handle_restart_confirmation(action);
240 }
241
242 match action {
243 KeyAction::Submit => {
244 match self.content.trim() {
246 "cursor-debug" => {
247 let debug_info = format!(
248 "šÆ CURSOR COLOR DEBUG:\n\
249 š Theme: {}\n\
250 šØ Expected input_cursor_color: {}\n\
251 šØ Actual cursor color: {}\n\
252 šØ Actual fg color: {}\n\
253 š Cursor details:\n\
254 {}",
255 self.config.current_theme_name,
256 self.config.theme.input_cursor_color.to_name(),
257 self.cursor.color.to_name(),
258 self.cursor.fg.to_name(),
259 self.cursor.debug_info()
260 );
261 self.content.clear();
262 self.cursor.reset_for_empty_text();
263 Some(debug_info)
264 }
265
266 "theme-config-debug" => {
267 let debug_info = format!(
268 "š COMPLETE THEME CONFIG DEBUG:\n\
269 š Current Theme: {}\n\
270 šØ input_cursor_color: {} ā¬
ļø CONFIG VALUE\n\
271 šØ input_cursor: {}\n\
272 šØ input_cursor_prefix: '{}'\n\
273 šØ output_cursor_color: {}\n\
274 šØ output_cursor: {}\n\
275 \nšÆ ACTUAL CURSOR STATE:\n\
276 šØ cursor.color: {} ā¬
ļø ACTUAL VALUE\n\
277 šÆ cursor.ctype: {:?}\n\
278 šļø cursor.visible: {}",
279 self.config.current_theme_name,
280 self.config.theme.input_cursor_color.to_name(),
281 self.config.theme.input_cursor,
282 self.config.theme.input_cursor_prefix,
283 self.config.theme.output_cursor_color.to_name(),
284 self.config.theme.output_cursor,
285 self.cursor.color.to_name(),
286 self.cursor.ctype,
287 self.cursor.is_visible()
288 );
289 self.content.clear();
290 self.cursor.reset_for_empty_text();
291 Some(debug_info)
292 }
293
294 "color-test" => {
295 let test_colors = vec![
296 "Red",
297 "Green",
298 "Blue",
299 "Yellow",
300 "Magenta",
301 "Cyan",
302 "LightRed",
303 "LightGreen",
304 "LightBlue",
305 "LightYellow",
306 "LightMagenta",
307 "LightCyan",
308 "White",
309 "Black",
310 ];
311
312 let mut results = String::from("šØ COLOR CONVERSION TEST:\n");
313 for color_name in test_colors {
314 match crate::ui::color::AppColor::from_string(color_name) {
315 Ok(color) => {
316 results.push_str(&format!(
317 "ā
'{}' ā '{}'\n",
318 color_name,
319 color.to_name()
320 ));
321 }
322 Err(e) => {
323 results
324 .push_str(&format!("ā '{}' ā ERROR: {}\n", color_name, e));
325 }
326 }
327 }
328
329 self.content.clear();
330 self.cursor.reset_for_empty_text();
331 Some(results)
332 }
333
334 "full-debug" => {
335 let (_, cursor_pos) = self.render_with_cursor();
336 let debug_info = format!(
337 "š FULL CURSOR DEBUG:\n\
338 šØ Config Theme: '{}'\n\
339 š input_cursor: '{}'\n\
340 šÆ Parsed Type: {:?}\n\
341 š¤ Symbol: '{}'\n\
342 šļø Is Visible: {}\n\
343 š Position: {}\n\
344 š„ļø Terminal Pos: {:?}\n\
345 š§ Match Block: {}\n\
346 ā” Should Use Terminal: {}",
347 self.config.current_theme_name,
348 self.config.theme.input_cursor,
349 self.cursor.ctype,
350 self.cursor.get_symbol(),
351 self.cursor.is_visible(),
352 self.cursor.get_position(),
353 cursor_pos,
354 matches!(self.cursor.ctype, CursorType::Block),
355 !matches!(self.cursor.ctype, CursorType::Block)
356 );
357 self.content.clear();
358 self.cursor.reset_for_empty_text();
359 Some(debug_info)
360 }
361
362 "term-test" => {
363 let info = format!(
364 "š„ļø TERMINAL INFO:\n\
365 šŗ Terminal: {:?}\n\
366 šÆ Cursor Support: Testing...\n\
367 š” Try: ESC[?25h (show cursor)\n\
368 š” Or: Different terminal app",
369 std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string())
370 );
371 self.content.clear();
372 self.cursor.reset_for_empty_text();
373 Some(info)
374 }
375
376 _ => {
378 if self.content.is_empty() {
379 return None;
380 }
381 if self.validate_input(&self.content).is_ok() {
382 let content = std::mem::take(&mut self.content);
383 self.cursor.reset_for_empty_text();
384 self.history_manager.add_entry(content.clone());
385 let result = self.command_handler.handle_input(&content);
386
387 if let Some(event) =
388 HistoryEventHandler::handle_command_result(&result.message)
389 {
390 return Some(self.handle_history_event(event));
391 }
392 if result.message.starts_with("__CONFIRM_EXIT__") {
393 self.waiting_for_exit_confirmation = true;
394 return Some(result.message.replace("__CONFIRM_EXIT__", ""));
395 }
396 if result.message.starts_with("__CONFIRM_RESTART__") {
397 self.waiting_for_restart_confirmation = true;
398 return Some(result.message.replace("__CONFIRM_RESTART__", ""));
399 }
400 if result.message.starts_with("__RESTART_FORCE__")
401 || result.message.starts_with("__RESTART__")
402 {
403 let feedback_text =
404 if result.message.starts_with("__RESTART_FORCE__") {
405 result
406 .message
407 .replace("__RESTART_FORCE__", "")
408 .trim()
409 .to_string()
410 } else {
411 result.message.replace("__RESTART__", "").trim().to_string()
412 };
413 if !feedback_text.is_empty() {
414 return Some(format!("__RESTART_WITH_MSG__{}", feedback_text));
415 } else {
416 return Some("__RESTART__".to_string());
417 }
418 }
419 if result.should_exit {
420 return Some(format!("__EXIT__{}", result.message));
421 }
422 return Some(result.message);
423 }
424 None
425 }
426 }
427 }
428 KeyAction::InsertChar(c) => {
429 if self.content.graphemes(true).count() < self.config.input_max_length {
430 let byte_pos = self.cursor.get_byte_position(&self.content);
431 self.content.insert(byte_pos, c);
432 self.cursor.update_text_length(&self.content);
433 self.cursor.move_right();
434 }
435 None
436 }
437 KeyAction::MoveLeft => {
438 self.cursor.move_left();
439 None
440 }
441 KeyAction::MoveRight => {
442 self.cursor.move_right();
443 None
444 }
445 KeyAction::MoveToStart => {
446 self.cursor.move_to_start();
447 None
448 }
449 KeyAction::MoveToEnd => {
450 self.cursor.move_to_end();
451 None
452 }
453 KeyAction::Backspace => {
454 if self.content.is_empty() || self.cursor.get_position() == 0 {
455 return None;
456 }
457 let current_byte_pos = self.cursor.get_byte_position(&self.content);
458 let prev_byte_pos = self.cursor.get_prev_byte_position(&self.content);
459 if prev_byte_pos >= current_byte_pos || current_byte_pos > self.content.len() {
460 self.cursor.update_text_length(&self.content);
461 return None;
462 }
463 self.cursor.move_left();
464 self.content
465 .replace_range(prev_byte_pos..current_byte_pos, "");
466 self.cursor.update_text_length(&self.content);
467 if self.content.is_empty() {
468 self.cursor.reset_for_empty_text();
469 }
470 None
471 }
472 KeyAction::Delete => {
473 let text_length = self.content.graphemes(true).count();
474 if self.cursor.get_position() >= text_length || text_length == 0 {
475 return None;
476 }
477 let current_byte_pos = self.cursor.get_byte_position(&self.content);
478 let next_byte_pos = self.cursor.get_next_byte_position(&self.content);
479 if current_byte_pos >= next_byte_pos || next_byte_pos > self.content.len() {
480 self.cursor.update_text_length(&self.content);
481 return None;
482 }
483 self.content
484 .replace_range(current_byte_pos..next_byte_pos, "");
485 self.cursor.update_text_length(&self.content);
486 if self.content.is_empty() {
487 self.cursor.reset_for_empty_text();
488 }
489 None
490 }
491 KeyAction::ClearLine
492 | KeyAction::ScrollUp
493 | KeyAction::ScrollDown
494 | KeyAction::PageUp
495 | KeyAction::PageDown
496 | KeyAction::Cancel
497 | KeyAction::Quit
498 | KeyAction::CopySelection
499 | KeyAction::PasteBuffer
500 | KeyAction::NoAction => None,
501 }
502 }
503
504 pub fn export_state(&self) -> InputStateBackup {
505 InputStateBackup {
506 content: self.content.clone(),
507 history: self.history_manager.get_all_entries(),
508 cursor_pos: self.cursor.get_current_position(),
509 }
510 }
511
512 pub fn import_state(&mut self, backup: InputStateBackup) {
513 self.content = backup.content;
514 self.history_manager.import_entries(backup.history);
515 self.cursor.update_text_length(&self.content);
516 log::debug!(
517 "ā
InputState imported: {} chars, {} history entries",
518 self.content.len(),
519 self.history_manager.entry_count()
520 );
521 }
522
523 pub fn get_content(&self) -> &str {
524 &self.content
525 }
526
527 pub fn get_history_count(&self) -> usize {
528 self.history_manager.entry_count()
529 }
530
531 fn render_block_cursor(
537 &self,
538 spans: &mut Vec<Span<'static>>,
539 graphemes: &[&str],
540 cursor_pos: usize,
541 viewport_start: usize,
542 available_width: usize,
543 ) {
544 if cursor_pos > viewport_start {
546 let visible_text = graphemes[viewport_start..cursor_pos].join("");
547 if !visible_text.is_empty() {
548 spans.push(Span::styled(
549 visible_text,
550 Style::default().fg(self.config.theme.input_text.into()),
551 ));
552 }
553 }
554
555 let char_at_cursor = graphemes.get(cursor_pos).copied().unwrap_or(" ");
557 if self.cursor.is_visible() {
558 spans.push(Span::styled(
560 char_at_cursor.to_string(),
561 Style::default()
562 .fg(self.config.theme.input_bg.into()) .bg(self.config.theme.input_cursor_color.into()), ));
565 } else {
566 spans.push(Span::styled(
568 char_at_cursor.to_string(),
569 Style::default().fg(self.config.theme.input_text.into()),
570 ));
571 }
572
573 let end_pos = (viewport_start + available_width).min(graphemes.len());
575 if cursor_pos + 1 < end_pos {
576 let remaining_text = graphemes[cursor_pos + 1..end_pos].join("");
577 if !remaining_text.is_empty() {
578 spans.push(Span::styled(
579 remaining_text,
580 Style::default().fg(self.config.theme.input_text.into()),
581 ));
582 }
583 }
584 }
585
586 fn render_symbol_cursor(
588 &self,
589 spans: &mut Vec<Span<'static>>,
590 graphemes: &[&str],
591 cursor_pos: usize,
592 viewport_start: usize,
593 available_width: usize,
594 ) {
595 let end_pos = (viewport_start + available_width).min(graphemes.len());
596
597 if cursor_pos > viewport_start {
599 let visible_text = graphemes[viewport_start..cursor_pos].join("");
600 if !visible_text.is_empty() {
601 spans.push(Span::styled(
602 visible_text,
603 Style::default().fg(self.config.theme.input_text.into()),
604 ));
605 }
606 }
607
608 if self.cursor.is_visible() {
610 let cursor_symbol = self.cursor.get_symbol(); spans.push(Span::styled(
612 cursor_symbol.to_string(),
613 Style::default()
614 .fg(self.config.theme.input_cursor_color.into()) .bg(self.config.theme.input_bg.into()),
616 ));
617 }
618
619 if cursor_pos < end_pos {
621 let remaining_text = graphemes[cursor_pos..end_pos].join("");
622 if !remaining_text.is_empty() {
623 spans.push(Span::styled(
624 remaining_text,
625 Style::default().fg(self.config.theme.input_text.into()),
626 ));
627 }
628 }
629 }
630}
631
632impl Widget for InputState {
634 fn render(&self) -> Paragraph {
635 self.render_with_cursor().0
636 }
637
638 fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
640 let graphemes: Vec<&str> = self.content.graphemes(true).collect();
641 let cursor_pos = self.cursor.get_position();
642 let mut spans = Vec::with_capacity(8);
643
644 let prompt_display = self.config.theme.input_cursor_prefix.clone();
646 let prompt_width = prompt_display.graphemes(true).count();
647 spans.push(Span::styled(
648 prompt_display,
649 Style::default().fg(self.config.theme.input_cursor_color.into()),
650 ));
651
652 let available_width = self
653 .config
654 .input_max_length
655 .saturating_sub(prompt_width + 4);
656
657 let viewport_start = if cursor_pos > available_width {
658 cursor_pos - available_width + 10
659 } else {
660 0
661 };
662
663 match self.cursor.ctype {
664 CursorType::Block => {
665 self.render_block_cursor(
667 &mut spans,
668 &graphemes,
669 cursor_pos,
670 viewport_start,
671 available_width,
672 );
673 let paragraph = Paragraph::new(Line::from(spans)).block(
674 Block::default()
675 .padding(Padding::new(3, 1, 1, 1))
676 .borders(Borders::NONE)
677 .style(Style::default().bg(self.config.theme.input_bg.into())),
678 );
679 (paragraph, None)
680 }
681 CursorType::Pipe | CursorType::Underscore => {
682 self.render_symbol_cursor(
684 &mut spans,
685 &graphemes,
686 cursor_pos,
687 viewport_start,
688 available_width,
689 );
690 let paragraph = Paragraph::new(Line::from(spans)).block(
691 Block::default()
692 .padding(Padding::new(3, 1, 1, 1))
693 .borders(Borders::NONE)
694 .style(Style::default().bg(self.config.theme.input_bg.into())),
695 );
696 (paragraph, None) }
698 }
699 }
700
701 fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
702 self.handle_key_event(key)
703 }
704
705 fn as_input_state(&mut self) -> Option<&mut dyn InputWidget> {
706 Some(self)
707 }
708
709 fn get_backup_data(&self) -> Option<InputStateBackup> {
710 Some(self.export_state())
711 }
712
713 fn restore_backup_data(&mut self, backup: InputStateBackup) {
714 self.import_state(backup);
715 }
716}
717
718impl InputWidget for InputState {
719 fn update_cursor_blink(&mut self) {
720 self.cursor.update_blink();
721 }
722}