use crate::interaction_surface::SurfaceInteraction;
use crate::markup::plain;
use crate::tty::{read_prompt_window_line, Echo, PromptLine, PromptWindow, MODAL_INPUT_GLYPH};
use crate::HumanQuestionOutcome;
use newt_interaction::{ControlKind, InteractionDefinition};
use std::io::{self, IsTerminal as _};
#[must_use]
pub fn echo_for(definition: &InteractionDefinition) -> Echo {
if definition
.controls
.iter()
.any(|control| matches!(control.kind, ControlKind::Secret))
{
Echo::Stars
} else {
Echo::Chars
}
}
#[must_use]
pub fn present_on_terminal(
window: &PromptWindow,
interaction: &SurfaceInteraction,
) -> HumanQuestionOutcome {
present_on_terminal_with_layout(window, interaction, None)
}
#[must_use]
pub fn present_on_terminal_with_width(
window: &PromptWindow,
interaction: &SurfaceInteraction,
width: usize,
) -> HumanQuestionOutcome {
let width = (io::stdin().is_terminal() && window.output_is_terminal()).then_some(width);
present_on_terminal_with_layout(window, interaction, width)
}
fn prompt_text(definition: &InteractionDefinition, width: Option<usize>) -> String {
let body = plain::render(definition);
let body = match width {
Some(width) => crate::tty::wrap_line(&body, width).join("\n"),
None => body,
};
format!("{body}\n{MODAL_INPUT_GLYPH}")
}
fn present_on_terminal_with_layout(
window: &PromptWindow,
interaction: &SurfaceInteraction,
width: Option<usize>,
) -> HumanQuestionOutcome {
let prompt = prompt_text(&interaction.definition, width);
match read_prompt_window_line(window, &prompt, echo_for(&interaction.definition)) {
Ok(PromptLine::Line(answer)) => {
HumanQuestionOutcome::Answer(interaction.answer_or_default(&answer).to_owned())
}
Ok(PromptLine::Eof) => HumanQuestionOutcome::InputClosed,
Ok(PromptLine::Back) => HumanQuestionOutcome::Cancelled,
Ok(PromptLine::Exit) => HumanQuestionOutcome::ExitRequested,
Err(_) => HumanQuestionOutcome::InputFailed,
}
}
#[must_use]
pub fn resolve_on_terminal(
window: &PromptWindow,
definition: &InteractionDefinition,
) -> Option<newt_interaction::OptionId> {
let interaction = SurfaceInteraction::blocking(definition.clone());
match present_on_terminal(window, &interaction) {
HumanQuestionOutcome::Answer(line) => {
crate::interaction_form::resolve(definition, line.trim())
}
HumanQuestionOutcome::InputClosed
| HumanQuestionOutcome::Cancelled
| HumanQuestionOutcome::ExitRequested
| HumanQuestionOutcome::InputFailed
| HumanQuestionOutcome::Unavailable => None,
}
}
#[must_use]
pub fn confirmed_on_terminal(
window: &PromptWindow,
definition: &InteractionDefinition,
blank: bool,
) -> bool {
let interaction = SurfaceInteraction::blocking(definition.clone());
confirm_from_outcome(
definition,
&present_on_terminal(window, &interaction),
blank,
)
}
#[must_use]
fn confirm_from_outcome(
definition: &InteractionDefinition,
outcome: &HumanQuestionOutcome,
blank: bool,
) -> bool {
match outcome {
HumanQuestionOutcome::Answer(line) => {
let line = line.trim();
if line.is_empty() {
return blank;
}
crate::interaction_form::resolve(definition, line)
.is_some_and(|picked| picked.as_str() == crate::interaction_form::YES)
}
HumanQuestionOutcome::InputClosed
| HumanQuestionOutcome::Cancelled
| HumanQuestionOutcome::ExitRequested
| HumanQuestionOutcome::InputFailed
| HumanQuestionOutcome::Unavailable => false,
}
}
#[cfg(test)]
mod width_layout {
use super::prompt_text;
use crate::markup::plain;
use crate::tty::{str_width, wrap_line, MODAL_INPUT_GLYPH};
use newt_interaction::{InteractionDefinition, InteractionKind};
fn definition() -> InteractionDefinition {
InteractionDefinition::new(
InteractionKind::Form,
"Prompt — choose a deliberately long destination path",
vec![],
)
}
#[test]
fn width_layout_wraps_only_the_canonical_plain_body() {
let definition = definition();
let canonical = plain::render(&definition);
let expected_body = wrap_line(&canonical, 12).join("\n");
let prompt = prompt_text(&definition, Some(12));
assert_eq!(prompt, format!("{expected_body}\n{MODAL_INPUT_GLYPH}"));
let (body, answer_row) = prompt.rsplit_once('\n').expect("answer row");
assert_eq!(answer_row, MODAL_INPUT_GLYPH);
assert!(
body.lines().all(|line| str_width(line) <= 12),
"body was not manually wrapped to the terminal width: {body:?}"
);
}
#[test]
fn no_width_preserves_the_exact_headless_prompt_bytes() {
let definition = definition();
assert_eq!(
prompt_text(&definition, None),
format!("{}\n{MODAL_INPUT_GLYPH}", plain::render(&definition))
);
}
}
#[cfg(test)]
mod f0a_confirm {
use super::confirm_from_outcome;
use crate::interaction_form::confirm;
use crate::HumanQuestionOutcome;
fn q() -> newt_interaction::InteractionDefinition {
confirm("proceed?", "", "yes, do it", "no, stop")
}
#[test]
fn no_outcome_but_an_affirmative_answer_confirms() {
for outcome in [
HumanQuestionOutcome::InputClosed,
HumanQuestionOutcome::Cancelled,
HumanQuestionOutcome::ExitRequested,
HumanQuestionOutcome::InputFailed,
HumanQuestionOutcome::Unavailable,
] {
assert!(!confirm_from_outcome(&q(), &outcome, true), "{outcome:?}");
assert!(!confirm_from_outcome(&q(), &outcome, false), "{outcome:?}");
}
for typed in ["n", "no", "maybe", "1", "yes please"] {
let a = HumanQuestionOutcome::Answer(typed.to_string());
assert!(!confirm_from_outcome(&q(), &a, true), "{typed:?}");
}
}
#[test]
fn an_affirmative_answer_does_confirm() {
for typed in ["y", "Y", "yes", " y "] {
let a = HumanQuestionOutcome::Answer(typed.to_string());
assert!(confirm_from_outcome(&q(), &a, false), "{typed:?}");
}
}
#[test]
fn blank_means_what_the_call_site_says_and_nothing_else() {
for blank in [true, false] {
for typed in ["", " ", "\t"] {
let a = HumanQuestionOutcome::Answer(typed.to_string());
assert_eq!(confirm_from_outcome(&q(), &a, blank), blank, "{typed:?}");
}
}
}
}
#[cfg(test)]
mod d1b_echo {
use super::echo_for;
use crate::tty::Echo;
use newt_interaction::{
ChoiceOption, Control, ControlId, ControlKind, InteractionDefinition, InteractionKind,
OptionId, Requirement, SemanticRole,
};
fn control(kind: ControlKind) -> Control {
Control {
id: ControlId::new("field").expect("valid"),
kind,
label: "API key".to_string(),
requirement: Requirement::Required,
}
}
fn form(controls: Vec<Control>) -> InteractionDefinition {
InteractionDefinition::new(InteractionKind::Form, "credentials", controls)
}
#[test]
fn a_secret_control_masks_the_read() {
assert_eq!(
echo_for(&form(vec![control(ControlKind::Secret)])),
Echo::Stars
);
}
#[test]
fn an_ordinary_control_still_echoes() {
for kind in [
ControlKind::Text,
ControlKind::Toggle,
ControlKind::Choice {
options: vec![ChoiceOption {
id: OptionId::new("yes").expect("valid"),
role: SemanticRole::Allow,
label: "yes".to_string(),
key: "y".to_string(),
aliases: vec![],
}],
},
] {
assert_eq!(
echo_for(&form(vec![control(kind.clone())])),
Echo::Chars,
"{kind:?} is not a secret and must echo"
);
}
assert_eq!(echo_for(&form(vec![])), Echo::Chars);
}
#[test]
fn a_secret_anywhere_masks_the_whole_form() {
let mixed = form(vec![
control(ControlKind::Text),
control(ControlKind::Secret),
]);
assert_eq!(echo_for(&mixed), Echo::Stars, "secret last");
let mixed = form(vec![
control(ControlKind::Secret),
control(ControlKind::Text),
]);
assert_eq!(echo_for(&mixed), Echo::Stars, "secret first");
}
}