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:
parent
e96d60ae91
commit
349e60924d
4 changed files with 229 additions and 11 deletions
17
cmd/help.go
17
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 "<file|dir|network>"
|
||||
}
|
||||
|
|
|
|||
194
cmd/tree.go
Normal file
194
cmd/tree.go
Normal 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
|
||||
}
|
||||
6
main.go
6
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",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const (
|
|||
Blue = "\033[34m"
|
||||
Purple = "\033[35m"
|
||||
Cyan = "\033[36m"
|
||||
White = "\033[97m"
|
||||
Gray = "\033[90m"
|
||||
Bold = "\033[1m"
|
||||
Italic = "\033[3m"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue