Skip to main content

grit_lib/
progress.rs

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