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 structural self-checks inside `scaffold` always run), with
|
||||||
// the scaffold outcome (e.g. a failed vendoring) shaping the
|
// the scaffold outcome (e.g. a failed vendoring) shaping the
|
||||||
// offer.
|
// offer.
|
||||||
|
let prompter = pkh::ui::prompt::TerminalPrompter;
|
||||||
if let Err(e) = rt.block_on(async {
|
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)?;
|
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>>(())
|
Ok::<(), Box<dyn std::error::Error>>(())
|
||||||
}) {
|
}) {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
|
|||||||
+75
-65
@@ -1,11 +1,11 @@
|
|||||||
//! The `pkh new` interactive wizard.
|
//! The `pkh new` interactive wizard.
|
||||||
//!
|
//!
|
||||||
//! [`run`] is the single entry point: on an interactive terminal it asks the
|
//! [`run`] is the single entry point: when the prompter can interact it asks
|
||||||
//! questions of the spec's "Proposed UX" transcript, fills a
|
//! the questions of the spec's "Proposed UX" transcript, fills a
|
||||||
//! [`NewCli`] with the answers (explicit flags are never re-asked), and
|
//! [`NewCli`] with the answers (explicit flags are never re-asked), and
|
||||||
//! reuses [`options::resolve`] as the single source of truth for defaults,
|
//! reuses [`options::resolve`] as the single source of truth for defaults,
|
||||||
//! detection and validation — so the non-interactive and interactive paths
|
//! 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
|
//! straight through [`options::resolve`], whose error lists every missing
|
||||||
//! answer.
|
//! answer.
|
||||||
//!
|
//!
|
||||||
@@ -13,11 +13,10 @@
|
|||||||
//! verification builds of the spec ([`offer_verification`]); a failed
|
//! verification builds of the spec ([`offer_verification`]); a failed
|
||||||
//! verification never undoes the scaffold.
|
//! 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.
|
//! everything else in this module is pure and unit-tested.
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use indicatif::MultiProgress;
|
use indicatif::MultiProgress;
|
||||||
@@ -28,7 +27,7 @@ use crate::new::licenses;
|
|||||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
||||||
use crate::new::origin::GitOrigin;
|
use crate::new::origin::GitOrigin;
|
||||||
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
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.
|
/// Answer of the "where is the source code?" question: fresh skeleton.
|
||||||
const SOURCE_SKELETON: &str = "Create a new project skeleton here";
|
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.
|
/// The "everything else" entry of the license menu.
|
||||||
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
||||||
|
|
||||||
/// Labels of the interactive `select` questions. `prompt::select` renders
|
/// Labels of the interactive `select` questions. The prompter renders
|
||||||
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
/// `> <label><answer>` verbatim — unlike [`Prompter::text`], it appends no
|
||||||
/// formatting of its own — so each label carries its own separator:
|
/// formatting of its own — so each label carries its own separator:
|
||||||
/// field-style prompts end with `": "`, question-style ones with `"? "`.
|
/// field-style prompts end with `": "`, question-style ones with `"? "`.
|
||||||
const LANGUAGE_LABEL: &str = "Which language/build system is your program using? ";
|
const LANGUAGE_LABEL: &str = "Which language/build system is your program using? ";
|
||||||
@@ -62,20 +61,13 @@ const SELECT_LABELS: [&str; 6] = [
|
|||||||
ORIG_LABEL,
|
ORIG_LABEL,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Run the `pkh new` flow: the wizard on an interactive terminal, plain
|
/// Run the `pkh new` flow: the wizard when the prompter can interact,
|
||||||
/// [`options::resolve`] otherwise (and with `--defaults`).
|
/// plain [`options::resolve`] otherwise (and with `--defaults`).
|
||||||
pub async fn run(cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
pub async fn run(cli: NewCli, prompter: &dyn Prompter) -> Result<NewOptions, Box<dyn Error>> {
|
||||||
if cli.defaults || !is_interactive() {
|
if cli.defaults || !prompter.interactive() {
|
||||||
return Ok(options::resolve(cli).await?);
|
return Ok(options::resolve(cli).await?);
|
||||||
}
|
}
|
||||||
run_wizard(cli).await
|
run_wizard(cli, prompter).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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The wizard question flow (spec "Proposed UX"), in order:
|
/// 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
|
/// (`empty` template only), git init — then the summary screen and the
|
||||||
/// final `Generate?` confirmation. Every question with an explicit flag
|
/// final `Generate?` confirmation. Every question with an explicit flag
|
||||||
/// answer is skipped (flag > detected/probe > default merge order).
|
/// 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 cwd = std::env::current_dir()?;
|
||||||
let mut detect_dir = cli.source.clone().unwrap_or_else(|| cwd.clone());
|
let mut detect_dir = cli.source.clone().unwrap_or_else(|| cwd.clone());
|
||||||
let (detection, mut probe) = detect_and_probe(&detect_dir);
|
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.
|
// basename of the current directory.
|
||||||
if cli.name.is_none() {
|
if cli.name.is_none() {
|
||||||
let default = default_package_name(&cwd, probe.as_ref());
|
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);
|
cli.name = Some(answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
.unwrap_or(TemplateId::EMPTY)
|
.unwrap_or(TemplateId::EMPTY)
|
||||||
.display_name()
|
.display_name()
|
||||||
.to_string();
|
.to_string();
|
||||||
let id = select_template(&menu, &default)?;
|
let id = select_template(prompter, &menu, &default)?;
|
||||||
cli.lang = Some(id.as_str().to_string());
|
cli.lang = Some(id.as_str().to_string());
|
||||||
}
|
}
|
||||||
LanguageChoice::Ambiguous(candidates) => {
|
LanguageChoice::Ambiguous(candidates) => {
|
||||||
@@ -157,7 +157,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
let menu = language_menu(&candidates);
|
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());
|
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 {
|
} else {
|
||||||
SOURCE_HERE
|
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())
|
options.contains(&answer.to_string())
|
||||||
})?;
|
})?;
|
||||||
if answer == SOURCE_HERE {
|
if answer == SOURCE_HERE {
|
||||||
cli.source = Some(cwd.clone());
|
cli.source = Some(cwd.clone());
|
||||||
} else if answer == SOURCE_PATH {
|
} else if answer == SOURCE_PATH {
|
||||||
let validator = |path: &str| validate_directory_answer(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));
|
cli.source = Some(PathBuf::from(path));
|
||||||
}
|
}
|
||||||
// SOURCE_SKELETON: cli.source stays unset (the name decides).
|
// 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());
|
.unwrap_or_else(|| "0.1.0".to_string());
|
||||||
let revision = cli.revision.unwrap_or(1);
|
let revision = cli.revision.unwrap_or(1);
|
||||||
let validate = move |version: &str| options::validate_upstream_version(version, revision);
|
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());
|
cli.upstream_version = Some(answer.clone());
|
||||||
|
|
||||||
// The typed version names an existing tag HEAD is not on: offer to
|
// 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 \
|
"Version {answer} matches tag {tag}, but HEAD is not that \
|
||||||
tag. Check out {tag} now?"
|
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");
|
let dir = packaged_dir.as_deref().expect("origin implies a directory");
|
||||||
crate::new::origin::checkout_tag(dir, tag)?;
|
crate::new::origin::checkout_tag(dir, tag)?;
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -311,7 +311,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
} else {
|
} else {
|
||||||
"Snapshot this working tree".to_string()
|
"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())
|
labels.contains(&answer.to_string())
|
||||||
})?;
|
})?;
|
||||||
let chosen = choices
|
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());
|
cli.orig_from = Some(chosen.to_string());
|
||||||
if chosen == "path" {
|
if chosen == "path" {
|
||||||
let validator = |path: &str| options::validate_orig_path(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);
|
cli.orig_path = Some(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Debian revision.
|
// 5. Debian revision.
|
||||||
if cli.revision.is_none() {
|
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();
|
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())
|
.and_then(|p| p.description.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let validate = required_answer("the description");
|
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);
|
cli.description = Some(answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
options::validate_homepage(url)
|
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() {
|
if !answer.is_empty() {
|
||||||
cli.homepage = Some(answer);
|
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));
|
.or_else(|| detect::sniff_license(&detect_dir));
|
||||||
let (default, custom_default) = license_question_default(detected.as_deref());
|
let (default, custom_default) = license_question_default(detected.as_deref());
|
||||||
let options = license_menu();
|
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())
|
options.contains(&answer.to_string())
|
||||||
})?;
|
})?;
|
||||||
if answer == LICENSE_OTHER {
|
if answer == LICENSE_OTHER {
|
||||||
let validate = required_answer("the license identifier");
|
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);
|
cli.license = Some(license);
|
||||||
} else {
|
} else {
|
||||||
cli.license = Some(answer);
|
cli.license = Some(answer);
|
||||||
@@ -395,7 +400,12 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.command.clone())
|
.and_then(|p| p.command.clone())
|
||||||
.unwrap_or_else(|| cli.name.clone().unwrap_or_default());
|
.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);
|
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(|_| ());
|
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
|
// 11. Target distribution. The menu derives from the distro data pkh
|
||||||
// ships (sorted); ubuntu is moved to the front when present so it
|
// 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
|
// 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() {
|
if cli.dist.is_none() {
|
||||||
let vendor = crate::build::env::current_vendor().to_lowercase();
|
let vendor = crate::build::env::current_vendor().to_lowercase();
|
||||||
let mut options = crate::distro_info::supported_dists();
|
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 {
|
} else {
|
||||||
"ubuntu".to_string()
|
"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())
|
options.contains(&answer.to_string())
|
||||||
})?;
|
})?;
|
||||||
cli.dist = Some(answer);
|
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() {
|
if cli.series.is_none() {
|
||||||
match crate::distro_info::get_ordered_series_name(&dist).await {
|
match crate::distro_info::get_ordered_series_name(&dist).await {
|
||||||
Ok(series) if !series.is_empty() => {
|
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);
|
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() {
|
if template == TemplateId::EMPTY && cli.depends.is_empty() {
|
||||||
let validate = |answer: &str| options::validate_depends(answer).map(|_| ());
|
let validate = |answer: &str| options::validate_depends(answer).map(|_| ());
|
||||||
let answer = ask_text(
|
let answer = ask_text(
|
||||||
|
prompter,
|
||||||
"Depends (metapackage, comma-separated, blank for an empty base)",
|
"Depends (metapackage, comma-separated, blank for an empty base)",
|
||||||
"",
|
"",
|
||||||
validate,
|
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.
|
// `--no-git` already declined, or there is nothing to initialize.
|
||||||
cli.git = false;
|
cli.git = false;
|
||||||
} else {
|
} 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
|
// 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
|
// build resolve libraries through pkg-config? The project files prefill
|
||||||
// the default (dependency() / pkg_check_modules calls found).
|
// the default (dependency() / pkg_check_modules calls found).
|
||||||
if matches!(template, TemplateId::MESON | TemplateId::CMAKE)
|
if matches!(template, TemplateId::MESON | TemplateId::CMAKE)
|
||||||
&& prompt::confirm(
|
&& prompter.confirm(
|
||||||
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
|
"Does the build resolve libraries through pkg-config (add it to Build-Depends)?",
|
||||||
pkg_config_hint(&detect_dir, template),
|
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).
|
// Wizard-only extras (default off).
|
||||||
if template != TemplateId::EMPTY
|
if template != TemplateId::EMPTY
|
||||||
&& prompt::confirm(
|
&& prompter.confirm(
|
||||||
"Add an autopkgtest smoke test (debian/tests/control)?",
|
"Add an autopkgtest smoke test (debian/tests/control)?",
|
||||||
false,
|
false,
|
||||||
)?
|
)?
|
||||||
@@ -523,15 +534,15 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
opts.autopkgtest = true;
|
opts.autopkgtest = true;
|
||||||
}
|
}
|
||||||
if let Some(watch) = watch_template(opts.homepage.as_deref())
|
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);
|
opts.watch = Some(watch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary screen + final confirmation: Ctrl+C or 'n' abort with
|
// Summary screen + final confirmation: Ctrl+C or 'n' abort with
|
||||||
// nothing written (generation is all-or-nothing later anyway).
|
// nothing written (generation is all-or-nothing later anyway).
|
||||||
println!("{}", summary_text(&opts, toolchain_pin.as_deref()));
|
log::info!("{}", summary_text(&opts, toolchain_pin.as_deref()));
|
||||||
if !prompt::confirm("Generate?", true)? {
|
if !prompter.confirm("Generate?", true)? {
|
||||||
return Err("Aborted: nothing was written to disk.".into());
|
return Err("Aborted: nothing was written to disk.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,6 +564,7 @@ pub async fn offer_verification(
|
|||||||
outcome: &ScaffoldOutcome,
|
outcome: &ScaffoldOutcome,
|
||||||
multi: &MultiProgress,
|
multi: &MultiProgress,
|
||||||
no_verify: bool,
|
no_verify: bool,
|
||||||
|
prompter: &dyn Prompter,
|
||||||
) {
|
) {
|
||||||
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
||||||
let display = crate::report::display_path(&tree);
|
let display = crate::report::display_path(&tree);
|
||||||
@@ -563,9 +575,6 @@ pub async fn offer_verification(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if outcome.vendoring_failed {
|
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!(
|
log::warn!(
|
||||||
"The Cargo dependencies could NOT be vendored: this package will \
|
"The Cargo dependencies could NOT be vendored: this package will \
|
||||||
not build until the vendoring is completed by hand:\n\
|
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, \
|
\x20 2. add the printed source replacement to .cargo/config.toml, \
|
||||||
plus `[net] offline = true`"
|
plus `[net] offline = true`"
|
||||||
);
|
);
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if no_verify || !is_interactive() {
|
if no_verify || !prompter.interactive() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +593,7 @@ pub async fn offer_verification(
|
|||||||
} else {
|
} else {
|
||||||
"Verify with `pkh build` now?"
|
"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,
|
Ok(answer) => answer,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
@@ -598,7 +606,7 @@ pub async fn offer_verification(
|
|||||||
source: Some(tree.clone()),
|
source: Some(tree.clone()),
|
||||||
options: crate::build::SourceBuildOptions::default(),
|
options: crate::build::SourceBuildOptions::default(),
|
||||||
view: &*ui,
|
view: &*ui,
|
||||||
prompter: &crate::ui::prompt::TerminalPrompter,
|
prompter,
|
||||||
}) {
|
}) {
|
||||||
log::error!("Verification source build failed: {e}");
|
log::error!("Verification source build failed: {e}");
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -613,7 +621,7 @@ pub async fn offer_verification(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let verify_deb = match prompt::confirm(
|
let verify_deb = match prompter.confirm(
|
||||||
"Verify with `pkh deb` now? (needs network + build deps)",
|
"Verify with `pkh deb` now? (needs network + build deps)",
|
||||||
false,
|
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
|
/// Ask the language question until a known template label (or CLI
|
||||||
/// identifier) is answered — the selector allows typing arbitrary text.
|
/// 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 {
|
loop {
|
||||||
let answer = prompt::select(LANGUAGE_LABEL, options, default)?;
|
let answer = prompter.select(LANGUAGE_LABEL, options, default)?;
|
||||||
match TemplateId::from_label(&answer) {
|
match TemplateId::from_label(&answer) {
|
||||||
Some(id) => return Ok(id),
|
Some(id) => return Ok(id),
|
||||||
None => log::warn!(
|
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
|
/// Ask a `select` question until `accept` holds for the answer (the
|
||||||
/// selector allows typing arbitrary text, which callers may need to reject).
|
/// selector allows typing arbitrary text, which callers may need to reject).
|
||||||
fn select_from(
|
fn select_from(
|
||||||
|
prompter: &dyn Prompter,
|
||||||
label: &str,
|
label: &str,
|
||||||
options: &[String],
|
options: &[String],
|
||||||
default: &str,
|
default: &str,
|
||||||
accept: impl Fn(&str) -> bool,
|
accept: impl Fn(&str) -> bool,
|
||||||
) -> Result<String, Box<dyn Error>> {
|
) -> Result<String, Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
let answer = prompt::select(label, options, default)?;
|
let answer = prompter.select(label, options, default)?;
|
||||||
if accept(&answer) {
|
if accept(&answer) {
|
||||||
return Ok(answer);
|
return Ok(answer);
|
||||||
}
|
}
|
||||||
@@ -832,7 +845,7 @@ fn select_from(
|
|||||||
/// upstream version like `1.0-2` carrying a Debian revision) is withheld —
|
/// upstream version like `1.0-2` carrying a Debian revision) is withheld —
|
||||||
/// the question is asked without a default instead of offering one that
|
/// the question is asked without a default instead of offering one that
|
||||||
/// Enter would accept verbatim.
|
/// 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() {
|
if default.is_empty() || validate(default).is_ok() {
|
||||||
default
|
default
|
||||||
} else {
|
} 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`, and whatever answer is finally proposed — typed or the
|
||||||
/// default — must pass `validate`. `Err` carries the validation error so the
|
/// default — must pass `validate`. `Err` carries the validation error so the
|
||||||
/// caller re-asks with it.
|
/// caller re-asks with it.
|
||||||
fn accept_answer(
|
fn accept_answer(answer: &str, default: &str, validate: &Validator) -> Result<String, String> {
|
||||||
answer: &str,
|
|
||||||
default: &str,
|
|
||||||
validate: &prompt::Validator,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let answer = if answer.is_empty() { default } else { answer };
|
let answer = if answer.is_empty() { default } else { answer };
|
||||||
validate(answer).map(|_| answer.to_string())
|
validate(answer).map(|_| answer.to_string())
|
||||||
}
|
}
|
||||||
@@ -862,6 +871,7 @@ fn accept_answer(
|
|||||||
/// validation error) — probe data can never bypass validation and only blow
|
/// validation error) — probe data can never bypass validation and only blow
|
||||||
/// up later in [`options::resolve`].
|
/// up later in [`options::resolve`].
|
||||||
fn ask_text(
|
fn ask_text(
|
||||||
|
prompter: &dyn Prompter,
|
||||||
label: &str,
|
label: &str,
|
||||||
default: &str,
|
default: &str,
|
||||||
validate: impl Fn(&str) -> Result<(), String> + 'static,
|
validate: impl Fn(&str) -> Result<(), String> + 'static,
|
||||||
@@ -883,7 +893,7 @@ fn ask_text(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loop {
|
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
|
// Typed non-empty answers were already validated by the prompt; only
|
||||||
// an empty one resolves to the default, pre-decided above.
|
// an empty one resolves to the default, pre-decided above.
|
||||||
let answer = if answer.is_empty() {
|
let answer = if answer.is_empty() {
|
||||||
@@ -1545,7 +1555,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_labels_carry_their_own_separator() {
|
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
|
// without a trailing separator glues the answer to the prompt
|
||||||
// (regression: the wizard once rendered "> LicenseMIT").
|
// (regression: the wizard once rendered "> LicenseMIT").
|
||||||
for label in SELECT_LABELS {
|
for label in SELECT_LABELS {
|
||||||
|
|||||||
Reference in New Issue
Block a user