feat: add z tree command with gitignore and hidden file support

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.
This commit is contained in:
selamanapps 2026-05-02 02:10:14 +03:00
parent e96d60ae91
commit 349e60924d
4 changed files with 229 additions and 11 deletions

View file

@ -120,6 +120,19 @@ var networkHelp = CommandHelp{
}, },
} }
var treeHelp = CommandHelp{
Name: "tree",
Help: "Display directory tree with file sizes and gitignore support.\n" +
"Shows [ignored by git] label for gitignored files.\n" +
"File sizes shown in parentheses for regular files.",
Examples: []string{
"z tree # Show current directory tree",
"z tree /path/to/dir # Show specific directory",
"z tree -a # Show hidden files",
"z tree -i # Show gitignored files",
},
}
func PrintErrorAndHelp(name string, err error) { func PrintErrorAndHelp(name string, err error) {
help := getHelpForCommand(name) help := getHelpForCommand(name)
@ -167,6 +180,8 @@ func getHelpForCommand(name string) *CommandHelp {
return &usagesHelp return &usagesHelp
case "network": case "network":
return &networkHelp return &networkHelp
case "tree":
return &treeHelp
} }
return nil return nil
} }
@ -191,6 +206,8 @@ func getUsageHint(name string) string {
return "[-m] [-n=N]" return "[-m] [-n=N]"
case "network": case "network":
return "" return ""
case "tree":
return "[path] [-a] [-i]"
case "info": case "info":
return "<file|dir|network>" return "<file|dir|network>"
} }

194
cmd/tree.go Normal file
View file

@ -0,0 +1,194 @@
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
}

View file

@ -69,6 +69,12 @@ func main() {
Handler: cmd.JunksCommand, Handler: cmd.JunksCommand,
}) })
registry.Register(&types.Command{
Name: "tree",
Description: "Display directory tree with file sizes and gitignore support",
Handler: cmd.TreeCommand,
})
registry.Register(&types.Command{ registry.Register(&types.Command{
Name: "version", Name: "version",
Description: "Show version information", Description: "Show version information",

View file

@ -8,6 +8,7 @@ const (
Blue = "\033[34m" Blue = "\033[34m"
Purple = "\033[35m" Purple = "\033[35m"
Cyan = "\033[36m" Cyan = "\033[36m"
White = "\033[97m"
Gray = "\033[90m" Gray = "\033[90m"
Bold = "\033[1m" Bold = "\033[1m"
Italic = "\033[3m" Italic = "\033[3m"