1use color_eyre::Result;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use ratatui::{
4 layout::Rect,
5 style::{Color, Modifier, Style},
6 widgets::Widget,
7};
8use tui_textarea::{Input, Key, TextArea};
9
10use crate::cache::CacheManager;
11use crate::config::Theme;
12
13use super::text_input_common::{add_to_history, load_history_impl, save_history_impl};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TextInputEvent {
18 None,
19 Submit, Cancel, HistoryChanged, }
23
24pub struct TextInput {
26 textarea: TextArea<'static>,
27 pub value: String,
29 pub cursor: usize,
30 pub history_id: Option<String>, pub history: Vec<String>, pub history_index: Option<usize>, pub history_temp: Option<String>, pub history_limit: usize, pub history_loaded: bool, text_color: Option<Color>,
38 cursor_bg: Option<Color>,
39 cursor_fg: Option<Color>,
40 background_color: Option<Color>,
41 cursor_focused: Option<Color>, focused: bool, }
44
45impl TextInput {
46 pub fn new() -> Self {
48 let mut textarea = TextArea::default();
49 textarea.set_cursor_line_style(Style::default()); let mut widget = Self {
54 textarea,
55 value: String::new(),
56 cursor: 0,
57 history_id: None,
58 history: Vec::new(),
59 history_index: None,
60 history_temp: None,
61 history_limit: 1000,
62 history_loaded: false,
63 text_color: None,
64 cursor_bg: None,
65 cursor_fg: None,
66 background_color: None,
67 cursor_focused: None,
68 focused: false,
69 };
70 widget.apply_colors_to_textarea();
72 widget
73 }
74
75 fn sync_from_textarea(&mut self) {
77 self.value = self.textarea.lines().first().cloned().unwrap_or_default();
78 self.cursor = self.textarea.cursor().1;
79 }
80
81 fn apply_colors_to_textarea(&mut self) {
83 let mut style = Style::default();
86 if let Some(text_color) = self.text_color {
87 style = style.fg(text_color);
88 }
89 if let Some(bg_color) = self.background_color {
90 style = style.bg(bg_color);
91 }
92 self.textarea.set_style(style);
94 self.textarea.set_cursor_line_style(Style::default());
96 }
97
98 fn sync_to_textarea(&mut self) {
100 let single_line = self.value.replace(['\n', '\r'], " ");
101 self.textarea = TextArea::new(vec![single_line]);
102 self.apply_colors_to_textarea();
105 let was_focused = self.focused;
107 self.focused = false; self.set_focused(was_focused);
109 use tui_textarea::CursorMove;
110 self.textarea.move_cursor(CursorMove::Jump(
111 0,
112 self.cursor.min(u16::MAX as usize) as u16,
113 ));
114 }
115
116 #[allow(deprecated)]
119 pub fn with_style(mut self, text_color: Color, cursor_bg: Color, cursor_fg: Color) -> Self {
120 self.text_color = Some(text_color);
121 self.cursor_bg = Some(cursor_bg);
122 self.cursor_fg = Some(cursor_fg);
123 self.apply_colors_to_textarea();
124 self
125 }
126
127 pub fn with_text_color(mut self, color: Color) -> Self {
129 self.text_color = Some(color);
130 self.apply_colors_to_textarea();
131 self
132 }
133
134 #[deprecated(note = "Cursor colors are now automatically reversed from text/background colors")]
136 pub fn with_cursor_colors(mut self, bg: Color, fg: Color) -> Self {
137 self.cursor_bg = Some(bg);
138 self.cursor_fg = Some(fg);
139 self
140 }
141
142 pub fn with_background(mut self, color: Color) -> Self {
144 self.background_color = Some(color);
145 self.apply_colors_to_textarea();
146 self
147 }
148
149 pub fn with_theme(mut self, theme: &Theme) -> Self {
153 let text_primary = theme.get("text_primary");
154 self.text_color = Some(text_primary);
156 self.cursor_focused = Some(theme.get("cursor_focused"));
158 self.apply_colors_to_textarea();
159 self
160 }
161
162 pub fn with_history(mut self, history_id: String) -> Self {
164 self.history_id = Some(history_id);
165 self
166 }
167
168 pub fn with_history_limit(mut self, limit: usize) -> Self {
170 self.history_limit = limit;
171 self
172 }
173
174 pub fn set_focused(&mut self, focused: bool) {
176 self.focused = focused;
177 if focused {
180 let cursor_color = self.cursor_focused.unwrap_or(Color::Reset);
183 let cursor_style = if cursor_color == Color::Reset {
184 Style::default().add_modifier(Modifier::REVERSED)
186 } else {
187 let text_color = match cursor_color {
189 Color::White => Color::Black,
190 Color::Black => Color::White,
191 Color::Red => Color::White,
192 Color::Green => Color::Black,
193 Color::Yellow => Color::Black,
194 Color::Blue => Color::White,
195 Color::Magenta => Color::White,
196 Color::Cyan => Color::Black,
197 Color::Gray => Color::Black,
198 Color::DarkGray => Color::White,
199 Color::LightRed => Color::Black,
200 Color::LightGreen => Color::Black,
201 Color::LightYellow => Color::Black,
202 Color::LightBlue => Color::Black,
203 Color::LightMagenta => Color::Black,
204 Color::LightCyan => Color::Black,
205 _ => Color::Black,
206 };
207 Style::default().bg(cursor_color).fg(text_color)
208 };
209 self.textarea.set_cursor_style(cursor_style);
210 } else {
211 let textarea_style = self.textarea.style();
214 self.textarea.set_cursor_style(textarea_style);
215 }
216 }
217
218 pub fn value(&self) -> &str {
220 &self.value
221 }
222
223 pub fn set_value(&mut self, value: String) {
225 self.value = value;
226 self.sync_to_textarea();
227 }
228
229 pub fn cursor(&self) -> usize {
231 self.cursor
232 }
233
234 pub fn set_cursor(&mut self, cursor: usize) {
236 self.cursor = cursor;
237 use tui_textarea::CursorMove;
238 self.textarea
239 .move_cursor(CursorMove::Jump(0, cursor.min(u16::MAX as usize) as u16));
240 }
241
242 pub fn load_history(&mut self, cache: &CacheManager) -> Result<()> {
244 if self.history_loaded {
245 return Ok(());
246 }
247 if let Some(ref history_id) = self.history_id {
248 self.history = load_history_impl(cache, history_id)?;
249 self.history_loaded = true;
250 }
251 Ok(())
252 }
253
254 pub fn save_to_history(&mut self, cache: &CacheManager) -> Result<()> {
256 if let Some(history_id) = self.history_id.clone() {
257 self.sync_from_textarea(); if !self.value.is_empty() {
259 add_to_history(&mut self.history, self.value.clone());
261 save_history_impl(cache, &history_id, &self.history, self.history_limit)?;
263 }
264 }
265 Ok(())
266 }
267
268 pub fn clear(&mut self) {
270 self.textarea = TextArea::default();
271 self.value.clear();
272 self.cursor = 0;
273 self.history_index = None;
274 self.history_temp = None;
275 }
276
277 pub fn is_empty(&self) -> bool {
279 self.value.is_empty()
280 }
281
282 pub fn navigate_history_up(&mut self, cache: Option<&CacheManager>) {
284 if self.history_id.is_none() {
285 return;
286 }
287
288 if !self.history_loaded {
290 if let Some(cache) = cache {
291 if let Err(e) = self.load_history(cache) {
292 eprintln!("Warning: Could not load history: {}", e);
293 return;
294 }
295 } else {
296 return;
297 }
298 }
299
300 if self.history.is_empty() {
301 return;
302 }
303
304 if self.history_index.is_none() {
306 self.sync_from_textarea(); self.history_temp = Some(self.value.clone());
308 }
309
310 let new_index = if let Some(current_idx) = self.history_index {
312 if current_idx > 0 {
313 current_idx - 1
314 } else {
315 current_idx }
317 } else {
318 self.history.len() - 1 };
320
321 self.history_index = Some(new_index);
322 if let Some(entry) = self.history.get(new_index) {
323 self.value = entry.clone();
324 self.cursor = self.value.chars().count();
325 self.sync_to_textarea();
326 }
327 }
328
329 pub fn navigate_history_down(&mut self) {
331 if self.history_id.is_none() || self.history_index.is_none() {
332 return;
333 }
334
335 let current_idx = self.history_index.unwrap();
336 if current_idx >= self.history.len() - 1 {
337 if let Some(ref temp) = self.history_temp {
339 self.value = temp.clone();
340 self.cursor = self.value.chars().count();
341 self.sync_to_textarea();
342 }
343 self.history_index = None;
344 self.history_temp = None;
345 } else {
346 let new_index = current_idx + 1;
348 self.history_index = Some(new_index);
349 if let Some(entry) = self.history.get(new_index) {
350 self.value = entry.clone();
351 self.cursor = self.value.chars().count();
352 self.sync_to_textarea();
353 }
354 }
355 }
356
357 pub fn handle_key(&mut self, event: &KeyEvent, cache: Option<&CacheManager>) -> TextInputEvent {
359 let input = self.key_event_to_input(event);
361
362 match event.code {
363 KeyCode::Enter => {
364 if let Some(cache) = cache {
367 let _ = self.save_to_history(cache);
368 }
369 return TextInputEvent::Submit;
370 }
371 KeyCode::Esc => {
372 return TextInputEvent::Cancel;
373 }
374 KeyCode::Up if self.history_id.is_some() => {
375 self.navigate_history_up(cache);
376 return TextInputEvent::HistoryChanged;
377 }
378 KeyCode::Down if self.history_id.is_some() => {
379 self.navigate_history_down();
380 return TextInputEvent::HistoryChanged;
381 }
382 _ => {
383 if matches!(input.key, Key::Char('\n') | Key::Char('\r')) {
385 return TextInputEvent::None;
386 }
387 self.textarea.input(input);
389 self.sync_from_textarea();
391 if self.history_index.is_some() {
393 self.history_index = None;
394 self.history_temp = None;
395 }
396 }
397 }
398 TextInputEvent::None
399 }
400
401 fn key_event_to_input(&self, event: &KeyEvent) -> Input {
403 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
404 let alt = event.modifiers.contains(KeyModifiers::ALT);
405 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
406
407 let key = match event.code {
408 KeyCode::Char(c) => Key::Char(c),
409 KeyCode::Backspace => Key::Backspace,
410 KeyCode::Enter => Key::Enter,
411 KeyCode::Left => Key::Left,
412 KeyCode::Right => Key::Right,
413 KeyCode::Up => Key::Up,
414 KeyCode::Down => Key::Down,
415 KeyCode::Home => Key::Home,
416 KeyCode::End => Key::End,
417 KeyCode::PageUp => Key::PageUp,
418 KeyCode::PageDown => Key::PageDown,
419 KeyCode::Tab => Key::Tab,
420 KeyCode::BackTab => Key::Tab, KeyCode::Delete => Key::Delete,
422 KeyCode::Insert => Key::Null, KeyCode::F(_) => Key::Null,
424 KeyCode::Null => Key::Null,
425 KeyCode::Esc => Key::Esc,
426 KeyCode::CapsLock
427 | KeyCode::ScrollLock
428 | KeyCode::NumLock
429 | KeyCode::PrintScreen
430 | KeyCode::Pause
431 | KeyCode::Menu
432 | KeyCode::Media(_)
433 | KeyCode::Modifier(_)
434 | KeyCode::KeypadBegin => Key::Null,
435 };
436
437 Input {
438 key,
439 ctrl,
440 alt,
441 shift,
442 }
443 }
444}
445
446impl Default for TextInput {
447 fn default() -> Self {
448 Self::new()
449 }
450}
451
452impl Widget for &TextInput {
453 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
454 self.textarea.render(area, buf);
456
457 for y in area.y..area.bottom() {
459 for x in area.x..area.right() {
460 let cell = &mut buf[(x, y)];
461 let mut style = cell.style();
462 style = style.remove_modifier(Modifier::UNDERLINED);
463 cell.set_style(style);
464 }
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_text_input_new() {
475 let input = TextInput::new();
476 assert_eq!(input.value(), "");
477 assert_eq!(input.cursor(), 0);
478 assert_eq!(input.history_id, None);
479 assert_eq!(input.history_limit, 1000);
480 assert!(!input.focused);
481 }
482
483 #[test]
484 fn test_set_value() {
485 let mut input = TextInput::new();
486 input.set_value("hello".to_string());
487 assert_eq!(input.value(), "hello");
488 }
489
490 #[test]
491 fn test_clear() {
492 let mut input = TextInput::new();
493 input.set_value("hello".to_string());
494 input.clear();
495 assert_eq!(input.value(), "");
496 }
497
498 #[test]
499 fn test_is_empty() {
500 let mut input = TextInput::new();
501 assert!(input.is_empty());
502 input.set_value("hello".to_string());
503 assert!(!input.is_empty());
504 }
505}