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) {
|
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
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,
|
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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue