ui: create the rolling pane lazily, on its first line
DebUi seeded both widgets with a "(starting...)" placeholder, replaced as soon as real content arrived during a build. pkh put reports through the same view but runs no subprocess, so nothing ever fed the pane: its seed line stayed on screen for the whole upload, stacked under the per-file byte progress. Drop the seeds and add the pane bar to the terminal only when the first classified line arrives; a phase change (and the final suspend) takes it off again. Flows without subprocess output now render the status bar alone.
This commit is contained in:
+87
-23
@@ -50,8 +50,11 @@ struct Pipeline {
|
||||
|
||||
/// State shared between [`DebUi`] and its sinks
|
||||
struct Shared {
|
||||
multi: MultiProgress,
|
||||
top: ProgressBar,
|
||||
pane: ProgressBar,
|
||||
/// The rolling pane bar, created on demand: flows without subprocess
|
||||
/// output (e.g. `pkh put`) never show it at all
|
||||
pane: Mutex<Option<ProgressBar>>,
|
||||
state: Mutex<Pipeline>,
|
||||
tee: Mutex<Option<File>>,
|
||||
log_path: Mutex<PathBuf>,
|
||||
@@ -85,34 +88,18 @@ impl DebUi {
|
||||
pb.enable_steady_tick(Duration::from_millis(80));
|
||||
pb.set_style(spinner_style());
|
||||
pb.set_prefix("Building package");
|
||||
pb.set_message("(starting…)");
|
||||
pb
|
||||
} else {
|
||||
ProgressBar::hidden()
|
||||
};
|
||||
|
||||
let pane = if enabled {
|
||||
let pb = multi.add(ProgressBar::new(0));
|
||||
pb.enable_steady_tick(Duration::from_millis(150));
|
||||
// No template margin: multi-line messages are only prefixed by
|
||||
// the template on their first line, which would misalign the
|
||||
// pane; each rendered line carries its own indent instead.
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{msg}")
|
||||
.expect("valid template"),
|
||||
);
|
||||
pb.set_message(" │ (starting…)");
|
||||
pb
|
||||
} else {
|
||||
ProgressBar::hidden()
|
||||
};
|
||||
let pane = Mutex::new(None);
|
||||
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%S").to_string();
|
||||
let log_path = default_log_path(×tamp);
|
||||
|
||||
let ui = Self {
|
||||
shared: Arc::new(Shared {
|
||||
multi: multi.clone(),
|
||||
top,
|
||||
pane,
|
||||
state: Mutex::new(Pipeline {
|
||||
@@ -198,7 +185,7 @@ impl DebUi {
|
||||
if self.shared.enabled {
|
||||
self.shared.top.set_style(spinner_style());
|
||||
self.shared.top.set_message(label.to_string());
|
||||
self.shared.pane.set_message("");
|
||||
drop_pane(&self.shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +208,8 @@ impl DebUi {
|
||||
return;
|
||||
}
|
||||
self.shared.top.disable_steady_tick();
|
||||
self.shared.pane.disable_steady_tick();
|
||||
drop_pane(&self.shared);
|
||||
self.shared.top.finish_and_clear();
|
||||
self.shared.pane.finish_and_clear();
|
||||
}
|
||||
|
||||
/// Success outcome body: clear the widget and print the artifacts,
|
||||
@@ -408,7 +394,44 @@ fn push_line(shared: &Shared, st: &mut Pipeline, kind: Kind, text: String) {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(st.last_draw) >= REDRAW_INTERVAL {
|
||||
st.last_draw = now;
|
||||
shared.pane.set_message(render_pane(&st.lines));
|
||||
if let Some(pane) = ensure_pane(shared) {
|
||||
pane.set_message(render_pane(&st.lines));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pane bar, added to the terminal on the first call and reused after
|
||||
///
|
||||
/// Returns `None` once the widget is suspended: a line racing the suspend
|
||||
/// must not re-add a bar the cleanup just cleared.
|
||||
fn ensure_pane(shared: &Shared) -> Option<ProgressBar> {
|
||||
let mut pane = shared.pane.lock().unwrap();
|
||||
if let Some(pb) = pane.as_ref() {
|
||||
return Some(pb.clone());
|
||||
}
|
||||
if shared.suspended.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
let pb = shared.multi.add(ProgressBar::new(0));
|
||||
pb.enable_steady_tick(Duration::from_millis(150));
|
||||
// No template margin: multi-line messages are only prefixed by the
|
||||
// template on their first line, which would misalign the pane; each
|
||||
// rendered line carries its own indent instead.
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{msg}")
|
||||
.expect("valid template"),
|
||||
);
|
||||
*pane = Some(pb.clone());
|
||||
Some(pb)
|
||||
}
|
||||
|
||||
/// Take the pane bar off the terminal; the next pushed line re-creates it
|
||||
fn drop_pane(shared: &Shared) {
|
||||
if let Some(pb) = shared.pane.lock().unwrap().take() {
|
||||
pb.disable_steady_tick();
|
||||
pb.finish_and_clear();
|
||||
shared.multi.remove(&pb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,6 +661,47 @@ mod tests {
|
||||
assert!(rendered.contains("a line that would overflow a narrow pane"));
|
||||
}
|
||||
|
||||
/// A fresh view has no pane at all: flows without subprocess output
|
||||
/// (`pkh put`) must not render anything until a line arrives, and a
|
||||
/// dropped or suspended pane stays gone
|
||||
#[test]
|
||||
fn pane_is_created_lazily_and_dropped_cleanly() {
|
||||
let multi = MultiProgress::new();
|
||||
let shared = Shared {
|
||||
multi: multi.clone(),
|
||||
top: multi.add(ProgressBar::new(0)),
|
||||
pane: Mutex::new(None),
|
||||
state: Mutex::new(Pipeline {
|
||||
classifier: Box::new(GenericClassifier::new()),
|
||||
lines: VecDeque::new(),
|
||||
errors: Vec::new(),
|
||||
last_draw: Instant::now(),
|
||||
bar_total: 0,
|
||||
}),
|
||||
tee: Mutex::new(None),
|
||||
log_path: Mutex::new(std::env::temp_dir().join("pkh-pane-test.log")),
|
||||
timestamp: String::new(),
|
||||
enabled: true,
|
||||
suspended: AtomicBool::new(false),
|
||||
started: Instant::now(),
|
||||
};
|
||||
|
||||
assert!(shared.pane.lock().unwrap().is_none());
|
||||
|
||||
ensure_pane(&shared).unwrap();
|
||||
assert!(shared.pane.lock().unwrap().is_some());
|
||||
// Later lines hit the stored bar instead of stacking another one
|
||||
ensure_pane(&shared).unwrap();
|
||||
assert!(shared.pane.lock().unwrap().is_some());
|
||||
|
||||
drop_pane(&shared);
|
||||
assert!(shared.pane.lock().unwrap().is_none());
|
||||
|
||||
// A line racing the suspend must not re-add the cleared bar
|
||||
shared.suspended.store(true, Ordering::SeqCst);
|
||||
assert!(ensure_pane(&shared).is_none());
|
||||
}
|
||||
|
||||
/// Best-effort ANSI escape stripper, enough for the assertions above
|
||||
fn strip_ansi(line: &str) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
Reference in New Issue
Block a user