Refresh is CLI tool for hot reloading your codebase based on file system changes using notify with the ablity to use as a golang library in your own projects.
While refresh was built for reloading codebases it can be used to execute terminal commands based on file system changes
- Based on Notify to allievate common problems with popular FS libraries on mac that open a listener per file by using apples built-in FSEvents.
- Allows for customization via code / config file / cli flags
- Extended customization when used as a library using reloadCallback to bypass refresh rulesets and add addtional logic/logging on your applications end
- Default slogger built in with the ablity to mute logs as well as pass in your own slog handler to be used in app
- MIT licensed
Installing via go CLI is the easiest method .tar.gz files per platform are available via github releases
go install github.com/atterpac/refresh/cmd/refesh@latest
Alternative if you wish to use as a package and not a cli
go get github.com/atterpac/refresh
-p
Root path that will be watched and commands will be executed in typically this is './'
-be
Command to be called before the exec command for example go mod tidy
-e
Command to be called when a modification is detected for example go run main.go
-ae
Command to b be called when a modifcation is detected after the main process closes
-l
Log Level to display options can include "debug", "info","warn","error"
-f
path to a TOML config file see Config File for details on the format of config
-id
Ignore directories provided as a comma-separated list
-if
Ignore files provided as a comma-separated list
-ie
Ignore extensions provided as a comma-separated list
-d
Debounce timer in milliseconds, used to ignore repetitive system
refresh -p ./ -e "go run main.go" -be "go mod tidy" -ae "rm ./main" -l "debug" -id ".git, node_modules" -if ".env" -ie ".db, .sqlite" -d 500
There can be some uses where you might want to start a watcher internally or for a tool for development refresh provides a function NewEngineFromOptions
which takes an refresh.Config
and allows for the engine.Start()
function
Using refresh as a library also opens the ability to add a Callback function that is called on every FS notification
type Config struct {
RootPath string `toml:"root_path"`
PreExec string `toml:"pre_exec"`
ExecCommand string `toml:"exec_command"`
PostExec string `toml:"post_exec"`
Ignore Ignore `toml:"ignore"`
LogLevel string `toml:"log_level"`
Debounce int `toml:"debounce"`
Slog *slog.Logger
ExternalSlog bool
}
type Ignore struct {
Dir map[string]bool `toml:"dir"`
File map[string]bool `toml:"file"`
Extension map[string]bool `toml:"extension"`
}
import ( // other imports
refresh "github.com/atterpac/refresh/engine"
)
func main () {
ignore := refresh.Ignore{
File: map[string]bool{{"ignore.go",true},{".env", true}},
Dir: map[string]bool{{".git",true},{"node_modules", true}},
Extension: map[string]bool{{".txt",true},{".db", true}},
}
config := refresh.Config{
RootPath: "./subExecProcess",
ExecCommand: "go run main.go",
LogLevel: "info", // debug | info | warn | error | mute (discards all logs)
Ignore: ignore,
Debounce: 1000,
Slog: nil, // Optionally provide a slog interface
// if nil a default will be provided
// If provided stdout will not be piped through gotato
// Optionally provide a callback function to be called upon file notification events
Callback: func(*EventCallback) EventHandle
}
engine := refresh.NewEngineFromConfig(config)
engine.Start()
// Stop monitoring files and kill child processes
engine.Stop()
}
The following are all the file system event types that can be passed into the callback functions. Important to note that some actions only are emitted are certain OSs and you may have to handle those if you wish to bypass refresh rulesets
const (
// Base Actions
Create Event = iota
Write
Remove
Rename
// Windows Specific Actions
ActionModified
ActionRenamedNewName
ActionRenamedOldName
ActionAdded
ActionRemoved
ChangeLastWrite
ChangeAttributes
ChangeSize
ChangeDirName
ChangeFileName
ChangeSecurity
ChangeCreation
ChangeLastAccess
// Linux Specific Actions
InCloseWrite
InModify
InMovedTo
InMovedFrom
InCreate
InDelete
)
// Used as a response to the Callback
const (
EventContinue EventHandle = iota
EventBypass
EventIgnore
)
Below describes the data that you recieve in the callback function as well as an example of how this could be used.
Callbacks should return an refresh.EventHandle
refresh.EventContinue
continues with the reload process as normal and follows the hotato ruleset defined in the config
refresh.EventBypass
disregards all config rulesets and restarts the exec process
refresh.EventIgnore
ignores the event and continues monitoring
// Called whenever a change is detected in the filesystem
// By default we ignore file rename/remove and a bunch of other events that would likely cause breaking changes on a reload see eventmap_[oos].go for default rules
type EventCallback struct {
Type Event // Type of Notification (Write/Create/Remove...)
Time time.Time // time.Now() when event was triggered
Path string // Relative path based on root if root is ./myProject paths start with "myProject/..."
}
// Available returns from the Callback function
const (
EventContinue EventHandle = iota
EventBypass
EventIgnore
)
func ExampleCallback(e refresh.EventCallback) refresh.EventHandle {
switch e.Type {
case refresh.Create:
// Continue with reload process based on configured ruleset
return refresh.EventContinue
case refresh.Write:
// Ignore a file that would normally trigger a reload based on config
if e.Path == "./path/to/watched/file" {
return refresh.EventIgnore
}
// Continue with reload ruleset but add some extra logs/logic
fmt.Println("File Modified: %s", e.Path)
return EventContinue
case refresh.Remove:
// Hotato will ignore this event by default
// Return EventBypass to force reload process
return refresh.EventBypass
}
return refresh.EventContinue
}
If you would prefer to load from a config file rather than building the structs you can use
refresh.NewEngineFromTOML("path/to/toml")
[config]
# Relative to this files location
root_path = "./"
# Runs prior to the exec command starting
pre_exec = "go mod tidy"
# Command to run on reload
exec_command = "go run main.go"
# Runs when a file reload is triggered after killing the previous process
post_exec = ""
# debug | info | warn | error | mute
# Defaults to Info if not provided
log_level = "info"
# Debounce setting for ignoring reptitive file system notifications
debounce = 1000 # Milliseconds
# Sets what files the watcher should ignore
[config.ignore]
# Directories to ignore
dir = [".git", "node_modules", "newdir"]
# Files to ignore
file = [".DS_Store", ".gitignore", ".gitkeep", "newfile.go"]
# File extensions to ignore
extension = [".db", ".sqlite"]
Hotato not for you? Here are some popular hot reload alternatives