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:
+5
-2
@@ -170,10 +170,13 @@ fn retry_after_revendor(
|
|||||||
original: Box<dyn Error>,
|
original: Box<dyn Error>,
|
||||||
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
) -> Result<SourceBuildOutput, Box<dyn Error>> {
|
||||||
log::error!("{original}");
|
log::error!("{original}");
|
||||||
if !prompter.confirm(
|
if !prompter
|
||||||
|
.confirm(
|
||||||
"Re-vendor the Cargo dependencies and retry the build?",
|
"Re-vendor the Cargo dependencies and retry the build?",
|
||||||
false,
|
false,
|
||||||
) {
|
)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
view.finish_failure();
|
view.finish_failure();
|
||||||
return Err(original);
|
return Err(original);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -518,7 +518,7 @@ fn main() {
|
|||||||
// print them as plain lines.
|
// print them as plain lines.
|
||||||
if !view.is_enabled() {
|
if !view.is_enabled() {
|
||||||
for artifact in output.artifacts() {
|
for artifact in output.artifacts() {
|
||||||
println!(" {}", pkh::ui::display_path(&artifact));
|
println!(" {}", pkh::report::display_path(&artifact));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if output.signed {
|
if output.signed {
|
||||||
|
|||||||
+1
-1
@@ -436,7 +436,7 @@ pub fn create_orig_tarball_excluding(
|
|||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created orig tarball {}",
|
"Created orig tarball {}",
|
||||||
crate::ui::display_path(&tarball_path)
|
crate::report::display_path(&tarball_path)
|
||||||
);
|
);
|
||||||
Ok(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
|
// `display_path` yields an empty string when the target is the cwd
|
||||||
// itself (Here mode): `Created .` would be cryptic, so spell the
|
// itself (Here mode): `Created .` would be cryptic, so spell the
|
||||||
// location out; the skeleton/path modes keep the `<dir>` display.
|
// 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() {
|
let location = if display.is_empty() {
|
||||||
"package in the current directory".to_string()
|
"package in the current directory".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+4
-4
@@ -183,7 +183,7 @@ pub fn create_vendor_component(
|
|||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created vendored-dependencies component {}",
|
"Created vendored-dependencies component {}",
|
||||||
crate::ui::display_path(&component_path)
|
crate::report::display_path(&component_path)
|
||||||
);
|
);
|
||||||
Ok(component_path)
|
Ok(component_path)
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ fn git_archive_tarball(
|
|||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created orig tarball from git archive of {tag}: {}",
|
"Created orig tarball from git archive of {tag}: {}",
|
||||||
crate::ui::display_path(&dest)
|
crate::report::display_path(&dest)
|
||||||
);
|
);
|
||||||
Ok(dest)
|
Ok(dest)
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ fn download_release(
|
|||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created orig tarball from the release download of {tag}: {}",
|
"Created orig tarball from the release download of {tag}: {}",
|
||||||
crate::ui::display_path(&path)
|
crate::report::display_path(&path)
|
||||||
);
|
);
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ fn fetch_and_repack(
|
|||||||
log::info!(
|
log::info!(
|
||||||
"Created orig tarball from {}: {}",
|
"Created orig tarball from {}: {}",
|
||||||
source,
|
source,
|
||||||
crate::ui::display_path(&dest)
|
crate::report::display_path(&dest)
|
||||||
);
|
);
|
||||||
Ok(dest)
|
Ok(dest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ pub async fn offer_verification(
|
|||||||
no_verify: bool,
|
no_verify: bool,
|
||||||
) {
|
) {
|
||||||
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::ui::display_path(&tree);
|
let display = crate::report::display_path(&tree);
|
||||||
let display = if display.is_empty() {
|
let display = if display.is_empty() {
|
||||||
".".to_string()
|
".".to_string()
|
||||||
} else {
|
} 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",
|
Pass the one to upload explicitly",
|
||||||
entry.source,
|
entry.source,
|
||||||
many.iter()
|
many.iter()
|
||||||
.map(|p| ui::display_path(p))
|
.map(|p| crate::report::display_path(p))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)
|
)
|
||||||
|
|||||||
+67
-6
@@ -14,13 +14,20 @@
|
|||||||
//! implementations used by headless runs and tests. [`Prompter`] is
|
//! implementations used by headless runs and tests. [`Prompter`] is
|
||||||
//! deliberately blocking: an implementation may round-trip each question to
|
//! deliberately blocking: an implementation may round-trip each question to
|
||||||
//! a remote user, as long as it eventually answers (or takes the default).
|
//! 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 std::sync::Arc;
|
||||||
|
|
||||||
use crate::context::LineSink;
|
use crate::context::LineSink;
|
||||||
use crate::logfmt::Classifier;
|
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
|
/// Identity of the build whose events follow, as announced through
|
||||||
/// [`BuildView::target`].
|
/// [`BuildView::target`].
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[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
|
/// Answerer of the questions a core flow may ask mid-run.
|
||||||
/// now, selections and free-text input as more flows migrate).
|
|
||||||
///
|
///
|
||||||
/// Questions are blocking on purpose: a terminal implementation waits for
|
/// Questions are blocking on purpose: a terminal implementation waits for
|
||||||
/// key presses, and a builder-server implementation may forward the question
|
/// key presses, and a builder-server implementation may forward the question
|
||||||
/// to a web client and await the answer on a channel. Implementations that
|
/// to a web client and await the answer on a channel. Implementations that
|
||||||
/// cannot ask anyone answer with the question's default.
|
/// cannot ask anyone answer with the question's default.
|
||||||
pub trait Prompter: Send + Sync {
|
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
|
/// Ask a yes/no question. `default` is the answer to take when no user
|
||||||
/// can be reached or the question is cancelled.
|
/// can be reached; `Err` means the question was cancelled (Ctrl+C,
|
||||||
fn confirm(&self, _question: &str, default: bool) -> bool {
|
/// dropped connection) and the flow should abort.
|
||||||
default
|
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 BuildView for Quiet {}
|
||||||
impl Prompter 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)]
|
#[cfg(test)]
|
||||||
use indicatif::ProgressDrawTarget;
|
use indicatif::ProgressDrawTarget;
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use std::path::Path;
|
|
||||||
use std::time::Duration;
|
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
|
/// Style of an unsized operation: spinner and prefix on one line
|
||||||
pub(crate) fn spinner_style() -> ProgressStyle {
|
pub(crate) fn spinner_style() -> ProgressStyle {
|
||||||
ProgressStyle::default_bar()
|
ProgressStyle::default_bar()
|
||||||
|
|||||||
+1
-1
@@ -232,7 +232,7 @@ impl DebUi {
|
|||||||
if self.shared.enabled && !artifacts.is_empty() {
|
if self.shared.enabled && !artifacts.is_empty() {
|
||||||
println!("Built in {}s:", elapsed.as_secs());
|
println!("Built in {}s:", elapsed.as_secs());
|
||||||
for artifact in artifacts {
|
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},
|
style::{self, Color, Print, SetForegroundColor},
|
||||||
terminal,
|
terminal,
|
||||||
};
|
};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, IsTerminal, Write};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Why a prompt could not run interactively
|
/// Why a prompt could not run interactively
|
||||||
@@ -32,9 +32,8 @@ impl std::fmt::Display for PromptError {
|
|||||||
|
|
||||||
impl std::error::Error for PromptError {}
|
impl std::error::Error for PromptError {}
|
||||||
|
|
||||||
/// Answer validator of [`text`]: accepts the answer, or explains why it is
|
/// Answer validator of [`text`] (re-exported from [`crate::report`])
|
||||||
/// rejected
|
pub use crate::report::Validator;
|
||||||
pub type Validator = dyn Fn(&str) -> Result<(), String>;
|
|
||||||
|
|
||||||
/// What a prompt's event loop asks its drawing helper to render; keeping all
|
/// 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
|
/// 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
|
/// [`crate::report::Prompter`] answered by the interactive terminal: each
|
||||||
/// question runs the matching raw-mode prompt, falling back to the default
|
/// question runs the matching raw-mode prompt. Without an interactive
|
||||||
/// answer when no interactive terminal is attached (CI, piped input).
|
/// 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;
|
pub struct TerminalPrompter;
|
||||||
|
|
||||||
impl crate::report::Prompter for TerminalPrompter {
|
impl crate::report::Prompter for TerminalPrompter {
|
||||||
fn confirm(&self, question: &str, default: bool) -> bool {
|
fn interactive(&self) -> bool {
|
||||||
confirm(question, default).unwrap_or(default)
|
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