#[non_exhaustive]pub enum ControlKind {
Choice {
options: Vec<ChoiceOption>,
},
Text,
Toggle,
Secret,
Number {
min: Option<i64>,
max: Option<i64>,
step: Option<i64>,
},
Range {
min: i64,
max: i64,
step: i64,
},
Temporal {
precision: TemporalPrecision,
},
Color,
Path {
kind: PathKind,
accept: Vec<String>,
},
}Expand description
How a control accepts input.
Choice carries its options inline because they belong to it: one
field, N mutually-exclusive answers. Modelling each option as its own
control would make Requirement incoherent — under “the response
must include this control”, answering an allow would require also
answering the deny.
§A kind is a MEANING; an affordance is not a kind
The line this vocabulary is drawn on, because the HTML input catalog it mirrors does not draw it and copying that list variant-for-variant would have produced twenty-two.
A kind earns its place when it changes what a well-formed ANSWER is —
#c0ffee is a color and not a date, and no surface may accept it as one.
It does not earn a place merely by suggesting a different widget: a slider
and a typed number accept the same answers, so Range is
separate from Number only because a bounded-and-stepped
value space IS different (its bounds are mandatory), not because one draws
as a slider.
What this excludes, deliberately:
button,submit,reset,image— form ACTIONS. A definition’s actions are itsSemanticRoles; a submit button that carried its own meaning would be a second authority vocabulary.hidden— not a control at all. A value the operator cannot see is not a thing they answered, and putting one here would let a surface claim an answer nobody gave.radio,checkbox,password,select— already spelledChoice,Toggle,SecretandChoiceagain.<select>and a radio group are one meaning with two presentations.
§Every kind here is answerable by typing
None of these mint a SurfaceFeature demand (see
downgrade::intrinsic_demands), and that is deliberate rather than
unfinished. A date answered by typing 2026-09-01 is a satisfied date, so
a surface with no calendar widget has NOT failed to present the question —
it presented it plainly. Only Secret demands a feature,
because it changes what may be DISCLOSED rather than how something is
drawn. Minting a demand per kind would make plain and headless surfaces
refuse questions they can answer perfectly well, which is law 5 pointed at
the wrong axis.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Choice
Pick exactly one of the options offered.
Fields
options: Vec<ChoiceOption>The options, in presentation order.
Text
Free text.
Deliberately still a unit variant. HTML spells five flavours of
text — email, url, tel, search, plain — and carrying one here
as Text { format } would re-encode the most common kind in the
vocabulary: a unit variant is the dag-cbor string "text", a struct
variant is a map. That breaks tests/data/interaction-vectors.json,
whose frozen canonical bytes are DECODED rather than rebuilt, and the
external-consumer corpus answering them — the compatibility fixtures
CLAUDE.md names as what a rewrite is written against.
The flavours differ, in a terminal, only in what counts as valid. That
is not worth re-addressing every definition ever written. If one earns
its own kind later it can be ADDED as a new variant, which breaks
nothing — the way Number, Temporal, Color and Path were.
Toggle
A boolean.
Secret
A secret: never persisted in markup or logs (ADR D1).
Number
A whole number, optionally bounded and stepped.
Integers, and that is a decision. A binary float cannot be
canonically encoded without deciding what to do about NaN and -0.0,
and this vocabulary is content-addressed — two encodings of one value
would be two identities. Every number newt asks for today (a round cap,
a port, a timeout, a percentage) is integral. A fractional control is a
later, separate decision, and it will carry its digits as a decimal
STRING rather than reopening this one.
Fields
Range
A number on a bounded, stepped scale — HTML’s range.
Distinct from Number because its bounds are
MANDATORY: a slider without ends is not a slider, and a surface that
draws one needs both to draw anything at all.
Fields
Temporal
A point in time at the given precision.
Fields
precision: TemporalPrecisionHow much of a moment is being asked for.
Color
An sRGB color, written #rrggbb.
Path
A filesystem path.
The protocol says what is being NAMED, never whether it exists: an existence check is the host’s business, is racy by nature, and would make a definition’s validity depend on the machine reading it.
Implementations§
Source§impl ControlKind
impl ControlKind
Sourcepub fn check_text(&self, text: &str) -> Result<(), String>
pub fn check_text(&self, text: &str) -> Result<(), String>
Is text a well-formed answer to this control?
Err carries what was EXPECTED, phrased for a human — “an integer in
1..=10000”, “YYYY-MM-DD”, “#rrggbb”. The caller decides what a
malformed answer means: the binding layer refuses it, an interactive
surface can show it as you type. One implementation either way, which
is the point — a panel that validated Number its own way would be a
second opinion about what the definition means.
Choice always answers Ok: typed input for a choice
is RESOLVED (canonical-first, aliases second, ambiguity denies) by
binding::resolve_typed, and a second, weaker check here is exactly
the duplicate that rule exists to prevent. Toggle and
Secret do not travel as text at all.
§Errors
The expectation this control’s answers must meet, when text does not.
Sourcepub fn hint(&self) -> String
pub fn hint(&self) -> String
What this kind advertises after a control’s label — [y/n],
[YYYY-MM-DD], (secret, not echoed).
One table, not one per surface. The plain projection and the RichTUI view model each carried this suffix list, which is two places for one piece of knowledge and the shape the reuse discipline exists to stop: a kind added to one and forgotten in the other renders as a bare label on that surface, silently. Here a new kind advertises itself everywhere by construction.
The strings for the pre-existing kinds are EXACT: the plain projection
is a byte-identity contract, so Toggle is [y/n] — never [y/N],
because a rendered default is how a headless surface chooses one by
accident.
Choice advertises nothing here: its options are
rendered as their own lines, not as a suffix.
Sourcepub fn travels_as_text(&self) -> bool
pub fn travels_as_text(&self) -> bool
Whether an answer to this control travels as ControlValue::Text.
True for every value-shaped kind — text, number, range, temporal, color, path. False for the three that have their own value shape: a choice names an option, a toggle carries a bool, and a secret carries a REFERENCE (there is deliberately no variant that can hold plaintext).