leenfetch_core/modules/linux/info/
disk.rs1use std::fs;
2
3use crate::modules::{
4 enums::{DiskDisplayPart, DiskSubtitle, parse_disk_display_parts},
5 utils::get_bar,
6};
7
8pub fn get_disks(
9 subtitle_mode: DiskSubtitle,
10 display_mode: &str,
11 paths: Option<Vec<&str>>,
12) -> Option<Vec<(String, String)>> {
13 let mount_points = if let Some(ref user_paths) = paths {
15 user_paths.iter().map(|s| s.to_string()).collect()
16 } else {
17 get_default_mount_points()
19 };
20
21 let mut results = Vec::new();
22
23 for mount_point in mount_points {
24 if let Some(disk_info) = get_disk_info_for_path(&mount_point, subtitle_mode, &display_mode)
25 {
26 results.push(disk_info);
27 }
28 }
29
30 if results.is_empty() {
31 return None;
32 }
33
34 Some(results)
35}
36
37fn get_default_mount_points() -> Vec<String> {
38 let mut points: Vec<(String, String)> = Vec::new();
39
40 if let Ok(content) = fs::read_to_string("/proc/mounts") {
42 for line in content.lines() {
43 let parts: Vec<&str> = line.split_whitespace().collect();
44 if parts.len() >= 2 {
45 let mount = parts[0]; let mount_point = parts[1];
47
48 if mount_point == "/" || is_visible_storage_mount(mount_point) {
49 push_preferred_mount(&mut points, mount, mount_point);
50 }
51 }
52 }
53 }
54
55 points.into_iter().map(|(_, mount)| mount).take(5).collect()
57}
58
59fn get_disk_info_for_path(
60 path: &str,
61 subtitle_mode: DiskSubtitle,
62 display_mode: &str,
63) -> Option<(String, String)> {
64 let path_cstr = std::ffi::CString::new(path).ok()?;
65
66 let mut statfs: libc::statvfs = unsafe { std::mem::zeroed() };
68
69 if unsafe { libc::statvfs(path_cstr.as_ptr(), &mut statfs) } != 0 {
70 return None;
71 }
72
73 let total = statfs.f_blocks as u64 * statfs.f_frsize as u64;
75 let available = statfs.f_bavail as u64 * statfs.f_frsize as u64;
76 let used = total.saturating_sub(available);
77
78 if total == 0 {
79 return None;
80 }
81
82 let percent = ((used as f64 / total as f64) * 100.0)
83 .round()
84 .clamp(0.0, 100.0) as u8;
85
86 let total_h = format_size(total);
88 let used_h = format_size(used);
89
90 let usage_display = format!("{} / {}", used_h, total_h);
91 let perc_val = percent.min(100);
92
93 let final_str = render_disk_display(display_mode, &usage_display, percent, perc_val);
94
95 let subtitle = match subtitle_mode {
97 DiskSubtitle::Name => get_device_name(path),
98 DiskSubtitle::Dir => mount_dir_label(path),
99 DiskSubtitle::None => "".to_string(),
100 DiskSubtitle::Mount => path.to_string(),
101 };
102
103 Some((subtitle, final_str))
104}
105
106fn render_disk_display(spec: &str, usage_display: &str, percent: u8, perc_val: u8) -> String {
107 let parts = parse_disk_display_parts(spec).unwrap_or_else(|_| vec![DiskDisplayPart::Info]);
108
109 let mut rendered = Vec::new();
110 for part in parts {
111 match part {
112 DiskDisplayPart::Info => rendered.push(usage_display.to_string()),
113 DiskDisplayPart::Percentage => rendered.push(format!("{}%", percent)),
114 DiskDisplayPart::Bar => rendered.push(get_bar(perc_val)),
115 }
116 }
117
118 rendered.join(" ")
119}
120
121fn get_device_name(path: &str) -> String {
122 if let Ok(content) = fs::read_to_string("/proc/mounts") {
123 for line in content.lines() {
124 let parts: Vec<&str> = line.split_whitespace().collect();
125 if parts.len() >= 2 && parts[1] == path {
126 return parts[0].to_string();
127 }
128 }
129 }
130 "".to_string()
131}
132
133fn is_visible_storage_mount(mount_point: &str) -> bool {
134 matches!(
135 mount_point,
136 "/home" | "/data" | "/mnt" | "/media" | "/run/media"
137 ) || mount_point.starts_with("/home/")
138 || mount_point.starts_with("/data/")
139 || mount_point.starts_with("/mnt/")
140 || mount_point.starts_with("/media/")
141 || mount_point.starts_with("/run/media/")
142}
143
144fn push_preferred_mount(points: &mut Vec<(String, String)>, device: &str, mount_point: &str) {
145 let candidate = mount_point.to_string();
146
147 if let Some((_, current_mount)) = points
148 .iter_mut()
149 .find(|(existing_device, _)| existing_device == device)
150 {
151 if mount_priority(&candidate) > mount_priority(current_mount) {
152 *current_mount = candidate;
153 }
154 return;
155 }
156
157 points.push((device.to_string(), candidate));
158}
159
160fn mount_priority(path: &str) -> usize {
161 let trimmed = path.trim_matches('/');
162 if trimmed.is_empty() {
163 0
164 } else {
165 trimmed.split('/').count() * 10 + trimmed.len()
166 }
167}
168
169fn mount_dir_label(path: &str) -> String {
170 path.trim_end_matches('/')
171 .rsplit('/')
172 .next()
173 .filter(|segment| !segment.is_empty())
174 .unwrap_or("")
175 .to_string()
176}
177
178fn format_size(bytes: u64) -> String {
179 const KB: u64 = 1024;
180 const MB: u64 = KB * 1024;
181 const GB: u64 = MB * 1024;
182 const TB: u64 = GB * 1024;
183
184 if bytes >= TB {
185 format!("{:.1}T", bytes as f64 / TB as f64)
186 } else if bytes >= GB {
187 format!("{:.1}G", bytes as f64 / GB as f64)
188 } else if bytes >= MB {
189 format!("{:.1}M", bytes as f64 / MB as f64)
190 } else if bytes >= KB {
191 format!("{:.1}K", bytes as f64 / KB as f64)
192 } else {
193 format!("{}B", bytes)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{
200 is_visible_storage_mount, mount_dir_label, mount_priority, push_preferred_mount,
201 render_disk_display,
202 };
203 use crate::modules::utils::get_bar;
204
205 #[test]
206 fn keeps_user_visible_storage_mounts() {
207 assert!(is_visible_storage_mount("/home"));
208 assert!(is_visible_storage_mount("/home/snape"));
209 assert!(is_visible_storage_mount("/data"));
210 assert!(is_visible_storage_mount("/mnt/usb"));
211 assert!(is_visible_storage_mount("/media/disk"));
212 assert!(is_visible_storage_mount("/run/media/snape/USB"));
213 }
214
215 #[test]
216 fn rejects_system_mounts() {
217 assert!(!is_visible_storage_mount("/"));
218 assert!(!is_visible_storage_mount("/boot"));
219 assert!(!is_visible_storage_mount("/var"));
220 assert!(!is_visible_storage_mount("/usr"));
221 assert!(!is_visible_storage_mount("/opt"));
222 assert!(!is_visible_storage_mount("/proc"));
223 assert!(!is_visible_storage_mount("/sys"));
224 assert!(!is_visible_storage_mount("/snap"));
225 assert!(!is_visible_storage_mount("/dev/sda1"));
226 }
227
228 #[test]
229 fn dir_label_uses_last_path_segment() {
230 assert_eq!(mount_dir_label("/home"), "home");
231 assert_eq!(mount_dir_label("/run/media/snape/USB"), "USB");
232 assert_eq!(mount_dir_label("/mnt/external-drive/"), "external-drive");
233 assert_eq!(mount_dir_label("/"), "");
234 }
235
236 #[test]
237 fn prefers_more_useful_mount_for_same_device() {
238 let mut points = Vec::new();
239
240 push_preferred_mount(&mut points, "/dev/nvme0n1p2", "/");
241 push_preferred_mount(&mut points, "/dev/nvme0n1p2", "/home");
242 push_preferred_mount(&mut points, "/dev/nvme0n1p2", "/home/snape");
243
244 assert_eq!(points.len(), 1);
245 assert_eq!(points[0].1, "/home/snape");
246 assert!(mount_priority("/home/snape") > mount_priority("/home"));
247 assert!(mount_priority("/home") > mount_priority("/"));
248 }
249
250 #[test]
251 fn renders_disk_display_parts_in_order() {
252 assert_eq!(render_disk_display("percentage", "1G / 2G", 50, 50), "50%");
253 assert_eq!(
254 render_disk_display("info, percentage", "1G / 2G", 50, 50),
255 "1G / 2G 50%"
256 );
257 assert_eq!(
258 render_disk_display("percentage, bar", "1G / 2G", 50, 50),
259 format!("50% {}", get_bar(50))
260 );
261 }
262}
263
264