///|
fn html_escape(text : String) -> String {
text
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
.replace_all(old="'", new="'")
}
///|
fn change_kind_name(kind : ChangeKind) -> String {
match kind {
Added => "added"
Removed => "removed"
Grew => "grew"
Shrank => "shrank"
Modified => "modified"
KindChanged => "kind changed"
}
}
///|
pub fn render_summary(snapshot : Snapshot, limit? : Int = 10) -> String {
let lines : Array[String] = [
"MoonDisk scan summary",
"Root: " + snapshot.root,
"Files: " + snapshot.file_count().to_string(),
"Directories: " + snapshot.directory_count().to_string(),
"Total size: " + format_bytes(snapshot.total_size()),
"Errors: " + snapshot.errors.length().to_string(),
"",
"Largest files",
]
let total = snapshot.total_size()
for entry in largest_files(snapshot, limit) {
lines.push(
pad_left(format_bytes(entry.size), 12) +
" " +
pad_left(format_percent(entry.size, total), 7) +
" " +
entry.path,
)
}
lines.push("")
lines.push("Largest directories")
for stat in largest_directories(snapshot, limit) {
let name = if stat.path == "" { "." } else { stat.path }
lines.push(
pad_left(format_bytes(stat.recursive_size), 12) +
" " +
pad_left(format_percent(stat.recursive_size, total), 7) +
" " +
name,
)
}
lines.push("")
lines.push("File categories")
for stat in category_stats(snapshot) {
lines.push(
pad_left(format_bytes(stat.size), 12) +
" " +
pad_left(format_percent(stat.size, total), 7) +
" " +
pad_left(stat.files.to_string(), 7) +
" files " +
stat.name,
)
}
lines.join("\n")
}
///|
pub fn render_diff_summary(diff : SnapshotDiff, limit? : Int = 20) -> String {
let lines : Array[String] = [
"MoonDisk snapshot comparison",
"Added files: " +
diff.added_count().to_string() +
" (" +
format_bytes(diff.added_size) +
")",
"Removed files: " +
diff.removed_count().to_string() +
" (" +
format_bytes(diff.removed_size) +
")",
"Changed entries: " + diff.changed_count().to_string(),
"Net change: " + format_bytes(diff.net_size),
"",
"Largest changes",
]
let count = if limit < diff.changes.length() {
limit
} else {
diff.changes.length()
}
for i = 0; i < count; i = i + 1 {
let change = diff.changes[i]
lines.push(
pad_right(change_kind_name(change.kind), 12) +
" " +
pad_left(format_bytes(change.size_delta), 12) +
" " +
change.path,
)
}
lines.join("\n")
}
///|
pub fn render_plan(plan : CleanupPlan) -> String {
let lines : Array[String] = [
"MoonDisk cleanup plan",
"Root: " + plan.root,
"Quarantine: " + plan.quarantine_root,
"Candidates: " + plan.items.length().to_string(),
"Potentially reclaimable: " + format_bytes(plan.reclaimable_size),
"Plan issues: " + plan.issues.length().to_string(),
"",
]
for item in plan.items {
lines.push(
"[" +
risk_name(item.risk) +
"] " +
pad_left(format_bytes(item.expected_size), 12) +
" " +
item.source +
" — " +
item.reason,
)
}
if !plan.issues.is_empty() {
lines.push("")
lines.push("Issues requiring review")
for issue in plan.issues {
lines.push("- " + issue.path + ": " + issue.message)
}
}
lines.join("\n")
}
///|
pub fn render_tree(
snapshot : Snapshot,
max_depth? : Int = 4,
minimum_size? : Int64 = 0L,
) -> String {
let total = snapshot.total_size()
let lines : Array[String] = [". " + format_bytes(total) + " 100.0%"]
let stats = directory_stats(snapshot)
stats.sort_by(fn(a, b) {
if a.path < b.path {
-1
} else if a.path > b.path {
1
} else {
0
}
})
for stat in stats {
if stat.path == "" ||
path_depth(stat.path) > max_depth ||
stat.recursive_size < minimum_size {
continue
}
let depth = path_depth(stat.path)
let branch = " ".repeat(depth - 1) + "└─ "
lines.push(
branch +
basename(stat.path) +
"/ " +
format_bytes(stat.recursive_size) +
" " +
format_percent(stat.recursive_size, total),
)
}
lines.join("\n")
}
///|
pub fn render_html_report(
snapshot : Snapshot,
title? : String = "MoonDisk report",
) -> String {
let total = snapshot.total_size()
let rows : Array[String] = []
for entry in largest_files(snapshot, 100) {
rows.push(
"| " +
html_escape(entry.path) +
" | " +
html_escape(format_bytes(entry.size)) +
" | " +
html_escape(format_percent(entry.size, total)) +
" |
",
)
}
let directory_rows : Array[String] = []
for stat in largest_directories(snapshot, 100) {
let path = if stat.path == "" { "." } else { stat.path }
directory_rows.push(
"| " +
html_escape(path) +
" | " +
html_escape(format_bytes(stat.recursive_size)) +
" | " +
stat.recursive_files.to_string() +
" |
",
)
}
let category_rows : Array[String] = []
for stat in category_stats(snapshot) {
category_rows.push(
"| " +
html_escape(stat.name) +
" | " +
html_escape(format_bytes(stat.size)) +
" | " +
html_escape(format_percent(stat.size, total)) +
" | " +
stat.files.to_string() +
" |
",
)
}
let insight_rows : Array[String] = []
for insight in storage_insights(snapshot, snapshot.created_at) {
insight_rows.push(
"[" +
html_escape(severity_name(insight.severity)) +
"] " +
html_escape(insight.title) +
"" +
html_escape(insight.detail) +
"
",
)
}
let insight_section = if insight_rows.is_empty() {
"No notable storage patterns were detected.
"
} else {
insight_rows.join("")
}
"\n" +
"" +
"" +
html_escape(title) +
"" +
html_escape(title) +
"
" +
"Scan root: " +
html_escape(snapshot.root) +
"
" +
"Total size
" +
html_escape(format_bytes(total)) +
"
Files
" +
snapshot.file_count().to_string() +
"
Scan errors
" +
snapshot.errors.length().to_string() +
"
" +
"Largest files
| Path | Size | Share |
" +
rows.join("") +
"
" +
"Largest directories
| Path | Size | Files |
" +
directory_rows.join("") +
"
" +
"File categories
| Category | Size | Share | Files |
" +
category_rows.join("") +
"
" +
"Storage insights
" +
insight_section +
""
}