agent_air_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())].chars().count();
263
264 let mut visual_y = 0;
266 for (i, line) in self.buffer.split('\n').enumerate() {
267 if i >= logical_line {
268 break;
269 }
270 let prefix_len = if i == 0 { prompt_len } else { indent_len };
271 let first_width = width.saturating_sub(prefix_len);
272 let breaks = self.word_wrap_line(line, first_width, width);
273 visual_y += breaks.len();
274 }
275
276 let prefix_len = if logical_line == 0 {
278 prompt_len
279 } else {
280 indent_len
281 };
282 let first_width = width.saturating_sub(prefix_len);
283 let breaks = self.word_wrap_line(current_line, first_width, width);
284
285 for (i, (start, end)) in breaks.iter().enumerate() {
290 if col_chars >= *start && col_chars < *end {
291 let x = if i == 0 {
292 prefix_len + (col_chars - start)
293 } else {
294 col_chars - start
295 };
296 return (x as u16, (visual_y + i) as u16);
297 }
298 }
299
300 let last_break = breaks.last().unwrap_or(&(0, 0));
302 let x = if breaks.len() == 1 {
303 prefix_len + (col_chars - last_break.0)
304 } else {
305 col_chars - last_break.0
306 };
307 (x as u16, (visual_y + breaks.len() - 1) as u16)
308 }
309
310 pub fn cursor_display_position(&self, prompt_len: usize, indent_len: usize) -> (u16, u16) {
312 let (line, col) = self.cursor_line_col();
313 let x = if line == 0 {
314 prompt_len + col
315 } else {
316 indent_len + col
317 };
318 (x as u16, line as u16)
319 }
320}
321
322impl Default for TextInput {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328use super::{Widget, WidgetKeyContext, WidgetKeyResult, widget_ids};
331use crate::themes::Theme;
332use crossterm::event::KeyEvent;
333use ratatui::{Frame, layout::Rect};
334use std::any::Any;
335
336impl Widget for TextInput {
337 fn id(&self) -> &'static str {
338 widget_ids::TEXT_INPUT
339 }
340
341 fn priority(&self) -> u8 {
342 50 }
344
345 fn is_active(&self) -> bool {
346 true }
348
349 fn handle_key(&mut self, _key: KeyEvent, _ctx: &WidgetKeyContext) -> WidgetKeyResult {
350 WidgetKeyResult::NotHandled
353 }
354
355 fn render(&mut self, _frame: &mut Frame, _area: Rect, _theme: &Theme) {
356 }
359
360 fn required_height(&self, _available: u16) -> u16 {
361 0 }
363
364 fn blocks_input(&self) -> bool {
365 false
366 }
367
368 fn is_overlay(&self) -> bool {
369 false
370 }
371
372 fn as_any(&self) -> &dyn Any {
373 self
374 }
375
376 fn as_any_mut(&mut self) -> &mut dyn Any {
377 self
378 }
379
380 fn into_any(self: Box<Self>) -> Box<dyn Any> {
381 self
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_new_input() {
391 let input = TextInput::new();
392 assert!(input.is_empty());
393 assert_eq!(input.buffer(), "");
394 }
395
396 #[test]
397 fn test_insert_char() {
398 let mut input = TextInput::new();
399 input.insert_char('a');
400 input.insert_char('b');
401 input.insert_char('c');
402 assert_eq!(input.buffer(), "abc");
403 }
404
405 #[test]
406 fn test_delete_char_before() {
407 let mut input = TextInput::new();
408 input.insert_char('a');
409 input.insert_char('b');
410 input.delete_char_before();
411 assert_eq!(input.buffer(), "a");
412 }
413
414 #[test]
415 fn test_cursor_movement() {
416 let mut input = TextInput::new();
417 input.insert_char('a');
418 input.insert_char('b');
419 input.insert_char('c');
420
421 input.move_left();
422 input.move_left();
423 input.insert_char('x');
424 assert_eq!(input.buffer(), "axbc");
425 }
426
427 #[test]
428 fn test_cursor_line_col() {
429 let mut input = TextInput::new();
430 input.insert_char('a');
431 input.insert_char('\n');
432 input.insert_char('b');
433
434 let (line, col) = input.cursor_line_col();
435 assert_eq!(line, 1);
436 assert_eq!(col, 1);
437 }
438
439 #[test]
440 fn test_line_count() {
441 let mut input = TextInput::new();
442 assert_eq!(input.line_count(), 1);
443
444 input.insert_char('a');
445 input.insert_char('\n');
446 input.insert_char('b');
447 assert_eq!(input.line_count(), 2);
448
449 input.insert_char('\n');
450 input.insert_char('c');
451 assert_eq!(input.line_count(), 3);
452 }
453
454 #[test]
455 fn test_take() {
456 let mut input = TextInput::new();
457 input.insert_char('a');
458 input.insert_char('b');
459
460 let taken = input.take();
461 assert_eq!(taken, "ab");
462 assert!(input.is_empty());
463 }
464}