Skip to main content

leenfetch_core/modules/linux/info/
disk.rs

1use std::fs;
2
3use crate::modules::{
4    enums::{DiskDisplay, DiskSubtitle},
5    utils::get_bar,
6};
7
8pub fn get_disks(
9    subtitle_mode: DiskSubtitle,
10    display_mode: DiskDisplay,
11    paths: Option<Vec<&str>>,
12) -> Option<Vec<(String, String)>> {
13    // Get mount points - read /proc/mounts directly instead of spawning df
14    let mount_points = if let Some(ref user_paths) = paths {
15        user_paths.iter().map(|s| s.to_string()).collect()
16    } else {
17        // Default: get root and common mount points
18        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!["/".to_string()];
39    let mut seen_devices: std::collections::HashSet<String> = std::collections::HashSet::new();
40
41    // Read /proc/mounts to find common mount points
42    if let Ok(content) = fs::read_to_string("/proc/mounts") {
43        let common_prefixes = ["/home", "/boot", "/var", "/usr", "/opt", "/data"];
44
45        for line in content.lines() {
46            let parts: Vec<&str> = line.split_whitespace().collect();
47            if parts.len() >= 2 {
48                let mount = parts[0]; // Device
49                let mount_point = parts[1];
50
51                // Skip if we've already seen this device (same filesystem)
52                if seen_devices.contains(mount) {
53                    continue;
54                }
55
56                // Skip pseudo filesystems and already added paths
57                if !mount_point.starts_with("/dev")
58                    && !mount_point.starts_with("/sys")
59                    && !mount_point.starts_with("/proc")
60                    && !mount_point.starts_with("/run")
61                    && !mount_point.starts_with("/snap")
62                {
63                    // Only add if it's a significant mount point
64                    for prefix in &common_prefixes {
65                        if mount_point.starts_with(prefix) && mount_point != "/" {
66                            seen_devices.insert(mount.to_string());
67                            points.push(mount_point.to_string());
68                            break;
69                        }
70                    }
71                }
72            }
73        }
74    }
75
76    // Limit to avoid too many entries
77    points.truncate(5);
78    points
79}
80
81fn get_disk_info_for_path(
82    path: &str,
83    subtitle_mode: DiskSubtitle,
84    display_mode: &DiskDisplay,
85) -> Option<(String, String)> {
86    let path_cstr = std::ffi::CString::new(path).ok()?;
87
88    // Use statvfs to get disk usage
89    let mut statfs: libc::statvfs = unsafe { std::mem::zeroed() };
90
91    if unsafe { libc::statvfs(path_cstr.as_ptr(), &mut statfs) } != 0 {
92        return None;
93    }
94
95    // Calculate sizes in bytes
96    let total = statfs.f_blocks as u64 * statfs.f_frsize as u64;
97    let available = statfs.f_bavail as u64 * statfs.f_frsize as u64;
98    let used = total.saturating_sub(available);
99
100    if total == 0 {
101        return None;
102    }
103
104    let percent = ((used as f64 / total as f64) * 100.0).round().clamp(0.0, 100.0) as u8;
105
106    // Format sizes in human-readable form
107    let total_h = format_size(total);
108    let used_h = format_size(used);
109
110    let usage_display = format!("{} / {}", used_h, total_h);
111    let perc_val = percent.min(100);
112
113    let final_str = match display_mode {
114        DiskDisplay::Info => usage_display,
115        DiskDisplay::Percentage => format!("{}% {}", percent, get_bar(perc_val)),
116        DiskDisplay::InfoBar => format!("{} {}", usage_display, get_bar(perc_val)),
117        DiskDisplay::BarInfo => format!("{} {}", get_bar(perc_val), usage_display),
118        DiskDisplay::Bar => get_bar(perc_val),
119    };
120
121    // Get device name from /proc/mounts
122    let subtitle = match subtitle_mode {
123        DiskSubtitle::Name => get_device_name(path),
124        DiskSubtitle::Dir => path
125            .trim_start_matches('/')
126            .split('/')
127            .next()
128            .unwrap_or("")
129            .to_string(),
130        DiskSubtitle::None => "".to_string(),
131        DiskSubtitle::Mount => path.to_string(),
132    };
133
134    let full_subtitle = if subtitle.is_empty() {
135        "Disk".to_string()
136    } else {
137        format!("Disk ({})", subtitle)
138    };
139
140    Some((full_subtitle, final_str))
141}
142
143fn get_device_name(path: &str) -> String {
144    if let Ok(content) = fs::read_to_string("/proc/mounts") {
145        for line in content.lines() {
146            let parts: Vec<&str> = line.split_whitespace().collect();
147            if parts.len() >= 2 && parts[1] == path {
148                return parts[0].to_string();
149            }
150        }
151    }
152    "".to_string()
153}
154
155fn format_size(bytes: u64) -> String {
156    const KB: u64 = 1024;
157    const MB: u64 = KB * 1024;
158    const GB: u64 = MB * 1024;
159    const TB: u64 = GB * 1024;
160
161    if bytes >= TB {
162        format!("{:.1}T", bytes as f64 / TB as f64)
163    } else if bytes >= GB {
164        format!("{:.1}G", bytes as f64 / GB as f64)
165    } else if bytes >= MB {
166        format!("{:.1}M", bytes as f64 / MB as f64)
167    } else if bytes >= KB {
168        format!("{:.1}K", bytes as f64 / KB as f64)
169    } else {
170        format!("{}B", bytes)
171    }
172}
173
174// fn parse_disk_output(
175//     stdout: &str,
176//     subtitle_mode: DiskSubtitle,
177//     display_mode: DiskDisplay,
178// ) -> Vec<(String, String)> {
179//     let mut lines = stdout.lines().skip(1);
180//     let mut results = Vec::new();
181
182//     while let Some(line) = lines.next() {
183//         let parts: Vec<&str> = line.split_whitespace().filter(|s| !s.is_empty()).collect();
184
185//         if parts.len() < 6 {
186//             continue;
187//         }
188
189//         let total = parts[1];
190//         let used = parts[2];
191//         let perc = parts[4].trim_end_matches('%');
192//         let mount = parts[5];
193
194//         let usage_display = format!("{} / {}", used, total);
195//         let perc_val = perc.parse::<u8>().unwrap_or(0);
196
197//         let final_str = match display_mode {
198//             DiskDisplay::Info => usage_display,
199//             DiskDisplay::Percentage => format!("{}% {}", perc, get_bar(perc_val)),
200//             DiskDisplay::InfoBar => format!("{} {}", usage_display, get_bar(perc_val)),
201//             DiskDisplay::BarInfo => format!("{} {}", get_bar(perc_val), usage_display),
202//             DiskDisplay::Bar => get_bar(perc_val),
203//         };
204
205//         let subtitle = match subtitle_mode {
206//             DiskSubtitle::Name => parts[0].to_string(),
207//             DiskSubtitle::Dir => mount.split('/').last().unwrap_or("").to_string(),
208//             DiskSubtitle::None => "".to_string(),
209//             DiskSubtitle::Mount => mount.to_string(),
210//         };
211
212//         let full_subtitle = if subtitle.is_empty() {
213//             "Disk".to_string()
214//         } else {
215//             format!("Disk ({})", subtitle)
216//         };
217
218//         results.push((full_subtitle, final_str));
219//     }
220
221//     results
222// }
223