use newt_interaction::{ChoiceOption, Control, ControlKind, InteractionDefinition};
const CHOICE_SEPARATOR: &str = "\n";
#[must_use]
pub fn render(definition: &InteractionDefinition) -> String {
let mut parts: Vec<&str> = Vec::with_capacity(3);
parts.push(&definition.markdown);
if let Some(note) = definition.note.as_deref() {
parts.push(note);
}
let control_lines: Vec<String> = definition
.controls
.iter()
.filter_map(control_line)
.filter(|line| !line.is_empty())
.collect();
parts.extend(control_lines.iter().map(String::as_str));
let joined = parts
.into_iter()
.filter(|part| !part.is_empty())
.collect::<Vec<_>>()
.join("\n");
crate::notes_scan::neutralize_for_display(&joined).into_owned()
}
fn control_line(control: &Control) -> Option<String> {
match &control.kind {
ControlKind::Choice { options } => Some(choice_lines(options)),
kind => labelled_field(control, &kind.hint()),
}
}
fn labelled_field(control: &Control, suffix: &str) -> Option<String> {
(!control.label.is_empty()).then(|| format!("{}:{suffix}", control.label))
}
fn choice_lines(options: &[ChoiceOption]) -> String {
options
.iter()
.map(bracket_key)
.collect::<Vec<_>>()
.join(CHOICE_SEPARATOR)
}
fn bracket_key(option: &ChoiceOption) -> String {
option
.label
.replacen(&option.key, &format!("[{}]", option.key), 1)
}
#[cfg(test)]
pub(super) mod tests {
use super::*;
use newt_interaction::{
Control, ControlId, InteractionKind, OptionId, Requirement, SemanticRole,
};
pub(super) fn option(id: &str, key: &str, label: &str) -> ChoiceOption {
ChoiceOption {
id: OptionId::new(id).expect("valid option id"),
role: SemanticRole::Allow,
label: label.to_string(),
key: key.to_string(),
aliases: Vec::new(),
}
}
pub(super) fn choice(options: Vec<ChoiceOption>) -> Control {
Control {
id: ControlId::new("decision").expect("valid control id"),
kind: ControlKind::Choice { options },
label: String::new(),
requirement: Requirement::Required,
}
}
pub(super) fn definition(controls: Vec<Control>) -> InteractionDefinition {
InteractionDefinition::new(InteractionKind::Choice, "body", controls)
}
#[test]
fn body_note_and_choices_join_with_single_newlines_in_that_order() {
let mut d = definition(vec![choice(vec![
option("allow_once", "a", "allow once"),
option("deny", "d", "deny (default)"),
])]);
d.note = Some("hint".to_string());
assert_eq!(render(&d), "body\nhint\n[a]llow once\n[d]eny (default)");
}
#[test]
fn an_absent_note_is_dropped_rather_than_rendered_as_a_blank_line() {
let d = definition(vec![choice(vec![option("deny", "d", "deny")])]);
assert_eq!(render(&d), "body\n[d]eny");
}
#[test]
fn an_empty_note_is_dropped_too() {
let mut d = definition(vec![choice(vec![option("deny", "d", "deny")])]);
d.note = Some(String::new());
assert_eq!(render(&d), "body\n[d]eny");
}
#[test]
fn a_form_with_no_options_renders_body_and_note_only() {
let mut d = InteractionDefinition::new(
InteractionKind::Prompt,
"? what is your name",
vec![Control {
id: ControlId::new("answer").expect("valid control id"),
kind: ControlKind::Text,
label: String::new(),
requirement: Requirement::Required,
}],
);
d.note = Some("Esc=back".to_string());
assert_eq!(render(&d), "? what is your name\nEsc=back");
}
#[test]
fn a_choice_control_with_zero_options_adds_no_line() {
let d = definition(vec![choice(Vec::new())]);
assert_eq!(render(&d), "body");
}
#[test]
fn options_get_one_line_each() {
let d = definition(vec![choice(vec![
option("a", "a", "aa"),
option("b", "b", "bb"),
option("c", "c", "cc"),
])]);
assert_eq!(render(&d), "body\n[a]a\n[b]b\n[c]c");
assert_eq!(CHOICE_SEPARATOR, "\n");
}
#[test]
fn aliases_are_never_rendered() {
let mut opt = option("deny", "n", "n to skip");
opt.aliases = vec!["N".to_string(), "no".to_string()];
let d = definition(vec![choice(vec![opt])]);
let out = render(&d);
assert_eq!(out, "body\n[n] to skip");
assert!(
!out.contains('N'),
"an alias reached the rendered form: {out}"
);
assert!(
!out.contains("no"),
"an alias reached the rendered form: {out}"
);
}
#[test]
fn the_key_is_bracketed_at_its_first_occurrence_only() {
let d = definition(vec![choice(vec![option("x", "a", "banana")])]);
assert_eq!(render(&d), "body\nb[a]nana");
}
#[test]
fn a_key_absent_from_its_label_brackets_nothing_and_an_empty_key_brackets_at_zero() {
let missing = definition(vec![choice(vec![option("x", "z", "allow once")])]);
assert_eq!(render(&missing), "body\nallow once");
let empty = definition(vec![choice(vec![option("x", "", "allow once")])]);
assert_eq!(render(&empty), "body\n[]allow once");
}
#[test]
fn there_is_no_trailing_newline() {
let d = definition(vec![choice(vec![option("deny", "d", "deny")])]);
assert!(!render(&d).ends_with('\n'));
}
#[test]
fn rendering_is_deterministic() {
let mut d = definition(vec![choice(vec![
option("allow_once", "a", "allow once"),
option("deny", "d", "deny (default)"),
])]);
d.note = Some("hint\nsecond line".to_string());
assert_eq!(render(&d), render(&d));
}
#[test]
fn a_multi_line_note_is_emitted_verbatim() {
let mut d = definition(vec![choice(vec![option("deny", "d", "deny")])]);
d.note = Some("high-danger: refused\nEsc=back".to_string());
assert_eq!(render(&d), "body\nhigh-danger: refused\nEsc=back\n[d]eny");
}
#[test]
fn every_choice_control_contributes_its_own_line() {
let d = definition(vec![
choice(vec![option("a", "a", "aa")]),
Control {
id: ControlId::new("second").expect("valid control id"),
kind: ControlKind::Choice {
options: vec![option("b", "b", "bb")],
},
label: String::new(),
requirement: Requirement::Optional,
},
]);
assert_eq!(render(&d), "body\n[a]a\n[b]b");
}
}
#[cfg(test)]
mod c0b {
use super::*;
use newt_interaction::{
Control, ControlId, InteractionKind, OptionId, Requirement, SemanticRole,
};
fn control(id: &str, kind: ControlKind, label: &str) -> Control {
Control {
id: ControlId::new(id).expect("valid control id"),
kind,
label: label.to_string(),
requirement: Requirement::Required,
}
}
fn choice_of(pairs: &[(&str, &str, &str)]) -> ControlKind {
ControlKind::Choice {
options: pairs
.iter()
.map(|(id, key, label)| ChoiceOption {
id: OptionId::new(*id).expect("valid option id"),
role: SemanticRole::Allow,
label: (*label).to_string(),
key: (*key).to_string(),
aliases: Vec::new(),
})
.collect(),
}
}
fn definition(
kind: InteractionKind,
body: &str,
controls: Vec<Control>,
) -> InteractionDefinition {
InteractionDefinition::new(kind, body, controls)
}
#[test]
fn every_interaction_kind_has_a_plain_projection() {
let cases: Vec<(InteractionKind, InteractionDefinition, &str)> = vec![
(
InteractionKind::Choice,
definition(
InteractionKind::Choice,
"pick one",
vec![control(
"decision",
choice_of(&[("a", "a", "allow once"), ("d", "d", "deny")]),
"",
)],
),
"pick one\n[a]llow once\n[d]eny",
),
(
InteractionKind::Prompt,
definition(
InteractionKind::Prompt,
"? which file",
vec![control("answer", ControlKind::Text, "path")],
),
"? which file\npath:",
),
(
InteractionKind::Confirm,
definition(
InteractionKind::Confirm,
"delete it?",
vec![control("confirm", ControlKind::Toggle, "delete")],
),
"delete it?\ndelete: [y/n]",
),
(
InteractionKind::Form,
definition(
InteractionKind::Form,
"credentials",
vec![
control("user", ControlKind::Text, "username"),
control("pass", ControlKind::Secret, "API key"),
],
),
"credentials\nusername:\nAPI key: (secret, not echoed)",
),
(
InteractionKind::Notice,
definition(InteractionKind::Notice, "the build finished", Vec::new()),
"the build finished",
),
];
assert_eq!(cases.len(), 5, "a kind was added without a projection");
for (kind, def, expected) in cases {
assert_eq!(render(&def), expected, "{kind:?} projected wrongly");
}
}
#[test]
fn every_control_kind_renders() {
for (kind, expected) in [
(ControlKind::Text, "body\nfield:"),
(ControlKind::Toggle, "body\nfield: [y/n]"),
(ControlKind::Secret, "body\nfield: (secret, not echoed)"),
(
ControlKind::Number {
min: Some(1),
max: Some(10),
step: None,
},
"body\nfield: [an integer in 1..=10]",
),
(
ControlKind::Range {
min: 0,
max: 100,
step: 5,
},
"body\nfield: [0..=100]",
),
(
ControlKind::Temporal {
precision: newt_interaction::TemporalPrecision::Date,
},
"body\nfield: [YYYY-MM-DD]",
),
(ControlKind::Color, "body\nfield: [#rrggbb]"),
(
ControlKind::Path {
kind: newt_interaction::PathKind::File,
accept: Vec::new(),
},
"body\nfield: [file path]",
),
] {
let def = definition(
InteractionKind::Form,
"body",
vec![control("c", kind.clone(), "field")],
);
assert_eq!(render(&def), expected);
}
let def = definition(
InteractionKind::Choice,
"body",
vec![control("c", choice_of(&[("y", "y", "yes")]), "")],
);
assert_eq!(render(&def), "body\n[y]es");
}
#[test]
fn a_secret_renders_its_label_and_marks_itself_unechoed() {
let def = definition(
InteractionKind::Form,
"auth",
vec![control("k", ControlKind::Secret, "API key")],
);
let out = render(&def);
assert_eq!(out, "auth\nAPI key: (secret, not echoed)");
assert!(out.contains("API key"), "the label must be shown: {out}");
assert!(
out.contains("not echoed"),
"a secret field must say it is not echoed: {out}"
);
}
#[test]
fn an_unlabelled_control_adds_no_line() {
for kind in [ControlKind::Text, ControlKind::Toggle, ControlKind::Secret] {
let def = definition(
InteractionKind::Prompt,
"? ask",
vec![control("c", kind, "")],
);
assert_eq!(render(&def), "? ask");
}
}
#[test]
fn a_toggle_advertises_no_default() {
let def = definition(
InteractionKind::Confirm,
"go?",
vec![control("c", ControlKind::Toggle, "proceed")],
);
let out = render(&def);
assert!(out.contains("[y/n]"), "{out}");
assert!(!out.contains("[y/N]"), "a default was advertised: {out}");
assert!(!out.contains("[Y/n]"), "a default was advertised: {out}");
}
}
#[cfg(test)]
mod c0c {
use super::render;
use super::tests::{choice, definition, option};
fn menu(labels: &[&str]) -> String {
let options = labels
.iter()
.enumerate()
.map(|(i, label)| {
let key = (b'1' + u8::try_from(i).expect("small")) as char;
option(
&key.to_string(),
&key.to_string(),
&format!("{key} {label}"),
)
})
.collect();
render(&definition(vec![choice(options)]))
}
#[test]
fn the_rule_is_unconditional_and_has_no_threshold() {
let short = menu(&["a", "b"]);
assert_eq!(short, "body\n[1] a\n[2] b");
let long = menu(&[
"Ollama on this machine (http://127.0.0.1:11434)",
"Custom host or URL (llama.cpp, vLLM, a gateway)",
"A hosted provider (OpenAI, Anthropic, OpenRouter, NVIDIA)",
"d",
"e",
"f",
"g",
"h",
"i",
]);
assert_eq!(long.lines().count(), 10, "body plus one line per option");
assert!(
long.lines().skip(1).all(|l| l.starts_with('[')),
"each option starts its own line: {long}"
);
}
#[test]
fn the_option_count_drives_the_line_count() {
for n in 1..6 {
let labels: Vec<String> = (0..n).map(|i| format!("opt{i}")).collect();
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
assert_eq!(
menu(&refs).lines().count(),
n + 1,
"{n} options must render {n} lines under the body"
);
}
let rendered = menu(&["a", "b"]);
assert_ne!(rendered, "body\n[1] a [2] b");
assert!(!rendered.contains(" "), "no three-space join survives");
}
#[test]
fn no_option_is_torn_by_a_narrow_terminal() {
let rendered = render(&definition(vec![choice(vec![
option("allow-once", "a", "allow once"),
option("session-allow", "s", "session allow"),
option(
"allow-permanent",
"A",
"Allow permanently (adds host to config)",
),
option("deny", "d", "deny (default)"),
option("deny-always", "D", "Deny always"),
option("deny-permanent", "P", "Permanently deny"),
])]));
assert_eq!(rendered.lines().count(), 7, "body plus six options");
assert!(
rendered.lines().any(|l| l == "[d]eny (default)"),
"the fail-closed option is whole, on its own line: {rendered}"
);
assert!(
rendered.lines().skip(1).all(|l| l.chars().count() <= 41),
"no option line exceeds the widest option: {rendered}"
);
}
}