2026-05-02 00:05:08 +03:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sort"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
ignore "github.com/sabhiram/go-gitignore"
|
2026-05-02 00:05:08 +03:00
|
|
|
"github.com/zemenawi/zutils/pkg/colors"
|
|
|
|
|
"github.com/zemenawi/zutils/pkg/formatter"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type FileItem struct {
|
2026-05-02 01:12:55 +03:00
|
|
|
Path string
|
|
|
|
|
Size int64
|
|
|
|
|
Ignored bool
|
|
|
|
|
RelPath string
|
2026-05-02 00:05:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DirStats struct {
|
2026-05-02 01:12:55 +03:00
|
|
|
TotalSize int64
|
|
|
|
|
TotalSizeIgnoring int64
|
|
|
|
|
FileCount int
|
|
|
|
|
DirCount int
|
|
|
|
|
IgnoredFileCount int
|
|
|
|
|
IgnoredSize int64
|
|
|
|
|
LargestFiles []FileItem
|
|
|
|
|
LargestDirs []FileItem
|
|
|
|
|
LastModified time.Time
|
|
|
|
|
FileExtensions map[string]int
|
|
|
|
|
HasGitignore bool
|
2026-05-02 00:05:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func DirInfoCommand(args []string) error {
|
|
|
|
|
if len(args) < 1 {
|
|
|
|
|
return fmt.Errorf("please specify a directory path")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dirPath := args[0]
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
showIgnored := false
|
|
|
|
|
if len(args) > 1 && args[1] == "--all" {
|
|
|
|
|
showIgnored = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := analyzeDirectory(dirPath, showIgnored)
|
2026-05-02 00:05:08 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error analyzing directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size := formatter.FormatSize(stats.TotalSize)
|
2026-05-02 01:12:55 +03:00
|
|
|
ignoringSize := formatter.FormatSize(stats.TotalSizeIgnoring)
|
2026-05-02 00:05:08 +03:00
|
|
|
modTime := stats.LastModified.Format(time.RFC1123)
|
|
|
|
|
|
|
|
|
|
PrintBoxHeader("DIRECTORY INFORMATION", colors.Purple)
|
|
|
|
|
|
|
|
|
|
fmt.Printf("%s%s📁 Path:%s %s\n", colors.Blue, colors.Bold, colors.Reset, dirPath)
|
|
|
|
|
fmt.Printf("%s%s📏 Total Size:%s %s\n", colors.Blue, colors.Bold, colors.Reset, size)
|
2026-05-02 01:12:55 +03:00
|
|
|
if stats.HasGitignore {
|
|
|
|
|
fmt.Printf("%s%s📏 Size (excl. ignored):%s %s\n", colors.Blue, colors.Bold, colors.Reset, ignoringSize)
|
|
|
|
|
fmt.Printf("%s%s🚫 Ignored:%s %d files (%s)\n", colors.Gray, colors.Bold, colors.Reset, stats.IgnoredFileCount, formatter.FormatSize(stats.IgnoredSize))
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
fmt.Printf("%s%s📊 Files:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.FileCount)
|
|
|
|
|
fmt.Printf("%s%s📂 Directories:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.DirCount)
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
|
|
|
|
if len(stats.LargestFiles) > 0 {
|
|
|
|
|
PrintSectionHeader("LARGEST FILES")
|
|
|
|
|
headers := []string{"#", "File Name", "Size"}
|
2026-05-02 01:12:55 +03:00
|
|
|
if stats.HasGitignore {
|
|
|
|
|
headers = []string{"#", "File Name", "Size", "Status"}
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
data := make([][]string, 0, len(stats.LargestFiles))
|
|
|
|
|
|
|
|
|
|
for i, file := range stats.LargestFiles {
|
2026-05-02 01:12:55 +03:00
|
|
|
name := file.RelPath
|
2026-05-02 00:05:08 +03:00
|
|
|
row := []string{
|
|
|
|
|
fmt.Sprintf("%d", i+1),
|
|
|
|
|
name,
|
|
|
|
|
formatter.FormatSize(file.Size),
|
|
|
|
|
}
|
2026-05-02 01:12:55 +03:00
|
|
|
if stats.HasGitignore {
|
|
|
|
|
if file.Ignored {
|
|
|
|
|
row = append(row, colors.Gray+"[ignored]"+colors.Reset)
|
|
|
|
|
} else {
|
|
|
|
|
row = append(row, "")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
data = append(data, row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printSimpleTable(headers, data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(stats.LargestDirs) > 0 {
|
|
|
|
|
PrintSectionHeader("LARGEST DIRS")
|
|
|
|
|
headers := []string{"#", "Directory Name", "Size"}
|
|
|
|
|
data := make([][]string, 0, len(stats.LargestDirs))
|
|
|
|
|
|
|
|
|
|
for i, dir := range stats.LargestDirs {
|
2026-05-02 01:12:55 +03:00
|
|
|
name := dir.RelPath
|
2026-05-02 00:05:08 +03:00
|
|
|
row := []string{
|
|
|
|
|
fmt.Sprintf("%d", i+1),
|
|
|
|
|
name,
|
|
|
|
|
formatter.FormatSize(dir.Size),
|
|
|
|
|
}
|
|
|
|
|
data = append(data, row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printSimpleTable(headers, data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(stats.FileExtensions) > 0 {
|
|
|
|
|
PrintSectionHeader("FILE TYPE DISTRIBUTION")
|
|
|
|
|
type extCount struct {
|
|
|
|
|
ext string
|
|
|
|
|
count int
|
|
|
|
|
}
|
|
|
|
|
var sortedExts []extCount
|
|
|
|
|
for ext, count := range stats.FileExtensions {
|
|
|
|
|
sortedExts = append(sortedExts, extCount{ext, count})
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(sortedExts, func(i, j int) bool {
|
|
|
|
|
return sortedExts[i].count > sortedExts[j].count
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
maxShow := 10
|
|
|
|
|
if len(sortedExts) < maxShow {
|
|
|
|
|
maxShow = len(sortedExts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headers := []string{"#", "Extension", "File Count", "Percentage"}
|
|
|
|
|
data := make([][]string, 0, maxShow)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < maxShow; i++ {
|
|
|
|
|
ext := sortedExts[i].ext
|
|
|
|
|
if ext == "" {
|
|
|
|
|
ext = "(no extension)"
|
|
|
|
|
}
|
|
|
|
|
percentage := float64(sortedExts[i].count) / float64(stats.FileCount) * 100
|
|
|
|
|
row := []string{
|
|
|
|
|
fmt.Sprintf("%d", i+1),
|
|
|
|
|
ext,
|
|
|
|
|
fmt.Sprintf("%d", sortedExts[i].count),
|
|
|
|
|
fmt.Sprintf("%.1f%%", percentage),
|
|
|
|
|
}
|
|
|
|
|
data = append(data, row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printSimpleTable(headers, data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PrintSectionHeader("DIRECTORY DETAILS")
|
|
|
|
|
fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
|
2026-05-02 01:12:55 +03:00
|
|
|
if stats.HasGitignore {
|
|
|
|
|
fmt.Printf("%s%s📜 Gitignore:%s %s\n", colors.Green, colors.Bold, colors.Reset, "Detected")
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
fmt.Println()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func printSimpleTable(headers []string, data [][]string) {
|
|
|
|
|
formatter.PrintTable(headers, data, colors.Cyan)
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
func analyzeDirectory(path string, showIgnored bool) (*DirStats, error) {
|
2026-05-02 00:05:08 +03:00
|
|
|
stats := &DirStats{
|
|
|
|
|
LargestFiles: make([]FileItem, 0),
|
|
|
|
|
LargestDirs: make([]FileItem, 0),
|
|
|
|
|
FileExtensions: make(map[string]int),
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
ignoreMatcher, err := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
|
|
|
|
|
if err == nil {
|
|
|
|
|
stats.HasGitignore = true
|
|
|
|
|
_ = ignoreMatcher
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
2026-05-02 00:05:08 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
relPath, _ := filepath.Rel(path, filePath)
|
|
|
|
|
|
2026-05-02 00:05:08 +03:00
|
|
|
if filePath == path {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
ignored := false
|
|
|
|
|
if stats.HasGitignore {
|
|
|
|
|
ignored = ignoreMatcher.MatchesPath(relPath)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 00:05:08 +03:00
|
|
|
if info.IsDir() {
|
2026-05-02 01:12:55 +03:00
|
|
|
if !ignored || showIgnored {
|
|
|
|
|
stats.DirCount++
|
|
|
|
|
stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{
|
|
|
|
|
Path: filePath,
|
|
|
|
|
Size: info.Size(),
|
|
|
|
|
Ignored: ignored,
|
|
|
|
|
RelPath: relPath,
|
|
|
|
|
}, 5)
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
} else {
|
|
|
|
|
stats.FileCount++
|
|
|
|
|
stats.TotalSize += info.Size()
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
if ignored {
|
|
|
|
|
stats.IgnoredFileCount++
|
|
|
|
|
stats.IgnoredSize += info.Size()
|
|
|
|
|
} else {
|
|
|
|
|
stats.TotalSizeIgnoring += info.Size()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 00:05:08 +03:00
|
|
|
ext := filepath.Ext(filePath)
|
|
|
|
|
stats.FileExtensions[ext]++
|
|
|
|
|
|
2026-05-02 01:12:55 +03:00
|
|
|
if !ignored || showIgnored {
|
|
|
|
|
stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{
|
|
|
|
|
Path: filePath,
|
|
|
|
|
Size: info.Size(),
|
|
|
|
|
Ignored: ignored,
|
|
|
|
|
RelPath: relPath,
|
|
|
|
|
}, 5)
|
|
|
|
|
}
|
2026-05-02 00:05:08 +03:00
|
|
|
|
|
|
|
|
if info.ModTime().After(stats.LastModified) {
|
|
|
|
|
stats.LastModified = info.ModTime()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return stats, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appendSorted(items []FileItem, newItem FileItem, maxSize int) []FileItem {
|
|
|
|
|
items = append(items, newItem)
|
|
|
|
|
|
|
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
|
|
|
return items[i].Size > items[j].Size
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if len(items) > maxSize {
|
|
|
|
|
items = items[:maxSize]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return items
|
2026-05-02 01:12:55 +03:00
|
|
|
}
|