mirror of
https://github.com/tw93/Mole.git
synced 2026-02-10 13:09:16 +00:00
feat: Separate Home and Library entries in overview, exclude Library from Home's size, and sort overview entries by size.
This commit is contained in:
BIN
bin/analyze-go
BIN
bin/analyze-go
Binary file not shown.
BIN
bin/status-go
BIN
bin/status-go
Binary file not shown.
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -204,11 +205,16 @@ func createOverviewEntries() []dirEntry {
|
|||||||
home := os.Getenv("HOME")
|
home := os.Getenv("HOME")
|
||||||
entries := []dirEntry{}
|
entries := []dirEntry{}
|
||||||
|
|
||||||
|
// Separate Home and ~/Library for better visibility and performance
|
||||||
|
// Home excludes Library to avoid duplicate scanning
|
||||||
if home != "" {
|
if home != "" {
|
||||||
entries = append(entries,
|
entries = append(entries, dirEntry{Name: "Home", Path: home, IsDir: true, Size: -1})
|
||||||
dirEntry{Name: "Home (~)", Path: home, IsDir: true, Size: -1},
|
|
||||||
dirEntry{Name: "Library (~/Library)", Path: filepath.Join(home, "Library"), IsDir: true, Size: -1},
|
// Add ~/Library separately so users can see app data usage
|
||||||
)
|
userLibrary := filepath.Join(home, "Library")
|
||||||
|
if _, err := os.Stat(userLibrary); err == nil {
|
||||||
|
entries = append(entries, dirEntry{Name: "App Library", Path: userLibrary, IsDir: true, Size: -1})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = append(entries,
|
entries = append(entries,
|
||||||
@@ -269,6 +275,14 @@ func (m *model) hydrateOverviewEntries() {
|
|||||||
m.totalSize = sumKnownEntrySizes(m.entries)
|
m.totalSize = sumKnownEntrySizes(m.entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *model) sortOverviewEntriesBySize() {
|
||||||
|
// Sort entries by size (largest first)
|
||||||
|
// Use stable sort to maintain order when sizes are equal
|
||||||
|
sort.SliceStable(m.entries, func(i, j int) bool {
|
||||||
|
return m.entries[i].Size > m.entries[j].Size
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) scheduleOverviewScans() tea.Cmd {
|
func (m *model) scheduleOverviewScans() tea.Cmd {
|
||||||
if !m.inOverviewMode() {
|
if !m.inOverviewMode() {
|
||||||
return nil
|
return nil
|
||||||
@@ -289,6 +303,8 @@ func (m *model) scheduleOverviewScans() tea.Cmd {
|
|||||||
if len(pendingIndices) == 0 {
|
if len(pendingIndices) == 0 {
|
||||||
m.overviewScanning = false
|
m.overviewScanning = false
|
||||||
if !hasPendingOverviewEntries(m.entries) {
|
if !hasPendingOverviewEntries(m.entries) {
|
||||||
|
// All scans complete - sort entries by size (largest first)
|
||||||
|
m.sortOverviewEntriesBySize()
|
||||||
m.status = "Ready"
|
m.status = "Ready"
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
isRootDir := root == "/"
|
isRootDir := root == "/"
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
isHomeDir := home != "" && root == home
|
||||||
|
|
||||||
for _, child := range children {
|
for _, child := range children {
|
||||||
fullPath := filepath.Join(root, child.Name())
|
fullPath := filepath.Join(root, child.Name())
|
||||||
@@ -136,6 +138,40 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for ~/Library - reuse cache to avoid duplicate scanning
|
||||||
|
// This is scanned separately in overview mode
|
||||||
|
if isHomeDir && child.Name() == "Library" {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(name, path string) {
|
||||||
|
defer wg.Done()
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
var size int64
|
||||||
|
// Try overview cache first (from overview scan)
|
||||||
|
if cached, err := loadStoredOverviewSize(path); err == nil && cached > 0 {
|
||||||
|
size = cached
|
||||||
|
} else if cached, err := loadCacheFromDisk(path); err == nil {
|
||||||
|
// Try disk cache
|
||||||
|
size = cached.TotalSize
|
||||||
|
} else {
|
||||||
|
// No cache available, scan normally
|
||||||
|
size = calculateDirSizeConcurrent(path, largeFileChan, filesScanned, dirsScanned, bytesScanned, currentPath)
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&total, size)
|
||||||
|
atomic.AddInt64(dirsScanned, 1)
|
||||||
|
|
||||||
|
entryChan <- dirEntry{
|
||||||
|
Name: name,
|
||||||
|
Path: path,
|
||||||
|
Size: size,
|
||||||
|
IsDir: true,
|
||||||
|
LastAccess: time.Time{},
|
||||||
|
}
|
||||||
|
}(child.Name(), fullPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// For folded directories, calculate size quickly without expanding
|
// For folded directories, calculate size quickly without expanding
|
||||||
if shouldFoldDirWithPath(child.Name(), fullPath) {
|
if shouldFoldDirWithPath(child.Name(), fullPath) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -522,6 +558,7 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
// measureOverviewSize calculates the size of a directory using multiple strategies.
|
// measureOverviewSize calculates the size of a directory using multiple strategies.
|
||||||
|
// When scanning Home, it excludes ~/Library to avoid duplicate counting.
|
||||||
func measureOverviewSize(path string) (int64, error) {
|
func measureOverviewSize(path string) (int64, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return 0, fmt.Errorf("empty path")
|
return 0, fmt.Errorf("empty path")
|
||||||
@@ -536,16 +573,23 @@ func measureOverviewSize(path string) (int64, error) {
|
|||||||
return 0, fmt.Errorf("cannot access path: %v", err)
|
return 0, fmt.Errorf("cannot access path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if we should exclude ~/Library (when scanning Home)
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
excludePath := ""
|
||||||
|
if home != "" && path == home {
|
||||||
|
excludePath = filepath.Join(home, "Library")
|
||||||
|
}
|
||||||
|
|
||||||
if cached, err := loadStoredOverviewSize(path); err == nil && cached > 0 {
|
if cached, err := loadStoredOverviewSize(path); err == nil && cached > 0 {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if duSize, err := getDirectorySizeFromDu(path); err == nil && duSize > 0 {
|
if duSize, err := getDirectorySizeFromDuWithExclude(path, excludePath); err == nil && duSize > 0 {
|
||||||
_ = storeOverviewSize(path, duSize)
|
_ = storeOverviewSize(path, duSize)
|
||||||
return duSize, nil
|
return duSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if logicalSize, err := getDirectoryLogicalSize(path); err == nil && logicalSize > 0 {
|
if logicalSize, err := getDirectoryLogicalSizeWithExclude(path, excludePath); err == nil && logicalSize > 0 {
|
||||||
_ = storeOverviewSize(path, logicalSize)
|
_ = storeOverviewSize(path, logicalSize)
|
||||||
return logicalSize, nil
|
return logicalSize, nil
|
||||||
}
|
}
|
||||||
@@ -559,10 +603,23 @@ func measureOverviewSize(path string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDirectorySizeFromDu(path string) (int64, error) {
|
func getDirectorySizeFromDu(path string) (int64, error) {
|
||||||
|
return getDirectorySizeFromDuWithExclude(path, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirectorySizeFromDuWithExclude(path string, excludePath string) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
args := []string{"-sk"}
|
||||||
|
// macOS du uses -I to ignore files/directories matching a pattern
|
||||||
|
if excludePath != "" {
|
||||||
|
// Extract just the directory name from the full path
|
||||||
|
excludeName := filepath.Base(excludePath)
|
||||||
|
args = append(args, "-I", excludeName)
|
||||||
|
}
|
||||||
|
args = append(args, path)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "du", args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
@@ -591,6 +648,10 @@ func getDirectorySizeFromDu(path string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDirectoryLogicalSize(path string) (int64, error) {
|
func getDirectoryLogicalSize(path string) (int64, error) {
|
||||||
|
return getDirectoryLogicalSizeWithExclude(path, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirectoryLogicalSizeWithExclude(path string, excludePath string) (int64, error) {
|
||||||
var total int64
|
var total int64
|
||||||
err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -599,6 +660,10 @@ func getDirectoryLogicalSize(path string) (int64, error) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Skip excluded path
|
||||||
|
if excludePath != "" && p == excludePath {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,9 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalSize := m.totalSize
|
totalSize := m.totalSize
|
||||||
nameWidth := calculateNameWidth(m.width)
|
// For overview mode, use a fixed small width since path names are short
|
||||||
|
// (~/Downloads, ~/Library, etc. - max ~15 chars)
|
||||||
|
nameWidth := 20
|
||||||
for idx, entry := range m.entries {
|
for idx, entry := range m.entries {
|
||||||
icon := "📁"
|
icon := "📁"
|
||||||
sizeVal := entry.Size
|
sizeVal := entry.Size
|
||||||
|
|||||||
Reference in New Issue
Block a user