report: grow the Prompter port and move display_path out of the ui module
Prompter gains interactive(), select() and text() (with the Validator type), and confirm() now propagates cancellation as Err so flows abort instead of silently taking a default when the user hits Ctrl+C. The terminal prompter implements the full port; the port also re-exports the path display helper, which is pure presentation formatting used by events and messages rather than terminal code.
This commit is contained in:
+7
-4
@@ -170,10 +170,13 @@ fn retry_after_revendor(
|
||||
original: Box<dyn Error>,
|
||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||
log::error!("{original}");
|
||||
if !prompter.confirm(
|
||||
"Re-vendor the Cargo dependencies and retry the build?",
|
||||
false,
|
||||
) {
|
||||
if !prompter
|
||||
.confirm(
|
||||
"Re-vendor the Cargo dependencies and retry the build?",
|
||||
false,
|
||||
)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
view.finish_failure();
|
||||
return Err(original);
|
||||
}
|
||||
|
||||
+1
-1
@@ -518,7 +518,7 @@ fn main() {
|
||||
// print them as plain lines.
|
||||
if !view.is_enabled() {
|
||||
for artifact in output.artifacts() {
|
||||
println!(" {}", pkh::ui::display_path(&artifact));
|
||||
println!(" {}", pkh::report::display_path(&artifact));
|
||||
}
|
||||
}
|
||||
if output.signed {
|
||||
|
||||
+1
-1
@@ -436,7 +436,7 @@ pub fn create_orig_tarball_excluding(
|
||||
|
||||
log::info!(
|
||||
"Created orig tarball {}",
|
||||
crate::ui::display_path(&tarball_path)
|
||||
crate::report::display_path(&tarball_path)
|
||||
);
|
||||
Ok(tarball_path)
|
||||
}
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ fn print_success(opts: &NewOptions, outcome: &ScaffoldOutcome) {
|
||||
// `display_path` yields an empty string when the target is the cwd
|
||||
// itself (Here mode): `Created .` would be cryptic, so spell the
|
||||
// location out; the skeleton/path modes keep the `<dir>` display.
|
||||
let display = crate::ui::display_path(&target);
|
||||
let display = crate::report::display_path(&target);
|
||||
let location = if display.is_empty() {
|
||||
"package in the current directory".to_string()
|
||||
} else {
|
||||
|
||||
+4
-4
@@ -183,7 +183,7 @@ pub fn create_vendor_component(
|
||||
|
||||
log::info!(
|
||||
"Created vendored-dependencies component {}",
|
||||
crate::ui::display_path(&component_path)
|
||||
crate::report::display_path(&component_path)
|
||||
);
|
||||
Ok(component_path)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ fn git_archive_tarball(
|
||||
|
||||
log::info!(
|
||||
"Created orig tarball from git archive of {tag}: {}",
|
||||
crate::ui::display_path(&dest)
|
||||
crate::report::display_path(&dest)
|
||||
);
|
||||
Ok(dest)
|
||||
}
|
||||
@@ -311,7 +311,7 @@ fn download_release(
|
||||
Ok(path) => {
|
||||
log::info!(
|
||||
"Created orig tarball from the release download of {tag}: {}",
|
||||
crate::ui::display_path(&path)
|
||||
crate::report::display_path(&path)
|
||||
);
|
||||
Ok(path)
|
||||
}
|
||||
@@ -352,7 +352,7 @@ fn fetch_and_repack(
|
||||
log::info!(
|
||||
"Created orig tarball from {}: {}",
|
||||
source,
|
||||
crate::ui::display_path(&dest)
|
||||
crate::report::display_path(&dest)
|
||||
);
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
@@ -555,7 +555,7 @@ pub async fn offer_verification(
|
||||
no_verify: bool,
|
||||
) {
|
||||
let tree = opts.target_dir(&std::env::current_dir().unwrap_or_default());
|
||||
let display = crate::ui::display_path(&tree);
|
||||
let display = crate::report::display_path(&tree);
|
||||
let display = if display.is_empty() {
|
||||
".".to_string()
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -521,7 +521,7 @@ fn discover_changes(cwd: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
Pass the one to upload explicitly",
|
||||
entry.source,
|
||||
many.iter()
|
||||
.map(|p| ui::display_path(p))
|
||||
.map(|p| crate::report::display_path(p))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
|
||||
+67
-6
@@ -14,13 +14,20 @@
|
||||
//! implementations used by headless runs and tests. [`Prompter`] is
|
||||
//! deliberately blocking: an implementation may round-trip each question to
|
||||
//! a remote user, as long as it eventually answers (or takes the default).
|
||||
//! Asking can fail (the user cancels, the connection drops); flows
|
||||
//! propagate the error, which aborts them.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::context::LineSink;
|
||||
use crate::logfmt::Classifier;
|
||||
|
||||
/// Answer validator of [`Prompter::text`]: accepts the answer, or explains
|
||||
/// why it is rejected (the implementation re-asks with the explanation).
|
||||
pub type Validator = dyn Fn(&str) -> Result<(), String>;
|
||||
|
||||
/// Identity of the build whose events follow, as announced through
|
||||
/// [`BuildView::target`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -94,18 +101,51 @@ pub trait BuildView: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
/// Answerer of the questions a core flow may ask mid-run (confirmations
|
||||
/// now, selections and free-text input as more flows migrate).
|
||||
/// Answerer of the questions a core flow may ask mid-run.
|
||||
///
|
||||
/// Questions are blocking on purpose: a terminal implementation waits for
|
||||
/// key presses, and a builder-server implementation may forward the question
|
||||
/// to a web client and await the answer on a channel. Implementations that
|
||||
/// cannot ask anyone answer with the question's default.
|
||||
pub trait Prompter: Send + Sync {
|
||||
/// Whether this prompter can interact with a user at all. Flows with an
|
||||
/// interactive and a headless path use this to pick one: a headless run
|
||||
/// takes the defaults or fails with the list of missing answers instead
|
||||
/// of asking question by question.
|
||||
fn interactive(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Ask a yes/no question. `default` is the answer to take when no user
|
||||
/// can be reached or the question is cancelled.
|
||||
fn confirm(&self, _question: &str, default: bool) -> bool {
|
||||
default
|
||||
/// can be reached; `Err` means the question was cancelled (Ctrl+C,
|
||||
/// dropped connection) and the flow should abort.
|
||||
fn confirm(&self, _question: &str, default: bool) -> Result<bool, Box<dyn Error>> {
|
||||
Ok(default)
|
||||
}
|
||||
|
||||
/// Ask a one-line selection among `options`, with `default`
|
||||
/// preselected. Selector implementations may also accept typed
|
||||
/// arbitrary text; callers validate the answer and re-ask through the
|
||||
/// same method when they must reject one. `Err` cancels the flow.
|
||||
fn select(
|
||||
&self,
|
||||
_label: &str,
|
||||
_options: &[String],
|
||||
default: &str,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
Ok(default.to_string())
|
||||
}
|
||||
|
||||
/// Ask a free-text answer. `validate` is applied by the implementation
|
||||
/// so invalid input re-asks at the source (mid-input for a terminal,
|
||||
/// round-trip for a server). `Err` cancels the flow.
|
||||
fn text(
|
||||
&self,
|
||||
_label: &str,
|
||||
default: &str,
|
||||
_validate: Option<&Validator>,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
Ok(default.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,3 +156,24 @@ pub struct Quiet;
|
||||
|
||||
impl BuildView for Quiet {}
|
||||
impl Prompter for Quiet {}
|
||||
|
||||
/// Render a path for terminal display: relative to the current working
|
||||
/// directory when the target lives inside it or directly next to it
|
||||
/// (`../name`, the usual layout of build artifacts), absolute otherwise.
|
||||
///
|
||||
/// Pure formatting shared by the flows that mention paths in their events
|
||||
/// and messages; a remote frontend reproduces it (or not) on its side.
|
||||
pub fn display_path(path: &Path) -> String {
|
||||
let Ok(cwd) = std::env::current_dir() else {
|
||||
return path.display().to_string();
|
||||
};
|
||||
if let Ok(rel) = path.strip_prefix(&cwd) {
|
||||
return rel.display().to_string();
|
||||
}
|
||||
if let Some(parent) = cwd.parent()
|
||||
&& let Ok(rel) = path.strip_prefix(parent)
|
||||
{
|
||||
return format!("../{}", rel.display());
|
||||
}
|
||||
path.display().to_string()
|
||||
}
|
||||
|
||||
@@ -10,27 +10,8 @@ pub mod prompt;
|
||||
#[cfg(test)]
|
||||
use indicatif::ProgressDrawTarget;
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Render a path for terminal display: relative to the current working
|
||||
/// directory when the target lives inside it or directly next to it
|
||||
/// (`../name`, the usual layout of build artifacts), absolute otherwise.
|
||||
pub fn display_path(path: &Path) -> String {
|
||||
let Ok(cwd) = std::env::current_dir() else {
|
||||
return path.display().to_string();
|
||||
};
|
||||
if let Ok(rel) = path.strip_prefix(&cwd) {
|
||||
return rel.display().to_string();
|
||||
}
|
||||
if let Some(parent) = cwd.parent()
|
||||
&& let Ok(rel) = path.strip_prefix(parent)
|
||||
{
|
||||
return format!("../{}", rel.display());
|
||||
}
|
||||
path.display().to_string()
|
||||
}
|
||||
|
||||
/// Style of an unsized operation: spinner and prefix on one line
|
||||
pub(crate) fn spinner_style() -> ProgressStyle {
|
||||
ProgressStyle::default_bar()
|
||||
|
||||
+1
-1
@@ -232,7 +232,7 @@ impl DebUi {
|
||||
if self.shared.enabled && !artifacts.is_empty() {
|
||||
println!("Built in {}s:", elapsed.as_secs());
|
||||
for artifact in artifacts {
|
||||
println!(" {}", crate::ui::display_path(artifact));
|
||||
println!(" {}", crate::report::display_path(artifact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-8
@@ -12,7 +12,7 @@ use crossterm::{
|
||||
style::{self, Color, Print, SetForegroundColor},
|
||||
terminal,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Why a prompt could not run interactively
|
||||
@@ -32,9 +32,8 @@ impl std::fmt::Display for PromptError {
|
||||
|
||||
impl std::error::Error for PromptError {}
|
||||
|
||||
/// Answer validator of [`text`]: accepts the answer, or explains why it is
|
||||
/// rejected
|
||||
pub type Validator = dyn Fn(&str) -> Result<(), String>;
|
||||
/// Answer validator of [`text`] (re-exported from [`crate::report`])
|
||||
pub use crate::report::Validator;
|
||||
|
||||
/// What a prompt's event loop asks its drawing helper to render; keeping all
|
||||
/// terminal access behind this callback makes the loops pure logic that unit
|
||||
@@ -128,13 +127,37 @@ pub fn confirm(label: &str, default: bool) -> Result<bool, Box<dyn std::error::E
|
||||
}
|
||||
|
||||
/// [`crate::report::Prompter`] answered by the interactive terminal: each
|
||||
/// question runs the matching raw-mode prompt, falling back to the default
|
||||
/// answer when no interactive terminal is attached (CI, piped input).
|
||||
/// question runs the matching raw-mode prompt. Without an interactive
|
||||
/// terminal (CI, piped input) [`Prompter::interactive`] is false — flows
|
||||
/// then take their headless path instead of asking — and a cancel (Ctrl+C)
|
||||
/// propagates as `Err` so flows can abort.
|
||||
pub struct TerminalPrompter;
|
||||
|
||||
impl crate::report::Prompter for TerminalPrompter {
|
||||
fn confirm(&self, question: &str, default: bool) -> bool {
|
||||
confirm(question, default).unwrap_or(default)
|
||||
fn interactive(&self) -> bool {
|
||||
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
fn confirm(&self, question: &str, default: bool) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
confirm(question, default)
|
||||
}
|
||||
|
||||
fn select(
|
||||
&self,
|
||||
label: &str,
|
||||
options: &[String],
|
||||
default: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
select(label, options, default)
|
||||
}
|
||||
|
||||
fn text(
|
||||
&self,
|
||||
label: &str,
|
||||
default: &str,
|
||||
validate: Option<&Validator>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
text(label, default, validate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user