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 self.load_history(cache).is_err() {
292 return;
293 }
294 } else {
295 return;
296 }
297 }
298
299 if self.history.is_empty() {
300 return;
301 }
302
303 if self.history_index.is_none() {
305 self.sync_from_textarea(); self.history_temp = Some(self.value.clone());
307 }
308
309 let new_index = if let Some(current_idx) = self.history_index {
311 if current_idx > 0 {
312 current_idx - 1
313 } else {
314 current_idx }
316 } else {
317 self.history.len() - 1 };
319
320 self.history_index = Some(new_index);
321 if let Some(entry) = self.history.get(new_index) {
322 self.value = entry.clone();
323 self.cursor = self.value.chars().count();
324 self.sync_to_textarea();
325 }
326 }
327
328 pub fn navigate_history_down(&mut self) {
330 if self.history_id.is_none() || self.history_index.is_none() {
331 return;
332 }
333
334 let current_idx = self.history_index.unwrap();
335 if current_idx >= self.history.len() - 1 {
336 if let Some(ref temp) = self.history_temp {
338 self.value = temp.clone();
339 self.cursor = self.value.chars().count();
340 self.sync_to_textarea();
341 }
342 self.history_index = None;
343 self.history_temp = None;
344 } else {
345 let new_index = current_idx + 1;
347 self.history_index = Some(new_index);
348 if let Some(entry) = self.history.get(new_index) {
349 self.value = entry.clone();
350 self.cursor = self.value.chars().count();
351 self.sync_to_textarea();
352 }
353 }
354 }
355
356 pub fn handle_key(&mut self, event: &KeyEvent, cache: Option<&CacheManager>) -> TextInputEvent {
358 let input = self.key_event_to_input(event);
360
361 match event.code {
362 KeyCode::Enter => {
363 if let Some(cache) = cache {
366 let _ = self.save_to_history(cache);
367 }
368 return TextInputEvent::Submit;
369 }
370 KeyCode::Esc => {
371 return TextInputEvent::Cancel;
372 }
373 KeyCode::Up if self.history_id.is_some() => {
374 self.navigate_history_up(cache);
375 return TextInputEvent::HistoryChanged;
376 }
377 KeyCode::Down if self.history_id.is_some() => {
378 self.navigate_history_down();
379 return TextInputEvent::HistoryChanged;
380 }
381 _ => {
382 if matches!(input.key, Key::Char('\n') | Key::Char('\r')) {
384 return TextInputEvent::None;
385 }
386 self.textarea.input(input);
388 self.sync_from_textarea();
390 if self.history_index.is_some() {
392 self.history_index = None;
393 self.history_temp = None;
394 }
395 }
396 }
397 TextInputEvent::None
398 }
399
400 fn key_event_to_input(&self, event: &KeyEvent) -> Input {
402 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
403 let alt = event.modifiers.contains(KeyModifiers::ALT);
404 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
405
406 let key = match event.code {
407 KeyCode::Char(c) => Key::Char(c),
408 KeyCode::Backspace => Key::Backspace,
409 KeyCode::Enter => Key::Enter,
410 KeyCode::Left => Key::Left,
411 KeyCode::Right => Key::Right,
412 KeyCode::Up => Key::Up,
413 KeyCode::Down => Key::Down,
414 KeyCode::Home => Key::Home,
415 KeyCode::End => Key::End,
416 KeyCode::PageUp => Key::PageUp,
417 KeyCode::PageDown => Key::PageDown,
418 KeyCode::Tab => Key::Tab,
419 KeyCode::BackTab => Key::Tab, KeyCode::Delete => Key::Delete,
421 KeyCode::Insert => Key::Null, KeyCode::F(_) => Key::Null,
423 KeyCode::Null => Key::Null,
424 KeyCode::Esc => Key::Esc,
425 KeyCode::CapsLock
426 | KeyCode::ScrollLock
427 | KeyCode::NumLock
428 | KeyCode::PrintScreen
429 | KeyCode::Pause
430 | KeyCode::Menu
431 | KeyCode::Media(_)
432 | KeyCode::Modifier(_)
433 | KeyCode::KeypadBegin => Key::Null,
434 };
435
436 Input {
437 key,
438 ctrl,
439 alt,
440 shift,
441 }
442 }
443}
444
445impl Default for TextInput {
446 fn default() -> Self {
447 Self::new()
448 }
449}
450
451impl Widget for &TextInput {
452 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
453 self.textarea.render(area, buf);
455
456 for y in area.y..area.bottom() {
458 for x in area.x..area.right() {
459 let cell = &mut buf[(x, y)];
460 let mut style = cell.style();
461 style = style.remove_modifier(Modifier::UNDERLINED);
462 cell.set_style(style);
463 }
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_text_input_new() {
474 let input = TextInput::new();
475 assert_eq!(input.value(), "");
476 assert_eq!(input.cursor(), 0);
477 assert_eq!(input.history_id, None);
478 assert_eq!(input.history_limit, 1000);
479 assert!(!input.focused);
480 }
481
482 #[test]
483 fn test_set_value() {
484 let mut input = TextInput::new();
485 input.set_value("hello".to_string());
486 assert_eq!(input.value(), "hello");
487 }
488
489 #[test]
490 fn test_clear() {
491 let mut input = TextInput::new();
492 input.set_value("hello".to_string());
493 input.clear();
494 assert_eq!(input.value(), "");
495 }
496
497 #[test]
498 fn test_is_empty() {
499 let mut input = TextInput::new();
500 assert!(input.is_empty());
501 input.set_value("hello".to_string());
502 assert!(!input.is_empty());
503 }
504}