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 let Err(e) = self.load_history(cache) {
343 eprintln!("Warning: Could not load history: {}", e);
344 return;
345 }
346 } else {
347 return;
348 }
349 }
350
351 if self.history.is_empty() {
352 return;
353 }
354
355 if self.history_index.is_none() {
357 self.sync_from_textarea(); self.history_temp = Some(self.value.clone());
359 }
360
361 let new_index = if let Some(current_idx) = self.history_index {
363 if current_idx > 0 {
364 current_idx - 1
365 } else {
366 current_idx }
368 } else {
369 self.history.len() - 1 };
371
372 self.history_index = Some(new_index);
373 if let Some(entry) = self.history.get(new_index) {
374 self.value = entry.clone();
375 let lines: Vec<&str> = self.value.lines().collect();
377 if let Some(last_line) = lines.last() {
378 self.cursor_line = lines.len().saturating_sub(1);
379 self.cursor_col = last_line.chars().count();
380 }
381 self.sync_to_textarea();
382 }
383 }
384
385 pub fn navigate_history_down(&mut self) {
387 if self.history_id.is_none() || self.history_index.is_none() {
388 return;
389 }
390
391 let current_idx = self.history_index.unwrap();
392 if current_idx >= self.history.len() - 1 {
393 if let Some(ref temp) = self.history_temp {
395 self.value = temp.clone();
396 let lines: Vec<&str> = self.value.lines().collect();
398 if let Some(last_line) = lines.last() {
399 self.cursor_line = lines.len().saturating_sub(1);
400 self.cursor_col = last_line.chars().count();
401 }
402 self.sync_to_textarea();
403 }
404 self.history_index = None;
405 self.history_temp = None;
406 } else {
407 let new_index = current_idx + 1;
409 self.history_index = Some(new_index);
410 if let Some(entry) = self.history.get(new_index) {
411 self.value = entry.clone();
412 let lines: Vec<&str> = self.value.lines().collect();
414 if let Some(last_line) = lines.last() {
415 self.cursor_line = lines.len().saturating_sub(1);
416 self.cursor_col = last_line.chars().count();
417 }
418 self.sync_to_textarea();
419 }
420 }
421 }
422
423 pub fn handle_key(&mut self, event: &KeyEvent, cache: Option<&CacheManager>) -> TextInputEvent {
425 let input = self.key_event_to_input(event);
427
428 match event.code {
429 KeyCode::Esc => {
430 return TextInputEvent::Cancel;
431 }
432 KeyCode::Char('p') | KeyCode::Char('P')
433 if event.modifiers.contains(KeyModifiers::CONTROL) =>
434 {
435 if self.history_id.is_some() {
437 self.navigate_history_up(cache);
438 return TextInputEvent::HistoryChanged;
439 }
440 }
441 KeyCode::Char('n') | KeyCode::Char('N')
442 if event.modifiers.contains(KeyModifiers::CONTROL) =>
443 {
444 if self.history_id.is_some() {
446 self.navigate_history_down();
447 return TextInputEvent::HistoryChanged;
448 }
449 }
450 _ => {
451 self.textarea.input(input);
453 self.sync_from_textarea();
455 if self.history_index.is_some() {
457 self.history_index = None;
458 self.history_temp = None;
459 }
460 }
461 }
462 TextInputEvent::None
463 }
464
465 fn key_event_to_input(&self, event: &KeyEvent) -> Input {
467 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
468 let alt = event.modifiers.contains(KeyModifiers::ALT);
469 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
470
471 let key = match event.code {
472 KeyCode::Char(c) => Key::Char(c),
473 KeyCode::Backspace => Key::Backspace,
474 KeyCode::Enter => Key::Enter,
475 KeyCode::Left => Key::Left,
476 KeyCode::Right => Key::Right,
477 KeyCode::Up => Key::Up,
478 KeyCode::Down => Key::Down,
479 KeyCode::Home => Key::Home,
480 KeyCode::End => Key::End,
481 KeyCode::PageUp => Key::PageUp,
482 KeyCode::PageDown => Key::PageDown,
483 KeyCode::Tab => Key::Tab,
484 KeyCode::BackTab => Key::Tab,
485 KeyCode::Delete => Key::Delete,
486 KeyCode::Insert => Key::Null,
487 KeyCode::F(_) => Key::Null,
488 KeyCode::Null => Key::Null,
489 KeyCode::Esc => Key::Esc,
490 KeyCode::CapsLock
491 | KeyCode::ScrollLock
492 | KeyCode::NumLock
493 | KeyCode::PrintScreen
494 | KeyCode::Pause
495 | KeyCode::Menu
496 | KeyCode::Media(_)
497 | KeyCode::Modifier(_)
498 | KeyCode::KeypadBegin => Key::Null,
499 };
500
501 Input {
502 key,
503 ctrl,
504 alt,
505 shift,
506 }
507 }
508}
509
510impl Default for MultiLineTextInput {
511 fn default() -> Self {
512 Self::new()
513 }
514}
515
516impl Widget for &MultiLineTextInput {
517 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
518 self.textarea.render(area, buf);
520
521 for y in area.y..area.bottom() {
523 for x in area.x..area.right() {
524 let cell = &mut buf[(x, y)];
525 let mut style = cell.style();
526 style = style.remove_modifier(Modifier::UNDERLINED);
527 cell.set_style(style);
528 }
529 }
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_multiline_text_input_new() {
539 let input = MultiLineTextInput::new();
540 assert_eq!(input.value(), "");
541 assert_eq!(input.cursor_line, 0);
542 assert_eq!(input.cursor_col, 0);
543 assert_eq!(input.history_id, None);
544 assert_eq!(input.history_limit, 1000);
545 assert!(!input.focused);
546 }
547
548 #[test]
549 fn test_set_value() {
550 let mut input = MultiLineTextInput::new();
551 input.value = "line1\nline2".to_string();
552 input.sync_to_textarea();
553 assert_eq!(input.line_count(), 2);
554 }
555
556 #[test]
557 fn test_clear() {
558 let mut input = MultiLineTextInput::new();
559 input.value = "hello".to_string();
560 input.clear();
561 assert_eq!(input.value, "");
562 assert_eq!(input.cursor, 0);
563 }
564
565 #[test]
566 fn test_is_empty() {
567 let mut input = MultiLineTextInput::new();
568 assert!(input.is_empty());
569 input.value = "hello".to_string();
570 assert!(!input.is_empty());
571 }
572}