diff --git a/cmd/help.go b/cmd/help.go index f5d117e..4dd3b25 100644 --- a/cmd/help.go +++ b/cmd/help.go @@ -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) { help := getHelpForCommand(name) @@ -167,6 +180,8 @@ func getHelpForCommand(name string) *CommandHelp { return &usagesHelp case "network": return &networkHelp + case "tree": + return &treeHelp } return nil } @@ -191,6 +206,8 @@ func getUsageHint(name string) string { return "[-m] [-n=N]" case "network": return "" + case "tree": + return "[path] [-a] [-i]" case "info": return "" } diff --git a/cmd/tree.go b/cmd/tree.go new file mode 100644 index 0000000..ff39fc8 --- /dev/null +++ b/cmd/tree.go @@ -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 +} \ No newline at end of file diff --git a/main.go b/main.go index 89a188f..791e112 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,12 @@ func main() { 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{ Name: "version", Description: "Show version information", diff --git a/pkg/colors/colors.go b/pkg/colors/colors.go index 37ac7d4..4dd7ba0 100644 --- a/pkg/colors/colors.go +++ b/pkg/colors/colors.go @@ -1,16 +1,17 @@ package colors const ( - Reset = "\033[0m" - Red = "\033[31m" - Green = "\033[32m" - Yellow = "\033[33m" - Blue = "\033[34m" - Purple = "\033[35m" - Cyan = "\033[36m" - Gray = "\033[90m" - Bold = "\033[1m" - Italic = "\033[3m" - Dim = "\033[2m" + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Purple = "\033[35m" + Cyan = "\033[36m" + White = "\033[97m" + Gray = "\033[90m" + Bold = "\033[1m" + Italic = "\033[3m" + Dim = "\033[2m" Underline = "\033[4m" )