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 fallback = use_cwd.then(|| launch_dir.lock().clone());
110 if let Some(snapshot) =
111 snapshot_for(disks.list(), &target, fallback.as_deref())
112 {
113 *data.lock() = snapshot;
114 }
115
116 let deadline = Instant::now() + interval;
117 while Instant::now() < deadline && running.load(Ordering::Relaxed) {
118 std::thread::sleep(Duration::from_millis(50));
119 }
120 }
121 });
122
123 match handle {
124 Ok(h) => *self.thread.lock() = Some(h),
125 Err(e) => {
126 self.running.store(false, Ordering::SeqCst);
129 crate::debug_error!(
130 "SESSION_LOGGER",
131 "failed to spawn disk monitor thread: {:?}",
132 e
133 );
134 }
135 }
136 }
137
138 pub fn signal_stop(&self) {
140 self.running.store(false, Ordering::SeqCst);
141 }
142
143 pub fn stop(&self) {
145 self.signal_stop();
146 if let Some(handle) = self.thread.lock().take() {
147 let _ = handle.join();
148 }
149 }
150
151 pub fn is_running(&self) -> bool {
153 self.running.load(Ordering::SeqCst)
154 }
155
156 pub fn data(&self) -> DiskMonitorData {
158 self.data.lock().clone()
159 }
160
161 pub fn set_cwd(&self, new_cwd: Option<&str>) {
163 *self.cwd.lock() = new_cwd.map(String::from);
164 }
165
166 pub fn set_follow_cwd(&self, follow: bool) {
168 *self.follow_cwd.lock() = follow;
169 }
170 }
171
172 impl Default for DiskMonitor {
173 fn default() -> Self {
174 Self::new()
175 }
176 }
177
178 impl Drop for DiskMonitor {
179 fn drop(&mut self) {
180 self.stop();
181 }
182 }
183
184 pub fn snapshot_for(
187 disks: &[Disk],
188 target: &Path,
189 fallback: Option<&Path>,
190 ) -> Option<DiskMonitorData> {
191 let disk = disk_for_path(disks, target).or_else(|| disk_for_path(disks, fallback?))?;
192 let total = disk.total_space();
193 let free = disk.available_space();
194 let free_percent = if total > 0 {
195 (free as f64 / total as f64 * 100.0) as f32
196 } else {
197 0.0
198 };
199 Some(DiskMonitorData {
200 free_bytes: free,
201 total_bytes: total,
202 free_percent,
203 last_update: Some(Instant::now()),
204 })
205 }
206
207 pub fn disk_for_path<'a>(disks: &'a [Disk], target: &Path) -> Option<&'a Disk> {
212 disks
213 .iter()
214 .filter(|d| target.starts_with(d.mount_point()))
215 .max_by_key(|d| d.mount_point().components().count())
216 }
217}
218
219#[cfg(feature = "system-monitor")]
220pub use inner::{DiskMonitor, disk_for_path, snapshot_for};
221
222#[cfg(not(feature = "system-monitor"))]
227mod inner {
228 use super::DiskMonitorData;
229
230 pub struct DiskMonitor;
232
233 impl DiskMonitor {
234 pub fn new() -> Self {
235 Self
236 }
237 pub fn start(&self, _poll_interval_secs: f32) {}
238 pub fn signal_stop(&self) {}
239 pub fn stop(&self) {}
240 pub fn is_running(&self) -> bool {
241 false
242 }
243 pub fn data(&self) -> DiskMonitorData {
244 DiskMonitorData::default()
245 }
246 pub fn set_cwd(&self, _new_cwd: Option<&str>) {}
247 pub fn set_follow_cwd(&self, _follow: bool) {}
248 }
249
250 impl Default for DiskMonitor {
251 fn default() -> Self {
252 Self::new()
253 }
254 }
255}
256
257#[cfg(not(feature = "system-monitor"))]
258pub use inner::DiskMonitor;
259
260#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_disk_monitor_data_default() {
270 let d = DiskMonitorData::default();
271 assert_eq!(d.free_bytes, 0);
272 assert_eq!(d.total_bytes, 0);
273 assert_eq!(d.free_percent, 0.0);
274 assert!(d.last_update.is_none());
275 }
276
277 #[cfg(feature = "system-monitor")]
278 #[test]
279 fn test_disk_for_path_resolves() {
280 use sysinfo::Disks;
281 let disks = Disks::new_with_refreshed_list();
282 let tmp = std::env::temp_dir();
284 let d = disk_for_path(disks.list(), &tmp);
285 assert!(d.is_some(), "no disk matched temp dir {:?}", tmp);
286 let d = d.unwrap();
287 assert!(d.total_space() > 0);
288 assert!(d.available_space() <= d.total_space());
289 }
290
291 #[cfg(feature = "system-monitor")]
292 #[test]
293 fn test_disk_for_path_returns_mount_prefix() {
294 use sysinfo::Disks;
295 let disks = Disks::new_with_refreshed_list();
296 if disks.list().is_empty() {
297 return; }
299 let target = std::env::temp_dir();
304 let matched = disk_for_path(disks.list(), &target);
305 assert!(matched.is_some(), "no disk matched temp dir {target:?}");
306 assert!(
307 target.starts_with(matched.unwrap().mount_point()),
308 "matched mount point must be a prefix of the target path"
309 );
310 }
311
312 #[cfg(feature = "system-monitor")]
313 #[test]
314 fn test_snapshot_for_target_and_fallback() {
315 use sysinfo::Disks;
316 let disks = Disks::new_with_refreshed_list();
317 let tmp = std::env::temp_dir();
318
319 let s = snapshot_for(disks.list(), &tmp, None).expect("temp dir resolves to a disk");
320 assert!(s.total_bytes > 0 && s.free_bytes <= s.total_bytes);
321 assert!(s.free_percent >= 0.0 && s.free_percent <= 100.0);
322 assert!(s.last_update.is_some());
323
324 let unmatched = std::path::Path::new("relative/nowhere");
327 assert!(snapshot_for(disks.list(), unmatched, None).is_none());
328 assert!(snapshot_for(disks.list(), unmatched, Some(&tmp)).is_some());
329 }
330
331 #[cfg(feature = "system-monitor")]
336 #[test]
337 fn test_disk_monitor_start_stop() {
338 let monitor = DiskMonitor::new();
339 assert!(!monitor.is_running());
340 monitor.start(5.0);
341 assert!(monitor.is_running());
342 monitor.stop();
343 assert!(!monitor.is_running());
344 }
345}