new: drive the wizard and verification offers through the Prompter port
The interactive half of pkh new no longer touches the terminal prompt module directly: run() takes a Prompter, picks the wizard or the plain resolve path through interactive(), and every select/text/confirm question (including the verification offers) goes through the port. Cancellations propagate as Err, preserving Ctrl+C-aborts; the summary and vendoring-notice prints become log lines. A builder-server embed can now drive the whole scaffold wizard over its own wire format by implementing Prompter.
This commit is contained in:
+6
-2
@@ -321,10 +321,14 @@ fn main() {
|
||||
// the structural self-checks inside `scaffold` always run), with
|
||||
// the scaffold outcome (e.g. a failed vendoring) shaping the
|
||||
// offer.
|
||||
let prompter = pkh::ui::prompt::TerminalPrompter;
|
||||
if let Err(e) = rt.block_on(async {
|
||||
let opts = pkh::new::questions::run(cli).await?;
|
||||
let opts = pkh::new::questions::run(cli, &prompter).await?;
|
||||
let outcome = pkh::new::scaffold(opts.clone(), &multi)?;
|
||||
pkh::new::questions::offer_verification(&opts, &outcome, &multi, no_verify).await;
|
||||
pkh::new::questions::offer_verification(
|
||||
&opts, &outcome, &multi, no_verify, &prompter,
|
||||
)
|
||||
.await;
|
||||
Ok::<(), Box<dyn std::error::Error>>(())
|
||||
}) {
|
||||
error!("{}", e);
|
||||
|
||||
+75
-65
@@ -1,11 +1,11 @@
|
||||
//! The `pkh new` interactive wizard.
|
||||
//!
|
||||
//! [`run`] is the single entry point: on an interactive terminal it asks the
|
||||
//! questions of the spec's "Proposed UX" transcript, fills a
|
||||
//! [`run`] is the single entry point: when the prompter can interact it asks
|
||||
//! the questions of the spec's "Proposed UX" transcript, fills a
|
||||
//! [`NewCli`] with the answers (explicit flags are never re-asked), and
|
||||
//! reuses [`options::resolve`] as the single source of truth for defaults,
|
||||
//! detection and validation — so the non-interactive and interactive paths
|
||||
//! cannot drift apart. Without a terminal (or with `--defaults`) it goes
|
||||
//! cannot drift apart. Headless (or with `--defaults`) it goes
|
||||
//! straight through [`options::resolve`], whose error lists every missing
|
||||
//! answer.
|
||||
//!
|
||||
@@ -13,11 +13,10 @@
|
||||
//! verification builds of the spec ([`offer_verification`]); a failed
|
||||
//! verification never undoes the scaffold.
|
||||
//!
|
||||
//! The prompt calls live in `run_wizard` and `offer_verification` only;
|
||||
//! The prompter calls live in `run_wizard` and `offer_verification` only;
|
||||
//! everything else in this module is pure and unit-tested.
|
||||
|
||||
use std::error::Error;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use indicatif::MultiProgress;
|
||||
@@ -28,7 +27,7 @@ use crate::new::licenses;
|
||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
||||
use crate::new::origin::GitOrigin;
|
||||
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
||||
use crate::ui::prompt;
|
||||
use crate::report::{Prompter, Validator};
|
||||
|
||||
/// Answer of the "where is the source code?" question: fresh skeleton.
|
||||
const SOURCE_SKELETON: &str = "Create a new project skeleton here";
|
||||
@@ -40,8 +39,8 @@ const SOURCE_PATH: &str = "Package the sources in another directory…";
|
||||
/// The "everything else" entry of the license menu.
|
||||
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
||||
|
||||
/// Labels of the interactive `select` questions. `prompt::select` renders
|
||||
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
||||
/// Labels of the interactive `select` questions. The prompter renders
|
||||
/// `> <label><answer>` verbatim — unlike [`Prompter::text`], it appends no
|
||||
/// formatting of its own — so each label carries its own separator:
|
||||
/// field-style prompts end with `": "`, question-style ones with `"? "`.
|
||||
const LANGUAGE_LABEL: &str = "Which language/build system is your program using? ";
|
||||
@@ -62,20 +61,13 @@ const SELECT_LABELS: [&str; 6] = [
|
||||
ORIG_LABEL,
|
||||
];
|
||||
|
||||
/// Run the `pkh new` flow: the wizard on an interactive terminal, plain
|
||||
/// [`options::resolve`] otherwise (and with `--defaults`).
|
||||
pub async fn run(cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
if cli.defaults || !is_interactive() {
|
||||
/// Run the `pkh new` flow: the wizard when the prompter can interact,
|
||||
/// plain [`options::resolve`] otherwise (and with `--defaults`).
|
||||
pub async fn run(cli: NewCli, prompter: &dyn Prompter) -> Result<NewOptions, Box<dyn Error>> {
|
||||
if cli.defaults || !prompter.interactive() {
|
||||
return Ok(options::resolve(cli).await?);
|
||||
}
|
||||
run_wizard(cli).await
|
||||
}
|
||||
|
||||
/// Whether both ends of the terminal are interactive; the wizard and the
|
||||
/// verification offers only run when this holds (the prompts' non-TTY
|
||||
/// fallbacks would otherwise silently take defaults).
|
||||
fn is_interactive() -> bool {
|
||||
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
|
||||
run_wizard(cli, prompter).await
|
||||
}
|
||||
|
||||
/// The wizard question flow (spec "Proposed UX"), in order:
|
||||
@@ -87,7 +79,10 @@ fn is_interactive() -> bool {
|
||||
/// (`empty` template only), git init — then the summary screen and the
|
||||
/// final `Generate?` confirmation. Every question with an explicit flag
|
||||
/// answer is skipped (flag > detected/probe > default merge order).
|
||||
async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
async fn run_wizard(
|
||||
mut cli: NewCli,
|
||||
prompter: &dyn Prompter,
|
||||
) -> Result<NewOptions, Box<dyn Error>> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let mut detect_dir = cli.source.clone().unwrap_or_else(|| cwd.clone());
|
||||
let (detection, mut probe) = detect_and_probe(&detect_dir);
|
||||
@@ -101,7 +96,12 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
// basename of the current directory.
|
||||
if cli.name.is_none() {
|
||||
let default = default_package_name(&cwd, probe.as_ref());
|
||||
let answer = ask_text("Package name", &default, options::validate_source_name)?;
|
||||
let answer = ask_text(
|
||||
prompter,
|
||||
"Package name",
|
||||
&default,
|
||||
options::validate_source_name,
|
||||
)?;
|
||||
cli.name = Some(answer);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.unwrap_or(TemplateId::EMPTY)
|
||||
.display_name()
|
||||
.to_string();
|
||||
let id = select_template(&menu, &default)?;
|
||||
let id = select_template(prompter, &menu, &default)?;
|
||||
cli.lang = Some(id.as_str().to_string());
|
||||
}
|
||||
LanguageChoice::Ambiguous(candidates) => {
|
||||
@@ -157,7 +157,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.join(", ")
|
||||
);
|
||||
let menu = language_menu(&candidates);
|
||||
let id = select_template(&menu, &menu[0])?;
|
||||
let id = select_template(prompter, &menu, &menu[0])?;
|
||||
cli.lang = Some(id.as_str().to_string());
|
||||
}
|
||||
}
|
||||
@@ -193,14 +193,14 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
} else {
|
||||
SOURCE_HERE
|
||||
};
|
||||
let answer = select_from(SOURCE_LABEL, &options, default, |answer| {
|
||||
let answer = select_from(prompter, SOURCE_LABEL, &options, default, |answer| {
|
||||
options.contains(&answer.to_string())
|
||||
})?;
|
||||
if answer == SOURCE_HERE {
|
||||
cli.source = Some(cwd.clone());
|
||||
} else if answer == SOURCE_PATH {
|
||||
let validator = |path: &str| validate_directory_answer(path);
|
||||
let path = prompt::text("Source directory", "", Some(&validator))?;
|
||||
let path = prompter.text("Source directory", "", Some(&validator))?;
|
||||
cli.source = Some(PathBuf::from(path));
|
||||
}
|
||||
// SOURCE_SKELETON: cli.source stays unset (the name decides).
|
||||
@@ -266,7 +266,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.unwrap_or_else(|| "0.1.0".to_string());
|
||||
let revision = cli.revision.unwrap_or(1);
|
||||
let validate = move |version: &str| options::validate_upstream_version(version, revision);
|
||||
let answer = ask_text("Upstream version", &default, validate)?;
|
||||
let answer = ask_text(prompter, "Upstream version", &default, validate)?;
|
||||
cli.upstream_version = Some(answer.clone());
|
||||
|
||||
// The typed version names an existing tag HEAD is not on: offer to
|
||||
@@ -279,7 +279,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
"Version {answer} matches tag {tag}, but HEAD is not that \
|
||||
tag. Check out {tag} now?"
|
||||
);
|
||||
if prompt::confirm(&question, false)? {
|
||||
if prompter.confirm(&question, false)? {
|
||||
let dir = packaged_dir.as_deref().expect("origin implies a directory");
|
||||
crate::new::origin::checkout_tag(dir, tag)?;
|
||||
log::info!(
|
||||
@@ -311,7 +311,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
} else {
|
||||
"Snapshot this working tree".to_string()
|
||||
};
|
||||
let answer = select_from(ORIG_LABEL, &labels, &default, |answer| {
|
||||
let answer = select_from(prompter, ORIG_LABEL, &labels, &default, |answer| {
|
||||
labels.contains(&answer.to_string())
|
||||
})?;
|
||||
let chosen = choices
|
||||
@@ -322,14 +322,14 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
cli.orig_from = Some(chosen.to_string());
|
||||
if chosen == "path" {
|
||||
let validator = |path: &str| options::validate_orig_path(path);
|
||||
let path = prompt::text("Tarball path or URL", "", Some(&validator))?;
|
||||
let path = prompter.text("Tarball path or URL", "", Some(&validator))?;
|
||||
cli.orig_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Debian revision.
|
||||
if cli.revision.is_none() {
|
||||
let answer = ask_text("Debian revision", "1", validate_revision_answer)?;
|
||||
let answer = ask_text(prompter, "Debian revision", "1", validate_revision_answer)?;
|
||||
cli.revision = answer.parse::<u32>().ok();
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.and_then(|p| p.description.clone())
|
||||
.unwrap_or_default();
|
||||
let validate = required_answer("the description");
|
||||
let answer = ask_text("One-line description", &default, validate)?;
|
||||
let answer = ask_text(prompter, "One-line description", &default, validate)?;
|
||||
cli.description = Some(answer);
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
options::validate_homepage(url)
|
||||
}
|
||||
};
|
||||
let answer = ask_text("Homepage (blank to skip)", &default, validate)?;
|
||||
let answer = ask_text(prompter, "Homepage (blank to skip)", &default, validate)?;
|
||||
if !answer.is_empty() {
|
||||
cli.homepage = Some(answer);
|
||||
}
|
||||
@@ -373,12 +373,17 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.or_else(|| detect::sniff_license(&detect_dir));
|
||||
let (default, custom_default) = license_question_default(detected.as_deref());
|
||||
let options = license_menu();
|
||||
let answer = select_from(LICENSE_LABEL, &options, &default, |answer| {
|
||||
let answer = select_from(prompter, LICENSE_LABEL, &options, &default, |answer| {
|
||||
options.contains(&answer.to_string())
|
||||
})?;
|
||||
if answer == LICENSE_OTHER {
|
||||
let validate = required_answer("the license identifier");
|
||||
let license = ask_text("License (SPDX identifier)", &custom_default, validate)?;
|
||||
let license = ask_text(
|
||||
prompter,
|
||||
"License (SPDX identifier)",
|
||||
&custom_default,
|
||||
validate,
|
||||
)?;
|
||||
cli.license = Some(license);
|
||||
} else {
|
||||
cli.license = Some(answer);
|
||||
@@ -395,7 +400,12 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
.as_ref()
|
||||
.and_then(|p| p.command.clone())
|
||||
.unwrap_or_else(|| cli.name.clone().unwrap_or_default());
|
||||
let command = ask_text("Command name", &default, options::validate_command)?;
|
||||
let command = ask_text(
|
||||
prompter,
|
||||
"Command name",
|
||||
&default,
|
||||
options::validate_command,
|
||||
)?;
|
||||
cli.command = Some(command);
|
||||
}
|
||||
|
||||
@@ -421,13 +431,13 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
);
|
||||
}
|
||||
let validate = |answer: &str| options::parse_maintainer(answer).map(|_| ());
|
||||
cli.maintainer = Some(ask_text("Maintainer", &default, validate)?);
|
||||
cli.maintainer = Some(ask_text(prompter, "Maintainer", &default, validate)?);
|
||||
}
|
||||
|
||||
// 11. Target distribution. The menu derives from the distro data pkh
|
||||
// ships (sorted); ubuntu is moved to the front when present so it
|
||||
// stays the menu's first entry and fallback default as it has always
|
||||
// been — prompt::select positions on a default value, not an index.
|
||||
// been — the selector positions on a default value, not an index.
|
||||
if cli.dist.is_none() {
|
||||
let vendor = crate::build::env::current_vendor().to_lowercase();
|
||||
let mut options = crate::distro_info::supported_dists();
|
||||
@@ -442,7 +452,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
} else {
|
||||
"ubuntu".to_string()
|
||||
};
|
||||
let answer = select_from(DIST_LABEL, &options, &default, |answer| {
|
||||
let answer = select_from(prompter, DIST_LABEL, &options, &default, |answer| {
|
||||
options.contains(&answer.to_string())
|
||||
})?;
|
||||
cli.dist = Some(answer);
|
||||
@@ -456,7 +466,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
if cli.series.is_none() {
|
||||
match crate::distro_info::get_ordered_series_name(&dist).await {
|
||||
Ok(series) if !series.is_empty() => {
|
||||
let answer = prompt::select(SERIES_LABEL, &series, &series[0])?;
|
||||
let answer = prompter.select(SERIES_LABEL, &series, &series[0])?;
|
||||
cli.series = Some(answer);
|
||||
}
|
||||
_ => {
|
||||
@@ -472,6 +482,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
if template == TemplateId::EMPTY && cli.depends.is_empty() {
|
||||
let validate = |answer: &str| options::validate_depends(answer).map(|_| ());
|
||||
let answer = ask_text(
|
||||
prompter,
|
||||
"Depends (metapackage, comma-separated, blank for an empty base)",
|
||||
"",
|
||||
validate,
|
||||
@@ -494,7 +505,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
// `--no-git` already declined, or there is nothing to initialize.
|
||||
cli.git = false;
|
||||
} else {
|
||||
cli.git = prompt::confirm("Initialize a git repository?", cli.git)?;
|
||||
cli.git = prompter.confirm("Initialize a git repository?", cli.git)?;
|
||||
}
|
||||
|
||||
// Resolve through the same pipeline as the non-interactive path: one
|
||||
@@ -505,7 +516,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
// build resolve libraries through pkg-config? The project files prefill
|
||||
// the default (dependency() / pkg_check_modules calls found).
|
||||
if matches!(template, TemplateId::MESON | TemplateId::CMAKE)
|
||||
&& prompt::confirm(
|
||||
&& prompter.confirm(
|
||||
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
|
||||
pkg_config_hint(&detect_dir, template),
|
||||
)?
|
||||
@@ -515,7 +526,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
|
||||
// Wizard-only extras (default off).
|
||||
if template != TemplateId::EMPTY
|
||||
&& prompt::confirm(
|
||||
&& prompter.confirm(
|
||||
"Add an autopkgtest smoke test (debian/tests/control)?",
|
||||
false,
|
||||
)?
|
||||
@@ -523,15 +534,15 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
opts.autopkgtest = true;
|
||||
}
|
||||
if let Some(watch) = watch_template(opts.homepage.as_deref())
|
||||
&& prompt::confirm("Add a debian/watch release watcher?", false)?
|
||||
&& prompter.confirm("Add a debian/watch release watcher?", false)?
|
||||
{
|
||||
opts.watch = Some(watch);
|
||||
}
|
||||
|
||||
// Summary screen + final confirmation: Ctrl+C or 'n' abort with
|
||||
// nothing written (generation is all-or-nothing later anyway).
|
||||
println!("{}", summary_text(&opts, toolchain_pin.as_deref()));
|
||||
if !prompt::confirm("Generate?", true)? {
|
||||
log::info!("{}", summary_text(&opts, toolchain_pin.as_deref()));
|
||||
if !prompter.confirm("Generate?", true)? {
|
||||
return Err("Aborted: nothing was written to disk.".into());
|
||||
}
|
||||
|
||||
@@ -553,6 +564,7 @@ pub async fn offer_verification(
|
||||
outcome: &ScaffoldOutcome,
|
||||
multi: &MultiProgress,
|
||||
no_verify: bool,
|
||||
prompter: &dyn Prompter,
|
||||
) {
|
||||
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
||||
let display = crate::report::display_path(&tree);
|
||||
@@ -563,9 +575,6 @@ pub async fn offer_verification(
|
||||
};
|
||||
|
||||
if outcome.vendoring_failed {
|
||||
// Set apart from the surrounding success output by blank lines: a
|
||||
// single warning between two success lines is easy to miss.
|
||||
println!();
|
||||
log::warn!(
|
||||
"The Cargo dependencies could NOT be vendored: this package will \
|
||||
not build until the vendoring is completed by hand:\n\
|
||||
@@ -573,10 +582,9 @@ pub async fn offer_verification(
|
||||
\x20 2. add the printed source replacement to .cargo/config.toml, \
|
||||
plus `[net] offline = true`"
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
if no_verify || !is_interactive() {
|
||||
if no_verify || !prompter.interactive() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,7 +593,7 @@ pub async fn offer_verification(
|
||||
} else {
|
||||
"Verify with `pkh build` now?"
|
||||
};
|
||||
let verify_source = match prompt::confirm(build_offer, !outcome.vendoring_failed) {
|
||||
let verify_source = match prompter.confirm(build_offer, !outcome.vendoring_failed) {
|
||||
Ok(answer) => answer,
|
||||
Err(_) => return,
|
||||
};
|
||||
@@ -598,7 +606,7 @@ pub async fn offer_verification(
|
||||
source: Some(tree.clone()),
|
||||
options: crate::build::SourceBuildOptions::default(),
|
||||
view: &*ui,
|
||||
prompter: &crate::ui::prompt::TerminalPrompter,
|
||||
prompter,
|
||||
}) {
|
||||
log::error!("Verification source build failed: {e}");
|
||||
log::info!(
|
||||
@@ -613,7 +621,7 @@ pub async fn offer_verification(
|
||||
return;
|
||||
}
|
||||
|
||||
let verify_deb = match prompt::confirm(
|
||||
let verify_deb = match prompter.confirm(
|
||||
"Verify with `pkh deb` now? (needs network + build deps)",
|
||||
false,
|
||||
) {
|
||||
@@ -797,9 +805,13 @@ fn license_question_default(probe_license: Option<&str>) -> (String, String) {
|
||||
|
||||
/// Ask the language question until a known template label (or CLI
|
||||
/// identifier) is answered — the selector allows typing arbitrary text.
|
||||
fn select_template(options: &[String], default: &str) -> Result<TemplateId, Box<dyn Error>> {
|
||||
fn select_template(
|
||||
prompter: &dyn Prompter,
|
||||
options: &[String],
|
||||
default: &str,
|
||||
) -> Result<TemplateId, Box<dyn Error>> {
|
||||
loop {
|
||||
let answer = prompt::select(LANGUAGE_LABEL, options, default)?;
|
||||
let answer = prompter.select(LANGUAGE_LABEL, options, default)?;
|
||||
match TemplateId::from_label(&answer) {
|
||||
Some(id) => return Ok(id),
|
||||
None => log::warn!(
|
||||
@@ -813,13 +825,14 @@ fn select_template(options: &[String], default: &str) -> Result<TemplateId, Box<
|
||||
/// Ask a `select` question until `accept` holds for the answer (the
|
||||
/// selector allows typing arbitrary text, which callers may need to reject).
|
||||
fn select_from(
|
||||
prompter: &dyn Prompter,
|
||||
label: &str,
|
||||
options: &[String],
|
||||
default: &str,
|
||||
accept: impl Fn(&str) -> bool,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
loop {
|
||||
let answer = prompt::select(label, options, default)?;
|
||||
let answer = prompter.select(label, options, default)?;
|
||||
if accept(&answer) {
|
||||
return Ok(answer);
|
||||
}
|
||||
@@ -832,7 +845,7 @@ fn select_from(
|
||||
/// upstream version like `1.0-2` carrying a Debian revision) is withheld —
|
||||
/// the question is asked without a default instead of offering one that
|
||||
/// Enter would accept verbatim.
|
||||
fn offered_default<'a>(default: &'a str, validate: &prompt::Validator) -> &'a str {
|
||||
fn offered_default<'a>(default: &'a str, validate: &Validator) -> &'a str {
|
||||
if default.is_empty() || validate(default).is_ok() {
|
||||
default
|
||||
} else {
|
||||
@@ -844,11 +857,7 @@ fn offered_default<'a>(default: &'a str, validate: &prompt::Validator) -> &'a st
|
||||
/// `default`, and whatever answer is finally proposed — typed or the
|
||||
/// default — must pass `validate`. `Err` carries the validation error so the
|
||||
/// caller re-asks with it.
|
||||
fn accept_answer(
|
||||
answer: &str,
|
||||
default: &str,
|
||||
validate: &prompt::Validator,
|
||||
) -> Result<String, String> {
|
||||
fn accept_answer(answer: &str, default: &str, validate: &Validator) -> Result<String, String> {
|
||||
let answer = if answer.is_empty() { default } else { answer };
|
||||
validate(answer).map(|_| answer.to_string())
|
||||
}
|
||||
@@ -862,6 +871,7 @@ fn accept_answer(
|
||||
/// validation error) — probe data can never bypass validation and only blow
|
||||
/// up later in [`options::resolve`].
|
||||
fn ask_text(
|
||||
prompter: &dyn Prompter,
|
||||
label: &str,
|
||||
default: &str,
|
||||
validate: impl Fn(&str) -> Result<(), String> + 'static,
|
||||
@@ -883,7 +893,7 @@ fn ask_text(
|
||||
}
|
||||
};
|
||||
loop {
|
||||
let answer = prompt::text(label, default, Some(&accept_empty))?;
|
||||
let answer = prompter.text(label, default, Some(&accept_empty))?;
|
||||
// Typed non-empty answers were already validated by the prompt; only
|
||||
// an empty one resolves to the default, pre-decided above.
|
||||
let answer = if answer.is_empty() {
|
||||
@@ -1545,7 +1555,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn select_labels_carry_their_own_separator() {
|
||||
// prompt::select renders `> <label><answer>` verbatim; a label
|
||||
// the selector renders `> <label><answer>` verbatim; a label
|
||||
// without a trailing separator glues the answer to the prompt
|
||||
// (regression: the wizard once rendered "> LicenseMIT").
|
||||
for label in SELECT_LABELS {
|
||||
|
||||
Reference in New Issue
Block a user