1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Progress reporting and cancellation across the library/CLI boundary.
//!
//! Library operations never touch the terminal. Long-running work reports
//! progress through a [`ProgressSink`] supplied by the caller and checks a
//! [`Cancel`] signal at loop boundaries. The CLI provides a concrete sink that
//! draws to stderr (gated on `isatty`) and decides colour, redraw rate, and
//! whether a pager is attached; the library makes none of those decisions.
//!
//! Tests, `grit-simple`, and any call site that does not want output use the
//! no-op [`NullProgress`] / [`NeverCancel`].
/// A sink for progress updates emitted by a library operation.
///
/// Every method has a no-op default, so a sink implements only what it needs.
/// The library calls these; it never decides whether output is a tty, what
/// colour to use, or how often to redraw — that is the CLI's responsibility.
pub trait ProgressSink {
/// Begin a new progress phase labelled `label`, optionally with a known
/// total count of units (e.g. objects). `None` means the total is unknown.
fn start(&mut self, label: &str, total: Option<u64>) {
let _ = (label, total);
}
/// Advance the current phase by `units`.
fn inc(&mut self, units: u64) {
let _ = units;
}
/// Set the absolute progress of the current phase to `current` units.
fn set(&mut self, current: u64) {
let _ = current;
}
/// Emit an out-of-band human-readable message (e.g. a remote sideband line
/// or `"Trying merge strategy ..."`).
fn message(&mut self, msg: &str) {
let _ = msg;
}
/// Finish the current phase.
fn finish(&mut self) {}
}
/// Forwarding impl so a `&mut P` can be threaded into sub-operations that also
/// take `impl ProgressSink` without re-borrowing gymnastics at every call site.
impl<T: ProgressSink + ?Sized> ProgressSink for &mut T {
fn start(&mut self, label: &str, total: Option<u64>) {
(**self).start(label, total);
}
fn inc(&mut self, units: u64) {
(**self).inc(units);
}
fn set(&mut self, current: u64) {
(**self).set(current);
}
fn message(&mut self, msg: &str) {
(**self).message(msg);
}
fn finish(&mut self) {
(**self).finish();
}
}
/// A [`ProgressSink`] that discards every update. The default for tests,
/// `grit-simple`, and any call site that does not want progress output.
#[derive(Debug, Default, Clone, Copy)]
pub struct NullProgress;
impl ProgressSink for NullProgress {}
/// A cancellation signal checked by long-running library operations at loop
/// boundaries. Implementations must be cheap to query.
pub trait Cancel {
/// Returns `true` once the operation should stop as soon as possible.
fn is_cancelled(&self) -> bool;
}
/// A [`Cancel`] that never signals cancellation. The default for call sites
/// that do not support interruption.
#[derive(Debug, Default, Clone, Copy)]
pub struct NeverCancel;
impl Cancel for NeverCancel {
fn is_cancelled(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A sink that records what it was told, to prove the trait + forwarding
/// impl route calls correctly.
#[derive(Default)]
struct Recorder {
started: Vec<(String, Option<u64>)>,
incs: u64,
messages: Vec<String>,
finished: usize,
}
impl ProgressSink for Recorder {
fn start(&mut self, label: &str, total: Option<u64>) {
self.started.push((label.to_string(), total));
}
fn inc(&mut self, units: u64) {
self.incs += units;
}
fn message(&mut self, msg: &str) {
self.messages.push(msg.to_string());
}
fn finish(&mut self) {
self.finished += 1;
}
}
fn drive(sink: &mut impl ProgressSink) {
sink.start("objects", Some(3));
sink.inc(1);
sink.inc(2);
sink.message("done");
sink.finish();
}
#[test]
fn null_progress_is_inert() {
// Must compile and run without panicking; nothing to assert.
drive(&mut NullProgress);
}
#[test]
fn forwarding_impl_routes_to_inner() {
let mut rec = Recorder::default();
// Pass `&mut Recorder` where `impl ProgressSink` is expected, exercising
// the `&mut T` forwarding impl.
drive(&mut &mut rec);
assert_eq!(rec.started, vec![("objects".to_string(), Some(3))]);
assert_eq!(rec.incs, 3);
assert_eq!(rec.messages, vec!["done".to_string()]);
assert_eq!(rec.finished, 1);
}
#[test]
fn never_cancel_never_cancels() {
assert!(!NeverCancel.is_cancelled());
}
}