package cmd import ( "fmt" "os" "path/filepath" "sort" "time" ignore "github.com/sabhiram/go-gitignore" "github.com/zemenawi/zutils/pkg/colors" "github.com/zemenawi/zutils/pkg/formatter" ) type FileItem struct { Path string Size int64 Ignored bool RelPath string } type DirStats struct { 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 } func DirInfoCommand(args []string) error { if len(args) < 1 { return fmt.Errorf("please specify a directory path") } dirPath := args[0] showIgnored := false if len(args) > 1 && args[1] == "--all" { showIgnored = true } stats, err := analyzeDirectory(dirPath, showIgnored) if err != nil { return fmt.Errorf("error analyzing directory: %w", err) } size := formatter.FormatSize(stats.TotalSize) ignoringSize := formatter.FormatSize(stats.TotalSizeIgnoring) 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) 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)) } 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"} if stats.HasGitignore { headers = []string{"#", "File Name", "Size", "Status"} } data := make([][]string, 0, len(stats.LargestFiles)) for i, file := range stats.LargestFiles { name := file.RelPath row := []string{ fmt.Sprintf("%d", i+1), name, formatter.FormatSize(file.Size), } if stats.HasGitignore { if file.Ignored { row = append(row, colors.Gray+"[ignored]"+colors.Reset) } else { row = append(row, "") } } 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 { name := dir.RelPath 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) if stats.HasGitignore { fmt.Printf("%s%s📜 Gitignore:%s %s\n", colors.Green, colors.Bold, colors.Reset, "Detected") } fmt.Println() return nil } func printSimpleTable(headers []string, data [][]string) { formatter.PrintTable(headers, data, colors.Cyan) fmt.Println() } func analyzeDirectory(path string, showIgnored bool) (*DirStats, error) { stats := &DirStats{ LargestFiles: make([]FileItem, 0), LargestDirs: make([]FileItem, 0), FileExtensions: make(map[string]int), } 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 { if err != nil { return err } relPath, _ := filepath.Rel(path, filePath) if filePath == path { return nil } ignored := false if stats.HasGitignore { ignored = ignoreMatcher.MatchesPath(relPath) } if info.IsDir() { if !ignored || showIgnored { stats.DirCount++ stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{ Path: filePath, Size: info.Size(), Ignored: ignored, RelPath: relPath, }, 5) } } else { stats.FileCount++ stats.TotalSize += info.Size() if ignored { stats.IgnoredFileCount++ stats.IgnoredSize += info.Size() } else { stats.TotalSizeIgnoring += info.Size() } ext := filepath.Ext(filePath) stats.FileExtensions[ext]++ if !ignored || showIgnored { stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{ Path: filePath, Size: info.Size(), Ignored: ignored, RelPath: relPath, }, 5) } 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 }