A small file-watcher CLI: it re-runs a command whenever watched files change — like entr or watchexec, but minimal and written in Go.
gaze -e go -- go test ./...Runs your tests once immediately, then again on every save.
- Runs on startup, then on every relevant change (no need to touch a file to get the first run).
- Recursive watching — every subdirectory is watched, not just the top level.
- Debouncing — a burst of events (editor writes,
git checkout, etc.) is coalesced into a single run. - Cancels the previous run when a new change arrives, so runs never pile up.
- Filtering by extension and by ignored path substrings.
- Tells the command which file changed via the
GAZE_FILEenvironment variable. - Graceful shutdown on Ctrl-C (the running command is cancelled cleanly).
brew install mangrisano/gaze/gazego install github.com/mangrisano/gaze/cmd/gaze@latestOr from a local clone:
go install ./cmd/gazeThe binary lands in $(go env GOPATH)/bin, which must be on your PATH.
No Go toolchain? Grab a ready-made binary for your platform (Linux, macOS and Windows, amd64/arm64) from the latest release. For example, on macOS (arm64):
VERSION=0.1.2
curl -sL "https://github.com/mangrisano/gaze/releases/download/v${VERSION}/gaze_${VERSION}_darwin_arm64.tar.gz" | tar xz
./gaze --versiongaze [flags] -- <command> [args...]
Everything after -- is the command to run.
| Flag | Repeatable | Default | Description |
|---|---|---|---|
-e, --ext |
yes | (all files) | Only react to files with this extension (e.g. -e go). |
-p, --path |
yes | . |
File or directory to watch (dirs are recursive). |
-i, --ignore |
yes | — | Ignore any path containing this substring (e.g. -i vendor). |
-d, --debounce |
no | 200ms |
Debounce window (any Go duration, e.g. 500ms, 1s). |
-c, --clear |
no | off | Clear the terminal before each run. |
-r, --restart |
no | off | Restart mode for long-running commands: on change, SIGTERM the whole process group (graceful), then start fresh. |
-k, --grace |
no | 5s |
Grace period before force-killing on restart (with -r), e.g. -k 10s. |
--no-initial |
no | off | Skip the run on startup; only run when a file actually changes. |
-v, --version |
no | — | Print the version and exit. |
gaze generates completion scripts for your shell:
gaze completion zsh # also: bash, fish, powershellInstall them once, then restart your shell. Examples:
# zsh (into a directory on your $fpath)
gaze completion zsh > "${fpath[1]}/_gaze"
# bash
gaze completion bash > /usr/local/etc/bash_completion.d/gaze
# fish
gaze completion fish > ~/.config/fish/completions/gaze.fishRe-run tests on any Go change:
gaze -e go -- go test ./...Watch two directories, ignore vendor, rebuild:
gaze -p ./cmd -p ./internal -i vendor -e go -- go build ./...Longer debounce for noisy editors:
gaze -d 500ms -e go -- go vet ./...Watch a single file — only that file triggers a run (its parent dir is watched, so editor "atomic saves" are caught too):
gaze -p notes.txt -- cat notes.txtRestart a server on change — -r gracefully SIGTERMs the whole process group, so the child frees its port before the new instance starts:
gaze -r -e go -- go run ./cmd/serverAct on the file that changed — gaze exports its path as GAZE_FILE (unset on the startup run, so use a :- fallback):
# test only the package of the changed file
gaze -e go -- sh -c 'go test ./"$(dirname "${GAZE_FILE:-.}")"'
# lint just the file that changed
gaze -e py -- sh -c 'ruff check "${GAZE_FILE:-.}"'The path is the one fsnotify reports, relative to the watched root (e.g. internal/watcher/match.go). When a burst of files changes at once, GAZE_FILE holds the last one.
The core is a small pipeline of channels:
fsnotify events → shouldRun (filter) → debounce → runLoop → runOnce → your command
shouldRundecides which file changes matter (ignore substring wins; empty-emeans all files; otherwise the path must end in.ext).debouncecollapses a burst of events into one signal usingselect+time.After, carrying the last changed path.runLoopstarts the command for each signal and cancels the previous run (viacontext) when a new one arrives.runOnceruns the command withexec.CommandContext, wiring stdout/stderr through and exporting the changed file asGAZE_FILE.
fsnotify is not recursive on its own, so collectDirs walks the tree with filepath.WalkDir and every directory is added to the watcher. Directories created while gaze is running are detected and watched too.
cmd/gaze/ package main — flag parsing and wiring
internal/watcher/ package watcher — the pipeline (Config + Run)
cmd/gaze only parses flags into a watcher.Config and calls watcher.Run; all the logic lives in internal/watcher, whose only public surface is Config and Run.
go build ./...
go test ./... # add -race for the concurrency parts
go vet ./...To embed the version in the binary, inject it at build time:
go build -ldflags "-X main.version=$(git describe --tags)" -o gaze ./cmd/gazeRequires Go 1.27+. The only dependency is github.com/fsnotify/fsnotify.
MIT — see LICENSE.