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