mirror of
https://github.com/tw93/Mole.git
synced 2026-02-10 22:59:17 +00:00
optimize code structure and reduce duplication
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ copilot-instructions.md
|
|||||||
cmd/analyze/analyze
|
cmd/analyze/analyze
|
||||||
cmd/status/status
|
cmd/status/status
|
||||||
/status
|
/status
|
||||||
|
bin/analyze-go
|
||||||
|
bin/status-go
|
||||||
|
mole-analyze
|
||||||
|
|||||||
BIN
bin/analyze-go
BIN
bin/analyze-go
Binary file not shown.
@@ -258,7 +258,7 @@ safe_clean() {
|
|||||||
for path in "${existing_paths[@]}"; do
|
for path in "${existing_paths[@]}"; do
|
||||||
(
|
(
|
||||||
local size
|
local size
|
||||||
size=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
|
size=$(get_path_size_kb "$path")
|
||||||
local count
|
local count
|
||||||
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
||||||
# Use index + PID for unique filename
|
# Use index + PID for unique filename
|
||||||
@@ -317,7 +317,7 @@ safe_clean() {
|
|||||||
|
|
||||||
for path in "${existing_paths[@]}"; do
|
for path in "${existing_paths[@]}"; do
|
||||||
local size_bytes
|
local size_bytes
|
||||||
size_bytes=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_bytes=$(get_path_size_kb "$path")
|
||||||
local count
|
local count
|
||||||
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
@@ -718,7 +718,7 @@ perform_cleanup() {
|
|||||||
if is_orphaned "$bundle_id" "$match"; then
|
if is_orphaned "$bundle_id" "$match"; then
|
||||||
# Use timeout to prevent du from hanging on large/problematic directories
|
# Use timeout to prevent du from hanging on large/problematic directories
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(run_with_timeout 2 du -sk "$match" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(run_with_timeout 2 get_path_size_kb "$match")
|
||||||
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ cleanup_path() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(du -sk "$expanded_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(get_path_size_kb "$expanded_path")
|
||||||
local size_display=""
|
local size_display=""
|
||||||
if [[ "$size_kb" =~ ^[0-9]+$ && "$size_kb" -gt 0 ]]; then
|
if [[ "$size_kb" =~ ^[0-9]+$ && "$size_kb" -gt 0 ]]; then
|
||||||
size_display=$(bytes_to_human "$((size_kb * 1024))")
|
size_display=$(bytes_to_human "$((size_kb * 1024))")
|
||||||
|
|||||||
BIN
bin/status-go
BIN
bin/status-go
Binary file not shown.
@@ -217,8 +217,8 @@ scan_applications() {
|
|||||||
local app_size="N/A"
|
local app_size="N/A"
|
||||||
local app_size_kb="0"
|
local app_size_kb="0"
|
||||||
if [[ -d "$app_path" ]]; then
|
if [[ -d "$app_path" ]]; then
|
||||||
# Get size in KB, then format for display (single du call)
|
# Get size in KB, then format for display
|
||||||
app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
app_size_kb=$(get_path_size_kb "$app_path")
|
||||||
app_size=$(bytes_to_human "$((app_size_kb * 1024))")
|
app_size=$(bytes_to_human "$((app_size_kb * 1024))")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -789,344 +789,6 @@ func (m model) enterSelectedDir() (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
|
||||||
var b strings.Builder
|
|
||||||
fmt.Fprintln(&b)
|
|
||||||
|
|
||||||
if m.inOverviewMode() {
|
|
||||||
fmt.Fprintf(&b, "%sAnalyze Disk%s\n", colorPurpleBold, colorReset)
|
|
||||||
if m.overviewScanning {
|
|
||||||
// Check if we're in initial scan (all entries are pending)
|
|
||||||
allPending := true
|
|
||||||
for _, entry := range m.entries {
|
|
||||||
if entry.Size >= 0 {
|
|
||||||
allPending = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if allPending {
|
|
||||||
// Show prominent loading screen for initial scan
|
|
||||||
fmt.Fprintf(&b, "%s%s%s%s Analyzing disk usage, please wait...%s\n",
|
|
||||||
colorCyan, colorBold,
|
|
||||||
spinnerFrames[m.spinner],
|
|
||||||
colorReset, colorReset)
|
|
||||||
return b.String()
|
|
||||||
} else {
|
|
||||||
// Progressive scanning - show subtle indicator
|
|
||||||
fmt.Fprintf(&b, "%sSelect a location to explore:%s ", colorGray, colorReset)
|
|
||||||
fmt.Fprintf(&b, "%s%s%s%s Scanning...\n\n", colorCyan, colorBold, spinnerFrames[m.spinner], colorReset)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if there are still pending items
|
|
||||||
hasPending := false
|
|
||||||
for _, entry := range m.entries {
|
|
||||||
if entry.Size < 0 {
|
|
||||||
hasPending = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasPending {
|
|
||||||
fmt.Fprintf(&b, "%sSelect a location to explore:%s ", colorGray, colorReset)
|
|
||||||
fmt.Fprintf(&b, "%s%s%s%s Scanning...\n\n", colorCyan, colorBold, spinnerFrames[m.spinner], colorReset)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%sSelect a location to explore:%s\n\n", colorGray, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%sAnalyze Disk%s %s%s%s", colorPurpleBold, colorReset, colorGray, displayPath(m.path), colorReset)
|
|
||||||
if !m.scanning {
|
|
||||||
fmt.Fprintf(&b, " | Total: %s", humanizeBytes(m.totalSize))
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&b, "\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.deleting {
|
|
||||||
// Show delete progress
|
|
||||||
count := int64(0)
|
|
||||||
if m.deleteCount != nil {
|
|
||||||
count = atomic.LoadInt64(m.deleteCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(&b, "%s%s%s%s Deleting: %s%s items%s removed, please wait...\n",
|
|
||||||
colorCyan, colorBold,
|
|
||||||
spinnerFrames[m.spinner],
|
|
||||||
colorReset,
|
|
||||||
colorYellow, formatNumber(count), colorReset)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.scanning {
|
|
||||||
filesScanned, dirsScanned, bytesScanned := m.getScanProgress()
|
|
||||||
|
|
||||||
fmt.Fprintf(&b, "%s%s%s%s Scanning: %s%s files%s, %s%s dirs%s, %s%s%s\n",
|
|
||||||
colorCyan, colorBold,
|
|
||||||
spinnerFrames[m.spinner],
|
|
||||||
colorReset,
|
|
||||||
colorYellow, formatNumber(filesScanned), colorReset,
|
|
||||||
colorYellow, formatNumber(dirsScanned), colorReset,
|
|
||||||
colorGreen, humanizeBytes(bytesScanned), colorReset)
|
|
||||||
|
|
||||||
if m.currentPath != nil {
|
|
||||||
currentPath := *m.currentPath
|
|
||||||
if currentPath != "" {
|
|
||||||
shortPath := displayPath(currentPath)
|
|
||||||
shortPath = truncateMiddle(shortPath, 50)
|
|
||||||
fmt.Fprintf(&b, "%s%s%s\n", colorGray, shortPath, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.showLargeFiles {
|
|
||||||
if len(m.largeFiles) == 0 {
|
|
||||||
fmt.Fprintln(&b, " No large files found (>=100MB)")
|
|
||||||
} else {
|
|
||||||
viewport := calculateViewport(m.height, true)
|
|
||||||
start := m.largeOffset
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
end := start + viewport
|
|
||||||
if end > len(m.largeFiles) {
|
|
||||||
end = len(m.largeFiles)
|
|
||||||
}
|
|
||||||
maxLargeSize := int64(1)
|
|
||||||
for _, file := range m.largeFiles {
|
|
||||||
if file.Size > maxLargeSize {
|
|
||||||
maxLargeSize = file.Size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for idx := start; idx < end; idx++ {
|
|
||||||
file := m.largeFiles[idx]
|
|
||||||
shortPath := displayPath(file.Path)
|
|
||||||
shortPath = truncateMiddle(shortPath, 35)
|
|
||||||
paddedPath := padName(shortPath, 35)
|
|
||||||
entryPrefix := " "
|
|
||||||
nameColor := ""
|
|
||||||
sizeColor := colorGray
|
|
||||||
numColor := ""
|
|
||||||
if idx == m.largeSelected {
|
|
||||||
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
|
||||||
nameColor = colorCyan
|
|
||||||
sizeColor = colorCyan
|
|
||||||
numColor = colorCyan
|
|
||||||
}
|
|
||||||
size := humanizeBytes(file.Size)
|
|
||||||
bar := coloredProgressBar(file.Size, maxLargeSize, 0)
|
|
||||||
fmt.Fprintf(&b, "%s%s%2d.%s %s | 📄 %s%s%s %s%10s%s\n",
|
|
||||||
entryPrefix, numColor, idx+1, colorReset, bar, nameColor, paddedPath, colorReset, sizeColor, size, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(m.entries) == 0 {
|
|
||||||
fmt.Fprintln(&b, " Empty directory")
|
|
||||||
} else {
|
|
||||||
if m.inOverviewMode() {
|
|
||||||
maxSize := int64(1)
|
|
||||||
for _, entry := range m.entries {
|
|
||||||
if entry.Size > maxSize {
|
|
||||||
maxSize = entry.Size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalSize := m.totalSize
|
|
||||||
for idx, entry := range m.entries {
|
|
||||||
icon := "📁"
|
|
||||||
sizeVal := entry.Size
|
|
||||||
barValue := sizeVal
|
|
||||||
if barValue < 0 {
|
|
||||||
barValue = 0
|
|
||||||
}
|
|
||||||
var percent float64
|
|
||||||
if totalSize > 0 && sizeVal >= 0 {
|
|
||||||
percent = float64(sizeVal) / float64(totalSize) * 100
|
|
||||||
} else {
|
|
||||||
percent = 0
|
|
||||||
}
|
|
||||||
percentStr := fmt.Sprintf("%5.1f%%", percent)
|
|
||||||
if totalSize == 0 || sizeVal < 0 {
|
|
||||||
percentStr = " -- "
|
|
||||||
}
|
|
||||||
bar := coloredProgressBar(barValue, maxSize, percent)
|
|
||||||
sizeText := "pending.."
|
|
||||||
if sizeVal >= 0 {
|
|
||||||
sizeText = humanizeBytes(sizeVal)
|
|
||||||
}
|
|
||||||
sizeColor := colorGray
|
|
||||||
if sizeVal >= 0 && totalSize > 0 {
|
|
||||||
switch {
|
|
||||||
case percent >= 50:
|
|
||||||
sizeColor = colorRed
|
|
||||||
case percent >= 20:
|
|
||||||
sizeColor = colorYellow
|
|
||||||
case percent >= 5:
|
|
||||||
sizeColor = colorBlue
|
|
||||||
default:
|
|
||||||
sizeColor = colorGray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entryPrefix := " "
|
|
||||||
name := trimName(entry.Name)
|
|
||||||
paddedName := padName(name, 28)
|
|
||||||
nameSegment := fmt.Sprintf("%s %s", icon, paddedName)
|
|
||||||
numColor := ""
|
|
||||||
percentColor := ""
|
|
||||||
if idx == m.selected {
|
|
||||||
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
|
||||||
nameSegment = fmt.Sprintf("%s%s %s%s", colorCyan, icon, paddedName, colorReset)
|
|
||||||
numColor = colorCyan
|
|
||||||
percentColor = colorCyan
|
|
||||||
sizeColor = colorCyan
|
|
||||||
}
|
|
||||||
displayIndex := idx + 1
|
|
||||||
|
|
||||||
// Priority: cleanable > unused time
|
|
||||||
var hintLabel string
|
|
||||||
if entry.IsDir && isCleanableDir(entry.Path) {
|
|
||||||
hintLabel = fmt.Sprintf("%s🧹%s", colorYellow, colorReset)
|
|
||||||
} else {
|
|
||||||
// For overview mode, get access time on-demand if not set
|
|
||||||
lastAccess := entry.LastAccess
|
|
||||||
if lastAccess.IsZero() && entry.Path != "" {
|
|
||||||
lastAccess = getLastAccessTime(entry.Path)
|
|
||||||
}
|
|
||||||
if unusedTime := formatUnusedTime(lastAccess); unusedTime != "" {
|
|
||||||
hintLabel = fmt.Sprintf("%s%s%s", colorGray, unusedTime, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hintLabel == "" {
|
|
||||||
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s\n",
|
|
||||||
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
|
||||||
nameSegment, sizeColor, sizeText, colorReset)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s %s\n",
|
|
||||||
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
|
||||||
nameSegment, sizeColor, sizeText, colorReset, hintLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal mode with sizes and progress bars
|
|
||||||
maxSize := int64(1)
|
|
||||||
for _, entry := range m.entries {
|
|
||||||
if entry.Size > maxSize {
|
|
||||||
maxSize = entry.Size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport := calculateViewport(m.height, false)
|
|
||||||
start := m.offset
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
end := start + viewport
|
|
||||||
if end > len(m.entries) {
|
|
||||||
end = len(m.entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx := start; idx < end; idx++ {
|
|
||||||
entry := m.entries[idx]
|
|
||||||
icon := "📄"
|
|
||||||
if entry.IsDir {
|
|
||||||
icon = "📁"
|
|
||||||
}
|
|
||||||
size := humanizeBytes(entry.Size)
|
|
||||||
name := trimName(entry.Name)
|
|
||||||
paddedName := padName(name, 28)
|
|
||||||
|
|
||||||
// Calculate percentage
|
|
||||||
percent := float64(entry.Size) / float64(m.totalSize) * 100
|
|
||||||
percentStr := fmt.Sprintf("%5.1f%%", percent)
|
|
||||||
|
|
||||||
// Get colored progress bar
|
|
||||||
bar := coloredProgressBar(entry.Size, maxSize, percent)
|
|
||||||
|
|
||||||
// Color the size based on magnitude
|
|
||||||
var sizeColor string
|
|
||||||
if percent >= 50 {
|
|
||||||
sizeColor = colorRed
|
|
||||||
} else if percent >= 20 {
|
|
||||||
sizeColor = colorYellow
|
|
||||||
} else if percent >= 5 {
|
|
||||||
sizeColor = colorBlue
|
|
||||||
} else {
|
|
||||||
sizeColor = colorGray
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep chart columns aligned even when arrow is shown
|
|
||||||
entryPrefix := " "
|
|
||||||
nameSegment := fmt.Sprintf("%s %s", icon, paddedName)
|
|
||||||
numColor := ""
|
|
||||||
percentColor := ""
|
|
||||||
if idx == m.selected {
|
|
||||||
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
|
||||||
nameSegment = fmt.Sprintf("%s%s %s%s", colorCyan, icon, paddedName, colorReset)
|
|
||||||
numColor = colorCyan
|
|
||||||
percentColor = colorCyan
|
|
||||||
sizeColor = colorCyan
|
|
||||||
}
|
|
||||||
|
|
||||||
displayIndex := idx + 1
|
|
||||||
|
|
||||||
// Priority: cleanable > unused time
|
|
||||||
var hintLabel string
|
|
||||||
if entry.IsDir && isCleanableDir(entry.Path) {
|
|
||||||
hintLabel = fmt.Sprintf("%s🧹%s", colorYellow, colorReset)
|
|
||||||
} else {
|
|
||||||
// Get access time on-demand if not set
|
|
||||||
lastAccess := entry.LastAccess
|
|
||||||
if lastAccess.IsZero() && entry.Path != "" {
|
|
||||||
lastAccess = getLastAccessTime(entry.Path)
|
|
||||||
}
|
|
||||||
if unusedTime := formatUnusedTime(lastAccess); unusedTime != "" {
|
|
||||||
hintLabel = fmt.Sprintf("%s%s%s", colorGray, unusedTime, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hintLabel == "" {
|
|
||||||
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s\n",
|
|
||||||
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
|
||||||
nameSegment, sizeColor, size, colorReset)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s %s\n",
|
|
||||||
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
|
||||||
nameSegment, sizeColor, size, colorReset, hintLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(&b)
|
|
||||||
if m.inOverviewMode() {
|
|
||||||
// Show ← Back if there's history (entered from a parent directory)
|
|
||||||
if len(m.history) > 0 {
|
|
||||||
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ← Back | Q Quit%s\n", colorGray, colorReset)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%s↑↓→ | Enter | R Refresh | O Open | F Show | Q Quit%s\n", colorGray, colorReset)
|
|
||||||
}
|
|
||||||
} else if m.showLargeFiles {
|
|
||||||
fmt.Fprintf(&b, "%s↑↓← | R Refresh | O Open | F Show | ⌫ Delete | ← Back | Q Quit%s\n", colorGray, colorReset)
|
|
||||||
} else {
|
|
||||||
largeFileCount := len(m.largeFiles)
|
|
||||||
if largeFileCount > 0 {
|
|
||||||
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ⌫ Delete | T Top(%d) | Q Quit%s\n", colorGray, largeFileCount, colorReset)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ⌫ Delete | Q Quit%s\n", colorGray, colorReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.deleteConfirm && m.deleteTarget != nil {
|
|
||||||
fmt.Fprintln(&b)
|
|
||||||
fmt.Fprintf(&b, "%sDelete:%s %s (%s) %sPress ⌫ again | ESC cancel%s\n",
|
|
||||||
colorRed, colorReset,
|
|
||||||
m.deleteTarget.Name, humanizeBytes(m.deleteTarget.Size),
|
|
||||||
colorGray, colorReset)
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) clampEntrySelection() {
|
func (m *model) clampEntrySelection() {
|
||||||
if len(m.entries) == 0 {
|
if len(m.entries) == 0 {
|
||||||
m.selected = 0
|
m.selected = 0
|
||||||
@@ -1256,29 +918,3 @@ func scanOverviewPathCmd(path string, index int) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateViewport dynamically calculates the viewport size based on terminal height
|
|
||||||
func calculateViewport(termHeight int, isLargeFiles bool) int {
|
|
||||||
if termHeight <= 0 {
|
|
||||||
// Terminal height unknown, use default
|
|
||||||
return defaultViewport
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate reserved space for UI elements
|
|
||||||
reserved := 6 // header (3-4 lines) + footer (2 lines)
|
|
||||||
if isLargeFiles {
|
|
||||||
reserved = 5 // Large files view has less overhead
|
|
||||||
}
|
|
||||||
|
|
||||||
available := termHeight - reserved
|
|
||||||
|
|
||||||
// Ensure minimum and maximum bounds
|
|
||||||
if available < 1 {
|
|
||||||
return 1 // Minimum 1 line for very short terminals
|
|
||||||
}
|
|
||||||
if available > 30 {
|
|
||||||
return 30 // Maximum 30 lines to avoid information overload
|
|
||||||
}
|
|
||||||
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|||||||
374
cmd/analyze/view.go
Normal file
374
cmd/analyze/view.go
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// View renders the TUI display.
|
||||||
|
func (m model) View() string {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
|
||||||
|
if m.inOverviewMode() {
|
||||||
|
fmt.Fprintf(&b, "%sAnalyze Disk%s\n", colorPurpleBold, colorReset)
|
||||||
|
if m.overviewScanning {
|
||||||
|
// Check if we're in initial scan (all entries are pending)
|
||||||
|
allPending := true
|
||||||
|
for _, entry := range m.entries {
|
||||||
|
if entry.Size >= 0 {
|
||||||
|
allPending = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allPending {
|
||||||
|
// Show prominent loading screen for initial scan
|
||||||
|
fmt.Fprintf(&b, "%s%s%s%s Analyzing disk usage, please wait...%s\n",
|
||||||
|
colorCyan, colorBold,
|
||||||
|
spinnerFrames[m.spinner],
|
||||||
|
colorReset, colorReset)
|
||||||
|
return b.String()
|
||||||
|
} else {
|
||||||
|
// Progressive scanning - show subtle indicator
|
||||||
|
fmt.Fprintf(&b, "%sSelect a location to explore:%s ", colorGray, colorReset)
|
||||||
|
fmt.Fprintf(&b, "%s%s%s%s Scanning...\n\n", colorCyan, colorBold, spinnerFrames[m.spinner], colorReset)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if there are still pending items
|
||||||
|
hasPending := false
|
||||||
|
for _, entry := range m.entries {
|
||||||
|
if entry.Size < 0 {
|
||||||
|
hasPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasPending {
|
||||||
|
fmt.Fprintf(&b, "%sSelect a location to explore:%s ", colorGray, colorReset)
|
||||||
|
fmt.Fprintf(&b, "%s%s%s%s Scanning...\n\n", colorCyan, colorBold, spinnerFrames[m.spinner], colorReset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%sSelect a location to explore:%s\n\n", colorGray, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%sAnalyze Disk%s %s%s%s", colorPurpleBold, colorReset, colorGray, displayPath(m.path), colorReset)
|
||||||
|
if !m.scanning {
|
||||||
|
fmt.Fprintf(&b, " | Total: %s", humanizeBytes(m.totalSize))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.deleting {
|
||||||
|
// Show delete progress
|
||||||
|
count := int64(0)
|
||||||
|
if m.deleteCount != nil {
|
||||||
|
count = atomic.LoadInt64(m.deleteCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, "%s%s%s%s Deleting: %s%s items%s removed, please wait...\n",
|
||||||
|
colorCyan, colorBold,
|
||||||
|
spinnerFrames[m.spinner],
|
||||||
|
colorReset,
|
||||||
|
colorYellow, formatNumber(count), colorReset)
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.scanning {
|
||||||
|
filesScanned, dirsScanned, bytesScanned := m.getScanProgress()
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, "%s%s%s%s Scanning: %s%s files%s, %s%s dirs%s, %s%s%s\n",
|
||||||
|
colorCyan, colorBold,
|
||||||
|
spinnerFrames[m.spinner],
|
||||||
|
colorReset,
|
||||||
|
colorYellow, formatNumber(filesScanned), colorReset,
|
||||||
|
colorYellow, formatNumber(dirsScanned), colorReset,
|
||||||
|
colorGreen, humanizeBytes(bytesScanned), colorReset)
|
||||||
|
|
||||||
|
if m.currentPath != nil {
|
||||||
|
currentPath := *m.currentPath
|
||||||
|
if currentPath != "" {
|
||||||
|
shortPath := displayPath(currentPath)
|
||||||
|
shortPath = truncateMiddle(shortPath, 50)
|
||||||
|
fmt.Fprintf(&b, "%s%s%s\n", colorGray, shortPath, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.showLargeFiles {
|
||||||
|
if len(m.largeFiles) == 0 {
|
||||||
|
fmt.Fprintln(&b, " No large files found (>=100MB)")
|
||||||
|
} else {
|
||||||
|
viewport := calculateViewport(m.height, true)
|
||||||
|
start := m.largeOffset
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
end := start + viewport
|
||||||
|
if end > len(m.largeFiles) {
|
||||||
|
end = len(m.largeFiles)
|
||||||
|
}
|
||||||
|
maxLargeSize := int64(1)
|
||||||
|
for _, file := range m.largeFiles {
|
||||||
|
if file.Size > maxLargeSize {
|
||||||
|
maxLargeSize = file.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for idx := start; idx < end; idx++ {
|
||||||
|
file := m.largeFiles[idx]
|
||||||
|
shortPath := displayPath(file.Path)
|
||||||
|
shortPath = truncateMiddle(shortPath, 35)
|
||||||
|
paddedPath := padName(shortPath, 35)
|
||||||
|
entryPrefix := " "
|
||||||
|
nameColor := ""
|
||||||
|
sizeColor := colorGray
|
||||||
|
numColor := ""
|
||||||
|
if idx == m.largeSelected {
|
||||||
|
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
||||||
|
nameColor = colorCyan
|
||||||
|
sizeColor = colorCyan
|
||||||
|
numColor = colorCyan
|
||||||
|
}
|
||||||
|
size := humanizeBytes(file.Size)
|
||||||
|
bar := coloredProgressBar(file.Size, maxLargeSize, 0)
|
||||||
|
fmt.Fprintf(&b, "%s%s%2d.%s %s | 📄 %s%s%s %s%10s%s\n",
|
||||||
|
entryPrefix, numColor, idx+1, colorReset, bar, nameColor, paddedPath, colorReset, sizeColor, size, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(m.entries) == 0 {
|
||||||
|
fmt.Fprintln(&b, " Empty directory")
|
||||||
|
} else {
|
||||||
|
if m.inOverviewMode() {
|
||||||
|
maxSize := int64(1)
|
||||||
|
for _, entry := range m.entries {
|
||||||
|
if entry.Size > maxSize {
|
||||||
|
maxSize = entry.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalSize := m.totalSize
|
||||||
|
for idx, entry := range m.entries {
|
||||||
|
icon := "📁"
|
||||||
|
sizeVal := entry.Size
|
||||||
|
barValue := sizeVal
|
||||||
|
if barValue < 0 {
|
||||||
|
barValue = 0
|
||||||
|
}
|
||||||
|
var percent float64
|
||||||
|
if totalSize > 0 && sizeVal >= 0 {
|
||||||
|
percent = float64(sizeVal) / float64(totalSize) * 100
|
||||||
|
} else {
|
||||||
|
percent = 0
|
||||||
|
}
|
||||||
|
percentStr := fmt.Sprintf("%5.1f%%", percent)
|
||||||
|
if totalSize == 0 || sizeVal < 0 {
|
||||||
|
percentStr = " -- "
|
||||||
|
}
|
||||||
|
bar := coloredProgressBar(barValue, maxSize, percent)
|
||||||
|
sizeText := "pending.."
|
||||||
|
if sizeVal >= 0 {
|
||||||
|
sizeText = humanizeBytes(sizeVal)
|
||||||
|
}
|
||||||
|
sizeColor := colorGray
|
||||||
|
if sizeVal >= 0 && totalSize > 0 {
|
||||||
|
switch {
|
||||||
|
case percent >= 50:
|
||||||
|
sizeColor = colorRed
|
||||||
|
case percent >= 20:
|
||||||
|
sizeColor = colorYellow
|
||||||
|
case percent >= 5:
|
||||||
|
sizeColor = colorBlue
|
||||||
|
default:
|
||||||
|
sizeColor = colorGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entryPrefix := " "
|
||||||
|
name := trimName(entry.Name)
|
||||||
|
paddedName := padName(name, 28)
|
||||||
|
nameSegment := fmt.Sprintf("%s %s", icon, paddedName)
|
||||||
|
numColor := ""
|
||||||
|
percentColor := ""
|
||||||
|
if idx == m.selected {
|
||||||
|
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
||||||
|
nameSegment = fmt.Sprintf("%s%s %s%s", colorCyan, icon, paddedName, colorReset)
|
||||||
|
numColor = colorCyan
|
||||||
|
percentColor = colorCyan
|
||||||
|
sizeColor = colorCyan
|
||||||
|
}
|
||||||
|
displayIndex := idx + 1
|
||||||
|
|
||||||
|
// Priority: cleanable > unused time
|
||||||
|
var hintLabel string
|
||||||
|
if entry.IsDir && isCleanableDir(entry.Path) {
|
||||||
|
hintLabel = fmt.Sprintf("%s🧹%s", colorYellow, colorReset)
|
||||||
|
} else {
|
||||||
|
// For overview mode, get access time on-demand if not set
|
||||||
|
lastAccess := entry.LastAccess
|
||||||
|
if lastAccess.IsZero() && entry.Path != "" {
|
||||||
|
lastAccess = getLastAccessTime(entry.Path)
|
||||||
|
}
|
||||||
|
if unusedTime := formatUnusedTime(lastAccess); unusedTime != "" {
|
||||||
|
hintLabel = fmt.Sprintf("%s%s%s", colorGray, unusedTime, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hintLabel == "" {
|
||||||
|
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s\n",
|
||||||
|
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
||||||
|
nameSegment, sizeColor, sizeText, colorReset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s %s\n",
|
||||||
|
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
||||||
|
nameSegment, sizeColor, sizeText, colorReset, hintLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal mode with sizes and progress bars
|
||||||
|
maxSize := int64(1)
|
||||||
|
for _, entry := range m.entries {
|
||||||
|
if entry.Size > maxSize {
|
||||||
|
maxSize = entry.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport := calculateViewport(m.height, false)
|
||||||
|
start := m.offset
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
end := start + viewport
|
||||||
|
if end > len(m.entries) {
|
||||||
|
end = len(m.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := start; idx < end; idx++ {
|
||||||
|
entry := m.entries[idx]
|
||||||
|
icon := "📄"
|
||||||
|
if entry.IsDir {
|
||||||
|
icon = "📁"
|
||||||
|
}
|
||||||
|
size := humanizeBytes(entry.Size)
|
||||||
|
name := trimName(entry.Name)
|
||||||
|
paddedName := padName(name, 28)
|
||||||
|
|
||||||
|
// Calculate percentage
|
||||||
|
percent := float64(entry.Size) / float64(m.totalSize) * 100
|
||||||
|
percentStr := fmt.Sprintf("%5.1f%%", percent)
|
||||||
|
|
||||||
|
// Get colored progress bar
|
||||||
|
bar := coloredProgressBar(entry.Size, maxSize, percent)
|
||||||
|
|
||||||
|
// Color the size based on magnitude
|
||||||
|
var sizeColor string
|
||||||
|
if percent >= 50 {
|
||||||
|
sizeColor = colorRed
|
||||||
|
} else if percent >= 20 {
|
||||||
|
sizeColor = colorYellow
|
||||||
|
} else if percent >= 5 {
|
||||||
|
sizeColor = colorBlue
|
||||||
|
} else {
|
||||||
|
sizeColor = colorGray
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep chart columns aligned even when arrow is shown
|
||||||
|
entryPrefix := " "
|
||||||
|
nameSegment := fmt.Sprintf("%s %s", icon, paddedName)
|
||||||
|
numColor := ""
|
||||||
|
percentColor := ""
|
||||||
|
if idx == m.selected {
|
||||||
|
entryPrefix = fmt.Sprintf(" %s%s▶%s ", colorCyan, colorBold, colorReset)
|
||||||
|
nameSegment = fmt.Sprintf("%s%s %s%s", colorCyan, icon, paddedName, colorReset)
|
||||||
|
numColor = colorCyan
|
||||||
|
percentColor = colorCyan
|
||||||
|
sizeColor = colorCyan
|
||||||
|
}
|
||||||
|
|
||||||
|
displayIndex := idx + 1
|
||||||
|
|
||||||
|
// Priority: cleanable > unused time
|
||||||
|
var hintLabel string
|
||||||
|
if entry.IsDir && isCleanableDir(entry.Path) {
|
||||||
|
hintLabel = fmt.Sprintf("%s🧹%s", colorYellow, colorReset)
|
||||||
|
} else {
|
||||||
|
// Get access time on-demand if not set
|
||||||
|
lastAccess := entry.LastAccess
|
||||||
|
if lastAccess.IsZero() && entry.Path != "" {
|
||||||
|
lastAccess = getLastAccessTime(entry.Path)
|
||||||
|
}
|
||||||
|
if unusedTime := formatUnusedTime(lastAccess); unusedTime != "" {
|
||||||
|
hintLabel = fmt.Sprintf("%s%s%s", colorGray, unusedTime, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hintLabel == "" {
|
||||||
|
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s\n",
|
||||||
|
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
||||||
|
nameSegment, sizeColor, size, colorReset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s%s%2d.%s %s %s%s%s | %s %s%10s%s %s\n",
|
||||||
|
entryPrefix, numColor, displayIndex, colorReset, bar, percentColor, percentStr, colorReset,
|
||||||
|
nameSegment, sizeColor, size, colorReset, hintLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
if m.inOverviewMode() {
|
||||||
|
// Show ← Back if there's history (entered from a parent directory)
|
||||||
|
if len(m.history) > 0 {
|
||||||
|
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ← Back | Q Quit%s\n", colorGray, colorReset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s↑↓→ | Enter | R Refresh | O Open | F Show | Q Quit%s\n", colorGray, colorReset)
|
||||||
|
}
|
||||||
|
} else if m.showLargeFiles {
|
||||||
|
fmt.Fprintf(&b, "%s↑↓← | R Refresh | O Open | F Show | ⌫ Delete | ← Back | Q Quit%s\n", colorGray, colorReset)
|
||||||
|
} else {
|
||||||
|
largeFileCount := len(m.largeFiles)
|
||||||
|
if largeFileCount > 0 {
|
||||||
|
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ⌫ Delete | T Top(%d) | Q Quit%s\n", colorGray, largeFileCount, colorReset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s↑↓←→ | Enter | R Refresh | O Open | F Show | ⌫ Delete | Q Quit%s\n", colorGray, colorReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.deleteConfirm && m.deleteTarget != nil {
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
fmt.Fprintf(&b, "%sDelete:%s %s (%s) %sPress ⌫ again | ESC cancel%s\n",
|
||||||
|
colorRed, colorReset,
|
||||||
|
m.deleteTarget.Name, humanizeBytes(m.deleteTarget.Size),
|
||||||
|
colorGray, colorReset)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateViewport computes the number of visible items based on terminal height.
|
||||||
|
func calculateViewport(termHeight int, isLargeFiles bool) int {
|
||||||
|
if termHeight <= 0 {
|
||||||
|
// Terminal height unknown, use default
|
||||||
|
return defaultViewport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate reserved space for UI elements
|
||||||
|
reserved := 6 // header (3-4 lines) + footer (2 lines)
|
||||||
|
if isLargeFiles {
|
||||||
|
reserved = 5 // Large files view has less overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
available := termHeight - reserved
|
||||||
|
|
||||||
|
// Ensure minimum and maximum bounds
|
||||||
|
if available < 1 {
|
||||||
|
return 1 // Minimum 1 line for very short terminals
|
||||||
|
}
|
||||||
|
if available > 30 {
|
||||||
|
return 30 // Maximum 30 lines to avoid information overload
|
||||||
|
}
|
||||||
|
|
||||||
|
return available
|
||||||
|
}
|
||||||
@@ -567,7 +567,7 @@ check_cache_size() {
|
|||||||
for cache_path in "${cache_paths[@]}"; do
|
for cache_path in "${cache_paths[@]}"; do
|
||||||
if [[ -d "$cache_path" ]]; then
|
if [[ -d "$cache_path" ]]; then
|
||||||
local size_output
|
local size_output
|
||||||
size_output=$(du -sk "$cache_path" 2> /dev/null | awk 'NR==1 {print $1}' | tr -d '[:space:]' || echo "")
|
size_output=$(get_path_size_kb "$cache_path")
|
||||||
[[ "$size_output" =~ ^[0-9]+$ ]] || size_output=0
|
[[ "$size_output" =~ ^[0-9]+$ ]] || size_output=0
|
||||||
cache_size_kb=$((cache_size_kb + size_output))
|
cache_size_kb=$((cache_size_kb + size_output))
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ clean_media_players() {
|
|||||||
has_offline_music=true
|
has_offline_music=true
|
||||||
elif [[ -d "$spotify_cache" ]]; then
|
elif [[ -d "$spotify_cache" ]]; then
|
||||||
local cache_size_kb
|
local cache_size_kb
|
||||||
cache_size_kb=$(du -sk "$spotify_cache" 2> /dev/null | awk '{print $1}' || echo "0")
|
cache_size_kb=$(get_path_size_kb "$spotify_cache")
|
||||||
# Large cache (>500MB) likely contains offline music
|
# Large cache (>500MB) likely contains offline music
|
||||||
if [[ $cache_size_kb -ge 512000 ]]; then
|
if [[ $cache_size_kb -ge 512000 ]]; then
|
||||||
has_offline_music=true
|
has_offline_music=true
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ clean_orphaned_app_data() {
|
|||||||
if is_orphaned "$bundle_id" "$match"; then
|
if is_orphaned "$bundle_id" "$match"; then
|
||||||
# Use timeout to prevent du from hanging on large/problematic directories
|
# Use timeout to prevent du from hanging on large/problematic directories
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(run_with_timeout 2 du -sk "$match" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(run_with_timeout 2 get_path_size_kb "$match")
|
||||||
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ clean_service_worker_cache() {
|
|||||||
# Pattern matches: letters/numbers, hyphens, then dot, then TLD
|
# Pattern matches: letters/numbers, hyphens, then dot, then TLD
|
||||||
# Example: "abc123_https_example.com_0" → "example.com"
|
# Example: "abc123_https_example.com_0" → "example.com"
|
||||||
local domain=$(basename "$cache_dir" | grep -oE '[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,}' | head -1 || echo "")
|
local domain=$(basename "$cache_dir" | grep -oE '[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,}' | head -1 || echo "")
|
||||||
local size=$(du -sk "$cache_dir" 2> /dev/null | awk '{print $1}')
|
local size=$(get_path_size_kb "$cache_dir")
|
||||||
|
|
||||||
# Check if domain is protected
|
# Check if domain is protected
|
||||||
local is_protected=false
|
local is_protected=false
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ clean_broken_preferences() {
|
|||||||
# Validate plist using plutil
|
# Validate plist using plutil
|
||||||
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(get_path_size_kb "$plist_file")
|
||||||
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
rm -f "$plist_file" 2> /dev/null || true
|
rm -f "$plist_file" 2> /dev/null || true
|
||||||
@@ -67,7 +67,7 @@ clean_broken_preferences() {
|
|||||||
|
|
||||||
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
if ! plutil -lint "$plist_file" > /dev/null 2>&1; then
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(get_path_size_kb "$plist_file")
|
||||||
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
rm -f "$plist_file" 2> /dev/null || true
|
rm -f "$plist_file" 2> /dev/null || true
|
||||||
@@ -143,7 +143,7 @@ clean_broken_login_items() {
|
|||||||
|
|
||||||
# Program doesn't exist - this is a broken login item
|
# Program doesn't exist - this is a broken login item
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(get_path_size_kb "$plist_file")
|
||||||
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
# Unload first if loaded
|
# Unload first if loaded
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ clean_time_machine_failed_backups() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||||
if [[ "$size_kb" -gt 0 ]]; then
|
if [[ "$size_kb" -gt 0 ]]; then
|
||||||
local backup_name=$(basename "$inprogress_file")
|
local backup_name=$(basename "$inprogress_file")
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ clean_time_machine_failed_backups() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||||
if [[ "$size_kb" -gt 0 ]]; then
|
if [[ "$size_kb" -gt 0 ]]; then
|
||||||
local backup_name=$(basename "$inprogress_file")
|
local backup_name=$(basename "$inprogress_file")
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ clean_application_support_logs() {
|
|||||||
check_ios_device_backups() {
|
check_ios_device_backups() {
|
||||||
local backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
local backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||||
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
|
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
|
||||||
local backup_kb=$(du -sk "$backup_dir" 2> /dev/null | awk '{print $1}')
|
local backup_kb=$(get_path_size_kb "$backup_dir")
|
||||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
|
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
|
||||||
local backup_human=$(du -sh "$backup_dir" 2> /dev/null | awk '{print $1}')
|
local backup_human=$(du -sh "$backup_dir" 2> /dev/null | awk '{print $1}')
|
||||||
note_activity
|
note_activity
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ is_sip_enabled() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if running in interactive terminal
|
||||||
|
# Returns: 0 if interactive (stdout is a terminal), 1 otherwise
|
||||||
|
# Usage: if is_interactive; then echo "Interactive mode"; fi
|
||||||
|
is_interactive() {
|
||||||
|
[[ -t 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
# Get spinner characters (overridable via MO_SPINNER_CHARS)
|
# Get spinner characters (overridable via MO_SPINNER_CHARS)
|
||||||
mo_spinner_chars() {
|
mo_spinner_chars() {
|
||||||
local chars="${MO_SPINNER_CHARS:-|/-\\}"
|
local chars="${MO_SPINNER_CHARS:-|/-\\}"
|
||||||
@@ -1131,6 +1138,15 @@ clean_tool_cache() {
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Size helpers
|
# Size helpers
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get path size in KB using du
|
||||||
|
# Args: $1 - path to measure
|
||||||
|
# Returns: size in KB, or 0 if path doesn't exist or error occurs
|
||||||
|
get_path_size_kb() {
|
||||||
|
local path="$1"
|
||||||
|
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
bytes_to_human_kb() { bytes_to_human "$((${1:-0} * 1024))"; }
|
bytes_to_human_kb() { bytes_to_human "$((${1:-0} * 1024))"; }
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -2064,7 +2080,7 @@ calculate_total_size() {
|
|||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ -n "$file" && -e "$file" ]]; then
|
if [[ -n "$file" && -e "$file" ]]; then
|
||||||
local size_kb
|
local size_kb
|
||||||
size_kb=$(du -sk "$file" 2> /dev/null | awk '{print $1}' || echo "0")
|
size_kb=$(get_path_size_kb "$file")
|
||||||
((total_kb += size_kb))
|
((total_kb += size_kb))
|
||||||
fi
|
fi
|
||||||
done <<< "$files"
|
done <<< "$files"
|
||||||
|
|||||||
@@ -3,15 +3,6 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
_opt_get_dir_size_kb() {
|
|
||||||
local path="$1"
|
|
||||||
[[ -e "$path" ]] || {
|
|
||||||
echo 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# System maintenance: rebuild databases and flush caches
|
# System maintenance: rebuild databases and flush caches
|
||||||
opt_system_maintenance() {
|
opt_system_maintenance() {
|
||||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..."
|
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..."
|
||||||
@@ -204,7 +195,7 @@ opt_mail_downloads() {
|
|||||||
|
|
||||||
local total_kb=0
|
local total_kb=0
|
||||||
for target_path in "${mail_dirs[@]}"; do
|
for target_path in "${mail_dirs[@]}"; do
|
||||||
total_kb=$((total_kb + $(_opt_get_dir_size_kb "$target_path")))
|
total_kb=$((total_kb + $(get_path_size_kb "$target_path")))
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ $total_kb -lt $MOLE_MAIL_DOWNLOADS_MIN_KB ]]; then
|
if [[ $total_kb -lt $MOLE_MAIL_DOWNLOADS_MIN_KB ]]; then
|
||||||
@@ -498,12 +489,6 @@ get_uptime_days() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Get directory size in KB
|
# Get directory size in KB
|
||||||
dir_size_kb() {
|
|
||||||
local path="$1"
|
|
||||||
[[ ! -e "$path" ]] && echo "0" && return
|
|
||||||
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Format size from KB
|
# Format size from KB
|
||||||
format_size_kb() {
|
format_size_kb() {
|
||||||
local kb="$1"
|
local kb="$1"
|
||||||
@@ -525,7 +510,7 @@ format_size_kb() {
|
|||||||
# Check cache size
|
# Check cache size
|
||||||
check_cache_refresh() {
|
check_cache_refresh() {
|
||||||
local cache_dir="$HOME/Library/Caches"
|
local cache_dir="$HOME/Library/Caches"
|
||||||
local size_kb=$(dir_size_kb "$cache_dir")
|
local size_kb=$(get_path_size_kb "$cache_dir")
|
||||||
local desc="Refresh Finder previews, Quick Look, and Safari caches"
|
local desc="Refresh Finder previews, Quick Look, and Safari caches"
|
||||||
|
|
||||||
if [[ $size_kb -gt 0 ]]; then
|
if [[ $size_kb -gt 0 ]]; then
|
||||||
@@ -545,7 +530,7 @@ check_mail_downloads() {
|
|||||||
|
|
||||||
local total_kb=0
|
local total_kb=0
|
||||||
for dir in "${dirs[@]}"; do
|
for dir in "${dirs[@]}"; do
|
||||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
total_kb=$((total_kb + $(get_path_size_kb "$dir")))
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ $total_kb -gt 0 ]]; then
|
if [[ $total_kb -gt 0 ]]; then
|
||||||
@@ -557,7 +542,7 @@ check_mail_downloads() {
|
|||||||
# Check saved state
|
# Check saved state
|
||||||
check_saved_state() {
|
check_saved_state() {
|
||||||
local state_dir="$HOME/Library/Saved Application State"
|
local state_dir="$HOME/Library/Saved Application State"
|
||||||
local size_kb=$(dir_size_kb "$state_dir")
|
local size_kb=$(get_path_size_kb "$state_dir")
|
||||||
|
|
||||||
if [[ $size_kb -gt 0 ]]; then
|
if [[ $size_kb -gt 0 ]]; then
|
||||||
local size_str=$(format_size_kb "$size_kb")
|
local size_str=$(format_size_kb "$size_kb")
|
||||||
@@ -605,7 +590,7 @@ check_developer_cleanup() {
|
|||||||
|
|
||||||
local total_kb=0
|
local total_kb=0
|
||||||
for dir in "${dirs[@]}"; do
|
for dir in "${dirs[@]}"; do
|
||||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
total_kb=$((total_kb + $(get_path_size_kb "$dir")))
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ $total_kb -gt 0 ]]; then
|
if [[ $total_kb -gt 0 ]]; then
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Calculate size for summary (including system files)
|
# Calculate size for summary (including system files)
|
||||||
local app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
local app_size_kb=$(get_path_size_kb "$app_path")
|
||||||
local related_files=$(find_app_files "$bundle_id" "$app_name")
|
local related_files=$(find_app_files "$bundle_id" "$app_name")
|
||||||
local related_size_kb=$(calculate_total_size "$related_files")
|
local related_size_kb=$(calculate_total_size "$related_files")
|
||||||
local system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
local system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
||||||
|
|||||||
2
mole
2
mole
@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/lib/core/common.sh"
|
source "$SCRIPT_DIR/lib/core/common.sh"
|
||||||
|
|
||||||
# Version info
|
# Version info
|
||||||
VERSION="1.11.12"
|
VERSION="1.11.13"
|
||||||
MOLE_TAGLINE="can dig deep to clean your Mac."
|
MOLE_TAGLINE="can dig deep to clean your Mac."
|
||||||
|
|
||||||
# Check if Touch ID is already configured
|
# Check if Touch ID is already configured
|
||||||
|
|||||||
BIN
mole-analyze
BIN
mole-analyze
Binary file not shown.
@@ -179,12 +179,11 @@ EOF
|
|||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "_opt_get_dir_size_kb returns zero for missing directory" {
|
@test "get_path_size_kb returns zero for missing directory" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
|
size=$(get_path_size_kb "/nonexistent/path")
|
||||||
size=$(_opt_get_dir_size_kb "/nonexistent/path")
|
|
||||||
echo "$size"
|
echo "$size"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -192,15 +191,14 @@ EOF
|
|||||||
[ "$output" = "0" ]
|
[ "$output" = "0" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "_opt_get_dir_size_kb calculates directory size" {
|
@test "get_path_size_kb calculates directory size" {
|
||||||
mkdir -p "$HOME/test_size"
|
mkdir -p "$HOME/test_size"
|
||||||
dd if=/dev/zero of="$HOME/test_size/file.dat" bs=1024 count=10 2>/dev/null
|
dd if=/dev/zero of="$HOME/test_size/file.dat" bs=1024 count=10 2>/dev/null
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
|
size=$(get_path_size_kb "$HOME/test_size")
|
||||||
size=$(_opt_get_dir_size_kb "$HOME/test_size")
|
|
||||||
echo "$size"
|
echo "$size"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user