df3 0.1.2

Advanced disk free utility - modern alternative to df command
Documentation
// File: src\disk.rs
// Author: Hadi Cahyadi <cumulus13@gmail.com>
// Date: 2026-06-16
// Description:
// License: MIT

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use sysinfo::Disks;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskInfo {
    pub entries: Vec<DiskEntry>,
    pub total: Option<DiskTotal>,
    pub collected_at: chrono::DateTime<chrono::Local>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskEntry {
    pub filesystem: String,
    pub disk_type: String,
    pub total: u64,
    pub used: u64,
    pub available: u64,
    pub percent: f64,
    pub mount_point: PathBuf,
    pub is_removable: bool,
    pub block_size: u64,
    pub inodes_total: Option<u64>,
    pub inodes_used: Option<u64>,
    pub inodes_free: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskTotal {
    pub total: u64,
    pub used: u64,
    pub available: u64,
}

impl DiskInfo {
    pub fn collect(
        _human_readable: bool,
        _local_only: bool,
        _all: bool,
        _fs_types: &[String],
        _exclude_types: &[String],
        block_size: u64,
    ) -> Result<Self> {
        let disks = Disks::new_with_refreshed_list();
        let mut entries = Vec::new();

        for disk in &disks {
            let mount_point = disk.mount_point().to_path_buf();
            let fs_type = disk.file_system().to_string_lossy().to_string();

            // Apply basic filters
            if !_all && fs_type.is_empty() {
                continue;
            }

            if !_fs_types.is_empty() && !_fs_types.contains(&fs_type) {
                continue;
            }

            if !_exclude_types.is_empty() && _exclude_types.contains(&fs_type) {
                continue;
            }

            let total = disk.total_space();
            let available = disk.available_space();
            let used = total.saturating_sub(available);
            let percent = if total > 0 {
                (used as f64 / total as f64) * 100.0
            } else {
                0.0
            };

            entries.push(DiskEntry {
                filesystem: disk.name().to_string_lossy().to_string(),
                disk_type: fs_type,
                total,
                used,
                available,
                percent,
                mount_point,
                is_removable: disk.is_removable(),
                block_size,
                inodes_total: None,
                inodes_used: None,
                inodes_free: None,
            });
        }

        let total = if entries.len() > 1 {
            Some(DiskTotal {
                total: entries.iter().map(|e| e.total).sum(),
                used: entries.iter().map(|e| e.used).sum(),
                available: entries.iter().map(|e| e.available).sum(),
            })
        } else {
            None
        };

        Ok(DiskInfo {
            entries,
            total,
            collected_at: chrono::Local::now(),
        })
    }

    /// Filter entries to only show those containing the specified paths
    pub fn filter_by_mounts(&mut self, mounts: &[String]) {
        if mounts.is_empty() {
            return;
        }

        self.entries.retain(|entry| {
            let mount_str = entry.mount_point.display().to_string().to_lowercase();
            mounts.iter().any(|m| {
                let m = m.to_lowercase();
                mount_str == m || m.starts_with(&mount_str) || mount_str.starts_with(&m)
            })
        });

        // Recalculate total after filtering
        self.total = if self.entries.len() > 1 {
            Some(DiskTotal {
                total: self.entries.iter().map(|e| e.total).sum(),
                used: self.entries.iter().map(|e| e.used).sum(),
                available: self.entries.iter().map(|e| e.available).sum(),
            })
        } else {
            None
        };
    }
}