// Package watch: debouncer for fsnotify events. Editors like nvim write the
// backup file, rename, then chmod -- that's three events for one "save".
// 150ms is the smallest window that collapses them on my laptop without
// feeling laggy. See commit 4ed509d.
package watch
import (
"sync"
"time"
)
// Debouncer coalesces calls to Trigger into a single Fn invocation. The
// scheduled call is reset every time Trigger runs within Window.
type Debouncer struct {
Window time.Duration
Fn func()
mu sync.Mutex
timer *time.Timer
done chan struct{}
}
// Trigger asks the debouncer to call Fn after Window of silence. If Trigger
// is called again before Window elapses, the timer is reset.
func (d *Debouncer) Trigger() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Reset(d.Window)
return
}
d.done = make(chan struct{})
d.timer = time.AfterFunc(d.Window, func() {
d.mu.Lock()
d.timer = nil
ch := d.done
d.mu.Unlock()
d.Fn()
close(ch)
})
}
// Wait blocks until the most recently scheduled Fn has returned. Safe to call
// with no outstanding trigger -- returns immediately.
func (d *Debouncer) Wait() {
d.mu.Lock()
ch := d.done
d.mu.Unlock()
if ch == nil {
return
}
<-ch
}
// Stop cancels any pending call. Any in-flight Fn is allowed to finish.
func (d *Debouncer) Stop() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
d.timer = nil
}
}
// New creates a Debouncer with sane defaults. Zero window becomes 150ms.
func New(window time.Duration, fn func()) *Debouncer {
if window <= 0 {
window = 150 * time.Millisecond
}
return &Debouncer{Window: window, Fn: fn}
}