Skip to main content

urushi_prompt/runtime/
field.rs

1//! The crate-owned field protocol used by form orchestration.
2
3use std::any::Any;
4
5use super::{
6    terminal::Event,
7    view::{FieldPresentation, PromptStyles},
8};
9
10/// A crate-provided prompt field.
11///
12/// The trait is sealed: applications compose fields supplied by this crate
13/// rather than coupling a field implementation to terminal lifecycle details.
14#[allow(
15    private_bounds,
16    reason = "Field stays externally sealed while crate sibling field modules implement its runtime seam."
17)]
18pub trait Field: private::Sealed + 'static {}
19
20impl<T> Field for T where T: private::Sealed + 'static {}
21
22pub(crate) mod private {
23    pub(crate) trait Sealed {
24        fn into_entry(self: Box<Self>) -> super::FieldEntry;
25    }
26}
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub(super) enum FieldState {
29    Active,
30    Invalid { message: String },
31    Accepted,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(crate) enum FieldAction {
36    Stay,
37    Handled,
38    Accept,
39    Back,
40    Cancel,
41}
42
43pub(crate) struct FieldEntry {
44    pub(super) name: String,
45    pub(super) state: FieldState,
46    pub(super) field: Box<dyn RuntimeField>,
47}
48
49impl FieldEntry {
50    pub(crate) fn new(name: String, field: Box<dyn RuntimeField>) -> Self {
51        Self {
52            name,
53            state: FieldState::Active,
54            field,
55        }
56    }
57
58    pub(super) fn name(&self) -> &str {
59        &self.name
60    }
61
62    pub(super) fn event(&mut self, event: Event) -> FieldAction {
63        let action = self.field.event(event);
64        self.state = match self.field.validation_error() {
65            Some(message) => FieldState::Invalid {
66                message: message.to_owned(),
67            },
68            None => FieldState::Active,
69        };
70        action
71    }
72
73    pub(super) fn activate(&mut self) {
74        self.field.focus();
75        self.state = FieldState::Active;
76    }
77
78    pub(super) fn deactivate(&mut self) {
79        self.field.blur();
80    }
81
82    pub(super) fn accept(&mut self) {
83        self.field.blur();
84        self.state = FieldState::Accepted;
85    }
86
87    pub(super) fn take_value(&mut self) -> Box<dyn Any> {
88        self.field.take_value()
89    }
90
91    pub(super) fn view(
92        &self,
93        styles: &PromptStyles,
94        focused: bool,
95        width: usize,
96    ) -> FieldPresentation {
97        self.field.view(styles, focused, width)
98    }
99
100    pub(super) fn captures_tab(&self) -> bool {
101        self.field.captures_tab()
102    }
103}
104
105pub(crate) trait RuntimeField {
106    fn event(&mut self, event: Event) -> FieldAction;
107    fn take_value(&mut self) -> Box<dyn Any>;
108    /// Builds this field's view with theme and profile already resolved.
109    ///
110    /// `focused` is passed rather than post-processed out of the result: once
111    /// a span carries a resolved style, an unfocused field's rows cannot be
112    /// derived from a focused field's rows without guessing which role a
113    /// style came from.
114    ///
115    /// `width` is the cells this field will be laid out in. A field that
116    /// carries a cursor needs it: the visible window of a value that is wider
117    /// than the terminal is chosen here, before a `View` exists, so that
118    /// resolution can be called with the real available area.
119    fn view(&self, styles: &PromptStyles, focused: bool, width: usize) -> FieldPresentation;
120
121    fn validation_error(&self) -> Option<&str> {
122        None
123    }
124
125    fn focus(&mut self) {}
126
127    fn blur(&mut self) {}
128
129    fn captures_tab(&self) -> bool {
130        false
131    }
132}