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)
105        .round()
106        .clamp(0.0, 100.0) as u8;
107
108    // Format sizes in human-readable form
109    let total_h = format_size(total);
110    let used_h = format_size(used);
111
112    let usage_display = format!("{} / {}", used_h, total_h);
113    let perc_val = percent.min(100);
114
115    let final_str = match display_mode {
116        DiskDisplay::Info => usage_display,
117        DiskDisplay::Percentage => format!("{}% {}", percent, get_bar(perc_val)),
118        DiskDisplay::InfoBar => format!("{} {}", usage_display, get_bar(perc_val)),
119        DiskDisplay::BarInfo => format!("{} {}", get_bar(perc_val), usage_display),
120        DiskDisplay::Bar => get_bar(perc_val),
121    };
122
123    // Get device name from /proc/mounts
124    let subtitle = match subtitle_mode {
125        DiskSubtitle::Name => get_device_name(path),
126        DiskSubtitle::Dir => path
127            .trim_start_matches('/')
128            .split('/')
129            .next()
130            .unwrap_or("")
131            .to_string(),
132        DiskSubtitle::None => "".to_string(),
133        DiskSubtitle::Mount => path.to_string(),
134    };
135
136    let full_subtitle = if subtitle.is_empty() {
137        "Disk".to_string()
138    } else {
139        format!("Disk ({})", subtitle)
140    };
141
142    Some((full_subtitle, final_str))
143}
144
145fn get_device_name(path: &str) -> String {
146    if let Ok(content) = fs::read_to_string("/proc/mounts") {
147        for line in content.lines() {
148            let parts: Vec<&str> = line.split_whitespace().collect();
149            if parts.len() >= 2 && parts[1] == path {
150                return parts[0].to_string();
151            }
152        }
153    }
154    "".to_string()
155}
156
157fn format_size(bytes: u64) -> String {
158    const KB: u64 = 1024;
159    const MB: u64 = KB * 1024;
160    const GB: u64 = MB * 1024;
161    const TB: u64 = GB * 1024;
162
163    if bytes >= TB {
164        format!("{:.1}T", bytes as f64 / TB as f64)
165    } else if bytes >= GB {
166        format!("{:.1}G", bytes as f64 / GB as f64)
167    } else if bytes >= MB {
168        format!("{:.1}M", bytes as f64 / MB as f64)
169    } else if bytes >= KB {
170        format!("{:.1}K", bytes as f64 / KB as f64)
171    } else {
172        format!("{}B", bytes)
173    }
174}
175
176// fn parse_disk_output(
177//     stdout: &str,
178//     subtitle_mode: DiskSubtitle,
179//     display_mode: DiskDisplay,
180// ) -> Vec<(String, String)> {
181//     let mut lines = stdout.lines().skip(1);
182//     let mut results = Vec::new();
183
184//     while let Some(line) = lines.next() {
185//         let parts: Vec<&str> = line.split_whitespace().filter(|s| !s.is_empty()).collect();
186
187//         if parts.len() < 6 {
188//             continue;
189//         }
190
191//         let total = parts[1];
192//         let used = parts[2];
193//         let perc = parts[4].trim_end_matches('%');
194//         let mount = parts[5];
195
196//         let usage_display = format!("{} / {}", used, total);
197//         let perc_val = perc.parse::<u8>().unwrap_or(0);
198
199//         let final_str = match display_mode {
200//             DiskDisplay::Info => usage_display,
201//             DiskDisplay::Percentage => format!("{}% {}", perc, get_bar(perc_val)),
202//             DiskDisplay::InfoBar => format!("{} {}", usage_display, get_bar(perc_val)),
203//             DiskDisplay::BarInfo => format!("{} {}", get_bar(perc_val), usage_display),
204//             DiskDisplay::Bar => get_bar(perc_val),
205//         };
206
207//         let subtitle = match subtitle_mode {
208//             DiskSubtitle::Name => parts[0].to_string(),
209//             DiskSubtitle::Dir => mount.split('/').last().unwrap_or("").to_string(),
210//             DiskSubtitle::None => "".to_string(),
211//             DiskSubtitle::Mount => mount.to_string(),
212//         };
213
214//         let full_subtitle = if subtitle.is_empty() {
215//             "Disk".to_string()
216//         } else {
217//             format!("Disk ({})", subtitle)
218//         };
219
220//         results.push((full_subtitle, final_str));
221//     }
222
223//     results
224// }