package main import ( "bytes" "errors" "flag" "fmt" "io" "io/fs" "os" "path/filepath" "strconv" "strings" "time" "git.awl.red/~neallred/touched/sortedset" ) const ( usage = `USAGE: touched --after time-ish (inclusive) --before time-ish (inclusive) --depth (-d) int --max (-m) max results --top bool (print the top most results, exclusive with bottom) --bottom bool (print the bottom most results, exclusive with top) --hidden (if passed, also show hidden files) --attrs [modified,created] <dir> respects .git project .gitignore files ` ) func dieUsage() { fmt.Printf(usage) os.Exit(1) } func parseTimish(in string, since bool) (time.Time, bool) { if unixTimestamp, err := strconv.ParseInt(in, 10, 64); err == nil { switch len(in) { case 10: return time.Unix(unixTimestamp, 0), true case 13: return time.UnixMilli(unixTimestamp), true case 16: return time.UnixMicro(unixTimestamp), true default: var tm time.Time return tm, false } } if dur, err := time.ParseDuration(in); err == nil { if since { return time.Now().Add(dur * -1), true } return time.Now().Add(dur), true } if tm, err := time.Parse(time.RFC3339Nano, in); err == nil { return tm, true } if tm, err := time.Parse(time.RFC3339, in); err == nil { return tm, true } if tm, err := time.Parse(time.DateTime, in); err == nil { return tm, true } if tm, err := time.Parse(time.DateOnly, in); err == nil { return tm, true } var zero time.Time return zero, false } var dur24fps = time.Second / 24 func main() { config := getConfig() initialSlashCount := 0 ss := sortedset.New[string, int64, struct{}]() fileCount := 0 var countInterrupted bool showHidden := config.showHidden maxDepth := config.depth maxResults := int(config.maxResults) userRequestedBefore := !config.before.IsZero() userRequestedAfter := !config.after.IsZero() userRequestedBottom := config.bottom lastUpdate := time.Now() err := filepath.WalkDir(config.searchDir, func(path string, d fs.DirEntry, err error) error { fileCount++ if (fileCount%1000 == 0 && time.Since(lastUpdate) > dur24fps) || fileCount == 1 { lastUpdate = time.Now() fmt.Fprintf(os.Stderr, "\rSearching file %d of", fileCount) } if initialSlashCount == 0 { initialSlashCount = strings.Count(path, "/") } // crude ignoring of build dirs if path == "node_modules" || strings.HasSuffix(path, "/node_modules") { return fs.SkipDir } // crude ignoring of git repos if path == "HEAD" || strings.HasSuffix(path, "/HEAD") { head, err := os.Open(path) if err == nil { buf := make([]byte, 4) _, err := io.ReadFull(head, buf) if err == nil { if bytes.Equal(buf, []byte("ref:")) { head.Close() return fs.SkipDir } } head.Close() } } if !showHidden && isHidden(path) { if d.IsDir() { return fs.SkipDir } return nil } if maxDepth > 0 && ((strings.Count(path, "/") - initialSlashCount) > int(maxDepth)-1) { return fs.SkipDir } info, err := d.Info() if err != nil { if errors.Is(err, fs.ErrNotExist) { if d.IsDir() { return fs.SkipDir } return nil } if errors.Is(err, fs.ErrPermission) { if d.IsDir() { return fs.SkipDir } return nil } // unrecognized error, print file countInterrupted = true fmt.Fprintf(os.Stderr, "\nError gathering info on file %q\n", path) return nil } scoreTime := info.ModTime() score := scoreTime.UnixMilli() if userRequestedBefore && scoreTime.After(config.before) { return nil } if userRequestedAfter && scoreTime.Before(config.after) { return nil } saturated := ss.GetCount() == maxResults if saturated { // checking if value to add is going to be evicted anyways. // if it is, skip adding to avoid the shuffle if userRequestedBottom { if max := ss.PeekMax(); max != nil && score > max.Score() { return nil } } else { if min := ss.PeekMin(); min != nil && score < min.Score() { return nil } } } ss.AddOrUpdate(path, score, struct{}{}) if saturated { if userRequestedBottom { ss.PopMax() } else { ss.PopMin() } } return nil }) if countInterrupted { fmt.Fprintf(os.Stderr, "\nSearched %d files\n", fileCount) } else { fmt.Fprintf(os.Stderr, "\rSearching file %d of %d\n", fileCount, fileCount) } if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } ss.IterFuncRangeByRank(1, ss.GetCount(), func(key string, score int64, _ struct{}) bool { fmt.Println(time.UnixMilli(score).Format(time.DateTime) + " " + key) return true }) } type config struct { showHidden bool top bool bottom bool dirs bool maxResults uint64 searchDir string depth uint64 before time.Time after time.Time } const nearPresumedTerminalHeight = 40 func getConfig() config { top := flag.Bool("top", true, "print the top most results, exclusive with bottom") bottom := flag.Bool("bottom", false, "print the bottom most results, exclusive with bottom") hidden := flag.Bool("hidden", false, "if passed, also search/show hidden files") dirs := flag.Bool("dirs", false, "if passed, also show directories as entries") maxResults := flag.Uint64("max", nearPresumedTerminalHeight, "maximum number of results to return") maxResultsShort := flag.Uint64("m", nearPresumedTerminalHeight, "maximum number of results to return") depth := flag.Uint64("depth", 0, "max depth to traverse") depthShort := flag.Uint64("d", 0, "max depth to traverse") afterStr := flag.String("after", "", "only include files after this time") beforeStr := flag.String("before", "", "only include files before this time") flag.Parse() cfg := config{} if top != nil { cfg.top = *top } if afterStr != nil { if after, ok := parseTimish(*afterStr, true); ok { cfg.after = after } } if beforeStr != nil { if before, ok := parseTimish(*beforeStr, true); ok { cfg.before = before } } if bottom != nil { cfg.bottom = *bottom if cfg.bottom { cfg.top = false } } if cfg.top && cfg.bottom { dieUsage() } if !cfg.top && !cfg.bottom { cfg.top = true } if hidden != nil { cfg.showHidden = *hidden } if dirs != nil { cfg.dirs = *dirs } if maxResults != nil { cfg.maxResults = *maxResults } if maxResultsShort != nil { cfg.maxResults = *maxResultsShort } if depth != nil { cfg.depth = *depth } if depthShort != nil { cfg.depth = *depthShort } nonCommandArgs := flag.Args() searchDir := "" var err error if len(nonCommandArgs) > 0 { searchDir, err = filepath.Abs(nonCommandArgs[0]) if err != nil { fmt.Println(err) dieUsage() } } else { searchDir, err = os.Getwd() if err != nil { fmt.Println(err) dieUsage() } } cfg.searchDir = searchDir return cfg }