12 "github.com/bmatcuk/doublestar/v4"
13 "github.com/charlievieth/fastwalk"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/home"
23func SkipHidden(path string) bool {
24 // Check for hidden files (starting with a dot)
25 base := filepath.Base(path)
26 if base != "." && strings.HasPrefix(base, ".") {
30 commonIgnoredDirs := map[string]bool{
47 "bower_components": true,
48 "jspm_packages": true,
51 parts := strings.SplitSeq(path, string(os.PathSeparator))
52 for part := range parts {
53 if commonIgnoredDirs[part] {
60// FastGlobWalker provides gitignore-aware file walking with fastwalk
61// It uses hierarchical ignore checking like git does, checking .gitignore/.crushignore
62// files in each directory from the root to the target path.
63type FastGlobWalker struct {
64 directoryLister *directoryLister
67func NewFastGlobWalker(searchPath string) *FastGlobWalker {
68 return &FastGlobWalker{
69 directoryLister: NewDirectoryLister(searchPath),
73// ShouldSkip checks if a path should be skipped based on hierarchical gitignore,
74// crushignore, and hidden file rules
75func (w *FastGlobWalker) ShouldSkip(path string) bool {
76 return w.directoryLister.shouldIgnore(path, nil)
79func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool, error) {
80 // Normalize pattern to forward slashes on Windows so their config can use
82 pattern = filepath.ToSlash(pattern)
84 walker := NewFastGlobWalker(searchPath)
85 found := csync.NewSlice[FileInfo]()
86 conf := fastwalk.Config{
88 ToSlash: fastwalk.DefaultToSlash(),
89 Sort: fastwalk.SortFilesFirst,
91 err := fastwalk.Walk(&conf, searchPath, func(path string, d os.DirEntry, err error) error {
93 return nil // Skip files we can't access
97 if walker.ShouldSkip(path) {
98 return filepath.SkipDir
102 if walker.ShouldSkip(path) {
106 relPath, err := filepath.Rel(searchPath, path)
111 // Normalize separators to forward slashes
112 relPath = filepath.ToSlash(relPath)
114 // Check if path matches the pattern
115 matched, err := doublestar.Match(pattern, relPath)
116 if err != nil || !matched {
120 info, err := d.Info()
125 found.Append(FileInfo{Path: path, ModTime: info.ModTime()})
126 if limit > 0 && found.Len() >= limit*2 { // NOTE: why x2?
127 return filepath.SkipAll
131 if err != nil && !errors.Is(err, filepath.SkipAll) {
132 return nil, false, fmt.Errorf("fastwalk error: %w", err)
135 matches := slices.SortedFunc(found.Seq(), func(a, b FileInfo) int {
136 return b.ModTime.Compare(a.ModTime)
138 matches, truncated := truncate(matches, limit)
140 results := make([]string, len(matches))
141 for i, m := range matches {
144 return results, truncated || errors.Is(err, filepath.SkipAll), nil
147// ShouldExcludeFile checks if a file should be excluded from processing
148// based on common patterns and ignore rules
149func ShouldExcludeFile(rootPath, filePath string) bool {
150 return NewDirectoryLister(rootPath).
151 shouldIgnore(filePath, nil)
154func PrettyPath(path string) string {
155 return home.Short(path)
158func DirTrim(pwd string, lim int) string {
161 sep = string(filepath.Separator)
163 dirs := strings.Split(pwd, sep)
164 if lim > len(dirs)-1 || lim <= 0 {
167 for i := len(dirs) - 1; i > 0; i-- {
169 if i == len(dirs)-1 {
171 } else if i >= len(dirs)-lim {
172 out = string(dirs[i][0]) + out
178 out = filepath.Join("~", out)
182// PathOrPrefix returns the prefix if the path starts with it, or falls back to
183// the path otherwise.
184func PathOrPrefix(path, prefix string) string {
185 if HasPrefix(path, prefix) {
191// HasPrefix checks if the given path starts with the specified prefix.
192// Uses filepath.Rel to determine if path is within prefix.
193func HasPrefix(path, prefix string) bool {
194 rel, err := filepath.Rel(prefix, path)
198 // If path is within prefix, Rel will not return a path starting with ".."
199 return !strings.HasPrefix(rel, "..")
202// ToUnixLineEndings converts Windows line endings (CRLF) to Unix line endings (LF).
203func ToUnixLineEndings(content string) (string, bool) {
204 if strings.Contains(content, "\r\n") {
205 return strings.ReplaceAll(content, "\r\n", "\n"), true
207 return content, false
210// ToWindowsLineEndings converts Unix line endings (LF) to Windows line endings (CRLF).
211func ToWindowsLineEndings(content string) (string, bool) {
212 if !strings.Contains(content, "\r\n") {
213 return strings.ReplaceAll(content, "\n", "\r\n"), true
215 return content, false
218func truncate[T any](input []T, limit int) ([]T, bool) {
219 if limit > 0 && len(input) > limit {
220 return input[:limit], true
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list