1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
use std::io;
use std::path::Path;
use std::time::Instant;
use acme_disk_use::{format_size, tui, DiskUse};
use clap::{Parser, Subcommand};
/// Format bytes into du-compatible human-readable format (e.g., 1K, 234M, 2G)
fn format_size_du(bytes: u64) -> String {
const UNITS: &[&str] = &["", "K", "M", "G", "T", "P"];
const THRESHOLD: f64 = 1024.0;
if bytes == 0 {
return "0".to_string();
}
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= THRESHOLD && unit_index < UNITS.len() - 1 {
size /= THRESHOLD;
unit_index += 1;
}
if unit_index == 0 {
format!("{}", bytes)
} else if size >= 10.0 {
// For larger values, show integer (like du)
format!("{:.0}{}", size, UNITS[unit_index])
} else {
// For smaller values, show one decimal (like du)
format!("{:.1}{}", size, UNITS[unit_index])
}
}
#[derive(Parser)]
#[command(name = "acme-disk-use")]
#[command(about = "A disk usage analyzer with caching support (du-compatible interface)")]
#[command(version = "0.1.0")]
#[command(disable_help_flag = true)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
/// Directory to analyze (defaults to current directory)
#[arg(value_name = "PATH")]
path: Option<String>,
/// Print sizes in human readable format (e.g., 1K 234M 2G)
#[arg(short = 'h', long)]
human_readable: bool,
/// Display only a total for each argument (du compatibility, this is already the default behavior)
#[arg(short = 's', long)]
#[allow(dead_code)]
summarize: bool,
/// Equivalent to '--apparent-size --block-size=1' (show raw bytes)
#[arg(short = 'b', long)]
bytes: bool,
/// Ignore cache and scan fresh
#[arg(long)]
ignore_cache: bool,
/// Show timing statistics and file count
#[arg(long)]
stats: bool,
}
#[derive(Subcommand)]
enum Commands {
/// Clean the cache contents
Clean,
/// Cache management commands
Cache {
#[command(subcommand)]
action: CacheCommands,
},
}
#[derive(Subcommand)]
enum CacheCommands {
/// Display an interactive TUI showing cached directory sizes (similar to ncdu)
Show {
/// Optional path to show (if omitted, shows all cached roots)
#[arg(value_name = "PATH")]
path: Option<String>,
},
}
fn main() -> io::Result<()> {
let cli = Cli::parse();
let mut disk_use = DiskUse::new_with_default_cache();
match cli.command {
Some(Commands::Clean) => match disk_use.clear_cache() {
Ok(_) => {
println!("Cache cleared successfully.");
Ok(())
}
Err(err) => {
eprintln!("Error: Failed to clear cache: {}", err);
std::process::exit(1);
}
},
Some(Commands::Cache { action }) => match action {
CacheCommands::Show { path } => {
if disk_use.is_cache_empty() {
eprintln!("Error: Cache is empty. Run a scan first to populate the cache.");
std::process::exit(1);
}
if let Some(path_str) = path {
// Show specific path from cache
let path = Path::new(&path_str);
match disk_use.get_stats(path) {
Some(stat) => {
if let Err(err) = tui::run_tui(stat) {
eprintln!("Error: Failed to run TUI: {}", err);
std::process::exit(1);
}
}
None => {
eprintln!(
"Error: Path '{}' not found in cache. Run a scan on this path first.",
path_str
);
std::process::exit(1);
}
}
} else {
// Show all cached roots
let roots = disk_use.get_cached_roots();
if let Err(err) = tui::run_tui_with_roots(roots) {
eprintln!("Error: Failed to run TUI: {}", err);
std::process::exit(1);
}
}
Ok(())
}
},
None => {
// Default scan command
let path = cli.path.as_deref().unwrap_or(".");
if !Path::new(path).exists() {
eprintln!("Error: Path '{}' does not exist", path);
std::process::exit(1);
}
// Start timing the scan
let start_time = Instant::now();
// Scan the directory with appropriate options
let total_size = match disk_use.scan_with_options(path, cli.ignore_cache) {
Ok(size) => size,
Err(err) => {
eprintln!("Error: {}", err);
std::process::exit(1);
}
};
// Determine output format: -b means raw bytes, -h means human-readable
// If neither specified, default to 1K blocks like du
let human_readable = cli.human_readable;
let show_bytes = cli.bytes;
// Format output in du-compatible style: SIZE\tPATH
let size_str = if show_bytes {
// -b: show raw bytes
format!("{}", total_size)
} else if human_readable {
// -h: human readable format (e.g., 1K 234M 2G)
format_size_du(total_size)
} else {
// Default: 1K blocks (like du default)
let kb = total_size.div_ceil(1024);
format!("{}", kb)
};
// Calculate elapsed time
let elapsed = start_time.elapsed();
// Format output based on user preference
if cli.stats {
// Get file count for stats
let file_count = disk_use
.get_file_count(path, cli.ignore_cache)
.unwrap_or_default();
let elapsed_secs = elapsed.as_secs_f64();
// Use a small epsilon to avoid division by zero for extremely fast scans
let files_per_sec = file_count as f64 / elapsed_secs.max(f64::MIN_POSITIVE);
println!(
"Found {} files, total size: {} (scanned in {:.2}s, {:.0} files/s)",
file_count,
format_size(total_size, true),
elapsed_secs,
files_per_sec
);
} else {
println!("{}\t{}", size_str, path);
}
// Explicitly save cache before exiting (Drop will save too, but be explicit)
if !cli.ignore_cache {
if let Err(err) = disk_use.save_cache() {
eprintln!("Warning: Failed to save cache: {}", err);
}
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_size_du() {
// Test zero bytes
assert_eq!(format_size_du(0), "0");
// Test bytes (no unit)
assert_eq!(format_size_du(512), "512");
assert_eq!(format_size_du(1023), "1023");
// Test kilobytes
assert_eq!(format_size_du(1024), "1.0K");
assert_eq!(format_size_du(1536), "1.5K");
assert_eq!(format_size_du(10240), "10K");
assert_eq!(format_size_du(102400), "100K");
// Test megabytes
assert_eq!(format_size_du(1024 * 1024), "1.0M");
assert_eq!(format_size_du(10 * 1024 * 1024), "10M");
// Test gigabytes
assert_eq!(format_size_du(1024 * 1024 * 1024), "1.0G");
assert_eq!(format_size_du(10 * 1024 * 1024 * 1024), "10G");
// Test terabytes
assert_eq!(format_size_du(1024_u64 * 1024 * 1024 * 1024), "1.0T");
}
}