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 #[cfg(test)]
83 pub(crate) fn set_last_caret_pos_for_tests(&mut self, pos: Option<(u16, u16)>) {
84 self.last_caret_pos = pos;
85 }
86
87 pub fn replace_range_bytes(
94 &mut self,
95 range: std::ops::Range<usize>,
96 new_text: &str,
97 new_cursor_byte: usize,
98 ) {
99 debug_assert!(self.value.is_char_boundary(range.start));
100 debug_assert!(self.value.is_char_boundary(range.end));
101 self.value.replace_range(range, new_text);
102 let clamped = new_cursor_byte.min(self.value.len());
103 debug_assert!(
104 self.value.is_char_boundary(clamped),
105 "new_cursor_byte must land on a char boundary"
106 );
107 self.cursor = clamped;
108 }
109
110 pub fn set_value(&mut self, value: impl Into<String>) {
112 self.value = value.into();
113 self.cursor = self.value.len();
114 }
115
116 pub fn clear(&mut self) {
117 self.value.clear();
118 self.cursor = 0;
119 }
120
121 #[cfg(test)]
125 pub(crate) fn cursor_char_offset(&self) -> usize {
126 self.value[..self.cursor].chars().count()
127 }
128
129 pub fn cursor_display_col(&self) -> usize {
132 self.value[..self.cursor].width()
133 }
134
135 pub fn display_width(&self) -> usize {
137 self.value.width()
138 }
139
140 pub fn handle_key(&mut self, key: &KeyEvent) -> InputOutcome {
141 match (key.modifiers, key.code) {
142 (_, KeyCode::Enter) => InputOutcome::Submit,
143 (_, KeyCode::Esc) => InputOutcome::Cancel,
144 (_, KeyCode::Backspace) => {
145 if self.cursor == 0 {
146 return InputOutcome::Consumed;
147 }
148 let prev = prev_char_boundary(&self.value, self.cursor);
149 self.value.drain(prev..self.cursor);
150 self.cursor = prev;
151 InputOutcome::Changed
152 }
153 (_, KeyCode::Delete) => {
154 if self.cursor >= self.value.len() {
155 return InputOutcome::Consumed;
156 }
157 let next = next_char_boundary(&self.value, self.cursor);
158 self.value.drain(self.cursor..next);
159 InputOutcome::Changed
160 }
161 (_, KeyCode::Left) => {
162 if self.cursor == 0 {
163 return InputOutcome::Consumed;
164 }
165 self.cursor = prev_char_boundary(&self.value, self.cursor);
166 InputOutcome::Consumed
167 }
168 (_, KeyCode::Right) => {
169 if self.cursor >= self.value.len() {
170 return InputOutcome::Consumed;
171 }
172 self.cursor = next_char_boundary(&self.value, self.cursor);
173 InputOutcome::Consumed
174 }
175 (_, KeyCode::Home) => {
176 self.cursor = 0;
177 InputOutcome::Consumed
178 }
179 (_, KeyCode::End) => {
180 self.cursor = self.value.len();
181 InputOutcome::Consumed
182 }
183 (m, KeyCode::Char(c)) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
186 self.value.insert(self.cursor, c);
187 self.cursor += c.len_utf8();
188 InputOutcome::Changed
189 }
190 _ => InputOutcome::NotConsumed,
191 }
192 }
193
194 pub fn render(
201 &mut self,
202 f: &mut Frame,
203 rect: Rect,
204 style: Style,
205 value_offset_x: u16,
206 focused: bool,
207 ) {
208 let inner = Rect {
209 x: rect.x.saturating_add(value_offset_x),
210 width: rect.width.saturating_sub(value_offset_x),
211 ..rect
212 };
213 f.render_widget(Paragraph::new(self.value.as_str()).style(style), inner);
214 self.last_caret_pos = None;
215 if focused {
216 let caret_x = inner
217 .x
218 .saturating_add(self.cursor_display_col() as u16)
219 .min(inner.x + inner.width.saturating_sub(1));
220 f.set_cursor_position(Position {
221 x: caret_x,
222 y: inner.y,
223 });
224 self.last_caret_pos = Some((caret_x, inner.y));
225 }
226 }
227}
228
229fn prev_char_boundary(s: &str, from: usize) -> usize {
230 s[..from]
231 .char_indices()
232 .next_back()
233 .map(|(i, _)| i)
234 .unwrap_or(0)
235}
236
237fn next_char_boundary(s: &str, from: usize) -> usize {
238 s[from..]
239 .char_indices()
240 .nth(1)
241 .map(|(i, _)| from + i)
242 .unwrap_or(s.len())
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn k(code: KeyCode) -> KeyEvent {
250 KeyEvent::new(code, KeyModifiers::NONE)
251 }
252
253 #[test]
254 fn new_is_empty_cursor_zero() {
255 let i = SingleLineInput::new();
256 assert!(i.is_empty());
257 assert_eq!(i.cursor_char_offset(), 0);
258 }
259
260 #[test]
261 fn with_value_places_cursor_at_end() {
262 let i = SingleLineInput::with_value("hello");
263 assert_eq!(i.value(), "hello");
264 assert_eq!(i.cursor_char_offset(), 5);
265 }
266
267 #[test]
268 fn typing_chars_appends_and_advances_cursor() {
269 let mut i = SingleLineInput::new();
270 assert_eq!(i.handle_key(&k(KeyCode::Char('a'))), InputOutcome::Changed);
271 assert_eq!(i.handle_key(&k(KeyCode::Char('b'))), InputOutcome::Changed);
272 assert_eq!(i.value(), "ab");
273 assert_eq!(i.cursor_char_offset(), 2);
274 }
275
276 #[test]
277 fn left_then_insert_inserts_mid_string() {
278 let mut i = SingleLineInput::with_value("ac");
279 i.handle_key(&k(KeyCode::Left));
280 assert_eq!(i.cursor_char_offset(), 1);
281 i.handle_key(&k(KeyCode::Char('b')));
282 assert_eq!(i.value(), "abc");
283 assert_eq!(i.cursor_char_offset(), 2);
284 }
285
286 #[test]
287 fn backspace_at_start_is_noop() {
288 let mut i = SingleLineInput::with_value("abc");
289 i.handle_key(&k(KeyCode::Home));
290 assert_eq!(i.handle_key(&k(KeyCode::Backspace)), InputOutcome::Consumed);
291 assert_eq!(i.value(), "abc");
292 }
293
294 #[test]
295 fn delete_at_end_is_noop() {
296 let mut i = SingleLineInput::with_value("abc");
297 assert_eq!(i.handle_key(&k(KeyCode::Delete)), InputOutcome::Consumed);
298 assert_eq!(i.value(), "abc");
299 }
300
301 #[test]
302 fn home_end_jump_cursor() {
303 let mut i = SingleLineInput::with_value("abc");
304 i.handle_key(&k(KeyCode::Home));
305 assert_eq!(i.cursor_char_offset(), 0);
306 i.handle_key(&k(KeyCode::End));
307 assert_eq!(i.cursor_char_offset(), 3);
308 }
309
310 #[test]
311 fn unicode_chars_count_by_codepoint_not_bytes() {
312 let mut i = SingleLineInput::new();
313 i.handle_key(&k(KeyCode::Char('あ')));
314 i.handle_key(&k(KeyCode::Char('い')));
315 assert_eq!(i.value(), "あい");
316 assert_eq!(i.cursor_char_offset(), 2);
317 i.handle_key(&k(KeyCode::Left));
318 assert_eq!(i.cursor_char_offset(), 1);
319 i.handle_key(&k(KeyCode::Backspace));
320 assert_eq!(i.value(), "い");
321 }
322
323 #[test]
324 fn enter_returns_submit_esc_returns_cancel() {
325 let mut i = SingleLineInput::with_value("x");
326 assert_eq!(i.handle_key(&k(KeyCode::Enter)), InputOutcome::Submit);
327 assert_eq!(i.handle_key(&k(KeyCode::Esc)), InputOutcome::Cancel);
328 }
329
330 #[test]
331 fn ctrl_char_is_not_consumed_as_text() {
332 let mut i = SingleLineInput::new();
333 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
334 assert_eq!(i.handle_key(&key), InputOutcome::NotConsumed);
335 assert!(i.is_empty());
336 }
337
338 #[test]
339 fn alt_char_is_not_consumed_as_text() {
340 let mut i = SingleLineInput::new();
341 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT);
342 assert_eq!(i.handle_key(&key), InputOutcome::NotConsumed);
343 assert!(i.is_empty());
344 }
345
346 #[test]
347 fn cjk_chars_count_two_display_cols_per_char() {
348 let mut i = SingleLineInput::new();
349 i.handle_key(&k(KeyCode::Char('あ')));
350 i.handle_key(&k(KeyCode::Char('い')));
351 assert_eq!(i.cursor_char_offset(), 2);
353 assert_eq!(i.cursor_display_col(), 4);
354 assert_eq!(i.display_width(), 4);
355 }
356
357 #[test]
358 fn mixed_ascii_and_cjk_caret_column() {
359 let mut i = SingleLineInput::with_value("ab猫");
360 assert_eq!(i.cursor_display_col(), 4);
362 i.handle_key(&k(KeyCode::Left));
363 assert_eq!(i.cursor_display_col(), 2);
365 }
366
367 #[test]
368 fn shift_char_inserts() {
369 let mut i = SingleLineInput::new();
370 let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
371 assert_eq!(i.handle_key(&key), InputOutcome::Changed);
372 assert_eq!(i.value(), "A");
373 }
374
375 #[test]
376 fn set_value_resets_cursor_to_end() {
377 let mut i = SingleLineInput::with_value("abc");
378 i.handle_key(&k(KeyCode::Home));
379 i.set_value("xyz!");
380 assert_eq!(i.value(), "xyz!");
381 assert_eq!(i.cursor_char_offset(), 4);
382 }
383
384 #[test]
385 fn clear_resets_both() {
386 let mut i = SingleLineInput::with_value("abc");
387 i.clear();
388 assert!(i.is_empty());
389 assert_eq!(i.cursor_char_offset(), 0);
390 }
391}