diff --git a/README.md b/README.md index 172a9ea..2952f95 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,24 @@ end - Value length: Length of found value string in bytes (64-bit little-endian unsigned integer) - Value string: String of above-specified length (UTF-8 encoded) +#### Reload Definition + +You can specify automatic reloading behavior for modes by using the `reload` keyword. + +```shell +mode + reload -- +end +``` + +- `cmd`: A shell command to run for the lifetime of each pager running in this mode. Each line printed to STDOUT by this process will trigger a reload of the pager. The process will be sent a SIGTERM when the pager is closed. + +For example, this works great with `inotifywait` (or similar for your OS) if you want to reload when certain files are modified. + +To reload on a consistent interval, try something like `reload -- while sleep 5; do echo; done`. + +Note: Reloads are debounced so that many lines printed quickly will only trigger one reload. + #### Keybinding Definitions The `bindkey` keyword starts a keybinding definition. diff --git a/lib/configurer.go b/lib/configurer.go index d032a74..e23b229 100644 --- a/lib/configurer.go +++ b/lib/configurer.go @@ -110,6 +110,8 @@ loop: switch directive { case "annotate": c.ProcessModeAnnotate(mode, args) + case "reload": + c.ProcessModeReload(mode, args) case "bindkey": c.ProcessModeBindkey(mode, args) case "end": @@ -156,6 +158,21 @@ func (c *configurer) ProcessModeAnnotate(mode Mode, args []string) { } } +func (c *configurer) ProcessModeReload(mode Mode, args []string) { + switch len(args) { + case 1: + mode.RegisterReloadWatcher(func(ctx Context) func(Pager) { + return func(p Pager) { + if rw, err := NewReloadWatcher(p, args[0], ctx); err == nil { + p.AddReloadWatcher(rw) + } + } + }) + default: + panic("Expected 1 arg for 'reload'") + } +} + func (c *configurer) ProcessModeBindkey(mode Mode, args []string) { switch len(args) { case 2: diff --git a/lib/mode.go b/lib/mode.go index f780e80..be123cb 100644 --- a/lib/mode.go +++ b/lib/mode.go @@ -1,15 +1,18 @@ package rat type Mode interface { - RegisterAnnotator(func(Context) Annotator) - RegisterEventHandler(func(Context) func(Pager)) InitAnnotators(Context) func() []Annotator AddEventHandlers(Context) func(Pager) + AddReloadWatcher(Context) func(Pager) + RegisterAnnotator(func(Context) Annotator) + RegisterEventHandler(func(Context) func(Pager)) + RegisterReloadWatcher(func(Context) func(Pager)) } type mode struct { annotatorCtors []func(Context) Annotator eventHandlerAdders []func(Context) func(Pager) + reloadWatcherAdder func(Context) func(Pager) } func NewMode() Mode { @@ -41,6 +44,14 @@ func (m *mode) AddEventHandlers(ctx Context) func(Pager) { } } +func (m *mode) AddReloadWatcher(ctx Context) func(Pager) { + return func(p Pager) { + if m.reloadWatcherAdder != nil { + m.reloadWatcherAdder(ctx)(p) + } + } +} + func (m *mode) RegisterAnnotator(ctor func(Context) Annotator) { m.annotatorCtors = append(m.annotatorCtors, ctor) } @@ -48,3 +59,7 @@ func (m *mode) RegisterAnnotator(ctor func(Context) Annotator) { func (m *mode) RegisterEventHandler(adder func(Context) func(Pager)) { m.eventHandlerAdders = append(m.eventHandlerAdders, adder) } + +func (m *mode) RegisterReloadWatcher(adder func(Context) func(Pager)) { + m.reloadWatcherAdder = adder +} diff --git a/lib/pager.go b/lib/pager.go index e0fecf3..89f7dd2 100644 --- a/lib/pager.go +++ b/lib/pager.go @@ -12,6 +12,7 @@ type Pager interface { Widget AddEventHandler(keyStr string, handler EventHandler) Reload() + AddReloadWatcher(ReloadWatcher) CursorUp() CursorDown() CursorFirstLine() @@ -23,14 +24,15 @@ type Pager interface { } type pager struct { - title string - modes []Mode - ctx Context - buffer Buffer - scrollOffsetY int - cursorY int - stop chan bool - eventHandlers HandlerRegistry + title string + modes []Mode + ctx Context + buffer Buffer + scrollOffsetY int + cursorY int + stop chan bool + eventHandlers HandlerRegistry + reloadWatchers []ReloadWatcher box Box headerBox Box @@ -46,6 +48,7 @@ func newPager(title string, modeNames string, ctx Context) *pager { splitModeNames := strings.Split(modeNames, ",") p.modes = make([]Mode, 0, len(splitModeNames)) + p.reloadWatchers = make([]ReloadWatcher, 0, 2) for _, modeName := range splitModeNames { if mode, ok := modes[modeName]; ok { @@ -73,7 +76,11 @@ func (p *pager) Stop() { } func (p *pager) Destroy() { - p.Stop() + for _, rw := range p.reloadWatchers { + rw.Stop() + } + + p.buffer.Close() } func (p *pager) HandleEvent(ks []keyEvent) bool { @@ -209,11 +216,16 @@ func (p *pager) PageDown() { func (p *pager) Reload() { } +func (p *pager) AddReloadWatcher(rw ReloadWatcher) { + p.reloadWatchers = append(p.reloadWatchers, rw) +} + func NewReadPager(rd io.Reader, title string, modeNames string, ctx Context) Pager { p := newPager(title, modeNames, ctx) for _, mode := range p.modes { mode.AddEventHandlers(ctx)(p) + mode.AddReloadWatcher(ctx)(p) } p.buffer = NewBuffer(rd) @@ -234,12 +246,13 @@ func NewCmdPager(modeNames string, cmd string, ctx Context) Pager { cp.cmd = cmd cp.pager = newPager(cmd, modeNames, ctx) + cp.RunCommand() + for _, mode := range cp.modes { mode.AddEventHandlers(ctx)(cp) + mode.AddReloadWatcher(ctx)(cp) } - cp.RunCommand() - return cp } @@ -248,6 +261,11 @@ func (cp *cmdPager) Stop() { cp.pager.Stop() } +func (cp *cmdPager) Destroy() { + cp.command.Close() + cp.pager.Destroy() +} + func (cp *cmdPager) Reload() { cp.Stop() cp.RunCommand() diff --git a/lib/reload_watcher.go b/lib/reload_watcher.go new file mode 100644 index 0000000..1367009 --- /dev/null +++ b/lib/reload_watcher.go @@ -0,0 +1,62 @@ +package rat + +import ( + "bufio" + "io" + "os" + "os/exec" + "syscall" + "time" +) + +type ReloadWatcher interface { + Stop() error +} + +type reloadWatcher struct { + cmd *exec.Cmd +} + +func NewReloadWatcher(p Pager, cmd string, ctx Context) (ReloadWatcher, error) { + rw := &reloadWatcher{} + + rw.cmd = exec.Command(os.Getenv("SHELL"), "-c", cmd) + rw.cmd.Env = ContextEnvironment(ctx) + rw.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + var ( + stdout io.Reader + err error + ) + + if stdout, err = rw.cmd.StdoutPipe(); err != nil { + return rw, err + } + + err = rw.cmd.Start() + go func() { rw.cmd.Wait() }() + + go rw.scanLines(stdout, p) + + return rw, err +} + +func (rw *reloadWatcher) scanLines(rd io.Reader, p Pager) { + scanner := bufio.NewScanner(rd) + + var timer *time.Timer + + for scanner.Scan() { + if timer != nil { + timer.Stop() + } + + timer = time.AfterFunc(500*time.Millisecond, p.Reload) + } +} + +func (rw *reloadWatcher) Stop() error { + err := syscall.Kill(-rw.cmd.Process.Pid, syscall.SIGTERM) + rw.cmd.Wait() + return err +}