From 9ccccff47ab565ac329d8b6b7f62e078b53e10b0 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Thu, 15 May 2025 14:28:58 +0800 Subject: [PATCH 01/12] upgrade packages --- Cargo.toml | 4 +-- src/main.rs | 76 ++++++++++++++++++++++++----------------------------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76cbaac..6eab1b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,5 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -colored = "2" -sysinfo = "0.3.1" +colored = "3" +sysinfo = "0.35.1" diff --git a/src/main.rs b/src/main.rs index b572f87..0848d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,45 +1,35 @@ -#![allow(non_snake_case)] - -const MAX_CHARS: usize = 50; // Max number of characters for the disk's available space bar. - use colored::*; -use sysinfo::{DiskExt, SystemExt}; -use std::ffi::{OsString}; use std::env; +use sysinfo::Disks; -fn get_frac(avail: &u64, total: &u64) -> f64 { - if *total == 0 { // Prevent divide by 0. - return 0 as f64; - } +const MAX_CHARS: usize = 50; - return 1 as f64 - (*avail as f64/ *total as f64); +fn get_frac(avail: u64, total: u64) -> f64 { + if total == 0 { + return 0.0; + } + 1.0 - (avail as f64 / total as f64) } struct NDFDisk { name: String, space_asfrac: f64, - mnt: String + mnt: String, } impl NDFDisk { - fn create_NDFDisk(disk: &sysinfo::Disk) -> NDFDisk { - let frac = get_frac(&disk.get_available_space(), &disk.get_total_space()); - - match OsString::from(disk.get_name()).into_string() { - Ok(s) => { - NDFDisk { - name: s, - space_asfrac: frac, - mnt: disk.get_mount_point().display().to_string() - } - }, - Err(_) => panic!("No name for disk.") + fn create_ndf_disk(disk: &sysinfo::Disk) -> NDFDisk { + let frac = get_frac(disk.available_space(), disk.total_space()); + NDFDisk { + name: disk.name().to_string_lossy().to_string(), + space_asfrac: frac, + mnt: disk.mount_point().to_string_lossy().to_string(), } } fn create_bar(&self) -> colored::ColoredString { - let chars_num = ((MAX_CHARS as f64*self.space_asfrac).ceil()) as usize; + let chars_num = ((MAX_CHARS as f64 * self.space_asfrac).ceil()) as usize; let chars = "▓".repeat(chars_num); - let rem_num = (MAX_CHARS - chars_num) as usize; + let rem_num = MAX_CHARS - chars_num; let rem = "░".repeat(rem_num); if rem_num < (MAX_CHARS as f64 * 0.2) as usize { @@ -52,35 +42,39 @@ impl NDFDisk { fn main() { let args: Vec = env::args().collect(); - let mut compactmode = false; - match args.get(1) { - Some(val) => { - if val == "compact" { - compactmode = true; - } - }, - None => { - (); + if let Some(val) = args.get(1) { + if val == "compact" { + compactmode = true; } } - let sys = sysinfo::System::new(); let mut disks: Vec = Vec::new(); - for disk in sys.get_disks() { - disks.push(NDFDisk::create_NDFDisk(disk)); - }; + for disk in &Disks::new_with_refreshed_list() { + disks.push(NDFDisk::create_ndf_disk(disk)); + } println!("{}", "\nndf - nice disk free".bold()); if compactmode { for disk in disks.into_iter() { - println!("{}: {} {:.0}%", disk.name, disk.create_bar(), disk.space_asfrac*100 as f64); + println!( + "{}: {} {:.0}%", + disk.name, + disk.create_bar(), + disk.space_asfrac * 100.0 + ); } } else { for disk in disks.into_iter() { - println!("{} @ {}\n{} {:.0}%\n", disk.name, disk.mnt, disk.create_bar(), disk.space_asfrac*100 as f64); + println!( + "{} @ {}\n{} {:.0}%\n", + disk.name, + disk.mnt, + disk.create_bar(), + disk.space_asfrac * 100.0 + ); } } } \ No newline at end of file From bdfbbf5a34ab0592f03654f400cbaddcc6c27881 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Thu, 15 May 2025 14:37:04 +0800 Subject: [PATCH 02/12] refactor --- src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0848d96..68573c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,8 @@ impl NDFDisk { mnt: disk.mount_point().to_string_lossy().to_string(), } } - fn create_bar(&self) -> colored::ColoredString { - let chars_num = ((MAX_CHARS as f64 * self.space_asfrac).ceil()) as usize; + fn create_bar(&self) -> ColoredString { + let chars_num = (MAX_CHARS as f64 * self.space_asfrac).ceil() as usize; let chars = "▓".repeat(chars_num); let rem_num = MAX_CHARS - chars_num; let rem = "░".repeat(rem_num); @@ -42,11 +42,11 @@ impl NDFDisk { fn main() { let args: Vec = env::args().collect(); - let mut compactmode = false; + let mut compact_mode = false; if let Some(val) = args.get(1) { if val == "compact" { - compactmode = true; + compact_mode = true; } } @@ -57,7 +57,7 @@ fn main() { println!("{}", "\nndf - nice disk free".bold()); - if compactmode { + if compact_mode { for disk in disks.into_iter() { println!( "{}: {} {:.0}%", @@ -77,4 +77,4 @@ fn main() { ); } } -} \ No newline at end of file +} From 3240face7134a8ce42d005f9dbcc4aa785333993 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Thu, 15 May 2025 14:52:55 +0800 Subject: [PATCH 03/12] ignore overlay fs --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 68573c7..f16e8cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,7 +51,10 @@ fn main() { } let mut disks: Vec = Vec::new(); - for disk in &Disks::new_with_refreshed_list() { + for disk in Disks::new_with_refreshed_list().list() { + if disk.file_system() == "overlay" { + continue; + } disks.push(NDFDisk::create_ndf_disk(disk)); } From 9b173845f6243ce9dc0b3c2632f593073f2be034 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Thu, 15 May 2025 14:55:57 +0800 Subject: [PATCH 04/12] ignore overlay and snap mounts --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f16e8cd..f238b0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,8 @@ fn main() { let mut disks: Vec = Vec::new(); for disk in Disks::new_with_refreshed_list().list() { - if disk.file_system() == "overlay" { + // ignore overlay and snap mounts + if disk.file_system() == "overlay" || disk.mount_point().starts_with("/var/snap/") { continue; } disks.push(NDFDisk::create_ndf_disk(disk)); From 0b4c6c60b308cbce0e933622819f13b538cc075a Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Sat, 31 May 2025 00:20:55 +0800 Subject: [PATCH 05/12] cli table mode --- Cargo.toml | 2 + src/main.rs | 158 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6eab1b1..0d374b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ edition = "2018" [dependencies] colored = "3" sysinfo = "0.35.1" +clap = { version = "4", features = ["derive"] } +tabled = "0.19.0" diff --git a/src/main.rs b/src/main.rs index f238b0d..a39e87f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,18 @@ +use clap::{Arg, Command, value_parser, ValueEnum}; use colored::*; -use std::env; +use std::collections::HashSet; use sysinfo::Disks; +use tabled::{Table, Tabled}; const MAX_CHARS: usize = 50; +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum OutputMode { + Normal, + Compact, + Table, +} + fn get_frac(avail: u64, total: u64) -> f64 { if total == 0 { return 0.0; @@ -13,8 +22,10 @@ fn get_frac(avail: u64, total: u64) -> f64 { struct NDFDisk { name: String, - space_asfrac: f64, + space_as_frac: f64, mnt: String, + size: u64, + free: u64, } impl NDFDisk { @@ -22,12 +33,15 @@ impl NDFDisk { let frac = get_frac(disk.available_space(), disk.total_space()); NDFDisk { name: disk.name().to_string_lossy().to_string(), - space_asfrac: frac, + space_as_frac: frac, mnt: disk.mount_point().to_string_lossy().to_string(), + size: disk.total_space(), + free: disk.available_space(), } } + fn create_bar(&self) -> ColoredString { - let chars_num = (MAX_CHARS as f64 * self.space_asfrac).ceil() as usize; + let chars_num = (MAX_CHARS as f64 * self.space_as_frac).ceil() as usize; let chars = "▓".repeat(chars_num); let rem_num = MAX_CHARS - chars_num; let rem = "░".repeat(rem_num); @@ -40,45 +54,127 @@ impl NDFDisk { } } +fn format_size(size: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + match size { + s if s >= TB => format!("{:.2}T", s as f64 / TB as f64), + s if s >= GB => format!("{:.2}G", s as f64 / GB as f64), + s if s >= MB => format!("{:.2}M", s as f64 / MB as f64), + s if s >= KB => format!("{:.2}K", s as f64 / KB as f64), + _ => format!("{}B", size), + } +} + + +#[derive(Tabled)] +struct DiskRow { + #[tabled(rename = "Mount")] + mnt: String, + #[tabled(rename = "Size")] + size: String, + #[tabled(rename = "Free")] + free: String, + #[tabled(rename = "Usage")] + usage: String, + #[tabled(rename = "Name")] + name: String, +} + + fn main() { - let args: Vec = env::args().collect(); - let mut compact_mode = false; + let matches = Command::new("ndf") + .about("Nice disk free.") + .arg( + Arg::new("output") + .long("output") + .value_parser(value_parser!(OutputMode)) + .default_value("normal") + .help("Output mode: normal | compact | table"), + ) + .arg( + Arg::new("only-mp") + .long("only-mp") + .value_name("MOUNTPOINTS") + .help("Show only specified mount points, comma separated"), + ) + .arg( + Arg::new("exclude-mp") + .long("exclude-mp") + .value_name("MOUNTPOINTS") + .help("Exclude specified mount points, comma separated"), + ) + .get_matches(); - if let Some(val) = args.get(1) { - if val == "compact" { - compact_mode = true; - } - } + let output_mode = *matches.get_one::("output").unwrap(); + + let only_mp: Option> = matches + .get_one::("only-mp") + .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); + + let exclude_mp: Option> = matches + .get_one::("exclude-mp") + .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); let mut disks: Vec = Vec::new(); for disk in Disks::new_with_refreshed_list().list() { + let mnt = disk.mount_point().to_string_lossy(); // ignore overlay and snap mounts - if disk.file_system() == "overlay" || disk.mount_point().starts_with("/var/snap/") { + if disk.file_system() == "overlay" || mnt.starts_with("/var/snap/") { continue; } + if let Some(ref only) = only_mp { + if !only.contains(mnt.as_ref()) { + continue; + } + } + if let Some(ref exclude) = exclude_mp { + if exclude.contains(mnt.as_ref()) { + continue; + } + } disks.push(NDFDisk::create_ndf_disk(disk)); } - println!("{}", "\nndf - nice disk free".bold()); + println!("{}", "ndf - nice disk free".bold()); - if compact_mode { - for disk in disks.into_iter() { - println!( - "{}: {} {:.0}%", - disk.name, - disk.create_bar(), - disk.space_asfrac * 100.0 - ); + match output_mode { + OutputMode::Compact => { + for disk in disks { + println!( + "{}: {} {:.0}%", + disk.name, + disk.create_bar(), + disk.space_as_frac * 100.0 + ); + } } - } else { - for disk in disks.into_iter() { - println!( - "{} @ {}\n{} {:.0}%\n", - disk.name, - disk.mnt, - disk.create_bar(), - disk.space_asfrac * 100.0 - ); + OutputMode::Table => { + let rows: Vec = disks + .iter() + .map(|disk| DiskRow { + mnt: disk.mnt.clone(), + size: format_size(disk.size), + free: format_size(disk.free), + usage: format!("{} {:>3.0}%", disk.create_bar(), disk.space_as_frac * 100.0), + name: disk.name.clone(), + }) + .collect(); + let table = Table::new(rows); + println!("{}", table); + } + OutputMode::Normal => { + for disk in disks { + println!( + "{} @ {}\n{} {:.0}%\n", + disk.name, + disk.mnt, + disk.create_bar(), + disk.space_as_frac * 100.0 + ); + } } } -} +} \ No newline at end of file From 35b8b47c8a6e5301b676b8f8ace0bd33916d0435 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Fri, 18 Jul 2025 14:12:57 +0800 Subject: [PATCH 06/12] fix table --- Cargo.toml | 1 - src/main.rs | 103 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0d374b0..f48859a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,3 @@ edition = "2018" colored = "3" sysinfo = "0.35.1" clap = { version = "4", features = ["derive"] } -tabled = "0.19.0" diff --git a/src/main.rs b/src/main.rs index a39e87f..e42490e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ -use clap::{Arg, Command, value_parser, ValueEnum}; +use clap::{value_parser, Arg, Command, ValueEnum}; use colored::*; use std::collections::HashSet; use sysinfo::Disks; -use tabled::{Table, Tabled}; const MAX_CHARS: usize = 50; @@ -52,6 +51,19 @@ impl NDFDisk { format!("{}{}", chars, rem).green() } } + + fn create_plain_bar(&self) -> String { + let chars_num = (MAX_CHARS as f64 * self.space_as_frac).ceil() as usize; + let chars = "▓".repeat(chars_num); + let rem_num = MAX_CHARS - chars_num; + let rem = "░".repeat(rem_num); + format!("{}{}", chars, rem) + } + + fn is_high_usage(&self) -> bool { + let rem_num = MAX_CHARS - (MAX_CHARS as f64 * self.space_as_frac).ceil() as usize; + rem_num < (MAX_CHARS as f64 * 0.2) as usize + } } fn format_size(size: u64) -> String { @@ -68,22 +80,6 @@ fn format_size(size: u64) -> String { } } - -#[derive(Tabled)] -struct DiskRow { - #[tabled(rename = "Mount")] - mnt: String, - #[tabled(rename = "Size")] - size: String, - #[tabled(rename = "Free")] - free: String, - #[tabled(rename = "Usage")] - usage: String, - #[tabled(rename = "Name")] - name: String, -} - - fn main() { let matches = Command::new("ndf") .about("Nice disk free.") @@ -152,18 +148,63 @@ fn main() { } } OutputMode::Table => { - let rows: Vec = disks - .iter() - .map(|disk| DiskRow { - mnt: disk.mnt.clone(), - size: format_size(disk.size), - free: format_size(disk.free), - usage: format!("{} {:>3.0}%", disk.create_bar(), disk.space_as_frac * 100.0), - name: disk.name.clone(), - }) - .collect(); - let table = Table::new(rows); - println!("{}", table); + // 手动创建表格以正确处理颜色 + println!( + "┌{:─<22}┬{:─<10}┬{:─<10}┬{:─<56}┬{:─<14}┐", + "", "", "", "", "" + ); + println!( + "│ {:<20} │ {:>8} │ {:>8} │ {:^54} │ {:<12} │", + "Mount", "Size", "Free", "Usage", "Name" + ); + println!( + "├{:─<22}┼{:─<10}┼{:─<10}┼{:─<56}┼{:─<14}┤", + "", "", "", "", "" + ); + + for disk in disks { + let mount_col = format!( + "│ {:<20} │", + if disk.mnt.len() > 20 { + disk.mnt[..17].to_string() + "..." + } else { + disk.mnt.clone() + } + ); + let size_col = format!(" {:>8} │", format_size(disk.size)); + let free_col = format!(" {:>8} │", format_size(disk.free)); + let name_col = format!( + " {:<12} │", + if disk.name.len() > 12 { + disk.name[..9].to_string() + "..." + } else { + disk.name.clone() + } + ); + + // 构建Usage列内容:1空格 + 50字符进度条 + 1空格 + 3字符百分比 + 1空格 = 56字符 + // 但我们用{:>3}格式化百分比,实际上是:1空格 + 50字符进度条 + 1空格 + 右对齐3字符百分比 + 1空格 = 55字符 + let plain_bar = disk.create_plain_bar(); + let percentage = format!("{:.0}%", disk.space_as_frac * 100.0); + + let colored_bar = if disk.is_high_usage() { + plain_bar.red() + } else { + plain_bar.green() + }; + + let usage_final = format!(" {} {:>3} │", colored_bar, percentage); + + // 手动打印每行,不使用格式化来处理颜色部分 + print!("{}{}{}", mount_col, size_col, free_col); + print!("{}", usage_final); + println!("{}", name_col); + } + + println!( + "└{:─<22}┴{:─<10}┴{:─<10}┴{:─<56}┴{:─<14}┘", + "", "", "", "", "" + ); } OutputMode::Normal => { for disk in disks { @@ -177,4 +218,4 @@ fn main() { } } } -} \ No newline at end of file +} From 1962d84c3e783411026987c85a778b1df7338d88 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Fri, 18 Jul 2025 14:24:37 +0800 Subject: [PATCH 07/12] chore(cli): rename "output" arg to "mode" and change default to table --- src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index e42490e..8d9d6c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,11 +84,10 @@ fn main() { let matches = Command::new("ndf") .about("Nice disk free.") .arg( - Arg::new("output") - .long("output") + Arg::new("mode") .value_parser(value_parser!(OutputMode)) - .default_value("normal") - .help("Output mode: normal | compact | table"), + .default_value("table") + .help("Display mode: normal | compact | table"), ) .arg( Arg::new("only-mp") @@ -104,7 +103,7 @@ fn main() { ) .get_matches(); - let output_mode = *matches.get_one::("output").unwrap(); + let output_mode = *matches.get_one::("mode").unwrap(); let only_mp: Option> = matches .get_one::("only-mp") From f5eca651495e2ae6aa9616f6eeb779c99796be53 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Fri, 18 Jul 2025 14:47:07 +0800 Subject: [PATCH 08/12] refactor: improve table formatting with dynamic column widths --- src/main.rs | 101 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 35 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8d9d6c1..fa3d69b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,42 +147,66 @@ fn main() { } } OutputMode::Table => { - // 手动创建表格以正确处理颜色 + // 计算每列的最大宽度 + let mut max_mount_len = "Mount".len(); + let mut max_size_len = "Size".len(); + let mut max_free_len = "Free".len(); + let mut max_name_len = "Name".len(); + + for disk in &disks { + max_mount_len = max_mount_len.max(disk.mnt.len().min(20)); + max_size_len = max_size_len.max(format_size(disk.size).len()); + max_free_len = max_free_len.max(format_size(disk.free).len()); + max_name_len = max_name_len.max(disk.name.len().min(15)); + } + + // Usage列固定为进度条宽度 + 百分比 + let usage_len = MAX_CHARS + 4; // 50字符进度条 + 空格 + 3字符百分比 + + // 手动创建表格 println!( - "┌{:─<22}┬{:─<10}┬{:─<10}┬{:─<56}┬{:─<14}┐", - "", "", "", "", "" + "┌{:─8} │ {:>8} │ {:^54} │ {:<12} │", - "Mount", "Size", "Free", "Usage", "Name" + "│ {:width_size$} │ {:>width_free$} │ {:^width_usage$} │ {: 20 { - disk.mnt[..17].to_string() + "..." - } else { - disk.mnt.clone() - } - ); - let size_col = format!(" {:>8} │", format_size(disk.size)); - let free_col = format!(" {:>8} │", format_size(disk.free)); - let name_col = format!( - " {:<12} │", - if disk.name.len() > 12 { - disk.name[..9].to_string() + "..." - } else { - disk.name.clone() - } - ); + let mount_text = if disk.mnt.len() > 20 { + disk.mnt[..17].to_string() + "..." + } else { + disk.mnt.clone() + }; + let size_text = format_size(disk.size); + let free_text = format_size(disk.free); + let name_text = if disk.name.len() > 15 { + disk.name[..12].to_string() + "..." + } else { + disk.name.clone() + }; - // 构建Usage列内容:1空格 + 50字符进度条 + 1空格 + 3字符百分比 + 1空格 = 56字符 - // 但我们用{:>3}格式化百分比,实际上是:1空格 + 50字符进度条 + 1空格 + 右对齐3字符百分比 + 1空格 = 55字符 + // 构建Usage列内容 let plain_bar = disk.create_plain_bar(); let percentage = format!("{:.0}%", disk.space_as_frac * 100.0); @@ -192,17 +216,24 @@ fn main() { plain_bar.green() }; - let usage_final = format!(" {} {:>3} │", colored_bar, percentage); - - // 手动打印每行,不使用格式化来处理颜色部分 - print!("{}{}{}", mount_col, size_col, free_col); - print!("{}", usage_final); - println!("{}", name_col); + println!( + "│ {:width_size$} │ {:>width_free$} │ {} {:>3} │ {: { From c1e41a01801acedcb699428a41fcaca6ddf481d5 Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Fri, 18 Jul 2025 16:06:34 +0800 Subject: [PATCH 09/12] update README.md --- README.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04728b1..90fc548 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,118 @@ -# ndf -Nice disk free. +# ndf - Nice Disk Free + +A modern, colorful disk usage utility written in Rust. `ndf` provides a clean and intuitive way to view disk space information with beautiful progress bars and multiple display modes. + +## Features + +- 🎨 **Colorful Progress Bars**: Visual representation of disk usage with red/green color coding +- 📊 **Multiple Display Modes**: Choose from normal, compact, or table formats +- 📏 **Adaptive Table Layout**: Automatically adjusts column widths for optimal display +- 🔍 **Mount Point Filtering**: Include or exclude specific mount points +- ⚡ **Fast and Lightweight**: Written in Rust for optimal performance +- 🎯 **Smart Filtering**: Automatically ignores overlay and snap mounts + +## Installation + +### From Source + +```bash +git clone https://github.com/{{owner}}/{{repo}}.git +cd ndf +cargo build --release +cargo install --path . +``` + +## Usage + +### Basic Usage + +```bash +# Default table mode +ndf + +# Specific display modes +ndf normal # Detailed view with mount points +ndf compact # One-line per disk +ndf table # Formatted table (default) +``` + +### Filtering Options + +```bash +# Show only specific mount points +ndf --only-mp "/" +ndf --only-mp "/,/home" + +# Exclude specific mount points +ndf --exclude-mp "/snap,/tmp" +``` + +### Help + +```bash +ndf --help +``` + +## Display Modes + +### Table Mode (Default) + +The table display mode is inspired by [duf](https://github.com/muesli/duf), providing a clean and organized view of disk information with adaptive column widths. + +```text +┌───────┬─────────┬─────────┬────────────────────────────────────────────────────────┬──────────────┐ +│ Mount │ Size │ Free │ Usage │ Name │ +├───────┼─────────┼─────────┼────────────────────────────────────────────────────────┼──────────────┤ +│ / │ 926.35G │ 303.61G │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░ 67% │ Macintosh HD │ +└───────┴─────────┴─────────┴────────────────────────────────────────────────────────┴──────────────┘ +``` + +### Normal Mode + +```text +Macintosh HD @ / +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░ 67% +``` + +### Compact Mode + +```text +Macintosh HD: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░ 67% +``` + +## Color Coding + +- 🟩 **Green**: Normal usage (< 80%) +- 🟥 **Red**: High usage (≥ 80%) + +## Command Line Options + +```text +Usage: ndf [OPTIONS] [mode] + +Arguments: + [mode] Display mode: normal | compact | table [default: table] + +Options: + --only-mp Show only specified mount points, comma separated + --exclude-mp Exclude specified mount points, comma separated + -h, --help Print help +``` + +## Requirements + +- Rust 1.70+ (for building from source) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Acknowledgments + +- Built with [clap](https://github.com/clap-rs/clap) for command-line parsing +- Uses [colored](https://github.com/mackwic/colored) for terminal colors +- System information provided by [sysinfo](https://github.com/GuillaumeGomez/sysinfo) From e48d1eab30a1fe829e2d9add1dac7f90cea1e0ef Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Fri, 18 Jul 2025 16:12:43 +0800 Subject: [PATCH 10/12] update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90fc548..c77ad68 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,17 @@ A modern, colorful disk usage utility written in Rust. `ndf` provides a clean an ### From Source +Clone this repository and build: + ```bash -git clone https://github.com/{{owner}}/{{repo}}.git +git clone https://github.com/kiobu/ndf.git cd ndf cargo build --release cargo install --path . ``` +Or use the "Clone" button above to get the current repository URL. + ## Usage ### Basic Usage From f29752e9405d4327e1fb72de0b4521b09fee489a Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Sun, 18 Jan 2026 21:23:50 +0800 Subject: [PATCH 11/12] mount point --- Cargo.toml | 2 + src/main.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f48859a..cbc405e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ edition = "2018" colored = "3" sysinfo = "0.35.1" clap = { version = "4", features = ["derive"] } +mountpoints = "0.2" +libc = "0.2" diff --git a/src/main.rs b/src/main.rs index fa3d69b..e9448d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,6 +80,46 @@ fn format_size(size: u64) -> String { } } +// Define a struct to hold disk usage information +struct DiskUsage { + total: u64, + free: u64, +} + +// Get disk usage for a given path using standard library +fn get_disk_usage_for_path(path: &str) -> std::io::Result { + use std::mem; + + #[cfg(unix)] + { + use std::ffi::CString; + + let c_path = CString::new(path.as_bytes()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?; + + let mut statvfs_buf: libc::statvfs = unsafe { mem::zeroed() }; + let result = unsafe { + libc::statvfs(c_path.as_ptr(), &mut statvfs_buf) + }; + + if result == 0 { + Ok(DiskUsage { + total: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_blocks as u64), + free: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_bavail as u64), + }) + } else { + Err(std::io::Error::last_os_error()) + } + } + + #[cfg(not(unix))] + { + // For non-Unix systems, we'll return an error for now + // In a full implementation, we would use platform-specific APIs like GetDiskFreeSpaceEx on Windows + Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "Platform not supported")) + } +} + fn main() { let matches = Command::new("ndf") .about("Nice disk free.") @@ -114,12 +154,27 @@ fn main() { .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); let mut disks: Vec = Vec::new(); + let mut processed_mounts = std::collections::HashSet::new(); + + // Process disks from sysinfo first for disk in Disks::new_with_refreshed_list().list() { let mnt = disk.mount_point().to_string_lossy(); - // ignore overlay and snap mounts - if disk.file_system() == "overlay" || mnt.starts_with("/var/snap/") { + processed_mounts.insert(mnt.to_string()); + + let fs_type = disk.file_system().to_string_lossy().to_string(); + + // ignore overlay, devfs and snap mounts, but allow CIFS/SMB mounts + if fs_type == "overlay" || fs_type == "devfs" || mnt.starts_with("/var/snap/") { continue; } + + // Skip system internal mount points that are not meaningful to users + if mnt.starts_with("/private/") || mnt.starts_with("/sys/") || + mnt.starts_with("/proc/") || mnt.starts_with("/run/") || + mnt.starts_with("/boot/") || mnt.starts_with("/var/") && mnt != "/var" { + continue; + } + if let Some(ref only) = only_mp { if !only.contains(mnt.as_ref()) { continue; @@ -133,6 +188,84 @@ fn main() { disks.push(NDFDisk::create_ndf_disk(disk)); } + // Now get all mount points using mountpoints crate to catch network mounts that sysinfo might miss + if let Ok(mount_paths) = mountpoints::mountpaths() { + for mount_path in mount_paths { + let mount_str = mount_path.to_string_lossy().to_string(); + + // Skip if already processed by sysinfo + if processed_mounts.contains(&mount_str) { + continue; + } + + // Attempt to get disk usage for this mount point + if let Ok(_metadata) = std::fs::metadata(&mount_path) { + if let Ok(usage) = get_disk_usage_for_path(&mount_str) { + + // Apply filters + if let Some(ref only) = only_mp { + if !only.contains(&mount_str) { + continue; + } + } + if let Some(ref exclude) = exclude_mp { + if exclude.contains(&mount_str) { + continue; + } + } + + // Skip system internal mount points + if mount_str.starts_with("/System/Volumes/") && mount_str != "/System/Volumes/Data" || + mount_str.starts_with("/dev/") || mount_str.starts_with("/private/") || + mount_str.starts_with("/sys/") || mount_str.starts_with("/proc/") || + mount_str.starts_with("/run/") || mount_str.starts_with("/boot/") || + mount_str.starts_with("/var/snap/") || mount_str.starts_with("/var/") && mount_str != "/var" { + continue; + } + + // Additionally, skip virtual filesystems that might be detected by mountpoints + // Check if the mount point is accessible and represents actual storage + if mount_str == "/dev" { + continue; // Skip /dev which is a virtual filesystem + } + + // Handle special cases for network mounts where usage stats might be unreliable + if usage.total == 0 && usage.free == 0 { + // Skip mount points that report zero bytes for both + } else if usage.total > 0 && usage.free <= usage.total { + // Regular case: total >= free + // Create an NDFDisk for this mount point + let frac = get_frac(usage.free, usage.total); + let mount_name = mount_str.split('/').last().unwrap_or("").to_string(); + disks.push(NDFDisk { + name: mount_name, + space_as_frac: frac, + mnt: mount_str.clone(), + size: usage.total, + free: usage.free, + }); + } else if usage.total > 0 && usage.free > usage.total { + // Special case: free > total (common with network shares) + // We'll try to get more accurate info by checking if the path is accessible + if std::path::Path::new(&mount_str).exists() { + // For network mounts where free > total, we'll show the total as reported + // but calculate usage differently - maybe show as low usage + let frac = 0.1; // Assume low usage if free > total + let mount_name = mount_str.split('/').last().unwrap_or("").to_string(); + disks.push(NDFDisk { + name: mount_name, + space_as_frac: frac, + mnt: mount_str.clone(), + size: usage.total, + free: usage.free, + }); + } + } + } + } + } + } + println!("{}", "ndf - nice disk free".bold()); match output_mode { From fb073fb441a31f1f32308c9d3e4705740c8e58af Mon Sep 17 00:00:00 2001 From: Ken Chou Date: Sun, 18 Jan 2026 23:37:32 +0800 Subject: [PATCH 12/12] fix --- src/main.rs | 122 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/src/main.rs b/src/main.rs index e9448d5..24c16db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,7 @@ fn format_size(size: u64) -> String { struct DiskUsage { total: u64, free: u64, + fs_type: u32, // File system type identifier (0 on non-Linux systems) } // Get disk usage for a given path using standard library @@ -97,18 +98,42 @@ fn get_disk_usage_for_path(path: &str) -> std::io::Result { let c_path = CString::new(path.as_bytes()) .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?; - let mut statvfs_buf: libc::statvfs = unsafe { mem::zeroed() }; - let result = unsafe { - libc::statvfs(c_path.as_ptr(), &mut statvfs_buf) - }; + // On Linux, we use statfs which provides filesystem type + #[cfg(target_os = "linux")] + { + let mut statfs_buf: libc::statfs = unsafe { mem::zeroed() }; + let result = unsafe { + libc::statfs(c_path.as_ptr(), &mut statfs_buf) + }; + + if result == 0 { + Ok(DiskUsage { + total: (statfs_buf.f_bsize as u64) * (statfs_buf.f_blocks as u64), + free: (statfs_buf.f_bsize as u64) * (statfs_buf.f_bavail as u64), + fs_type: statfs_buf.f_type as u32, + }) + } else { + Err(std::io::Error::last_os_error()) + } + } - if result == 0 { - Ok(DiskUsage { - total: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_blocks as u64), - free: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_bavail as u64), - }) - } else { - Err(std::io::Error::last_os_error()) + // On other Unix systems (like macOS), we use statvfs + #[cfg(not(target_os = "linux"))] + { + let mut statvfs_buf: libc::statvfs = unsafe { mem::zeroed() }; + let result = unsafe { + libc::statvfs(c_path.as_ptr(), &mut statvfs_buf) + }; + + if result == 0 { + Ok(DiskUsage { + total: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_blocks as u64), + free: (statvfs_buf.f_frsize as u64) * (statvfs_buf.f_bavail as u64), + fs_type: 0, // Not available on non-Linux systems + }) + } else { + Err(std::io::Error::last_os_error()) + } } } @@ -171,7 +196,8 @@ fn main() { // Skip system internal mount points that are not meaningful to users if mnt.starts_with("/private/") || mnt.starts_with("/sys/") || mnt.starts_with("/proc/") || mnt.starts_with("/run/") || - mnt.starts_with("/boot/") || mnt.starts_with("/var/") && mnt != "/var" { + mnt.starts_with("/boot/") || mnt.starts_with("/var/") && mnt != "/var" || + mnt.starts_with("/snap/") || mnt == "/run" { continue; } @@ -225,8 +251,38 @@ fn main() { // Additionally, skip virtual filesystems that might be detected by mountpoints // Check if the mount point is accessible and represents actual storage - if mount_str == "/dev" { - continue; // Skip /dev which is a virtual filesystem + if mount_str == "/dev" || mount_str.starts_with("/snap/") || mount_str == "/run" { + continue; // Skip /dev which is a virtual filesystem, snap packages, and /run + } + + // Define constants for filesystem types (from sys/magic.h on Linux) + const TMPFS_MAGIC: u32 = 0x01021994; + const DEVPTS_MAGIC: u32 = 0x1cd1; + const SYSFS_MAGIC: u32 = 0x62656572; + const PROC_MAGIC: u32 = 0x9fa0; + const DEVFS_MAGIC: u32 = 0x1373; + const RAMFS_MAGIC: u32 = 0x858458f6; + const SECURITYFS_MAGIC: u32 = 0x73636673; + const CGROUP_MAGIC: u32 = 0x27e0eb; + const CGROUP2_MAGIC: u32 = 0x63677270; + const OVERLAYFS_SUPER_MAGIC: u32 = 0x794c7630; + const FUSECTL_SUPER_MAGIC: u32 = 0x65735543; + + // Skip virtual filesystems based on filesystem type + if cfg!(target_os = "linux") && ( + usage.fs_type == TMPFS_MAGIC || + usage.fs_type == DEVPTS_MAGIC || + usage.fs_type == SYSFS_MAGIC || + usage.fs_type == PROC_MAGIC || + usage.fs_type == DEVFS_MAGIC || + usage.fs_type == RAMFS_MAGIC || + usage.fs_type == SECURITYFS_MAGIC || + usage.fs_type == CGROUP_MAGIC || + usage.fs_type == CGROUP2_MAGIC || + usage.fs_type == OVERLAYFS_SUPER_MAGIC || + usage.fs_type == FUSECTL_SUPER_MAGIC + ) { + continue; // Skip virtual filesystems } // Handle special cases for network mounts where usage stats might be unreliable @@ -237,13 +293,18 @@ fn main() { // Create an NDFDisk for this mount point let frac = get_frac(usage.free, usage.total); let mount_name = mount_str.split('/').last().unwrap_or("").to_string(); - disks.push(NDFDisk { - name: mount_name, - space_as_frac: frac, - mnt: mount_str.clone(), - size: usage.total, - free: usage.free, - }); + + // Check if this mount point is already added to avoid duplicates + if !processed_mounts.contains(&mount_str) { + disks.push(NDFDisk { + name: mount_name, + space_as_frac: frac, + mnt: mount_str.clone(), + size: usage.total, + free: usage.free, + }); + processed_mounts.insert(mount_str); + } } else if usage.total > 0 && usage.free > usage.total { // Special case: free > total (common with network shares) // We'll try to get more accurate info by checking if the path is accessible @@ -252,13 +313,18 @@ fn main() { // but calculate usage differently - maybe show as low usage let frac = 0.1; // Assume low usage if free > total let mount_name = mount_str.split('/').last().unwrap_or("").to_string(); - disks.push(NDFDisk { - name: mount_name, - space_as_frac: frac, - mnt: mount_str.clone(), - size: usage.total, - free: usage.free, - }); + + // Check if this mount point is already added to avoid duplicates + if !processed_mounts.contains(&mount_str) { + disks.push(NDFDisk { + name: mount_name, + space_as_frac: frac, + mnt: mount_str.clone(), + size: usage.total, + free: usage.free, + }); + processed_mounts.insert(mount_str); + } } } }