1use ratatui::Frame;
12use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13use ratatui::layout::{Position, Rect};
14use ratatui::style::Style;
15use ratatui::widgets::Paragraph;
16use unicode_width::UnicodeWidthStr;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum InputOutcome {
22 Consumed,
24 Changed,
26 Submit,
28 Cancel,
30 NotConsumed,
32}
33
34#[derive(Default)]
35pub struct SingleLineInput {
36 value: String,
37 cursor: usize,
39 last_caret_pos: Option<(u16, u16)>,
43}
44
45impl SingleLineInput {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_value(value: impl Into<String>) -> Self {
51 let value = value.into();
52 let cursor = value.len();
53 Self {
54 value,
55 cursor,
56 last_caret_pos: None,
57 }
58 }
59
60 pub fn value(&self) -> &str {
61 &self.value
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.value.is_empty()
66 }
67
68 pub fn cursor_byte(&self) -> usize {
70 self.cursor
71 }
72
73 pub fn last_caret_pos(&self) -> Option<(u16, u16)> {
76 self.last_caret_pos
77 }
78
79 pub fn replace_range_bytes(
86 &mut self,
87 range: std::ops::Range<usize>,
88 new_text: &str,
89 new_cursor_byte: usize,
90 ) {
91 debug_assert!(self.value.is_char_boundary(range.start));
92 debug_assert!(self.value.is_char_boundary(range.end));
93 self.value.replace_range(range, new_text);
94 let clamped = new_cursor_byte.min(self.value.len());
95 debug_assert!(
96 self.value.is_char_boundary(clamped),
97 "new_cursor_byte must land on a char boundary"
98 );
99 self.cursor = clamped;
100 }
101
102 pub fn set_value(&mut self, value: impl Into<String>) {
104 self.value = value.into();
105 self.cursor = self.value.len();
106 }
107
108 pub fn clear(&mut self) {
109 self.value.clear();
110 self.cursor = 0;
111 }
112
113 #[cfg(test)]
117 pub(crate) fn cursor_char_offset(&self) -> usize {
118 self.value[..self.cursor].chars().count()
119 }
120
121 pub fn cursor_display_col(&self) -> usize {
124 self.value[..self.cursor].width()
125 }
126
127 pub fn display_width(&self) -> usize {
129 self.value.width()
130 }
131
132 pub fn handle_key(&mut self, key: &KeyEvent) -> InputOutcome {
133 match (key.modifiers, key.code) {
134 (_, KeyCode::Enter) => InputOutcome::Submit,
135 (_, KeyCode::Esc) => InputOutcome::Cancel,
136 (_, KeyCode::Backspace) => {
137 if self.cursor == 0 {
138 return InputOutcome::Consumed;
139 }
140 let prev = prev_char_boundary(&self.value, self.cursor);
141 self.value.drain(prev..self.cursor);
142 self.cursor = prev;
143 InputOutcome::Changed
144 }
145 (_, KeyCode::Delete) => {
146 if self.cursor >= self.value.len() {
147 return InputOutcome::Consumed;
148 }
149 let next = next_char_boundary(&self.value, self.cursor);
150 self.value.drain(self.cursor..next);
151 InputOutcome::Changed
152 }
153 (_, KeyCode::Left) => {
154 if self.cursor == 0 {
155 return InputOutcome::Consumed;
156 }
157 self.cursor = prev_char_boundary(&self.value, self.cursor);
158 InputOutcome::Consumed
159 }
160 (_, KeyCode::Right) => {
161 if self.cursor >= self.value.len() {
162 return InputOutcome::Consumed;
163 }
164 self.cursor = next_char_boundary(&self.value, self.cursor);
165 InputOutcome::Consumed
166 }
167 (_, KeyCode::Home) => {
168 self.cursor = 0;
169 InputOutcome::Consumed
170 }
171 (_, KeyCode::End) => {
172 self.cursor = self.value.len();
173 InputOutcome::Consumed
174 }
175 (m, KeyCode::Char(c)) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
178 self.value.insert(self.cursor, c);
179 self.cursor += c.len_utf8();
180 InputOutcome::Changed
181 }
182 _ => InputOutcome::NotConsumed,
183 }
184 }
185
186 pub fn render(
193 &mut self,
194 f: &mut Frame,
195 rect: Rect,
196 style: Style,
197 value_offset_x: u16,
198 focused: bool,
199 ) {
200 let inner = Rect {
201 x: rect.x.saturating_add(value_offset_x),
202 width: rect.width.saturating_sub(value_offset_x),
203 ..rect
204 };
205 f.render_widget(Paragraph::new(self.value.as_str()).style(style), inner);
206 self.last_caret_pos = None;
207 if focused {
208 let caret_x = inner
209 .x
210 .saturating_add(self.cursor_display_col() as u16)
211 .min(inner.x + inner.width.saturating_sub(1));
212 f.set_cursor_position(Position {
213 x: caret_x,
214 y: inner.y,
215 });
216 self.last_caret_pos = Some((caret_x, inner.y));
217 }
218 }
219}
220
221fn prev_char_boundary(s: &str, from: usize) -> usize {
222 s[..from]
223 .char_indices()
224 .next_back()
225 .map(|(i, _)| i)
226 .unwrap_or(0)
227}
228
229fn next_char_boundary(s: &str, from: usize) -> usize {
230 s[from..]
231 .char_indices()
232 .nth(1)
233 .map(|(i, _)| from + i)
234 .unwrap_or(s.len())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn k(code: KeyCode) -> KeyEvent {
242 KeyEvent::new(code, KeyModifiers::NONE)
243 }
244
245 #[test]
246 fn new_is_empty_cursor_zero() {
247 let i = SingleLineInput::new();
248 assert!(i.is_empty());
249 assert_eq!(i.cursor_char_offset(), 0);
250 }
251
252 #[test]
253 fn with_value_places_cursor_at_end() {
254 let i = SingleLineInput::with_value("hello");
255 assert_eq!(i.value(), "hello");
256 assert_eq!(i.cursor_char_offset(), 5);
257 }
258
259 #[test]
260 fn typing_chars_appends_and_advances_cursor() {
261 let mut i = SingleLineInput::new();
262 assert_eq!(i.handle_key(&k(KeyCode::Char('a'))), InputOutcome::Changed);
263 assert_eq!(i.handle_key(&k(KeyCode::Char('b'))), InputOutcome::Changed);
264 assert_eq!(i.value(), "ab");
265 assert_eq!(i.cursor_char_offset(), 2);
266 }
267
268 #[test]
269 fn left_then_insert_inserts_mid_string() {
270 let mut i = SingleLineInput::with_value("ac");
271 i.handle_key(&k(KeyCode::Left));
272 assert_eq!(i.cursor_char_offset(), 1);
273 i.handle_key(&k(KeyCode::Char('b')));
274 assert_eq!(i.value(), "abc");
275 assert_eq!(i.cursor_char_offset(), 2);
276 }
277
278 #[test]
279 fn backspace_at_start_is_noop() {
280 let mut i = SingleLineInput::with_value("abc");
281 i.handle_key(&k(KeyCode::Home));
282 assert_eq!(i.handle_key(&k(KeyCode::Backspace)), InputOutcome::Consumed);
283 assert_eq!(i.value(), "abc");
284 }
285
286 #[test]
287 fn delete_at_end_is_noop() {
288 let mut i = SingleLineInput::with_value("abc");
289 assert_eq!(i.handle_key(&k(KeyCode::Delete)), InputOutcome::Consumed);
290 assert_eq!(i.value(), "abc");
291 }
292
293 #[test]
294 fn home_end_jump_cursor() {
295 let mut i = SingleLineInput::with_value("abc");
296 i.handle_key(&k(KeyCode::Home));
297 assert_eq!(i.cursor_char_offset(), 0);
298 i.handle_key(&k(KeyCode::End));
299 assert_eq!(i.cursor_char_offset(), 3);
300 }
301
302 #[test]
303 fn unicode_chars_count_by_codepoint_not_bytes() {
304 let mut i = SingleLineInput::new();
305 i.handle_key(&k(KeyCode::Char('あ')));
306 i.handle_key(&k(KeyCode::Char('い')));
307 assert_eq!(i.value(), "あい");
308 assert_eq!(i.cursor_char_offset(), 2);
309 i.handle_key(&k(KeyCode::Left));
310 assert_eq!(i.cursor_char_offset(), 1);
311 i.handle_key(&k(KeyCode::Backspace));
312 assert_eq!(i.value(), "い");
313 }
314
315 #[test]
316 fn enter_returns_submit_esc_returns_cancel() {
317 let mut i = SingleLineInput::with_value("x");
318 assert_eq!(i.handle_key(&k(KeyCode::Enter)), InputOutcome::Submit);
319 assert_eq!(i.handle_key(&k(KeyCode::Esc)), InputOutcome::Cancel);
320 }
321
322 #[test]
323 fn ctrl_char_is_not_consumed_as_text() {
324 let mut i = SingleLineInput::new();
325 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
326 assert_eq!(i.handle_key(&key), InputOutcome::NotConsumed);
327 assert!(i.is_empty());
328 }
329
330 #[test]
331 fn alt_char_is_not_consumed_as_text() {
332 let mut i = SingleLineInput::new();
333 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT);
334 assert_eq!(i.handle_key(&key), InputOutcome::NotConsumed);
335 assert!(i.is_empty());
336 }
337
338 #[test]
339 fn cjk_chars_count_two_display_cols_per_char() {
340 let mut i = SingleLineInput::new();
341 i.handle_key(&k(KeyCode::Char('あ')));
342 i.handle_key(&k(KeyCode::Char('い')));
343 assert_eq!(i.cursor_char_offset(), 2);
345 assert_eq!(i.cursor_display_col(), 4);
346 assert_eq!(i.display_width(), 4);
347 }
348
349 #[test]
350 fn mixed_ascii_and_cjk_caret_column() {
351 let mut i = SingleLineInput::with_value("ab猫");
352 assert_eq!(i.cursor_display_col(), 4);
354 i.handle_key(&k(KeyCode::Left));
355 assert_eq!(i.cursor_display_col(), 2);
357 }
358
359 #[test]
360 fn shift_char_inserts() {
361 let mut i = SingleLineInput::new();
362 let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
363 assert_eq!(i.handle_key(&key), InputOutcome::Changed);
364 assert_eq!(i.value(), "A");
365 }
366
367 #[test]
368 fn set_value_resets_cursor_to_end() {
369 let mut i = SingleLineInput::with_value("abc");
370 i.handle_key(&k(KeyCode::Home));
371 i.set_value("xyz!");
372 assert_eq!(i.value(), "xyz!");
373 assert_eq!(i.cursor_char_offset(), 4);
374 }
375
376 #[test]
377 fn clear_resets_both() {
378 let mut i = SingleLineInput::with_value("abc");
379 i.clear();
380 assert!(i.is_empty());
381 assert_eq!(i.cursor_char_offset(), 0);
382 }
383}