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 std::process::Command;
17use unicode_segmentation::UnicodeSegmentation;
18
19pub struct InputState {
20 content: String,
21 cursor: UiCursor,
22 prompt: String,
23 history_manager: HistoryManager,
24 config: Config,
25 command_handler: CommandHandler,
26 keyboard_manager: KeyboardManager,
27 waiting_for_exit_confirmation: bool,
28 waiting_for_restart_confirmation: bool,
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct InputStateBackup {
33 pub content: String,
34 pub history: Vec<String>,
35 pub cursor_pos: usize,
36}
37
38impl InputState {
39 pub fn new(config: &Config) -> Self {
40 let history_config = HistoryConfig::from_main_config(config);
41
42 Self {
43 content: String::with_capacity(100),
44 cursor: UiCursor::from_config(config, CursorKind::Input),
45 prompt: config.theme.input_cursor_prefix.clone(),
46 history_manager: HistoryManager::new(history_config.max_entries),
47 config: config.clone(),
48 command_handler: CommandHandler::new(),
49 keyboard_manager: KeyboardManager::new(),
50 waiting_for_exit_confirmation: false,
51 waiting_for_restart_confirmation: false,
52 }
53 }
54
55 pub fn update_from_config(&mut self, config: &Config) {
56 self.cursor.update_from_config(config);
57 self.prompt = config.theme.input_cursor_prefix.clone();
58 self.config = config.clone();
59
60 log::debug!(
61 "β
InputState cursor updated via central API: {}",
62 self.cursor.debug_info()
63 );
64 }
65
66 pub fn validate_input(&self, input: &str) -> crate::core::error::Result<()> {
67 if input.trim().is_empty() {
68 return Err(AppError::Validation(t!("system.input.empty")));
69 }
70 let grapheme_count = input.graphemes(true).count();
71 let max_length = 1024;
72
73 if grapheme_count > max_length {
74 return Err(AppError::Validation(t!(
75 "system.input.too_long",
76 &max_length.to_string()
77 )));
78 }
79 Ok(())
80 }
81
82 pub fn reset_for_language_change(&mut self) {
83 self.waiting_for_exit_confirmation = false;
84 self.waiting_for_restart_confirmation = false;
85 self.content.clear();
86 self.history_manager.reset_position();
87 self.cursor.move_to_start();
88 log::debug!("InputState reset for language change");
89 }
90
91 fn handle_exit_confirmation(&mut self, action: KeyAction) -> Option<String> {
92 match action {
93 KeyAction::Submit => {
94 self.waiting_for_exit_confirmation = false;
95 let confirm_short = t!("system.input.confirm.short");
96 let cancel_short = t!("system.input.cancel.short");
97 match self.content.trim().to_lowercase().as_str() {
98 input if input == confirm_short.to_lowercase() => {
99 self.content.clear();
100 Some("__EXIT__".to_string())
101 }
102 input if input == cancel_short.to_lowercase() => {
103 self.clear_input();
104 Some(t!("system.input.cancelled"))
105 }
106 _ => {
107 self.clear_input();
108 Some(t!("system.input.cancelled"))
109 }
110 }
111 }
112 KeyAction::InsertChar(c) => {
113 let confirm_short = t!("system.input.confirm.short");
114 let cancel_short = t!("system.input.cancel.short");
115 if c.to_lowercase().to_string() == confirm_short.to_lowercase()
116 || c.to_lowercase().to_string() == cancel_short.to_lowercase()
117 {
118 self.content.clear();
119 self.content.push(c);
120 self.cursor.update_text_length(&self.content);
121 self.cursor.move_to_end();
122 }
123 None
124 }
125 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
126 self.clear_input();
127 None
128 }
129 _ => None,
130 }
131 }
132
133 fn handle_restart_confirmation(&mut self, action: KeyAction) -> Option<String> {
134 match action {
135 KeyAction::Submit => {
136 self.waiting_for_restart_confirmation = false;
137 let confirm_short = t!("system.input.confirm.short");
138 let cancel_short = t!("system.input.cancel.short");
139 match self.content.trim().to_lowercase().as_str() {
140 input if input == confirm_short.to_lowercase() => {
141 self.content.clear();
142 Some("__RESTART__".to_string())
143 }
144 input if input == cancel_short.to_lowercase() => {
145 self.clear_input();
146 Some(t!("system.input.cancelled"))
147 }
148 _ => {
149 self.clear_input();
150 Some(t!("system.input.cancelled"))
151 }
152 }
153 }
154 KeyAction::InsertChar(c) => {
155 let confirm_short = t!("system.input.confirm.short");
156 let cancel_short = t!("system.input.cancel.short");
157 if c.to_lowercase().to_string() == confirm_short.to_lowercase()
158 || c.to_lowercase().to_string() == cancel_short.to_lowercase()
159 {
160 self.content.clear();
161 self.content.push(c);
162 self.cursor.update_text_length(&self.content);
163 self.cursor.move_to_end();
164 }
165 None
166 }
167 KeyAction::Backspace | KeyAction::Delete | KeyAction::ClearLine => {
168 self.clear_input();
169 None
170 }
171 _ => None,
172 }
173 }
174
175 fn clear_input(&mut self) {
176 self.content.clear();
177 self.history_manager.reset_position();
178 self.cursor.move_to_start();
179 }
180
181 fn handle_history_action(&mut self, action: HistoryAction) -> Option<String> {
182 match action {
183 HistoryAction::NavigatePrevious => {
184 if let Some(entry) = self.history_manager.navigate_previous() {
185 self.content = entry;
186 self.cursor.update_text_length(&self.content);
187 self.cursor.move_to_end();
188 }
189 }
190 HistoryAction::NavigateNext => {
191 if let Some(entry) = self.history_manager.navigate_next() {
192 self.content = entry;
193 self.cursor.update_text_length(&self.content);
194 self.cursor.move_to_end();
195 }
196 }
197 }
198 None
199 }
200
201 fn handle_history_event(&mut self, event: HistoryEvent) -> String {
202 match event {
203 HistoryEvent::Clear => {
204 self.history_manager.clear();
205 HistoryEventHandler::create_clear_response()
206 }
207 HistoryEvent::Add(entry) => {
208 self.history_manager.add_entry(entry);
209 String::new()
210 }
211 _ => String::new(),
212 }
213 }
214
215 pub fn execute(&self) -> crate::core::error::Result<String> {
216 Ok(format!(
217 "__CONFIRM_EXIT__{}",
218 t!("system.input.confirm_exit")
219 ))
220 }
221
222 fn read_clipboard(&self) -> Option<String> {
224 #[cfg(target_os = "macos")]
225 {
226 match Command::new("pbpaste").output() {
228 Ok(output) => {
229 let clipboard_text = String::from_utf8_lossy(&output.stdout).to_string();
230 if !clipboard_text.trim().is_empty() {
231 log::info!("π Mac clipboard read: {} chars", clipboard_text.len());
232 Some(clipboard_text.trim_end_matches('\n').to_string())
233 } else {
234 log::debug!("π Mac clipboard empty");
235 None
236 }
237 }
238 Err(e) => {
239 log::error!("π Failed to read Mac clipboard: {}", e);
240 None
241 }
242 }
243 }
244
245 #[cfg(target_os = "linux")]
246 {
247 match Command::new("xclip")
249 .args(["-selection", "clipboard", "-o"])
250 .output()
251 {
252 Ok(output) => {
253 let clipboard_text = String::from_utf8_lossy(&output.stdout).to_string();
254 if !clipboard_text.trim().is_empty() {
255 log::info!("π Linux clipboard read: {} chars", clipboard_text.len());
256 Some(clipboard_text.trim_end_matches('\n').to_string())
257 } else {
258 None
259 }
260 }
261 Err(_) => {
262 match Command::new("xsel").args(["-b", "-o"]).output() {
264 Ok(output) => {
265 let clipboard_text =
266 String::from_utf8_lossy(&output.stdout).to_string();
267 Some(clipboard_text.trim_end_matches('\n').to_string())
268 }
269 Err(e) => {
270 log::error!("π Failed to read Linux clipboard: {}", e);
271 None
272 }
273 }
274 }
275 }
276 }
277
278 #[cfg(target_os = "windows")]
279 {
280 match Command::new("powershell")
282 .args(["-Command", "Get-Clipboard"])
283 .output()
284 {
285 Ok(output) => {
286 let clipboard_text = String::from_utf8_lossy(&output.stdout).to_string();
287 if !clipboard_text.trim().is_empty() {
288 log::info!("π Windows clipboard read: {} chars", clipboard_text.len());
289 Some(clipboard_text.trim_end_matches('\n').to_string())
290 } else {
291 None
292 }
293 }
294 Err(e) => {
295 log::error!("π Failed to read Windows clipboard: {}", e);
296 None
297 }
298 }
299 }
300
301 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
302 {
303 log::warn!("π Clipboard not supported on this platform");
304 None
305 }
306 }
307
308 fn write_clipboard(&self, text: &str) -> bool {
310 #[cfg(target_os = "macos")]
311 {
312 match Command::new("pbcopy")
313 .stdin(std::process::Stdio::piped())
314 .spawn()
315 .and_then(|mut child| {
316 use std::io::Write;
317 if let Some(stdin) = child.stdin.as_mut() {
318 stdin.write_all(text.as_bytes())?;
319 }
320 child.wait().map(|_| ())
321 }) {
322 Ok(_) => {
323 log::info!("π Mac clipboard written: {} chars", text.len());
324 true
325 }
326 Err(e) => {
327 log::error!("π Failed to write Mac clipboard: {}", e);
328 false
329 }
330 }
331 }
332
333 #[cfg(target_os = "linux")]
334 {
335 match Command::new("xclip")
336 .args(["-selection", "clipboard"])
337 .stdin(std::process::Stdio::piped())
338 .spawn()
339 .and_then(|mut child| {
340 use std::io::Write;
341 if let Some(stdin) = child.stdin.as_mut() {
342 stdin.write_all(text.as_bytes())?;
343 }
344 child.wait().map(|_| ())
345 }) {
346 Ok(_) => {
347 log::info!("π Linux clipboard written: {} chars", text.len());
348 true
349 }
350 Err(e) => {
351 log::error!("π Failed to write Linux clipboard: {}", e);
352 false
353 }
354 }
355 }
356
357 #[cfg(target_os = "windows")]
358 {
359 match Command::new("powershell")
360 .args(["-Command", &format!("'{}' | Set-Clipboard", text)])
361 .output()
362 {
363 Ok(_) => {
364 log::info!("π Windows clipboard written: {} chars", text.len());
365 true
366 }
367 Err(e) => {
368 log::error!("π Failed to write Windows clipboard: {}", e);
369 false
370 }
371 }
372 }
373
374 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
375 {
376 log::warn!("π Clipboard write not supported on this platform");
377 false
378 }
379 }
380
381 pub fn handle_key_event(&mut self, key: KeyEvent) -> Option<String> {
382 if let Some(history_action) = HistoryKeyboardHandler::get_history_action(&key) {
383 return self.handle_history_action(history_action);
384 }
385
386 if key.code == KeyCode::Esc {
387 return None;
388 }
389
390 let action = self.keyboard_manager.get_action(&key);
391
392 if self.waiting_for_exit_confirmation {
393 return self.handle_exit_confirmation(action);
394 }
395 if self.waiting_for_restart_confirmation {
396 return self.handle_restart_confirmation(action);
397 }
398
399 match action {
402 KeyAction::Submit => {
403 match self.content.trim() {
405 "cursor-debug" => {
406 let debug_info = format!(
407 "π― CURSOR COLOR DEBUG:\n\
408 π Theme: {}\n\
409 π¨ Expected input_cursor_color: {}\n\
410 π¨ Actual cursor color: {}\n\
411 π¨ Actual fg color: {}\n\
412 π Cursor details:\n\
413 {}",
414 self.config.current_theme_name,
415 self.config.theme.input_cursor_color.to_name(),
416 self.cursor.color.to_name(),
417 self.cursor.fg.to_name(),
418 self.cursor.debug_info()
419 );
420 self.content.clear();
421 self.cursor.reset_for_empty_text();
422 Some(debug_info)
423 }
424
425 "theme-config-debug" => {
426 let debug_info = format!(
427 "π COMPLETE THEME CONFIG DEBUG:\n\
428 π Current Theme: {}\n\
429 π¨ input_cursor_color: {} β¬
οΈ CONFIG VALUE\n\
430 π¨ input_cursor: {}\n\
431 π¨ input_cursor_prefix: '{}'\n\
432 π¨ output_cursor_color: {}\n\
433 π¨ output_cursor: {}\n\
434 \nπ― ACTUAL CURSOR STATE:\n\
435 π¨ cursor.color: {} β¬
οΈ ACTUAL VALUE\n\
436 π― cursor.ctype: {:?}\n\
437 ποΈ cursor.visible: {}",
438 self.config.current_theme_name,
439 self.config.theme.input_cursor_color.to_name(),
440 self.config.theme.input_cursor,
441 self.config.theme.input_cursor_prefix,
442 self.config.theme.output_cursor_color.to_name(),
443 self.config.theme.output_cursor,
444 self.cursor.color.to_name(),
445 self.cursor.ctype,
446 self.cursor.is_visible()
447 );
448 self.content.clear();
449 self.cursor.reset_for_empty_text();
450 Some(debug_info)
451 }
452
453 "color-test" => {
454 let test_colors = vec![
455 "Red",
456 "Green",
457 "Blue",
458 "Yellow",
459 "Magenta",
460 "Cyan",
461 "LightRed",
462 "LightGreen",
463 "LightBlue",
464 "LightYellow",
465 "LightMagenta",
466 "LightCyan",
467 "White",
468 "Black",
469 ];
470
471 let mut results = String::from("π¨ COLOR CONVERSION TEST:\n");
472 for color_name in test_colors {
473 match crate::ui::color::AppColor::from_string(color_name) {
474 Ok(color) => {
475 results.push_str(&format!(
476 "β
'{}' β '{}'\n",
477 color_name,
478 color.to_name()
479 ));
480 }
481 Err(e) => {
482 results
483 .push_str(&format!("β '{}' β ERROR: {}\n", color_name, e));
484 }
485 }
486 }
487
488 self.content.clear();
489 self.cursor.reset_for_empty_text();
490 Some(results)
491 }
492
493 "full-debug" => {
494 let (_, cursor_pos) = self.render_with_cursor();
495 let debug_info = format!(
496 "π FULL CURSOR DEBUG:\n\
497 π¨ Config Theme: '{}'\n\
498 π input_cursor: '{}'\n\
499 π― Parsed Type: {:?}\n\
500 π€ Symbol: '{}'\n\
501 ποΈ Is Visible: {}\n\
502 π Position: {}\n\
503 π₯οΈ Terminal Pos: {:?}\n\
504 π§ Match Block: {}\n\
505 β‘ Should Use Terminal: {}",
506 self.config.current_theme_name,
507 self.config.theme.input_cursor,
508 self.cursor.ctype,
509 self.cursor.get_symbol(),
510 self.cursor.is_visible(),
511 self.cursor.get_position(),
512 cursor_pos,
513 matches!(self.cursor.ctype, CursorType::Block),
514 !matches!(self.cursor.ctype, CursorType::Block)
515 );
516 self.content.clear();
517 self.cursor.reset_for_empty_text();
518 Some(debug_info)
519 }
520
521 "term-test" => {
522 let info = format!(
523 "π₯οΈ TERMINAL INFO:\n\
524 πΊ Terminal: {:?}\n\
525 π― Cursor Support: Testing...\n\
526 π‘ Try: ESC[?25h (show cursor)\n\
527 π‘ Or: Different terminal app",
528 std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string())
529 );
530 self.content.clear();
531 self.cursor.reset_for_empty_text();
532 Some(info)
533 }
534
535 _ => {
537 if self.content.is_empty() {
538 return None;
539 }
540 if self.validate_input(&self.content).is_ok() {
541 let content = std::mem::take(&mut self.content);
542 self.cursor.reset_for_empty_text();
543 self.history_manager.add_entry(content.clone());
544 let result = self.command_handler.handle_input(&content);
545
546 if let Some(event) =
547 HistoryEventHandler::handle_command_result(&result.message)
548 {
549 return Some(self.handle_history_event(event));
550 }
551 if result.message.starts_with("__CONFIRM_EXIT__") {
552 self.waiting_for_exit_confirmation = true;
553 return Some(result.message.replace("__CONFIRM_EXIT__", ""));
554 }
555 if result.message.starts_with("__CONFIRM_RESTART__") {
556 self.waiting_for_restart_confirmation = true;
557 return Some(result.message.replace("__CONFIRM_RESTART__", ""));
558 }
559 if result.message.starts_with("__RESTART_FORCE__")
560 || result.message.starts_with("__RESTART__")
561 {
562 let feedback_text =
563 if result.message.starts_with("__RESTART_FORCE__") {
564 result
565 .message
566 .replace("__RESTART_FORCE__", "")
567 .trim()
568 .to_string()
569 } else {
570 result.message.replace("__RESTART__", "").trim().to_string()
571 };
572 if !feedback_text.is_empty() {
573 return Some(format!("__RESTART_WITH_MSG__{}", feedback_text));
574 } else {
575 return Some("__RESTART__".to_string());
576 }
577 }
578 if result.should_exit {
579 return Some(format!("__EXIT__{}", result.message));
580 }
581 return Some(result.message);
582 }
583 None
584 }
585 }
586 }
587
588 KeyAction::InsertChar(c) => {
589 if self.content.graphemes(true).count() < self.config.input_max_length {
590 let byte_pos = self.cursor.get_byte_position(&self.content);
591 self.content.insert(byte_pos, c);
592 self.cursor.update_text_length(&self.content);
593 self.cursor.move_right();
594 }
595 None
596 }
597
598 KeyAction::ClearLine => {
600 if !self.content.is_empty() {
601 log::info!("π§Ή Clearing input line ({} chars)", self.content.len());
602 self.content.clear();
603 self.cursor.reset_for_empty_text();
604 self.history_manager.reset_position();
605 Some("Input cleared".to_string())
606 } else {
607 log::debug!("π§Ή ClearLine called but input already empty");
608 None
609 }
610 }
611
612 KeyAction::MoveLeft => {
613 self.cursor.move_left();
614 None
615 }
616 KeyAction::MoveRight => {
617 self.cursor.move_right();
618 None
619 }
620 KeyAction::MoveToStart => {
621 self.cursor.move_to_start();
622 None
623 }
624 KeyAction::MoveToEnd => {
625 self.cursor.move_to_end();
626 None
627 }
628
629 KeyAction::Backspace => {
630 if self.content.is_empty() || self.cursor.get_position() == 0 {
631 return None;
632 }
633 let current_byte_pos = self.cursor.get_byte_position(&self.content);
634 let prev_byte_pos = self.cursor.get_prev_byte_position(&self.content);
635 if prev_byte_pos >= current_byte_pos || current_byte_pos > self.content.len() {
636 self.cursor.update_text_length(&self.content);
637 return None;
638 }
639 self.cursor.move_left();
640 self.content
641 .replace_range(prev_byte_pos..current_byte_pos, "");
642 self.cursor.update_text_length(&self.content);
643 if self.content.is_empty() {
644 self.cursor.reset_for_empty_text();
645 }
646 None
647 }
648
649 KeyAction::Delete => {
650 let text_length = self.content.graphemes(true).count();
651 if self.cursor.get_position() >= text_length || text_length == 0 {
652 return None;
653 }
654 let current_byte_pos = self.cursor.get_byte_position(&self.content);
655 let next_byte_pos = self.cursor.get_next_byte_position(&self.content);
656 if current_byte_pos >= next_byte_pos || next_byte_pos > self.content.len() {
657 self.cursor.update_text_length(&self.content);
658 return None;
659 }
660 self.content
661 .replace_range(current_byte_pos..next_byte_pos, "");
662 self.cursor.update_text_length(&self.content);
663 if self.content.is_empty() {
664 self.cursor.reset_for_empty_text();
665 }
666 None
667 }
668
669 KeyAction::CopySelection => {
671 if !self.content.is_empty() {
672 if self.write_clipboard(&self.content) {
673 log::info!("π Copied to clipboard: '{}'", self.content);
674 Some(format!("π Copied: {}", self.content))
675 } else {
676 log::error!("π Failed to copy to clipboard");
677 Some("β Copy failed".to_string())
678 }
679 } else {
680 log::debug!("π Copy called but nothing to copy");
681 Some("β Nothing to copy".to_string())
682 }
683 }
684
685 KeyAction::PasteBuffer => {
687 log::debug!("π Paste requested");
688
689 if let Some(clipboard_text) = self.read_clipboard() {
690 let sanitized = clipboard_text
692 .replace('\n', " ") .replace('\r', "") .chars()
695 .filter(|c| !c.is_control() || *c == ' ') .collect::<String>();
697
698 if !sanitized.is_empty() {
699 let available_space = self
701 .config
702 .input_max_length
703 .saturating_sub(self.content.graphemes(true).count());
704
705 let paste_text = if sanitized.graphemes(true).count() > available_space {
706 sanitized
708 .graphemes(true)
709 .take(available_space)
710 .collect::<String>()
711 } else {
712 sanitized
713 };
714
715 if !paste_text.is_empty() {
716 let byte_pos = self.cursor.get_byte_position(&self.content);
718 self.content.insert_str(byte_pos, &paste_text);
719
720 let chars_added = paste_text.graphemes(true).count();
722 self.cursor.update_text_length(&self.content);
723 for _ in 0..chars_added {
724 self.cursor.move_right();
725 }
726
727 log::info!("π Pasted {} chars at position {}", chars_added, byte_pos);
728 Some(format!("π Pasted: {} chars", chars_added))
729 } else {
730 Some("β Nothing to paste (text too long)".to_string())
731 }
732 } else {
733 Some("β Clipboard contains no valid text".to_string())
734 }
735 } else {
736 Some("β Clipboard empty or inaccessible".to_string())
737 }
738 }
739
740 KeyAction::ScrollUp
742 | KeyAction::ScrollDown
743 | KeyAction::PageUp
744 | KeyAction::PageDown
745 | KeyAction::Cancel
746 | KeyAction::Quit
747 | KeyAction::NoAction => None,
748 }
749 }
750
751 pub fn export_state(&self) -> InputStateBackup {
752 InputStateBackup {
753 content: self.content.clone(),
754 history: self.history_manager.get_all_entries(),
755 cursor_pos: self.cursor.get_current_position(),
756 }
757 }
758
759 pub fn import_state(&mut self, backup: InputStateBackup) {
760 self.content = backup.content;
761 self.history_manager.import_entries(backup.history);
762 self.cursor.update_text_length(&self.content);
763 log::debug!(
764 "β
InputState imported: {} chars, {} history entries",
765 self.content.len(),
766 self.history_manager.entry_count()
767 );
768 }
769
770 pub fn get_content(&self) -> &str {
771 &self.content
772 }
773
774 pub fn get_history_count(&self) -> usize {
775 self.history_manager.entry_count()
776 }
777
778 }
782
783impl Widget for InputState {
785 fn render(&self) -> Paragraph {
786 self.render_with_cursor().0
787 }
788
789 fn render_with_cursor(&self) -> (Paragraph, Option<(u16, u16)>) {
791 use unicode_width::UnicodeWidthStr;
792
793 let graphemes: Vec<&str> = self.content.graphemes(true).collect();
794 let cursor_pos = self.cursor.get_position();
795
796 let prompt_display = self.config.theme.input_cursor_prefix.clone();
797 let prompt_width = prompt_display.width(); let available_width = self
800 .config
801 .input_max_length
802 .saturating_sub(prompt_width + 4);
803
804 let viewport_start = if cursor_pos > available_width {
805 cursor_pos - available_width + 10
806 } else {
807 0
808 };
809
810 let mut spans = Vec::new();
812 spans.push(Span::styled(
813 prompt_display,
814 Style::default().fg(self.config.theme.input_cursor_color.into()),
815 ));
816
817 let end_pos = (viewport_start + available_width).min(graphemes.len());
819 let visible = graphemes
820 .get(viewport_start..end_pos)
821 .unwrap_or(&[])
822 .join("");
823 spans.push(Span::styled(
824 visible,
825 Style::default().fg(self.config.theme.input_text.into()),
826 ));
827
828 let paragraph = Paragraph::new(Line::from(spans)).block(
829 Block::default()
830 .padding(Padding::new(3, 1, 1, 1))
831 .borders(Borders::NONE)
832 .style(Style::default().bg(self.config.theme.input_bg.into())),
833 );
834
835 let cursor_coord = if self.cursor.is_visible() {
837 let visible_chars_before_cursor = if cursor_pos > viewport_start {
839 let chars_before = graphemes.get(viewport_start..cursor_pos).unwrap_or(&[]);
841 chars_before
842 .iter()
843 .map(|g| UnicodeWidthStr::width(*g))
844 .sum::<usize>()
845 } else {
846 0
847 };
848
849 let rel_x = (prompt_width + visible_chars_before_cursor) as u16;
851 let rel_y = 0u16;
852
853 log::debug!(
854 "π― CURSOR OVERLAY: cursor_pos={}, viewport_start={}, chars_before={}, rel_x={}, prompt_width={}",
855 cursor_pos, viewport_start, visible_chars_before_cursor, rel_x, prompt_width
856 );
857
858 Some((rel_x, rel_y))
859 } else {
860 None };
862
863 (paragraph, cursor_coord)
864 }
865
866 fn handle_input(&mut self, key: KeyEvent) -> Option<String> {
867 self.handle_key_event(key)
868 }
869
870 fn as_input_state(&mut self) -> Option<&mut dyn InputWidget> {
871 Some(self)
872 }
873
874 fn get_backup_data(&self) -> Option<InputStateBackup> {
875 Some(self.export_state())
876 }
877
878 fn restore_backup_data(&mut self, backup: InputStateBackup) {
879 self.import_state(backup);
880 }
881}
882
883impl InputWidget for InputState {
884 fn update_cursor_blink(&mut self) {
885 self.cursor.update_blink();
886 }
887}