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 if self.content.trim() == "full-debug" {
245 let (_, cursor_pos) = self.render_with_cursor();
246 let debug_info = format!(
247 "π FULL CURSOR DEBUG:\n\
248 π¨ Config Theme: '{}'\n\
249 π input_cursor: '{}'\n\
250 π― Parsed Type: {:?}\n\
251 π€ Symbol: '{}'\n\
252 ποΈ Is Visible: {}\n\
253 π Position: {}\n\
254 π₯οΈ Terminal Pos: {:?}\n\
255 π§ Match Block: {}\n\
256 β‘ Should Use Terminal: {}",
257 self.config.current_theme_name,
258 self.config.theme.input_cursor,
259 self.cursor.ctype,
260 self.cursor.get_symbol(),
261 self.cursor.is_visible(),
262 self.cursor.get_position(),
263 cursor_pos,
264 matches!(self.cursor.ctype, CursorType::Block),
265 !matches!(self.cursor.ctype, CursorType::Block)
266 );
267 self.content.clear();
268 self.cursor.reset_for_empty_text();
269 return Some(debug_info);
270 }
271
272 if self.content.trim() == "term-test" {
273 let info = format!(
274 "π₯οΈ TERMINAL INFO:\n\
275 πΊ Terminal: {:?}\n\
276 π― Cursor Support: Testing...\n\
277 π‘ Try: ESC[?25h (show cursor)\n\
278 π‘ Or: Different terminal app",
279 std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string())
280 );
281 self.content.clear();
282 self.cursor.reset_for_empty_text();
283 return Some(info);
284 }
285
286 if self.content.is_empty() {
287 return None;
288 }
289 if self.validate_input(&self.content).is_ok() {
290 let content = std::mem::take(&mut self.content);
291 self.cursor.reset_for_empty_text();
292 self.history_manager.add_entry(content.clone());
293 let result = self.command_handler.handle_input(&content);
294
295 if let Some(event) = HistoryEventHandler::handle_command_result(&result.message)
296 {
297 return Some(self.handle_history_event(event));
298 }
299 if result.message.starts_with("__CONFIRM_EXIT__") {
300 self.waiting_for_exit_confirmation = true;
301 return Some(result.message.replace("__CONFIRM_EXIT__", ""));
302 }
303 if result.message.starts_with("__CONFIRM_RESTART__") {
304 self.waiting_for_restart_confirmation = true;
305 return Some(result.message.replace("__CONFIRM_RESTART__", ""));
306 }
307 if result.message.starts_with("__RESTART_FORCE__")
308 || result.message.starts_with("__RESTART__")
309 {
310 let feedback_text = if result.message.starts_with("__RESTART_FORCE__") {
311 result
312 .message
313 .replace("__RESTART_FORCE__", "")
314 .trim()
315 .to_string()
316 } else {
317 result.message.replace("__RESTART__", "").trim().to_string()
318 };
319 if !feedback_text.is_empty() {
320 return Some(format!("__RESTART_WITH_MSG__{}", feedback_text));
321 } else {
322 return Some("__RESTART__".to_string());
323 }
324 }
325 if result.should_exit {
326 return Some(format!("__EXIT__{}", result.message));
327 }
328 return Some(result.message);
329 }
330 None
331 }
332 KeyAction::InsertChar(c) => {
333 if self.content.graphemes(true).count() < self.config.input_max_length {
334 let byte_pos = self.cursor.get_byte_position(&self.content);
335 self.content.insert(byte_pos, c);
336 self.cursor.update_text_length(&self.content);
337 self.cursor.move_right();
338 }
339 None
340 }
341 KeyAction::MoveLeft => {
342 self.cursor.move_left();
343 None
344 }
345 KeyAction::MoveRight => {
346 self.cursor.move_right();
347 None
348 }
349 KeyAction::MoveToStart => {
350 self.cursor.move_to_start();
351 None
352 }
353 KeyAction::MoveToEnd => {
354 self.cursor.move_to_end();
355 None
356 }
357 KeyAction::Backspace => {
358 if self.content.is_empty() || self.cursor.get_position() == 0 {
359 return None;
360 }
361 let current_byte_pos = self.cursor.get_byte_position(&self.content);
362 let prev_byte_pos = self.cursor.get_prev_byte_position(&self.content);
363 if prev_byte_pos >= current_byte_pos || current_byte_pos > self.content.len() {
364 self.cursor.update_text_length(&self.content);
365 return None;
366 }
367 self.cursor.move_left();
368 self.content
369 .replace_range(prev_byte_pos..current_byte_pos, "");
370 self.cursor.update_text_length(&self.content);
371 if self.content.is_empty() {
372 self.cursor.reset_for_empty_text();
373 }
374 None
375 }
376 KeyAction::Delete => {
377 let text_length = self.content.graphemes(true).count();
378 if self.cursor.get_position() >= text_length || text_length == 0 {
379 return None;
380 }
381 let current_byte_pos = self.cursor.get_byte_position(&self.content);
382 let next_byte_pos = self.cursor.get_next_byte_position(&self.content);
383 if current_byte_pos >= next_byte_pos || next_byte_pos > self.content.len() {
384 self.cursor.update_text_length(&self.content);
385 return None;
386 }
387 self.content
388 .replace_range(current_byte_pos..next_byte_pos, "");
389 self.cursor.update_text_length(&self.content);
390 if self.content.is_empty() {
391 self.cursor.reset_for_empty_text();
392 }
393 None
394 }
395 KeyAction::ClearLine
396 | KeyAction::ScrollUp
397 | KeyAction::ScrollDown
398 | KeyAction::PageUp
399 | KeyAction::PageDown
400 | KeyAction::Cancel
401 | KeyAction::Quit
402 | KeyAction::CopySelection
403 | KeyAction::PasteBuffer
404 | KeyAction::NoAction => None,
405 }
406 }
407
408 pub fn export_state(&self) -> InputStateBackup {
409 InputStateBackup {
410 content: self.content.clone(),
411 history: self.history_manager.get_all_entries(),
412 cursor_pos: self.cursor.get_current_position(),
413 }
414 }
415
416 pub fn import_state(&mut self, backup: InputStateBackup) {
417 self.content = backup.content;
418 self.history_manager.import_entries(backup.history);
419 self.cursor.update_text_length(&self.content);
420 log::debug!(
421 "β
InputState imported: {} chars, {} history entries",
422 self.content.len(),
423 self.history_manager.entry_count()
424 );
425 }
426
427 pub fn get_content(&self) -> &str {
428 &self.content
429 }
430
431 pub fn get_history_count(&self) -> usize {
432 self.history_manager.entry_count()
433 }
434
435 fn render_block_cursor(
441 &self,
442 spans: &mut Vec<Span<'static>>,
443 graphemes: &[&str],
444 cursor_pos: usize,
445 viewport_start: usize,
446 available_width: usize,
447 ) {
448 if cursor_pos > viewport_start {
450 let visible_text = graphemes[viewport_start..cursor_pos].join("");
451 if !visible_text.is_empty() {
452 spans.push(Span::styled(
453 visible_text,
454 Style::default().fg(self.config.theme.input_text.into()),
455 ));
456 }
457 }
458
459 let char_at_cursor = graphemes.get(cursor_pos).copied().unwrap_or(" ");
461 if self.cursor.is_visible() {
462 spans.push(Span::styled(
464 char_at_cursor.to_string(),
465 Style::default()
466 .fg(self.config.theme.input_bg.into()) .bg(self.config.theme.input_cursor_color.into()), ));
469 } else {
470 spans.push(Span::styled(
472 char_at_cursor.to_string(),
473 Style::default().fg(self.config.theme.input_text.into()),
474 ));
475 }
476
477 let end_pos = (viewport_start + available_width).min(graphemes.len());
479 if cursor_pos + 1 < end_pos {
480 let remaining_text = graphemes[cursor_pos + 1..end_pos].join("");
481 if !remaining_text.is_empty() {
482 spans.push(Span::styled(
483 remaining_text,
484 Style::default().fg(self.config.theme.input_text.into()),
485 ));
486 }
487 }
488 }
489
490 fn render_normal_text(
492 &self,
493 spans: &mut Vec<Span<'static>>,
494 graphemes: &[&str],
495 viewport_start: usize,
496 available_width: usize,
497 ) {
498 let end_pos = (viewport_start + available_width).min(graphemes.len());
501 if viewport_start < end_pos {
502 let visible_text = graphemes[viewport_start..end_pos].join("");
503 if !visible_text.is_empty() {
504 spans.push(Span::styled(
505 visible_text,
506 Style::default().fg(self.config.theme.input_text.into()),
507 ));
508 }
509 }
510 }
511}
512
513impl Widget for InputState {
515 fn render(&self) -> Paragraph {
516 self.render_with_cursor().0
517 }
518
519 fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
521 let graphemes: Vec<&str> = self.content.graphemes(true).collect();
522 let cursor_pos = self.cursor.get_position();
523 let mut spans = Vec::with_capacity(8);
524
525 let prompt_display = self.config.theme.input_cursor_prefix.clone();
527 let prompt_width = prompt_display.graphemes(true).count();
528 spans.push(Span::styled(
529 prompt_display,
530 Style::default().fg(self.config.theme.input_cursor_color.into()),
531 ));
532
533 let available_width = self
534 .config
535 .input_max_length
536 .saturating_sub(prompt_width + 4);
537
538 let viewport_start = if cursor_pos > available_width {
539 cursor_pos - available_width + 10
540 } else {
541 0
542 };
543
544 match self.cursor.ctype {
546 CursorType::Block => {
547 self.render_block_cursor(
549 &mut spans,
550 &graphemes,
551 cursor_pos,
552 viewport_start,
553 available_width,
554 );
555 let paragraph = Paragraph::new(Line::from(spans)).block(
557 Block::default()
558 .padding(Padding::new(3, 1, 1, 1))
559 .borders(Borders::NONE)
560 .style(Style::default().bg(self.config.theme.input_bg.into())),
561 );
562 (paragraph, None)
563 }
564 _ => {
565 self.render_normal_text(&mut spans, &graphemes, viewport_start, available_width);
567
568 let paragraph = Paragraph::new(Line::from(spans)).block(
569 Block::default()
570 .padding(Padding::new(3, 1, 1, 1))
571 .borders(Borders::NONE)
572 .style(Style::default().bg(self.config.theme.input_bg.into())),
573 );
574
575 let terminal_cursor_pos = if self.cursor.is_visible() {
577 let visible_chars_before_cursor = if cursor_pos > viewport_start {
578 cursor_pos - viewport_start
579 } else {
580 0
581 };
582 let cursor_x = 3 + prompt_width + visible_chars_before_cursor;
583 Some((cursor_x as u16, 1))
584 } else {
585 None };
587
588 (paragraph, terminal_cursor_pos)
589 }
590 }
591 }
592
593 fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
594 self.handle_key_event(key)
595 }
596
597 fn as_input_state(&mut self) -> Option<&mut dyn InputWidget> {
598 Some(self)
599 }
600
601 fn get_backup_data(&self) -> Option<InputStateBackup> {
602 Some(self.export_state())
603 }
604
605 fn restore_backup_data(&mut self, backup: InputStateBackup) {
606 self.import_state(backup);
607 }
608}
609
610impl InputWidget for InputState {
611 fn update_cursor_blink(&mut self) {
612 self.cursor.update_blink();
613 }
614}