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
10 changes: 10 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ Include a `.tuido` file in individual directories to add filetypes for parsing a
extensions=go,js,cpp
```

To exclude directories or files from traversal, use glob patterns (note: `.gitignore` patterns are also respected automatically):

```
exclude=node_modules,vendor,dist
```

```
exclude=*.gen.go,*.min.js
```

To configure the friction threshold that triggers the deterrence nag when adding items:

```
Expand Down
7 changes: 7 additions & 0 deletions tui/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type config struct {
// frictionThreshold is the number of items that can be displayed or added to
// before a nag deterrent is displayed.
frictionThreshold int

// exclude is a list of directory or file name globs to skip during traversal.
// Matched against each path component (e.g. "node_modules", "vendor", "*.gen.go").
exclude []string
}

func (cfg config) String() string {
Expand Down Expand Up @@ -86,6 +90,9 @@ func parseConfig(file *os.File) config {
cfg.frictionThreshold = n
}
}
if split[0] == "exclude" {
cfg.exclude = strings.Split(split[1], ",")
}

} else {
// not a config line:
Expand Down
113 changes: 113 additions & 0 deletions tui/getfiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package tui

import (
"os"
"path/filepath"
"testing"
)

func containsPath(paths []string, target string) bool {
for _, p := range paths {
if p == target {
return true
}
}
return false
}

func writeTempFile(t *testing.T, dir, name, content string) string {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writeTempFile %s: %v", path, err)
}
return path
}

func TestGetFiles_readsItemWithNoConfig(t *testing.T) {
dir := t.TempDir()
want := writeTempFile(t, dir, "ignoreme.xit", "[ ] a task\n")

got := getFiles(dir, []string{"xit"}, nil)

if !containsPath(got, want) {
t.Errorf("expected %s in results, got %v", want, got)
}
}

func TestGetFiles_excludeFile(t *testing.T) {
dir := t.TempDir()
excluded := writeTempFile(t, dir, "ignoreme.xit", "[ ] a task\n")
kept := writeTempFile(t, dir, "keepme.xit", "[ ] another task\n")

got := getFiles(dir, []string{"xit"}, []string{"ignoreme.xit"})

if containsPath(got, excluded) {
t.Errorf("expected %s to be excluded, got %v", excluded, got)
}
if !containsPath(got, kept) {
t.Errorf("expected %s to be kept, got %v", kept, got)
}
}

func TestGetFiles_excludeDir(t *testing.T) {
dir := t.TempDir()
subdir := filepath.Join(dir, "ignorethisdir")
if err := os.Mkdir(subdir, 0755); err != nil {
t.Fatal(err)
}
excluded := writeTempFile(t, subdir, "item.xit", "[ ] excluded task\n")
kept := writeTempFile(t, dir, "keepme.xit", "[ ] kept task\n")

got := getFiles(dir, []string{"xit"}, []string{"ignorethisdir"})

if containsPath(got, excluded) {
t.Errorf("expected %s to be excluded, got %v", excluded, got)
}
if !containsPath(got, kept) {
t.Errorf("expected %s to be kept, got %v", kept, got)
}
}

func TestGetFiles_excludeGlob(t *testing.T) {
dir := t.TempDir()
excluded1 := writeTempFile(t, dir, "ignore_this.xit", "[ ] task one\n")
excluded2 := writeTempFile(t, dir, "ignore_that.xit", "[ ] task two\n")
kept := writeTempFile(t, dir, "keepme.xit", "[ ] kept task\n")

got := getFiles(dir, []string{"xit"}, []string{"ignore_*"})

for _, ex := range []string{excluded1, excluded2} {
if containsPath(got, ex) {
t.Errorf("expected %s to be excluded by glob, got %v", ex, got)
}
}
if !containsPath(got, kept) {
t.Errorf("expected %s to be kept, got %v", kept, got)
}
}

func TestGetFiles_excludeViaSubdirConfig(t *testing.T) {
dir := t.TempDir()
subdir := filepath.Join(dir, "sub")
if err := os.Mkdir(subdir, 0755); err != nil {
t.Fatal(err)
}
ignoredDir := filepath.Join(subdir, "ignorethisdir")
if err := os.Mkdir(ignoredDir, 0755); err != nil {
t.Fatal(err)
}

writeTempFile(t, subdir, ".tuido", "exclude=ignorethisdir\n")
excluded := writeTempFile(t, ignoredDir, "item.xit", "[ ] excluded task\n")
kept := writeTempFile(t, subdir, "keepme.xit", "[ ] kept task\n")

got := getFiles(dir, []string{"xit"}, nil)

if containsPath(got, excluded) {
t.Errorf("expected %s to be excluded via .tuido config, got %v", excluded, got)
}
if !containsPath(got, kept) {
t.Errorf("expected %s to be kept, got %v", kept, got)
}
}
60 changes: 50 additions & 10 deletions tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,35 +475,75 @@ func GetItems(file string) []*tuido.Item {
}

func GetFiles(wd string, extensions []string) []string {
return getFiles(wd, extensions, runConfig.exclude)
}

func getFiles(wd string, extensions []string, exclude []string) []string {

files := []string{}

walkrepo.WalkRepo(wd, func(path string, d fs.FileInfo, err error) error {
// apply .tuido configured extensions if they exist, but do not
// read a configured writeto. writeto is decided by the root
// working directory or user config
if d.IsDir() {
if isExcluded(path, wd, exclude) {
return filepath.SkipDir
}
// apply .tuido configured extensions/excludes if they exist, but do not
// read a configured writeto. writeto is decided by the root
// working directory or user config
cfg := parseConfigIfExists(filepath.Join(path, ".tuido"))
if cfg != nil {
extensions = cfg.extensions
if len(cfg.extensions) > 0 {
extensions = cfg.extensions
}
if len(cfg.exclude) > 0 {
exclude = append(exclude, cfg.exclude...)
}
}
return nil
}

for _, suffix := range extensions {
if isExcluded(path, wd, exclude) {
return nil
}

if strings.HasSuffix(
strings.ToLower(path),
suffix,
) {
for _, suffix := range extensions {
if strings.HasSuffix(strings.ToLower(path), suffix) {
files = append(files, path)
}

}
return nil
})
return files
}

// isExcluded reports whether path matches any exclude glob against its components
// relative to root. Each pattern is matched against individual path components
// (e.g. "node_modules") or as a full relative path glob (e.g. "src/*.gen.go").
func isExcluded(path, root string, exclude []string) bool {
rel, err := filepath.Rel(root, path)
if err != nil {
return false
}
parts := strings.Split(rel, string(filepath.Separator))
for _, pattern := range exclude {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
// match against each individual component
for _, part := range parts {
if matched, _ := filepath.Match(pattern, part); matched {
return true
}
}
// match against full relative path
if matched, _ := filepath.Match(pattern, rel); matched {
return true
}
}
return false
}

func SortItems(items []*tuido.Item) {
sort.SliceStable(items, func(i, j int) bool {
if items[i].Importance() > items[j].Importance() {
Expand Down
Loading