New command z tree displays directory structure with: - Hidden directories shown as collapsed folders with their immediate size - Gitignored files marked with [ignored by git] label - File sizes shown for all files and collapsed dirs - Directories colored cyan, files in white Also added White color constant to pkg/colors for tree command.
194 lines
No EOL
4.5 KiB
Go
194 lines
No EOL
4.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
ignore "github.com/sabhiram/go-gitignore"
|
|
"github.com/zemenawi/zutils/pkg/colors"
|
|
"github.com/zemenawi/zutils/pkg/formatter"
|
|
)
|
|
|
|
type TreeOptions struct {
|
|
ShowHidden bool
|
|
ShowIgnored bool
|
|
MaxDepth int
|
|
}
|
|
|
|
type TreeItem struct {
|
|
Name string
|
|
Path string
|
|
IsDir bool
|
|
Size int64
|
|
Ignored bool
|
|
Depth int
|
|
Children []*TreeItem
|
|
}
|
|
|
|
func TreeCommand(args []string) error {
|
|
opts := TreeOptions{
|
|
ShowHidden: true,
|
|
ShowIgnored: true,
|
|
MaxDepth: -1,
|
|
}
|
|
|
|
path := "."
|
|
|
|
for _, arg := range args {
|
|
if arg == "-h" || arg == "--help" {
|
|
fmt.Printf("%sUsage:%s z tree [path] [options]\n\n", colors.Cyan, colors.Reset)
|
|
fmt.Printf("%sOptions:%s\n", colors.Bold, colors.Reset)
|
|
fmt.Printf(" %s-h, --help%s Show this help\n\n", colors.Green, colors.Reset)
|
|
fmt.Printf("%sDescription:%s\n", colors.Bold, colors.Reset)
|
|
fmt.Printf(" Displays directory tree with:\n")
|
|
fmt.Printf(" - Hidden files (files starting with .)\n")
|
|
fmt.Printf(" - Gitignored files (marked with [ignored by git])\n")
|
|
fmt.Printf(" - File sizes for regular files\n")
|
|
fmt.Printf("\n%sExamples:%s\n", colors.Bold, colors.Reset)
|
|
fmt.Printf(" z tree # Show current directory\n")
|
|
fmt.Printf(" z tree /path/to/dir # Show specific directory\n")
|
|
return nil
|
|
}
|
|
if !strings.HasPrefix(arg, "-") {
|
|
path = arg
|
|
}
|
|
}
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error accessing path: %w", err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("'%s' is not a directory", path)
|
|
}
|
|
|
|
ignoreMatcher, _ := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
|
|
|
|
tree, err := buildTree(path, opts, ignoreMatcher, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("error building tree: %w", err)
|
|
}
|
|
|
|
PrintBoxHeader("DIRECTORY TREE", colors.Green)
|
|
fmt.Printf("%s%s📁 %s%s\n\n", colors.Bold, colors.Cyan, path, colors.Reset)
|
|
|
|
totalSize := printTreeRecursive(tree, "", true, true)
|
|
|
|
fmt.Println()
|
|
PrintSectionHeader("SUMMARY")
|
|
fmt.Printf("%s%s📊 Total Size:%s %s\n", colors.Green, colors.Bold, colors.Reset, formatter.FormatSize(totalSize))
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildTree(rootPath string, opts TreeOptions, ignoreMatcher *ignore.GitIgnore, depth int) (*TreeItem, error) {
|
|
if opts.MaxDepth >= 0 && depth > opts.MaxDepth {
|
|
return nil, nil
|
|
}
|
|
|
|
info, err := os.Stat(rootPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(filepath.Dir(rootPath), rootPath)
|
|
ignored := false
|
|
if ignoreMatcher != nil {
|
|
ignored = ignoreMatcher.MatchesPath(relPath)
|
|
}
|
|
|
|
if ignored && !opts.ShowIgnored {
|
|
return nil, nil
|
|
}
|
|
|
|
baseName := filepath.Base(rootPath)
|
|
isHiddenDir := info.IsDir() && len(baseName) > 1 && strings.HasPrefix(baseName, ".")
|
|
|
|
item := &TreeItem{
|
|
Name: baseName,
|
|
Path: rootPath,
|
|
IsDir: info.IsDir(),
|
|
Size: info.Size(),
|
|
Ignored: ignored,
|
|
Depth: depth,
|
|
}
|
|
|
|
if !info.IsDir() || isHiddenDir {
|
|
if isHiddenDir {
|
|
item.Size = info.Size()
|
|
item.Children = nil
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(rootPath)
|
|
if err != nil {
|
|
return item, nil
|
|
}
|
|
|
|
var children []*TreeItem
|
|
for _, entry := range entries {
|
|
if !opts.ShowHidden && strings.HasPrefix(entry.Name(), ".") {
|
|
continue
|
|
}
|
|
|
|
childPath := filepath.Join(rootPath, entry.Name())
|
|
childItem, err := buildTree(childPath, opts, ignoreMatcher, depth+1)
|
|
if err != nil || childItem == nil {
|
|
continue
|
|
}
|
|
children = append(children, childItem)
|
|
}
|
|
|
|
item.Children = children
|
|
return item, nil
|
|
}
|
|
|
|
func printTreeRecursive(item *TreeItem, prefix string, isLast bool, isRoot bool) int64 {
|
|
var totalSize int64
|
|
|
|
if !isRoot {
|
|
branch := "└── "
|
|
if !isLast {
|
|
branch = "├── "
|
|
}
|
|
|
|
name := item.Name
|
|
if item.IsDir {
|
|
name = colors.Cyan + item.Name + colors.Reset
|
|
} else {
|
|
name = colors.White + item.Name + colors.Reset
|
|
}
|
|
|
|
if item.Ignored {
|
|
name += colors.Gray + " [ignored by git]" + colors.Reset
|
|
}
|
|
|
|
if !item.IsDir || (item.IsDir && item.Children == nil) {
|
|
name += colors.Gray + fmt.Sprintf(" (%s)", formatter.FormatSize(item.Size)) + colors.Reset
|
|
}
|
|
|
|
fmt.Printf("%s%s%s%s\n", prefix, branch, name, colors.Reset)
|
|
totalSize += item.Size
|
|
}
|
|
|
|
if item.Children != nil {
|
|
branch := ""
|
|
if !isRoot {
|
|
if isLast {
|
|
branch = " "
|
|
} else {
|
|
branch = "│ "
|
|
}
|
|
}
|
|
|
|
for i, child := range item.Children {
|
|
isLastChild := i == len(item.Children)-1
|
|
totalSize += printTreeRecursive(child, prefix+branch, isLastChild, false)
|
|
}
|
|
}
|
|
|
|
return totalSize
|
|
} |