Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ treels # compact ls-style view
treels -t # tree view
treels -t --depth 2 # tree view, limited to two levels
treels -t --gitignore # tree view, excluding .gitignore matches
treels -t --dirs-only # show directory structure only
treels --no-summary # hide the final count line
```

Expand Down Expand Up @@ -83,6 +84,7 @@ $ treels -t --depth 2 --no-icons service
- Optional Nerd Font icons and file-type colors.
- `--gitignore` support to skip generated files, dependencies, logs, and build output.
- `--depth N` to keep tree output readable in large repositories.
- `--dirs-only` to inspect folder structure without file-level noise.
- `--readable` file sizes.
- `--all` support for hidden files.
- `--no-icons` fallback for terminals without icon fonts.
Expand Down Expand Up @@ -147,6 +149,12 @@ Respect `.gitignore` rules:
treels --tree --gitignore
```

Show directories only:

```bash
treels --tree --dirs-only
```

Show readable sizes:

```bash
Expand All @@ -171,6 +179,7 @@ treels --no-summary
| --- | --- |
| `-a`, `--all` | List hidden files and directories. |
| `-t`, `--tree` | Show recursive tree view. |
| `--dirs-only` | Show only directories. |
| `--depth N` | Limit tree recursion depth. |
| `--gitignore` | Respect `.gitignore` rules from the target directory. |
| `--no-icons` | Disable file and folder icons. |
Expand Down
1 change: 1 addition & 0 deletions cmd/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func FlagDefinition(cmd *cobra.Command, flags *module.Flags) {
cmd.PersistentFlags().BoolVarP(&flags.ShowHidden, "all", "a", false, "List all files and directories")
cmd.PersistentFlags().BoolVarP(&flags.ShowTreeView, "tree", "t", false, "Tree view of the directory")
cmd.PersistentFlags().BoolVar(&flags.ShowDirsOnly, "dirs-only", false, "List directories only")
cmd.PersistentFlags().BoolVar(&flags.HideIcon, "no-icons", false, "Disable icons")
cmd.PersistentFlags().BoolVar(&flags.HideSummary, "no-summary", false, "Hide the final file and directory count")
cmd.PersistentFlags().BoolVar(&flags.RespectGitIgnore, "gitignore", false, "Respect .gitignore rules")
Expand Down
32 changes: 32 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,38 @@ func TestRootCmd_NoSummaryFlag(t *testing.T) {
}
}

func TestRootCmd_DirsOnlyFlag(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "cmd"), 0o755); err != nil {
t.Fatalf("Mkdir() error = %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}

output := captureStdout(t, func() {
cmd := newRootCmd()
cmd.SetArgs([]string{"--dirs-only", "--no-icons", dir})

if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v, want nil", err)
}
})

if !strings.Contains(output, "cmd") {
t.Fatalf("Execute() output = %q, want directory", output)
}
if strings.Contains(output, "main.go") {
t.Fatalf("Execute() output = %q, want file omitted", output)
}
if !strings.Contains(output, "1 directories") {
t.Fatalf("Execute() output = %q, want directory-only summary", output)
}
if strings.Contains(output, "0 files") {
t.Fatalf("Execute() output = %q, want no file count in dirs-only summary", output)
}
}

func TestRootCmd_GitIgnoreFlag(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("ignored.txt\n"), 0o644); err != nil {
Expand Down
1 change: 1 addition & 0 deletions module/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Flags struct {
HideIcon bool
ShowReadableSize bool
ShowVersion bool
ShowDirsOnly bool
HideSummary bool
RespectGitIgnore bool
TreeDepth int
Expand Down
13 changes: 11 additions & 2 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func dispatcher(options module.Options, output io.Writer) error {
return nil
}

return printNumberOfFilesAndDirectories(output, fileCount, dirCount)
return printNumberOfFilesAndDirectories(output, fileCount, dirCount, options.Flags)
}

// listDirectory func - lists the content of the directory.
Expand Down Expand Up @@ -198,6 +198,10 @@ func shouldShowFile(file os.FileInfo, options directoryOptions) bool {
return false
}

if options.Flags.ShowDirsOnly && !file.IsDir() {
return false
}

if options.gitIgnore == nil {
return true
}
Expand Down Expand Up @@ -245,7 +249,12 @@ func processDirectory(file os.FileInfo, options directoryOptions, output io.Writ
var errGetwd = errors.New("get current working directory")

// printNumberOfFilesAndDirectories returns number of files and directories
func printNumberOfFilesAndDirectories(output io.Writer, fileCount, dirCount int) error {
func printNumberOfFilesAndDirectories(output io.Writer, fileCount, dirCount int, flags module.Flags) error {
if flags.ShowDirsOnly {
_, err := fmt.Fprintf(output, "\n%d directories\n", dirCount)
return err
}

_, err := fmt.Fprintf(output, "\n%d directories, %d files\n", dirCount, fileCount)
return err
}
122 changes: 122 additions & 0 deletions service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,128 @@ func TestDispatcher_NoSummary(t *testing.T) {
}
}

func TestDispatcher_DirsOnly(t *testing.T) {
dir := t.TempDir()
mustMkdir(t, filepath.Join(dir, "cmd"))
mustMkdir(t, filepath.Join(dir, "service"))
mustWriteFile(t, filepath.Join(dir, "README.md"), "readme")
mustWriteFile(t, filepath.Join(dir, "main.go"), "package main")

tests := []struct {
name string
flags module.Flags
}{
{
name: "flat mode",
flags: module.Flags{HideIcon: true, ShowDirsOnly: true},
},
{
name: "tree mode",
flags: module.Flags{HideIcon: true, ShowDirsOnly: true, ShowTreeView: true},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var output bytes.Buffer
err := dispatcher(module.Options{Directory: dir, Flags: tt.flags}, &output)
if err != nil {
t.Fatalf("dispatcher() error = %v, want nil", err)
}

got := stripANSI(output.String())
for _, want := range []string{"cmd", "service", "2 directories"} {
if !strings.Contains(got, want) {
t.Fatalf("dispatcher() output = %q, want to contain %q", got, want)
}
}
if strings.Contains(got, "0 files") {
t.Fatalf("dispatcher() output = %q, want no file count in dirs-only summary", got)
}
for _, missing := range []string{"README.md", "main.go"} {
if strings.Contains(got, missing) {
t.Fatalf("dispatcher() output = %q, want not to contain %q", got, missing)
}
}
})
}
}

func TestDispatcher_DirsOnlyWithDepth(t *testing.T) {
dir := t.TempDir()
mustMkdir(t, filepath.Join(dir, "cmd"))
mustMkdir(t, filepath.Join(dir, "cmd", "internal"))
mustWriteFile(t, filepath.Join(dir, "cmd", "main.go"), "package main")
mustMkdir(t, filepath.Join(dir, "service"))

var output bytes.Buffer
err := dispatcher(module.Options{
Directory: dir,
Flags: module.Flags{
HideIcon: true,
ShowTreeView: true,
ShowDirsOnly: true,
TreeDepth: 1,
LimitTreeDepth: true,
},
}, &output)
if err != nil {
t.Fatalf("dispatcher() error = %v, want nil", err)
}

got := stripANSI(output.String())
for _, want := range []string{"cmd", "service", "2 directories"} {
if !strings.Contains(got, want) {
t.Fatalf("dispatcher() output = %q, want to contain %q", got, want)
}
}
if strings.Contains(got, "0 files") {
t.Fatalf("dispatcher() output = %q, want no file count in dirs-only summary", got)
}
for _, missing := range []string{"internal", "main.go"} {
if strings.Contains(got, missing) {
t.Fatalf("dispatcher() output = %q, want not to contain %q", got, missing)
}
}
}

func TestDispatcher_DirsOnlyWithGitIgnore(t *testing.T) {
dir := t.TempDir()
mustWriteFile(t, filepath.Join(dir, ".gitignore"), "ignored-dir/\n")
mustMkdir(t, filepath.Join(dir, "ignored-dir"))
mustMkdir(t, filepath.Join(dir, "visible-dir"))
mustWriteFile(t, filepath.Join(dir, "main.go"), "package main")

var output bytes.Buffer
err := dispatcher(module.Options{
Directory: dir,
Flags: module.Flags{
HideIcon: true,
ShowTreeView: true,
ShowDirsOnly: true,
RespectGitIgnore: true,
},
}, &output)
if err != nil {
t.Fatalf("dispatcher() error = %v, want nil", err)
}

got := stripANSI(output.String())
for _, want := range []string{"visible-dir", "1 directories"} {
if !strings.Contains(got, want) {
t.Fatalf("dispatcher() output = %q, want to contain %q", got, want)
}
}
if strings.Contains(got, "0 files") {
t.Fatalf("dispatcher() output = %q, want no file count in dirs-only summary", got)
}
for _, missing := range []string{"ignored-dir", "main.go"} {
if strings.Contains(got, missing) {
t.Fatalf("dispatcher() output = %q, want not to contain %q", got, missing)
}
}
}

func TestDispatcher_ListDirectoryGitIgnore(t *testing.T) {
dir := t.TempDir()
mustWriteFile(t, filepath.Join(dir, ".gitignore"), "*.log\nignored-dir/\n!keep.log\n")
Expand Down
Loading