diff --git a/bin/analyze-go b/bin/analyze-go index 9db7af6..e840f7a 100755 Binary files a/bin/analyze-go and b/bin/analyze-go differ diff --git a/cmd/analyze/constants.go b/cmd/analyze/constants.go index 7dc1646..d295f97 100644 --- a/cmd/analyze/constants.go +++ b/cmd/analyze/constants.go @@ -7,8 +7,8 @@ const ( maxLargeFiles = 30 barWidth = 24 minLargeFileSize = 100 << 20 // 100 MB - entryViewport = 10 - largeViewport = 10 + entryViewport = 12 + largeViewport = 12 overviewCacheTTL = 7 * 24 * time.Hour // 7 days overviewCacheFile = "overview_sizes.json" duTimeout = 60 * time.Second // Increased for large directories diff --git a/cmd/analyze/main.go b/cmd/analyze/main.go index 038daa0..ad235e3 100644 --- a/cmd/analyze/main.go +++ b/cmd/analyze/main.go @@ -137,6 +137,9 @@ func main() { isOverview = false } + // Prefetch overview cache in background (non-blocking) + go prefetchOverviewCache() + p := tea.NewProgram(newModel(abs, isOverview), tea.WithAltScreen()) if err := p.Start(); err != nil { fmt.Fprintf(os.Stderr, "analyzer error: %v\n", err) @@ -848,7 +851,7 @@ func (m model) View() string { currentPath := *m.currentPath if currentPath != "" { shortPath := displayPath(currentPath) - shortPath = truncateMiddle(shortPath, 60) + shortPath = truncateMiddle(shortPath, 50) fmt.Fprintf(&b, "%s%s%s\n", colorGray, shortPath, colorReset) } } @@ -957,22 +960,29 @@ func (m model) View() string { } displayIndex := idx + 1 - // Add unused time label if applicable - // For overview mode, get access time on-demand if not set - lastAccess := entry.LastAccess - if lastAccess.IsZero() && entry.Path != "" { - lastAccess = getLastAccessTime(entry.Path) + // 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) + } } - unusedLabel := formatUnusedTime(lastAccess) - if unusedLabel == "" { + + 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%s%s\n", + 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, - colorGray, unusedLabel, colorReset) + nameSegment, sizeColor, sizeText, colorReset, hintLabel) } } } else { @@ -1037,17 +1047,29 @@ func (m model) View() string { displayIndex := idx + 1 - // Add unused time label if applicable - unusedLabel := formatUnusedTime(entry.LastAccess) - if unusedLabel == "" { + // 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%s%s\n", + 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, - colorGray, unusedLabel, colorReset) + nameSegment, sizeColor, size, colorReset, hintLabel) } } } @@ -1069,7 +1091,7 @@ func (m model) View() string { } if m.deleteConfirm && m.deleteTarget != nil { fmt.Fprintln(&b) - fmt.Fprintf(&b, "%sDelete:%s %s (%s) %sConfirm: Delete | Cancel: ESC%s\n", + fmt.Fprintf(&b, "%sDelete:%s %s (%s) %sPress ⌫ again | ESC cancel%s\n", colorRed, colorReset, m.deleteTarget.Name, humanizeBytes(m.deleteTarget.Size), colorGray, colorReset) @@ -1204,22 +1226,3 @@ func scanOverviewPathCmd(path string, index int) tea.Cmd { } } } - -// deletePathCmd deletes a path recursively with progress tracking - -// measureOverviewSize calculates the size of a directory using multiple strategies: -// 1. Check JSON cache (fast) -// 2. Try du command (fast and accurate) -// 3. Walk the directory to get logical size (accurate but slower) -// 4. Check gob cache (fallback) - -// getDirectorySizeFromMetadata attempts to retrieve directory size using macOS Spotlight metadata. -// This is much faster than filesystem traversal but may not be available for all directories. - -// getDirectorySizeFromDu calculates directory size using the du command. -// Uses -s to summarize total size including all subdirectories. - -// getDirectoryLogicalSize walks the directory tree and sums file sizes to estimate -// the logical (Finder-style) usage. - -// Persistent cache functions