1use std::any::Any;
2
3use unicode_segmentation::UnicodeSegmentation;
4use urushi::{Align, PrintableText, VerticalAlign, View};
5
6use crate::{
7 FieldConfigError, FieldKey,
8 runtime::{
9 self, Event, FieldAction, FieldEntry, FieldPresentation, FieldRegionKind, KeyCode,
10 KeyModifiers, PromptStyles, RuntimeField, TextSpan, clipped_line_view, field_line_view,
11 fixed_view, line_view, line_view_with_cursor, region, window_spans,
12 },
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ValidationError {
18 pub message: String,
20}
21
22impl ValidationError {
23 pub fn new(message: impl Into<String>) -> Self {
28 Self {
29 message: message.into(),
30 }
31 }
32}
33
34pub type Validator<T> = Box<dyn Fn(&T) -> Result<(), ValidationError> + Send + Sync + 'static>;
36
37pub struct Input {
39 key: FieldKey<String>,
40 question: String,
41 description: Option<String>,
42 value: String,
43 placeholder: Option<String>,
44 help: String,
45 required: bool,
46 required_message: String,
47 validators: Vec<Validator<String>>,
48 cursor: usize,
49 validation_error: Option<String>,
50}
51
52impl Input {
53 pub fn new(
55 key: FieldKey<String>,
56 question: impl Into<String>,
57 initial_value: impl Into<String>,
58 ) -> Result<Self, FieldConfigError> {
59 if key.name().is_empty() {
60 return Err(FieldConfigError::EmptyName);
61 }
62
63 let value = initial_value.into();
64 let cursor = value.graphemes(true).count();
65 Ok(Self {
66 key,
67 question: question.into(),
68 description: None,
69 value,
70 placeholder: None,
71 help: "enter continue • shift+tab back • esc cancel".to_owned(),
72 required: false,
73 required_message: "This field is required.".to_owned(),
74 validators: Vec::new(),
75 cursor,
76 validation_error: None,
77 })
78 }
79
80 #[must_use]
85 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
86 self.placeholder = Some(placeholder.into());
87 self
88 }
89
90 #[must_use]
95 pub fn description(mut self, description: impl Into<String>) -> Self {
96 self.description = Some(description.into());
97 self
98 }
99
100 #[must_use]
105 pub fn help(mut self, help: impl Into<String>) -> Self {
106 self.help = help.into();
107 self
108 }
109
110 #[must_use]
112 pub fn required(mut self) -> Self {
113 self.required = true;
114 self
115 }
116
117 #[must_use]
119 pub fn required_message(mut self, message: impl Into<String>) -> Self {
120 self.required_message = message.into();
121 self
122 }
123
124 #[must_use]
126 pub fn validate(mut self, validator: Validator<String>) -> Self {
127 self.validators.push(validator);
128 self
129 }
130
131 pub fn question(&self) -> &str {
133 &self.question
134 }
135
136 pub fn value(&self) -> &str {
138 &self.value
139 }
140
141 fn edit(&mut self) {
142 self.validation_error = None;
143 }
144
145 fn byte_index(&self, grapheme_index: usize) -> usize {
146 self.value
147 .grapheme_indices(true)
148 .nth(grapheme_index)
149 .map_or(self.value.len(), |(byte_index, _)| byte_index)
150 }
151
152 fn delete_grapheme_at(&mut self, grapheme_index: usize) {
153 let start = self.byte_index(grapheme_index);
154 let end = self.byte_index(grapheme_index + 1);
155 if start != end {
156 self.value.replace_range(start..end, "");
157 }
158 }
159
160 fn insert_text(&mut self, text: &str) {
161 let text = text
162 .replace("\r\n", "\n")
163 .chars()
164 .filter_map(|character| match character {
165 '\r' | '\n' | '\t' => Some(' '),
166 character if character.is_control() => None,
167 character => Some(character),
168 })
169 .collect::<String>();
170 let byte_index = self.byte_index(self.cursor);
171 self.value.insert_str(byte_index, &text);
172 self.cursor += text.graphemes(true).count();
173 self.edit();
174 }
175
176 fn submit(&mut self) -> FieldAction {
177 if self.required && self.value.is_empty() {
178 self.validation_error = Some(self.required_message.clone());
179 return FieldAction::Stay;
180 }
181
182 for validator in &self.validators {
183 if let Err(error) = validator(&self.value) {
184 self.validation_error = Some(error.message);
185 return FieldAction::Stay;
186 }
187 }
188
189 self.validation_error = None;
190 FieldAction::Accept
191 }
192
193 fn answer_spans(&self, styles: &PromptStyles, focused: bool) -> Vec<TextSpan> {
194 if self.value.is_empty() {
195 let mut spans = if focused {
198 vec![TextSpan::new(" ", styles.cursor.clone())]
199 } else {
200 Vec::new()
201 };
202 if let Some(placeholder) = &self.placeholder {
203 spans.push(TextSpan::new(
204 placeholder.clone(),
205 styles.placeholder.clone(),
206 ));
207 }
208 return spans;
209 }
210
211 let cursor_byte = self.byte_index(self.cursor);
212 let mut spans = Vec::new();
213 if cursor_byte > 0 {
214 spans.push(TextSpan::new(
215 self.value[..cursor_byte].to_owned(),
216 styles.answer.clone(),
217 ));
218 }
219 if let Some(grapheme) = self.value[cursor_byte..].graphemes(true).next() {
220 let style = if focused {
223 &styles.cursor
224 } else {
225 &styles.answer
226 };
227 spans.push(TextSpan::new(grapheme, style.clone()));
228 let after_cursor = cursor_byte + grapheme.len();
229 if after_cursor < self.value.len() {
230 spans.push(TextSpan::new(
231 self.value[after_cursor..].to_owned(),
232 styles.answer.clone(),
233 ));
234 }
235 } else if focused {
236 spans.push(TextSpan::new(" ", styles.cursor.clone()));
237 }
238 spans
239 }
240}
241
242impl runtime::private::Sealed for Input {
243 fn into_entry(self: Box<Self>) -> FieldEntry {
244 FieldEntry::new(self.key.name().to_owned(), self)
245 }
246}
247
248impl RuntimeField for Input {
249 fn event(&mut self, event: Event) -> FieldAction {
250 if let Event::Paste(text) = event {
251 self.insert_text(&text);
252 return FieldAction::Stay;
253 }
254 let Event::Key(key) = event else {
255 return FieldAction::Stay;
256 };
257
258 match (
259 key.code,
260 key.modifiers.contains(KeyModifiers::CONTROL),
261 key.modifiers.contains(KeyModifiers::ALT),
262 ) {
263 (KeyCode::Escape, _, _) => FieldAction::Cancel,
264 (KeyCode::Char('a'), true, _) | (KeyCode::Home, _, _) => {
265 self.cursor = 0;
266 FieldAction::Stay
267 }
268 (KeyCode::Char('e'), true, _) | (KeyCode::End, _, _) => {
269 self.cursor = self.value.graphemes(true).count();
270 FieldAction::Stay
271 }
272 (KeyCode::Char('b'), true, _) | (KeyCode::Left, _, _) => {
273 self.cursor = self.cursor.saturating_sub(1);
274 FieldAction::Stay
275 }
276 (KeyCode::Char('f'), true, _) | (KeyCode::Right, _, _) => {
277 self.cursor = (self.cursor + 1).min(self.value.graphemes(true).count());
278 FieldAction::Stay
279 }
280 (KeyCode::Char('u'), true, _) => {
281 let end = self.byte_index(self.cursor);
282 self.value.replace_range(..end, "");
283 self.cursor = 0;
284 self.edit();
285 FieldAction::Stay
286 }
287 (KeyCode::Char('k'), true, _) => {
288 let start = self.byte_index(self.cursor);
289 self.value.truncate(start);
290 self.edit();
291 FieldAction::Stay
292 }
293 (KeyCode::Backspace, _, _) if self.cursor > 0 => {
294 self.delete_grapheme_at(self.cursor - 1);
295 self.cursor -= 1;
296 self.edit();
297 FieldAction::Stay
298 }
299 (KeyCode::Delete, _, _) if self.cursor < self.value.graphemes(true).count() => {
300 self.delete_grapheme_at(self.cursor);
301 self.edit();
302 FieldAction::Stay
303 }
304 (KeyCode::Enter | KeyCode::Tab, _, _) => self.submit(),
305 (KeyCode::Char(character), false, false) => {
306 self.insert_text(&character.to_string());
307 FieldAction::Stay
308 }
309 _ => FieldAction::Stay,
310 }
311 }
312
313 fn take_value(&mut self) -> Box<dyn Any> {
314 Box::new(std::mem::take(&mut self.value))
315 }
316
317 fn view(&self, styles: &PromptStyles, focused: bool, width: usize) -> FieldPresentation {
318 let answer_start = 2_usize;
321 let cursor_prefix = &self.value[..self.byte_index(self.cursor)];
322 let cursor_column = PrintableText::new(cursor_prefix).width();
323 let answer_spans = self.answer_spans(styles, focused);
324 let (answer_spans, cursor_column) = if focused {
329 window_spans(
330 answer_spans,
331 cursor_column,
332 width.saturating_sub(answer_start),
333 )
334 } else {
335 (answer_spans, cursor_column)
336 };
337 let answer_value = if focused {
338 line_view_with_cursor(answer_spans, cursor_column)
339 } else {
340 line_view(answer_spans)
341 };
342 let answer = field_line_view(
343 styles,
344 focused,
345 region(
346 FieldRegionKind::Control,
347 region(
348 FieldRegionKind::Focus,
349 View::row(
350 VerticalAlign::Top,
351 [
352 fixed_view(
353 answer_start,
354 vec![TextSpan::new("› ", styles.answer.clone())],
355 ),
356 answer_value,
357 ],
358 ),
359 ),
360 ),
361 );
362 let mut body = vec![region(
363 FieldRegionKind::Question,
364 field_line_view(
365 styles,
366 focused,
367 line_view(vec![TextSpan::new(
368 self.question.clone(),
369 styles.question(focused).clone(),
370 )]),
371 ),
372 )];
373 let mut regions = vec![
374 FieldRegionKind::Question,
375 FieldRegionKind::Control,
376 FieldRegionKind::Focus,
377 ];
378 if let Some(description) = &self.description {
379 body.push(region(
380 FieldRegionKind::Description,
381 field_line_view(
382 styles,
383 focused,
384 line_view(vec![TextSpan::new(
385 description.clone(),
386 styles.muted.clone(),
387 )]),
388 ),
389 ));
390 regions.push(FieldRegionKind::Description);
391 }
392 body.push(answer);
393 if let Some(message) = &self.validation_error {
394 body.push(region(
395 FieldRegionKind::Error,
396 field_line_view(
397 styles,
398 focused,
399 View::row(
400 VerticalAlign::Top,
401 [
402 fixed_view(2, vec![TextSpan::new("! ", styles.error.clone())]),
403 View::text(message.clone(), styles.error.clone()),
404 ],
405 ),
406 ),
407 ));
408 regions.push(FieldRegionKind::Error);
409 }
410 let presentation = FieldPresentation::new(View::column(Align::Left, body))
411 .with_help(clipped_line_view(vec![TextSpan::new(
412 self.help.clone(),
413 styles.help.clone(),
414 )]))
415 .with_regions(regions);
416 if focused {
417 presentation.with_cursor()
418 } else {
419 presentation
420 }
421 }
422
423 fn validation_error(&self) -> Option<&str> {
424 self.validation_error.as_deref()
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use std::{
431 collections::VecDeque,
432 io,
433 sync::{
434 Arc,
435 atomic::{AtomicUsize, Ordering},
436 },
437 };
438
439 use super::*;
440 use crate::runtime::PromptView;
441 use crate::{
442 Form, FormOutcome, Group,
443 runtime::{EventSource, RenderFinish, Renderer, test_styles},
444 };
445
446 fn key(code: KeyCode) -> Event {
447 Event::Key(runtime::KeyEvent::new(code))
448 }
449
450 #[test]
451 fn input_rejects_an_empty_field_name() {
452 assert!(matches!(
453 Input::new(FieldKey::new(""), "Name", ""),
454 Err(FieldConfigError::EmptyName)
455 ));
456 }
457
458 #[test]
459 fn edits_follow_unicode_grapheme_boundaries() {
460 let mut input = Input::new(FieldKey::new("name"), "Name", "aあb").expect("input is valid");
461
462 assert_eq!(input.event(key(KeyCode::Left)), FieldAction::Stay);
463 assert_eq!(input.event(key(KeyCode::Char('い'))), FieldAction::Stay);
464 assert_eq!(input.value(), "aあいb");
465 assert_eq!(input.event(key(KeyCode::Backspace)), FieldAction::Stay);
466 assert_eq!(input.value(), "aあb");
467 assert_eq!(input.event(key(KeyCode::Delete)), FieldAction::Stay);
468 assert_eq!(input.value(), "aあ");
469
470 let mut combined =
471 Input::new(FieldKey::new("combined"), "Name", "e\u{301}x").expect("input is valid");
472 assert_eq!(combined.event(key(KeyCode::Left)), FieldAction::Stay);
473 assert_eq!(combined.event(key(KeyCode::Backspace)), FieldAction::Stay);
474 assert_eq!(combined.value(), "x");
475 }
476
477 #[test]
478 fn paste_inserts_at_the_cursor_and_flattens_line_breaks() {
479 let mut input = Input::new(FieldKey::new("name"), "Name", "ab").expect("input is valid");
480 assert_eq!(input.event(key(KeyCode::Left)), FieldAction::Stay);
481 assert_eq!(
482 input.event(Event::Paste("あ\r\nい\tう".to_owned())),
483 FieldAction::Stay
484 );
485 assert_eq!(input.value(), "aあ い うb");
486 }
487
488 #[test]
489 fn view_uses_resolved_styles_and_cjk_display_columns() {
490 let mut input = Input::new(FieldKey::new("name"), "名前", "あ")
491 .expect("input is valid")
492 .placeholder("入力してください");
493 assert_eq!(input.event(key(KeyCode::Home)), FieldAction::Stay);
494
495 let styles = test_styles();
496 let view = input.view(&styles, true, 80);
497 let rows = view.rows();
498 assert!(rows[0].text().contains("名前"));
499 assert!(
500 rows[0]
501 .runs()
502 .iter()
503 .any(|run| run.style == styles.question)
504 );
505 assert!(rows[1].text().contains("› あ"));
506 assert!(rows[1].runs().iter().any(|run| run.style == styles.cursor));
507 let resolved = urushi::resolve(&view.body, urushi::Available::NONE).unwrap();
508 let cursor = resolved
509 .anchor(view.cursor.expect("focused input has a cursor anchor"))
510 .expect("cursor anchor resolves");
511 assert_eq!((cursor.y(), cursor.x()), (1, 4));
512 assert!(view.help.is_some());
513
514 let empty = Input::new(FieldKey::new("empty"), "Name", "")
515 .expect("input is valid")
516 .placeholder("Example");
517 let empty_view = empty.view(&styles, true, 80);
518 assert!(
519 empty_view.rows()[1]
520 .runs()
521 .iter()
522 .any(|run| run.style == styles.cursor)
523 );
524 assert!(
525 empty_view.rows()[1]
526 .runs()
527 .iter()
528 .any(|run| run.style == styles.placeholder)
529 );
530
531 assert_eq!(input.event(key(KeyCode::End)), FieldAction::Stay);
532 let rows = input.view(&styles, true, 80).rows();
533 let trailing = rows[1].runs();
534 let trailing = trailing.last().expect("the answer row has runs");
535 assert_eq!(trailing.text, " ");
536 assert_eq!(trailing.style, styles.cursor);
537 }
538
539 #[test]
540 fn an_unfocused_input_recedes_and_drops_the_cursor() {
541 let styles = test_styles();
542 let input = Input::new(FieldKey::new("name"), "Name", "value").expect("input is valid");
543
544 let unfocused = input.view(&styles, false, 80);
545 assert!(
546 unfocused.rows()[0]
547 .runs()
548 .iter()
549 .any(|run| run.style == styles.muted)
550 );
551 assert!(
552 unfocused.rows()[1]
553 .runs()
554 .iter()
555 .all(|span| span.style != styles.cursor)
556 );
557 assert_eq!(
558 unfocused.rows()[1]
559 .runs()
560 .iter()
561 .map(|span| span.text.as_str())
562 .collect::<String>(),
563 " › value"
564 );
565
566 let empty = Input::new(FieldKey::new("empty"), "Name", "").expect("input is valid");
567 assert_eq!(empty.view(&styles, false, 80).rows()[1].text(), " › ");
568 }
569
570 #[test]
571 fn an_unfocused_input_keeps_a_space_the_cursor_was_resting_on() {
572 let styles = test_styles();
576 let mut input = Input::new(FieldKey::new("name"), "Name", "John Doe")
577 .expect("input is valid")
578 .placeholder("");
579 assert_eq!(input.event(key(KeyCode::Home)), FieldAction::Stay);
580 for _ in 0..4 {
581 assert_eq!(input.event(key(KeyCode::Right)), FieldAction::Stay);
582 }
583
584 let unfocused = input.view(&styles, false, 80);
585 assert_eq!(
586 unfocused.rows()[1]
587 .runs()
588 .iter()
589 .map(|span| span.text.as_str())
590 .collect::<String>(),
591 " › John Doe"
592 );
593 }
594
595 #[test]
596 fn validators_run_in_registration_order_after_required() {
597 let second_runs = Arc::new(AtomicUsize::new(0));
598 let second_runs_for_validator = Arc::clone(&second_runs);
599 let mut input = Input::new(FieldKey::new("name"), "Name", "value")
600 .expect("input is valid")
601 .validate(Box::new(|_| Err(ValidationError::new("first failure"))))
602 .validate(Box::new(move |_| {
603 second_runs_for_validator.fetch_add(1, Ordering::SeqCst);
604 Ok(())
605 }));
606
607 assert_eq!(input.event(key(KeyCode::Enter)), FieldAction::Stay);
608 assert_eq!(input.validation_error(), Some("first failure"));
609 assert_eq!(second_runs.load(Ordering::SeqCst), 0);
610
611 let mut required = Input::new(FieldKey::new("required"), "Required", "")
612 .expect("input is valid")
613 .required()
614 .required_message("A value is needed.")
615 .validate(Box::new(|_| Err(ValidationError::new("validator failure"))));
616 assert_eq!(required.event(key(KeyCode::Tab)), FieldAction::Stay);
617 assert_eq!(required.validation_error(), Some("A value is needed."));
618 }
619
620 #[derive(Default)]
621 struct ScriptedEvents {
622 events: VecDeque<io::Result<Event>>,
623 }
624
625 impl ScriptedEvents {
626 fn new(events: impl IntoIterator<Item = io::Result<Event>>) -> Self {
627 Self {
628 events: events.into_iter().collect(),
629 }
630 }
631 }
632
633 impl EventSource for ScriptedEvents {
634 fn read_event(&mut self) -> io::Result<Event> {
635 self.events
636 .pop_front()
637 .unwrap_or_else(|| Err(io::Error::other("event script exhausted")))
638 }
639
640 fn poll_event(&mut self) -> io::Result<Option<Event>> {
641 Ok(None)
642 }
643
644 fn poll_event_timeout(
645 &mut self,
646 _timeout: std::time::Duration,
647 ) -> io::Result<Option<Event>> {
648 self.poll_event()
649 }
650 }
651
652 #[derive(Default)]
653 struct RecordingRenderer {
654 views: Vec<PromptView>,
655 }
656
657 impl Renderer for RecordingRenderer {
658 fn draw(
659 &mut self,
660 _output: &mut dyn urushi_terminal::CommandWriter,
661 view: &PromptView,
662 _start: crate::PromptStart,
663 _drawing_columns: u16,
664 ) -> io::Result<()> {
665 self.views.push(view.clone());
666 Ok(())
667 }
668
669 fn finish(
670 &mut self,
671 _output: &mut dyn urushi_terminal::CommandWriter,
672 _outcome: RenderFinish,
673 ) -> io::Result<()> {
674 Ok(())
675 }
676
677 fn clear_viewport(
678 &mut self,
679 _output: &mut dyn urushi_terminal::CommandWriter,
680 ) -> io::Result<()> {
681 Ok(())
682 }
683 }
684
685 fn rendered(view: &PromptView) -> crate::runtime::frame::FramedView {
686 crate::runtime::frame::frame(&crate::runtime::resolve::resolve_prompt(80, view), u16::MAX)
687 }
688
689 struct InteractiveTerminal;
690
691 impl urushi_terminal::RawModeControl for InteractiveTerminal {
692 fn is_interactive(&self) -> bool {
693 true
694 }
695
696 fn enable_raw_mode(&mut self) -> io::Result<()> {
697 Ok(())
698 }
699
700 fn disable_raw_mode(&mut self) -> io::Result<()> {
701 Ok(())
702 }
703 }
704
705 impl urushi_terminal::CommandWriter for InteractiveTerminal {
706 fn write_command(&mut self, _command: urushi_terminal::Command<'_>) -> io::Result<()> {
707 Ok(())
708 }
709 }
710
711 impl urushi_terminal::TerminalQuery for InteractiveTerminal {
712 fn terminal_size(&mut self) -> io::Result<urushi_terminal::TerminalSize> {
713 Ok(urushi_terminal::TerminalSize::new(80, 24))
714 }
715
716 fn cursor_position(&mut self) -> io::Result<urushi_terminal::Position> {
717 Ok(urushi_terminal::Position::new(0, 0))
718 }
719
720 fn window_size(&mut self) -> io::Result<urushi_terminal::WindowSize> {
721 Ok(urushi_terminal::WindowSize::new(
722 urushi_terminal::TerminalSize::new(80, 24),
723 None,
724 ))
725 }
726
727 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
728 Ok(false)
729 }
730 }
731
732 impl urushi_terminal::KeyboardEnhancementQuery for InteractiveTerminal {
733 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
734 Ok(false)
735 }
736 }
737
738 impl urushi_terminal::TerminalOutput for InteractiveTerminal {
739 fn flush(&mut self) -> io::Result<()> {
740 Ok(())
741 }
742 }
743
744 #[test]
745 fn invalid_edits_clear_the_error_and_then_advance_with_typed_values() {
746 let first = Input::new(FieldKey::new("first"), "First", "")
747 .expect("input is valid")
748 .required();
749 let second = Input::new(FieldKey::new("second"), "Second", "done").expect("input is valid");
750 let form = Form::builder()
751 .group(
752 Group::builder()
753 .field(first)
754 .field(second)
755 .build()
756 .expect("group is valid"),
757 )
758 .build()
759 .expect("form is valid");
760 let mut events = ScriptedEvents::new([
761 Ok(key(KeyCode::Enter)),
762 Ok(key(KeyCode::Char('値'))),
763 Ok(key(KeyCode::Tab)),
764 Ok(key(KeyCode::Enter)),
765 ]);
766 let mut renderer = RecordingRenderer::default();
767 let mut terminal = InteractiveTerminal;
768
769 let outcome = form
770 .run_with(&mut events, &mut renderer, &mut terminal, &test_styles())
771 .expect("form submits");
772 let FormOutcome::Submitted(values) = outcome else {
773 panic!("expected submitted values");
774 };
775 assert_eq!(values.get(&FieldKey::new("first")), Some(&"値".to_owned()));
776 assert_eq!(
777 values.get(&FieldKey::new("second")),
778 Some(&"done".to_owned())
779 );
780 assert_eq!(renderer.views.len(), 4);
781 let rejected = rendered(&renderer.views[1]);
782 assert!(rejected.rows.iter().any(|row| {
783 row.runs()
784 .iter()
785 .any(|run| run.style == test_styles().error)
786 }));
787 let final_view = rendered(&renderer.views[3]);
788 assert!(
789 final_view
790 .rows
791 .iter()
792 .any(|row| row.text().contains("Second"))
793 );
794 assert!(
795 final_view
796 .rows
797 .iter()
798 .any(|row| row.text().contains("› 値"))
799 );
800 assert_eq!(
801 final_view
802 .rows
803 .iter()
804 .flat_map(crate::runtime::frame::FramedRow::runs)
805 .filter(|run| run.style == test_styles().help)
806 .count(),
807 1
808 );
809 }
810}