thoughts_tool/git/
progress.rs1use std::io::Write;
2use std::io::{self};
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5use std::sync::atomic::AtomicUsize;
6use std::sync::atomic::Ordering;
7use std::time::Duration;
8use std::time::Instant;
9
10use gix::progress::Count;
11use gix::progress::NestedProgress;
12use gix::progress::Progress;
13use gix::progress::StepShared;
14use gix::progress::Unit;
15
16#[derive(Clone)]
19pub struct InlineProgress {
20 name: String,
21 state: Arc<State>,
22}
23
24struct State {
25 last_draw: std::sync::Mutex<Option<Instant>>,
26 current: StepShared,
27 max: AtomicUsize,
28 has_max: AtomicBool,
29 finished: AtomicBool,
30}
31
32impl InlineProgress {
33 pub fn new(name: impl Into<String>) -> Self {
34 Self {
35 name: name.into(),
36 state: Arc::new(State {
37 last_draw: std::sync::Mutex::new(None),
38 current: Arc::new(AtomicUsize::new(0)),
39 max: AtomicUsize::new(0),
40 has_max: AtomicBool::new(false),
41 finished: AtomicBool::new(false),
42 }),
43 }
44 }
45
46 fn draw(&self) {
47 let now = Instant::now();
48
49 {
51 let mut last = self.state.last_draw.lock().unwrap();
52 if let Some(last_time) = *last
53 && now.duration_since(last_time) < Duration::from_millis(50)
54 && self.state.has_max.load(Ordering::Relaxed)
55 {
56 return;
57 }
58 *last = Some(now);
59 }
60
61 let current = self.state.current.load(Ordering::Relaxed);
62 let has_max = self.state.has_max.load(Ordering::Relaxed);
63 let max = self.state.max.load(Ordering::Relaxed);
64
65 let mut line = String::new();
66 line.push_str(" ");
67 line.push_str(&self.name);
68 line.push_str(": ");
69
70 if has_max && max > 0 {
71 let pct = (current as f32 / max as f32) * 100.0;
72 line.push_str(&format!("{}/{} ({:.1}%)", current, max, pct));
73 } else {
74 line.push_str(&format!("{}", current));
75 }
76
77 print!("\r{}", line);
78 io::stdout().flush().ok();
79 }
80}
81
82impl Count for InlineProgress {
83 fn set(&self, step: usize) {
84 self.state.current.store(step, Ordering::Relaxed);
85 self.draw();
86 }
87
88 fn step(&self) -> usize {
89 self.state.current.load(Ordering::Relaxed)
90 }
91
92 fn inc_by(&self, step: usize) {
93 self.state.current.fetch_add(step, Ordering::Relaxed);
94 self.draw();
95 }
96
97 fn counter(&self) -> gix::progress::StepShared {
98 self.state.current.clone()
100 }
101}
102
103impl Progress for InlineProgress {
104 fn init(&mut self, max: Option<usize>, _unit: Option<Unit>) {
105 if let Some(m) = max {
106 self.state.max.store(m, Ordering::Relaxed);
107 self.state.has_max.store(true, Ordering::Relaxed);
108 } else {
109 self.state.has_max.store(false, Ordering::Relaxed);
110 }
111 self.state.current.store(0, Ordering::Relaxed);
112 self.state.finished.store(false, Ordering::Relaxed);
113 self.draw();
114 }
115
116 fn set_name(&mut self, _name: String) {
117 }
119
120 fn name(&self) -> Option<String> {
121 Some(self.name.clone())
122 }
123
124 fn id(&self) -> gix::progress::Id {
125 [0u8; 4]
126 }
127
128 fn message(&self, _level: gix::progress::MessageLevel, _message: String) {
129 }
131}
132
133impl NestedProgress for InlineProgress {
134 type SubProgress = InlineProgress;
135
136 fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress {
137 if !self.state.finished.load(Ordering::Relaxed) {
139 println!();
140 }
141 InlineProgress::new(name)
142 }
143
144 fn add_child_with_id(
145 &mut self,
146 name: impl Into<String>,
147 _id: gix::progress::Id,
148 ) -> Self::SubProgress {
149 self.add_child(name)
150 }
151}
152
153impl Drop for InlineProgress {
154 fn drop(&mut self) {
155 if !self.state.finished.swap(true, Ordering::Relaxed) {
157 if self.state.last_draw.lock().unwrap().is_some() {
159 println!();
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn init_and_inc() {
171 let mut p = InlineProgress::new("test");
172 p.init(Some(100), None);
173 p.inc_by(1);
174 p.inc_by(9);
175 p.set(25);
176 }
177
178 #[test]
179 fn nested_children() {
180 let mut p = InlineProgress::new("root");
181 let mut c1 = p.add_child("child-1");
182 c1.init(Some(10), None);
183 c1.inc_by(3);
184 }
185
186 #[test]
187 fn no_max_progress() {
188 let mut p = InlineProgress::new("bytes");
189 p.init(None, None);
190 p.inc_by(100);
191 p.inc_by(200);
192 }
193
194 #[test]
195 fn counter_is_shared() {
196 use std::sync::atomic::Ordering;
197 let p = InlineProgress::new("t");
198 let c = p.counter();
199 c.fetch_add(5, Ordering::Relaxed);
200 assert_eq!(p.step(), 5);
201 }
202}