diff --git a/src/put/mod.rs b/src/put/mod.rs index d02289a..8accffa 100644 --- a/src/put/mod.rs +++ b/src/put/mod.rs @@ -75,22 +75,37 @@ pub async fn put( None => discover_changes(&opts.cwd)?, }; let changes = changes::parse(&changes_path)?; - info!( - "Uploading {} {} ({}) to {}", - changes.source, - changes.version, - ui::display_path(&changes_path), - target.label - ); + + // The summary line stays up for the whole flow: the completion message + // replaces it on success, and the `PutBars` guard clears it on every + // early `?` bail so the error logged by main is not preceded by stale + // bars + let mut bars = PutBars::default(); + let summary = multi.add(ProgressBar::new(0)); + // Style and prefix go in before the steady tick: otherwise the first + // tick can render one frame with the default bar template + summary.set_style(ui::spinner_style()); + summary.set_prefix(format!( + "Uploading {} {} to {}", + changes.source, changes.version, target.label + )); + summary.enable_steady_tick(TICK); + bars.track(summary.clone()); changes::validate(&changes)?; // Pre-flight checks for everything the upload queue only rejects after // processing: a valid Section, a known target series, and the target // PPA actually existing (the SFTP queue itself is a blind write) check_control_section(&opts.cwd, "ubuntu")?; - info!("Checking {} on Launchpad...", target.label); + + let checking = multi.add(ProgressBar::new(0)); + checking.set_style(ui::spinner_style()); + checking.set_prefix(format!("Checking {} on Launchpad...", target.label)); + checking.enable_steady_tick(TICK); + bars.track(checking.clone()); launchpad::ppa_info(&opts.ppa).await?; launchpad::check_ppa_series(&changes.distribution).await?; + clear_bar(&checking); // Last pre-flight check: a version the PPA already publishes at or // above the changes' one would supersede (or reject) this upload @@ -108,9 +123,14 @@ pub async fn put( .into()); } - info!("Connecting to {login}@{host}:{port}..."); + let connecting = multi.add(ProgressBar::new(0)); + connecting.set_style(ui::spinner_style()); + connecting.set_prefix(format!("Connecting to {login}@{host}:{port}...")); + connecting.enable_steady_tick(TICK); + bars.track(connecting.clone()); let session = ssh::connect(&host, port, &login, &ssh_config)?; let sftp = ssh::sftp(&session)?; + clear_bar(&connecting); // Payload first, the .changes file last (like dput), so the server-side // queue processor can never pick up an incomplete upload @@ -146,14 +166,55 @@ pub async fn put( record_upload(&upload_log_path()?, &record)?; + // The completion lines replace the summary bar; the guard's own clear + // at scope exit is a no-op for the already-finished bars + clear_bar(&summary); info!( - "Upload of {changes_name} to {} complete. Launchpad processes it asynchronously; \ - watch the PPA page or your inbox for acceptance/rejection", - target.label + "Upload of {} {} to {} complete.", + changes.source, changes.version, target.label + ); + info!( + "Launchpad processes it asynchronously; watch the PPA page or your \ + inbox for acceptance/rejection" ); Ok(()) } +/// Steady tick interval of every bar rendered by a `put` run +const TICK: std::time::Duration = std::time::Duration::from_millis(50); + +/// Stop `bar`'s steady tick and clear it from the terminal. The tick is +/// disabled first: a tick firing right after the clear would redraw a stale +/// frame, the race [`crate::ui::deb::DebUi::suspend`] guards against too. +/// Clearing twice is harmless: finished bars stay finished. +fn clear_bar(bar: &ProgressBar) { + bar.disable_steady_tick(); + bar.finish_and_clear(); +} + +/// The bars rendered by one `put` run, cleared when the guard drops. The +/// error paths bail out early through `?` and `main` logs the error +/// afterwards, so a bar left unfinished would linger on screen above it. +#[derive(Default)] +struct PutBars { + bars: Vec, +} + +impl PutBars { + /// Track `bar` so it is cleared with the rest when the guard drops + fn track(&mut self, bar: ProgressBar) { + self.bars.push(bar); + } +} + +impl Drop for PutBars { + fn drop(&mut self) { + for bar in &self.bars { + clear_bar(bar); + } + } +} + /// Validate the source package's `Section` (debian/control source stanza) /// against the distribution's valid sections: a bare section or a /// `section/subsection` is accepted. Archives reject uploads carrying an