par_term/status_bar/
disk_monitor.rs1use std::time::Instant;
9
10#[derive(Debug, Clone, Default)]
12pub struct DiskMonitorData {
13 pub free_bytes: u64,
15 pub total_bytes: u64,
17 pub free_percent: f32,
19 pub last_update: Option<Instant>,
21}
22
23#[cfg(feature = "system-monitor")]
28mod inner {
29 use std::path::{Path, PathBuf};
30 use std::sync::Arc;
31 use std::sync::atomic::{AtomicBool, Ordering};
32 use std::thread::JoinHandle;
33 use std::time::{Duration, Instant};
34
35 use parking_lot::Mutex;
36 use sysinfo::{Disk, DiskRefreshKind, Disks};
37
38 use super::DiskMonitorData;
39
40 pub struct DiskMonitor {
47 data: Arc<Mutex<DiskMonitorData>>,
48 cwd: Arc<Mutex<Option<String>>>,
49 follow_cwd: Arc<Mutex<bool>>,
50 launch_dir: Arc<Mutex<PathBuf>>,
51 running: Arc<AtomicBool>,
52 thread: Mutex<Option<JoinHandle<()>>>,
53 }
54
55 impl DiskMonitor {
56 pub fn new() -> Self {
61 let launch_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
62 Self {
63 data: Arc::new(Mutex::new(DiskMonitorData::default())),
64 cwd: Arc::new(Mutex::new(None)),
65 follow_cwd: Arc::new(Mutex::new(false)),
66 launch_dir: Arc::new(Mutex::new(launch_dir)),
67 running: Arc::new(AtomicBool::new(false)),
68 thread: Mutex::new(None),
69 }
70 }
71
72 pub fn start(&self, poll_interval_secs: f32) {
74 if self
75 .running
76 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
77 .is_err()
78 {
79 return;
80 }
81
82 let data = Arc::clone(&self.data);
83 let cwd = Arc::clone(&self.cwd);
84 let follow_cwd = Arc::clone(&self.follow_cwd);
85 let launch_dir = Arc::clone(&self.launch_dir);
86 let running = Arc::clone(&self.running);
87 let interval = Duration::from_secs_f32(poll_interval_secs.max(5.0));
88
89 let handle = std::thread::Builder::new()
90 .name("status-bar-disk".to_string())
91 .spawn(move || {
92 let mut disks = Disks::new_with_refreshed_list();
93 while running.load(Ordering::SeqCst) {
94 disks.refresh_specifics(true, DiskRefreshKind::nothing().with_storage());
95
96 let use_cwd = *follow_cwd.lock();
97 let target: PathBuf = if use_cwd {
98 cwd.lock()
99 .clone()
100 .filter(|c| !c.is_empty())
101 .map(PathBuf::from)
102 .unwrap_or_else(|| launch_dir.lock().clone())
103 } else {
104 launch_dir.lock().clone()
105 };
106
107 let mut disk = disk_for_path(disks.list(), &target);
111 if disk.is_none() && use_cwd {
112 disk = disk_for_path(disks.list(), &launch_dir.lock());
113 }
114
115 if let Some(d) = disk {
116 let total = d.total_space();
117 let free = d.available_space();
118 let pct = if total > 0 {
119 (free as f64 / total as f64 * 100.0) as f32
120 } else {
121 0.0
122 };
123 let mut dat = data.lock();
124 dat.free_bytes = free;
125 dat.total_bytes = total;
126 dat.free_percent = pct;
127 dat.last_update = Some(Instant::now());
128 }
129
130 let deadline = Instant::now() + interval;
131 while Instant::now() < deadline && running.load(Ordering::Relaxed) {
132 std::thread::sleep(Duration::from_millis(50));
133 }
134 }
135 });
136
137 match handle {
138 Ok(h) => *self.thread.lock() = Some(h),
139 Err(e) => {
140 self.running.store(false, Ordering::SeqCst);
143 crate::debug_error!(
144 "SESSION_LOGGER",
145 "failed to spawn disk monitor thread: {:?}",
146 e
147 );
148 }
149 }
150 }
151
152 pub fn signal_stop(&self) {
154 self.running.store(false, Ordering::SeqCst);
155 }
156
157 pub fn stop(&self) {
159 self.signal_stop();
160 if let Some(handle) = self.thread.lock().take() {
161 let _ = handle.join();
162 }
163 }
164
165 pub fn is_running(&self) -> bool {
167 self.running.load(Ordering::SeqCst)
168 }
169
170 pub fn data(&self) -> DiskMonitorData {
172 self.data.lock().clone()
173 }
174
175 pub fn set_cwd(&self, new_cwd: Option<&str>) {
177 *self.cwd.lock() = new_cwd.map(String::from);
178 }
179
180 pub fn set_follow_cwd(&self, follow: bool) {
182 *self.follow_cwd.lock() = follow;
183 }
184 }
185
186 impl Default for DiskMonitor {
187 fn default() -> Self {
188 Self::new()
189 }
190 }
191
192 impl Drop for DiskMonitor {
193 fn drop(&mut self) {
194 self.stop();
195 }
196 }
197
198 pub fn disk_for_path<'a>(disks: &'a [Disk], target: &Path) -> Option<&'a Disk> {
203 disks
204 .iter()
205 .filter(|d| target.starts_with(d.mount_point()))
206 .max_by_key(|d| d.mount_point().components().count())
207 }
208}
209
210#[cfg(feature = "system-monitor")]
211pub use inner::{DiskMonitor, disk_for_path};
212
213#[cfg(not(feature = "system-monitor"))]
218mod inner {
219 use super::DiskMonitorData;
220
221 pub struct DiskMonitor;
223
224 impl DiskMonitor {
225 pub fn new() -> Self {
226 Self
227 }
228 pub fn start(&self, _poll_interval_secs: f32) {}
229 pub fn signal_stop(&self) {}
230 pub fn stop(&self) {}
231 pub fn is_running(&self) -> bool {
232 false
233 }
234 pub fn data(&self) -> DiskMonitorData {
235 DiskMonitorData::default()
236 }
237 pub fn set_cwd(&self, _new_cwd: Option<&str>) {}
238 pub fn set_follow_cwd(&self, _follow: bool) {}
239 }
240
241 impl Default for DiskMonitor {
242 fn default() -> Self {
243 Self::new()
244 }
245 }
246}
247
248#[cfg(not(feature = "system-monitor"))]
249pub use inner::DiskMonitor;
250
251#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_disk_monitor_data_default() {
261 let d = DiskMonitorData::default();
262 assert_eq!(d.free_bytes, 0);
263 assert_eq!(d.total_bytes, 0);
264 assert_eq!(d.free_percent, 0.0);
265 assert!(d.last_update.is_none());
266 }
267
268 #[cfg(feature = "system-monitor")]
269 #[test]
270 fn test_disk_for_path_resolves() {
271 use sysinfo::Disks;
272 let disks = Disks::new_with_refreshed_list();
273 let tmp = std::env::temp_dir();
275 let d = disk_for_path(disks.list(), &tmp);
276 assert!(d.is_some(), "no disk matched temp dir {:?}", tmp);
277 let d = d.unwrap();
278 assert!(d.total_space() > 0);
279 assert!(d.available_space() <= d.total_space());
280 }
281
282 #[cfg(feature = "system-monitor")]
283 #[test]
284 fn test_disk_for_path_returns_mount_prefix() {
285 use sysinfo::Disks;
286 let disks = Disks::new_with_refreshed_list();
287 if disks.list().is_empty() {
288 return; }
290 let target = std::env::temp_dir();
295 let matched = disk_for_path(disks.list(), &target);
296 assert!(matched.is_some(), "no disk matched temp dir {target:?}");
297 assert!(
298 target.starts_with(matched.unwrap().mount_point()),
299 "matched mount point must be a prefix of the target path"
300 );
301 }
302
303 #[cfg(feature = "system-monitor")]
304 #[test]
305 fn test_disk_monitor_start_stop() {
306 use std::time::Duration;
307 let monitor = DiskMonitor::new();
308 assert!(!monitor.is_running());
309 monitor.start(5.0);
310 assert!(monitor.is_running());
311 let deadline = std::time::Instant::now() + Duration::from_secs(10);
313 let mut polled = false;
314 while std::time::Instant::now() < deadline {
315 if monitor.data().last_update.is_some() {
316 polled = true;
317 break;
318 }
319 std::thread::sleep(Duration::from_millis(50));
320 }
321 assert!(polled, "disk monitor recorded no initial poll within 10s");
322 let d = monitor.data();
324 assert!(d.free_percent >= 0.0 && d.free_percent <= 100.0);
325 monitor.stop();
326 assert!(!monitor.is_running());
327 }
328}