agent_core/tui/widgets/
input.rs1pub struct TextInput {
8 buffer: String,
9 cursor_pos: usize,
10}
11
12impl TextInput {
13 pub fn new() -> Self {
15 Self {
16 buffer: String::new(),
17 cursor_pos: 0,
18 }
19 }
20
21 pub fn buffer(&self) -> &str {
23 &self.buffer
24 }
25
26 pub fn is_empty(&self) -> bool {
28 self.buffer.is_empty()
29 }
30
31 pub fn take(&mut self) -> String {
33 self.cursor_pos = 0;
34 std::mem::take(&mut self.buffer)
35 }
36
37 pub fn clear(&mut self) {
39 self.buffer.clear();
40 self.cursor_pos = 0;
41 }
42
43 pub fn insert_char(&mut self, c: char) {
47 self.buffer.insert(self.cursor_pos, c);
48 self.cursor_pos += c.len_utf8();
49 }
50
51 pub fn delete_char_before(&mut self) {
53 if self.cursor_pos > 0 {
54 let prev_char_boundary = self.buffer[..self.cursor_pos]
55 .char_indices()
56 .last()
57 .map(|(i, _)| i)
58 .unwrap_or(0);
59 self.buffer.remove(prev_char_boundary);
60 self.cursor_pos = prev_char_boundary;
61 }
62 }
63
64 pub fn delete_char_at(&mut self) {
66 if self.cursor_pos < self.buffer.len() {
67 self.buffer.remove(self.cursor_pos);
68 }
69 }
70
71 pub fn kill_line(&mut self) {
73 let line_end = self.buffer[self.cursor_pos..]
74 .find('\n')
75 .map(|i| self.cursor_pos + i)
76 .unwrap_or(self.buffer.len());
77
78 if line_end == self.cursor_pos && self.cursor_pos < self.buffer.len() {
79 self.buffer.remove(self.cursor_pos);
81 } else {
82 self.buffer.drain(self.cursor_pos..line_end);
84 }
85 }
86
87 pub fn move_left(&mut self) {
91 if self.cursor_pos > 0 {
92 self.cursor_pos = self.buffer[..self.cursor_pos]
93 .char_indices()
94 .last()
95 .map(|(i, _)| i)
96 .unwrap_or(0);
97 }
98 }
99
100 pub fn move_right(&mut self) {
102 if self.cursor_pos < self.buffer.len() {
103 self.cursor_pos += self.buffer[self.cursor_pos..]
104 .chars()
105 .next()
106 .map(|c| c.len_utf8())
107 .unwrap_or(0);
108 }
109 }
110
111 pub fn move_up(&mut self) {
113 let (current_line, col) = self.cursor_line_col();
114 if current_line > 0 {
115 let lines: Vec<&str> = self.buffer.split('\n').collect();
116 let prev_line_len = lines[current_line - 1].len();
117 let new_col = col.min(prev_line_len);
118 self.cursor_pos = self.line_col_to_pos(current_line - 1, new_col);
119 }
120 }
121
122 pub fn move_down(&mut self) {
124 let (current_line, col) = self.cursor_line_col();
125 let lines: Vec<&str> = self.buffer.split('\n').collect();
126 if current_line < lines.len() - 1 {
127 let next_line_len = lines[current_line + 1].len();
128 let new_col = col.min(next_line_len);
129 self.cursor_pos = self.line_col_to_pos(current_line + 1, new_col);
130 }
131 }
132
133 pub fn move_to_line_start(&mut self) {
135 let (current_line, _) = self.cursor_line_col();
136 self.cursor_pos = self.line_col_to_pos(current_line, 0);
137 }
138
139 pub fn move_to_line_end(&mut self) {
141 let (current_line, _) = self.cursor_line_col();
142 let lines: Vec<&str> = self.buffer.split('\n').collect();
143 let line_len = lines.get(current_line).map(|l| l.len()).unwrap_or(0);
144 self.cursor_pos = self.line_col_to_pos(current_line, line_len);
145 }
146
147 pub fn cursor_line_col(&self) -> (usize, usize) {
151 let before_cursor = &self.buffer[..self.cursor_pos];
152 let line = before_cursor.matches('\n').count();
153 let col = before_cursor
154 .rfind('\n')
155 .map(|i| self.cursor_pos - i - 1)
156 .unwrap_or(self.cursor_pos);
157 (line, col)
158 }
159
160 fn line_col_to_pos(&self, line: usize, col: usize) -> usize {
161 let mut pos = 0;
162 for (i, l) in self.buffer.split('\n').enumerate() {
163 if i == line {
164 return pos + col.min(l.len());
165 }
166 pos += l.len() + 1; }
168 self.buffer.len()
169 }
170
171 pub fn line_count(&self) -> usize {
173 self.buffer.split('\n').count().max(1)
174 }
175
176 fn word_wrap_line(
179 &self,
180 line: &str,
181 first_line_width: usize,
182 subsequent_width: usize,
183 ) -> Vec<(usize, usize)> {
184 let chars: Vec<char> = line.chars().collect();
185 let mut breaks = Vec::new();
186
187 if chars.is_empty() {
188 breaks.push((0, 0));
189 return breaks;
190 }
191
192 let mut start = 0;
193 let mut is_first_line = true;
194
195 while start < chars.len() {
196 let width = if is_first_line {
197 first_line_width
198 } else {
199 subsequent_width
200 };
201 let max_end = (start + width).min(chars.len());
202
203 if max_end >= chars.len() {
204 breaks.push((start, chars.len()));
206 break;
207 }
208
209 let mut break_at = max_end;
212 for i in (start..max_end).rev() {
213 if chars[i] == ' ' {
214 break_at = i + 1; break;
216 }
217 }
218
219 breaks.push((start, break_at));
220 start = break_at;
221 is_first_line = false;
222 }
223
224 if breaks.is_empty() {
225 breaks.push((0, 0));
226 }
227
228 breaks
229 }
230
231 pub fn visual_line_count(&self, width: usize, prompt_len: usize, indent_len: usize) -> usize {
233 if width == 0 {
234 return self.line_count();
235 }
236
237 let mut visual_lines = 0;
238 for (i, line) in self.buffer.split('\n').enumerate() {
239 let prefix_len = if i == 0 { prompt_len } else { indent_len };
240 let first_width = width.saturating_sub(prefix_len);
241 let breaks = self.word_wrap_line(line, first_width, width);
242 visual_lines += breaks.len();
243 }
244 visual_lines.max(1)
245 }
246
247 pub fn cursor_display_position_wrapped(
249 &self,
250 width: usize,
251 prompt_len: usize,
252 indent_len: usize,
253 ) -> (u16, u16) {
254 if width == 0 {
255 return self.cursor_display_position(prompt_len, indent_len);
256 }
257
258 let (logical_line, col) = self.cursor_line_col();
259
260 let current_line = self.buffer.split('\n').nth(logical_line).unwrap_or("");
262 let col_chars = current_line[..col.min(current_line.len())]
263 .chars()
264 .count();
265
266 let mut visual_y = 0;
268 for (i, line) in self.buffer.split('\n').enumerate() {
269 if i >= logical_line {
270 break;
271 }
272 let prefix_len = if i == 0 { prompt_len } else { indent_len };
273 let first_width = width.saturating_sub(prefix_len);
274 let breaks = self.word_wrap_line(line, first_width, width);
275 visual_y += breaks.len();
276 }
277
278 let prefix_len = if logical_line == 0 {
280 prompt_len
281 } else {
282 indent_len
283 };
284 let first_width = width.saturating_sub(prefix_len);
285 let breaks = self.word_wrap_line(current_line, first_width, width);
286
287 for (i, (start, end)) in breaks.iter().enumerate() {
292 if col_chars >= *start && col_chars < *end {
293 let x = if i == 0 {
294 prefix_len + (col_chars - start)
295 } else {
296 col_chars - start
297 };
298 return (x as u16, (visual_y + i) as u16);
299 }
300 }
301
302 let last_break = breaks.last().unwrap_or(&(0, 0));
304 let x = if breaks.len() == 1 {
305 prefix_len + (col_chars - last_break.0)
306 } else {
307 col_chars - last_break.0
308 };
309 (x as u16, (visual_y + breaks.len() - 1) as u16)
310 }
311
312 pub fn cursor_display_position(&self, prompt_len: usize, indent_len: usize) -> (u16, u16) {
314 let (line, col) = self.cursor_line_col();
315 let x = if line == 0 {
316 prompt_len + col
317 } else {
318 indent_len + col
319 };
320 (x as u16, line as u16)
321 }
322}
323
324impl Default for TextInput {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330use std::any::Any;
333use crossterm::event::KeyEvent;
334use ratatui::{layout::Rect, Frame};
335use crate::tui::themes::Theme;
336use super::{widget_ids, Widget, WidgetKeyContext, WidgetKeyResult};
337
338impl Widget for TextInput {
339 fn id(&self) -> &'static str {
340 widget_ids::TEXT_INPUT
341 }
342
343 fn priority(&self) -> u8 {
344 50 }
346
347 fn is_active(&self) -> bool {
348 true }
350
351 fn handle_key(&mut self, _key: KeyEvent, _ctx: &WidgetKeyContext) -> WidgetKeyResult {
352 WidgetKeyResult::NotHandled
355 }
356
357 fn render(&mut self, _frame: &mut Frame, _area: Rect, _theme: &Theme) {
358 }
361
362 fn required_height(&self, _available: u16) -> u16 {
363 0 }
365
366 fn blocks_input(&self) -> bool {
367 false
368 }
369
370 fn is_overlay(&self) -> bool {
371 false
372 }
373
374 fn as_any(&self) -> &dyn Any {
375 self
376 }
377
378 fn as_any_mut(&mut self) -> &mut dyn Any {
379 self
380 }
381
382 fn into_any(self: Box<Self>) -> Box<dyn Any> {
383 self
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_new_input() {
393 let input = TextInput::new();
394 assert!(input.is_empty());
395 assert_eq!(input.buffer(), "");
396 }
397
398 #[test]
399 fn test_insert_char() {
400 let mut input = TextInput::new();
401 input.insert_char('a');
402 input.insert_char('b');
403 input.insert_char('c');
404 assert_eq!(input.buffer(), "abc");
405 }
406
407 #[test]
408 fn test_delete_char_before() {
409 let mut input = TextInput::new();
410 input.insert_char('a');
411 input.insert_char('b');
412 input.delete_char_before();
413 assert_eq!(input.buffer(), "a");
414 }
415
416 #[test]
417 fn test_cursor_movement() {
418 let mut input = TextInput::new();
419 input.insert_char('a');
420 input.insert_char('b');
421 input.insert_char('c');
422
423 input.move_left();
424 input.move_left();
425 input.insert_char('x');
426 assert_eq!(input.buffer(), "axbc");
427 }
428
429 #[test]
430 fn test_cursor_line_col() {
431 let mut input = TextInput::new();
432 input.insert_char('a');
433 input.insert_char('\n');
434 input.insert_char('b');
435
436 let (line, col) = input.cursor_line_col();
437 assert_eq!(line, 1);
438 assert_eq!(col, 1);
439 }
440
441 #[test]
442 fn test_line_count() {
443 let mut input = TextInput::new();
444 assert_eq!(input.line_count(), 1);
445
446 input.insert_char('a');
447 input.insert_char('\n');
448 input.insert_char('b');
449 assert_eq!(input.line_count(), 2);
450
451 input.insert_char('\n');
452 input.insert_char('c');
453 assert_eq!(input.line_count(), 3);
454 }
455
456 #[test]
457 fn test_take() {
458 let mut input = TextInput::new();
459 input.insert_char('a');
460 input.insert_char('b');
461
462 let taken = input.take();
463 assert_eq!(taken, "ab");
464 assert!(input.is_empty());
465 }
466}