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 MultiLineTextInput {
26 textarea: TextArea<'static>,
27 pub value: String,
29 pub cursor: usize,
30 pub history_id: Option<String>,
31 pub history: Vec<String>,
32 pub history_index: Option<usize>,
33 pub history_temp: Option<String>,
34 pub history_limit: usize,
35 pub history_loaded: bool,
36 pub scroll_offset: usize, pub horizontal_scroll_offset: usize, pub cursor_line: usize, pub cursor_col: usize, text_color: Option<Color>,
43 cursor_bg: Option<Color>,
44 cursor_fg: Option<Color>,
45 background_color: Option<Color>,
46 cursor_focused: Option<Color>, focused: bool, }
49
50impl MultiLineTextInput {
51 pub fn new() -> Self {
53 let mut textarea = TextArea::default();
54 use ratatui::style::Style;
57 textarea.set_cursor_line_style(Style::default()); let mut widget = Self {
60 textarea,
61 value: String::new(),
62 cursor: 0,
63 history_id: None,
64 history: Vec::new(),
65 history_index: None,
66 history_temp: None,
67 history_limit: 1000,
68 history_loaded: false,
69 scroll_offset: 0,
70 horizontal_scroll_offset: 0,
71 cursor_line: 0,
72 cursor_col: 0,
73 text_color: None,
74 cursor_bg: None,
75 cursor_fg: None,
76 background_color: None,
77 cursor_focused: None,
78 focused: false,
79 };
80 widget.apply_colors_to_textarea();
82 widget
83 }
84
85 #[allow(deprecated)]
88 pub fn with_style(mut self, text_color: Color, cursor_bg: Color, cursor_fg: Color) -> Self {
89 self.text_color = Some(text_color);
90 self.cursor_bg = Some(cursor_bg);
91 self.cursor_fg = Some(cursor_fg);
92 self.apply_colors_to_textarea();
93 self
94 }
95
96 pub fn with_text_color(mut self, color: Color) -> Self {
98 self.text_color = Some(color);
99 self.apply_colors_to_textarea();
100 self
101 }
102
103 #[deprecated(note = "Cursor colors are now automatically reversed from text/background colors")]
105 pub fn with_cursor_colors(mut self, bg: Color, fg: Color) -> Self {
106 self.cursor_bg = Some(bg);
107 self.cursor_fg = Some(fg);
108 self
109 }
110
111 pub fn with_background(mut self, color: Color) -> Self {
113 self.background_color = Some(color);
114 self.apply_colors_to_textarea();
115 self
116 }
117
118 pub fn with_theme(mut self, theme: &Theme) -> Self {
121 let text_primary = theme.get("text_primary");
122 self.text_color = Some(text_primary);
124 self.cursor_focused = Some(theme.get("cursor_focused"));
126 self.apply_colors_to_textarea();
127 self
128 }
129
130 pub fn with_history(mut self, history_id: String) -> Self {
132 self.history_id = Some(history_id);
133 self
134 }
135
136 pub fn with_history_limit(mut self, limit: usize) -> Self {
138 self.history_limit = limit;
139 self
140 }
141
142 pub fn set_focused(&mut self, focused: bool) {
144 self.focused = focused;
145 if focused {
148 let cursor_color = self.cursor_focused.unwrap_or(Color::Reset);
151 let cursor_style = if cursor_color == Color::Reset {
152 Style::default().add_modifier(Modifier::REVERSED)
154 } else {
155 let text_color = match cursor_color {
157 Color::White => Color::Black,
158 Color::Black => Color::White,
159 Color::Red => Color::White,
160 Color::Green => Color::Black,
161 Color::Yellow => Color::Black,
162 Color::Blue => Color::White,
163 Color::Magenta => Color::White,
164 Color::Cyan => Color::Black,
165 Color::Gray => Color::Black,
166 Color::DarkGray => Color::White,
167 Color::LightRed => Color::Black,
168 Color::LightGreen => Color::Black,
169 Color::LightYellow => Color::Black,
170 Color::LightBlue => Color::Black,
171 Color::LightMagenta => Color::Black,
172 Color::LightCyan => Color::Black,
173 _ => Color::Black,
174 };
175 Style::default().bg(cursor_color).fg(text_color)
176 };
177 self.textarea.set_cursor_style(cursor_style);
178 } else {
179 let textarea_style = self.textarea.style();
182 self.textarea.set_cursor_style(textarea_style);
183 }
184 }
185
186 pub fn value(&self) -> &str {
188 &self.value
189 }
190
191 fn sync_from_textarea(&mut self) {
193 let lines = self.textarea.lines();
195 self.value = lines.join("\n");
196
197 let (line, col) = self.textarea.cursor();
199 self.cursor_line = line;
200 self.cursor_col = col;
201
202 let mut char_pos = 0;
204 for (i, line_text) in lines.iter().enumerate() {
205 if i < self.cursor_line {
206 char_pos += line_text.chars().count() + 1; } else if i == self.cursor_line {
208 char_pos += self.cursor_col;
209 break;
210 }
211 }
212 self.cursor = char_pos;
213 }
214
215 fn apply_colors_to_textarea(&mut self) {
217 let mut style = Style::default();
219 if let Some(text_color) = self.text_color {
220 style = style.fg(text_color);
221 }
222 if let Some(bg_color) = self.background_color {
223 style = style.bg(bg_color);
224 }
225 self.textarea.set_style(style);
227 self.textarea.set_cursor_line_style(Style::default());
229 }
230
231 fn sync_to_textarea(&mut self) {
233 let lines: Vec<String> = self.value.lines().map(|s| s.to_string()).collect();
234 self.textarea = if lines.is_empty() {
235 TextArea::default()
236 } else {
237 TextArea::new(lines)
238 };
239 self.apply_colors_to_textarea();
242 let was_focused = self.focused;
244 self.focused = false; self.set_focused(was_focused);
246 use tui_textarea::CursorMove;
248 self.textarea.move_cursor(CursorMove::Jump(
249 self.cursor_line.min(u16::MAX as usize) as u16,
250 self.cursor_col.min(u16::MAX as usize) as u16,
251 ));
252 }
253
254 pub fn line_count(&self) -> usize {
256 self.textarea.lines().len()
257 }
258
259 pub fn line_at(&self, line_idx: usize) -> Option<&str> {
261 self.textarea.lines().get(line_idx).map(|s| s.as_str())
262 }
263
264 pub fn update_line_col_from_cursor(&mut self) {
266 self.sync_from_textarea();
267 }
268
269 pub fn line_col_to_cursor(&self, line: usize, col: usize) -> usize {
271 let lines = self.textarea.lines();
272 let mut char_pos = 0;
273 for (i, line_text) in lines.iter().enumerate() {
274 if i < line {
275 char_pos += line_text.chars().count() + 1; } else if i == line {
277 char_pos += col.min(line_text.chars().count());
278 break;
279 }
280 }
281 char_pos
282 }
283
284 pub fn ensure_cursor_visible(&mut self, _area_height: u16, _area_width: u16) {
286 self.sync_from_textarea();
287 }
290
291 pub fn load_history(&mut self, cache: &CacheManager) -> Result<()> {
293 if self.history_loaded {
294 return Ok(());
295 }
296 if let Some(ref history_id) = self.history_id {
297 self.history = load_history_impl(cache, history_id)?;
298 self.history_loaded = true;
299 }
300 Ok(())
301 }
302
303 pub fn save_to_history(&mut self, cache: &CacheManager) -> Result<()> {
305 if let Some(history_id) = self.history_id.clone() {
306 self.sync_from_textarea(); if !self.value.is_empty() {
308 add_to_history(&mut self.history, self.value.clone());
310 save_history_impl(cache, &history_id, &self.history, self.history_limit)?;
312 }
313 }
314 Ok(())
315 }
316
317 pub fn clear(&mut self) {
319 self.textarea = TextArea::default();
320 self.value.clear();
321 self.cursor = 0;
322 self.cursor_line = 0;
323 self.cursor_col = 0;
324 self.history_index = None;
325 self.history_temp = None;
326 }
327
328 pub fn is_empty(&self) -> bool {
330 self.value.is_empty()
331 }
332
333 pub fn navigate_history_up(&mut self, cache: Option<&CacheManager>) {
335 if self.history_id.is_none() {
336 return;
337 }
338
339 if !self.history_loaded {
341 if let Some(cache) = cache {
342 if self.load_history(cache).is_err() {
343 return;
344 }
345 } else {
346 return;
347 }
348 }
349
350 if self.history.is_empty() {
351 return;
352 }
353
354 if self.history_index.is_none() {
356 self.sync_from_textarea(); self.history_temp = Some(self.value.clone());
358 }
359
360 let new_index = if let Some(current_idx) = self.history_index {
362 if current_idx > 0 {
363 current_idx - 1
364 } else {
365 current_idx }
367 } else {
368 self.history.len() - 1 };
370
371 self.history_index = Some(new_index);
372 if let Some(entry) = self.history.get(new_index) {
373 self.value = entry.clone();
374 let lines: Vec<&str> = self.value.lines().collect();
376 if let Some(last_line) = lines.last() {
377 self.cursor_line = lines.len().saturating_sub(1);
378 self.cursor_col = last_line.chars().count();
379 }
380 self.sync_to_textarea();
381 }
382 }
383
384 pub fn navigate_history_down(&mut self) {
386 if self.history_id.is_none() || self.history_index.is_none() {
387 return;
388 }
389
390 let current_idx = self.history_index.unwrap();
391 if current_idx >= self.history.len() - 1 {
392 if let Some(ref temp) = self.history_temp {
394 self.value = temp.clone();
395 let lines: Vec<&str> = self.value.lines().collect();
397 if let Some(last_line) = lines.last() {
398 self.cursor_line = lines.len().saturating_sub(1);
399 self.cursor_col = last_line.chars().count();
400 }
401 self.sync_to_textarea();
402 }
403 self.history_index = None;
404 self.history_temp = None;
405 } else {
406 let new_index = current_idx + 1;
408 self.history_index = Some(new_index);
409 if let Some(entry) = self.history.get(new_index) {
410 self.value = entry.clone();
411 let lines: Vec<&str> = self.value.lines().collect();
413 if let Some(last_line) = lines.last() {
414 self.cursor_line = lines.len().saturating_sub(1);
415 self.cursor_col = last_line.chars().count();
416 }
417 self.sync_to_textarea();
418 }
419 }
420 }
421
422 pub fn handle_key(&mut self, event: &KeyEvent, cache: Option<&CacheManager>) -> TextInputEvent {
424 let input = self.key_event_to_input(event);
426
427 match event.code {
428 KeyCode::Esc => {
429 return TextInputEvent::Cancel;
430 }
431 KeyCode::Char('p') | KeyCode::Char('P')
432 if event.modifiers.contains(KeyModifiers::CONTROL) =>
433 {
434 if self.history_id.is_some() {
436 self.navigate_history_up(cache);
437 return TextInputEvent::HistoryChanged;
438 }
439 }
440 KeyCode::Char('n') | KeyCode::Char('N')
441 if event.modifiers.contains(KeyModifiers::CONTROL) =>
442 {
443 if self.history_id.is_some() {
445 self.navigate_history_down();
446 return TextInputEvent::HistoryChanged;
447 }
448 }
449 _ => {
450 self.textarea.input(input);
452 self.sync_from_textarea();
454 if self.history_index.is_some() {
456 self.history_index = None;
457 self.history_temp = None;
458 }
459 }
460 }
461 TextInputEvent::None
462 }
463
464 fn key_event_to_input(&self, event: &KeyEvent) -> Input {
466 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
467 let alt = event.modifiers.contains(KeyModifiers::ALT);
468 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
469
470 let key = match event.code {
471 KeyCode::Char(c) => Key::Char(c),
472 KeyCode::Backspace => Key::Backspace,
473 KeyCode::Enter => Key::Enter,
474 KeyCode::Left => Key::Left,
475 KeyCode::Right => Key::Right,
476 KeyCode::Up => Key::Up,
477 KeyCode::Down => Key::Down,
478 KeyCode::Home => Key::Home,
479 KeyCode::End => Key::End,
480 KeyCode::PageUp => Key::PageUp,
481 KeyCode::PageDown => Key::PageDown,
482 KeyCode::Tab => Key::Tab,
483 KeyCode::BackTab => Key::Tab,
484 KeyCode::Delete => Key::Delete,
485 KeyCode::Insert => Key::Null,
486 KeyCode::F(_) => Key::Null,
487 KeyCode::Null => Key::Null,
488 KeyCode::Esc => Key::Esc,
489 KeyCode::CapsLock
490 | KeyCode::ScrollLock
491 | KeyCode::NumLock
492 | KeyCode::PrintScreen
493 | KeyCode::Pause
494 | KeyCode::Menu
495 | KeyCode::Media(_)
496 | KeyCode::Modifier(_)
497 | KeyCode::KeypadBegin => Key::Null,
498 };
499
500 Input {
501 key,
502 ctrl,
503 alt,
504 shift,
505 }
506 }
507}
508
509impl Default for MultiLineTextInput {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515impl Widget for &MultiLineTextInput {
516 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
517 self.textarea.render(area, buf);
519
520 for y in area.y..area.bottom() {
522 for x in area.x..area.right() {
523 let cell = &mut buf[(x, y)];
524 let mut style = cell.style();
525 style = style.remove_modifier(Modifier::UNDERLINED);
526 cell.set_style(style);
527 }
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_multiline_text_input_new() {
538 let input = MultiLineTextInput::new();
539 assert_eq!(input.value(), "");
540 assert_eq!(input.cursor_line, 0);
541 assert_eq!(input.cursor_col, 0);
542 assert_eq!(input.history_id, None);
543 assert_eq!(input.history_limit, 1000);
544 assert!(!input.focused);
545 }
546
547 #[test]
548 fn test_set_value() {
549 let mut input = MultiLineTextInput::new();
550 input.value = "line1\nline2".to_string();
551 input.sync_to_textarea();
552 assert_eq!(input.line_count(), 2);
553 }
554
555 #[test]
556 fn test_clear() {
557 let mut input = MultiLineTextInput::new();
558 input.value = "hello".to_string();
559 input.clear();
560 assert_eq!(input.value, "");
561 assert_eq!(input.cursor, 0);
562 }
563
564 #[test]
565 fn test_is_empty() {
566 let mut input = MultiLineTextInput::new();
567 assert!(input.is_empty());
568 input.value = "hello".to_string();
569 assert!(!input.is_empty());
570 }
571}