1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Prevent flickering in cmdRunner widgets (#778)

* Prevent flickering in cmdRunner widgets

This commit removes flickering in the cmdRunner widgets while preserving
the live-update functionality. It amends 45b955 by not redrawing on
every write call. Instead, the logic in Refresh is as follows:

1. If the command is already running, it will not try to re-run the
command. The default case in the select will trigger a re-draw instead
so that new output can be seen. This accommodates long-runing commands
eg. tailing a log.
2. If the command is not already running, it will trigger a new run.
When the command terminates, it will trigger a re-draw. This means the
widget refreshes as soon as possible, to accommodate the original use
case of running a command and displaying its output in the widget.

In all cases, the widget will not re-draw more often than the refresh
interval. This is what eliminates flickering, since the previous
implementation before using goroutines was not redrawing more than once
per refresh interval.

* Remove useless locking in Refresh

Since the Refresh command doesn't actually block on anything, and the
goroutines have their own locking, Refresh shouldn't lock.
This commit is contained in:
Charlie Wang 2019-12-13 11:47:09 -05:00 committed by Chris Cummer
parent 5f23a0c11f
commit f1ed15a8e4

View File

@ -18,9 +18,10 @@ type Widget struct {
settings *Settings
m sync.Mutex
buffer *bytes.Buffer
running bool
m sync.Mutex
buffer *bytes.Buffer
runChan chan bool
redrawChan chan bool
}
// NewWidget creates a new instance of the widget
@ -35,30 +36,25 @@ func NewWidget(app *tview.Application, settings *Settings) *Widget {
widget.View.SetWrap(true)
widget.View.SetScrollable(true)
widget.runChan = make(chan bool)
widget.redrawChan = make(chan bool)
go runCommandLoop(&widget)
go redrawLoop(&widget)
widget.runChan <- true
return &widget
}
func (widget *Widget) content() (string, string, bool) {
result := widget.buffer.String()
ansiTitle := tview.TranslateANSI(widget.CommonSettings().Title)
if ansiTitle == defaultTitle {
ansiTitle = tview.TranslateANSI(widget.String())
}
ansiResult := tview.TranslateANSI(result)
return ansiTitle, ansiResult, false
}
// Refresh executes the command and updates the view with the results
// Refresh signals the runCommandLoop to continue, or triggers a re-draw if the
// command is still running.
func (widget *Widget) Refresh() {
widget.m.Lock()
defer widget.m.Unlock()
widget.execute()
widget.Redraw(widget.content)
if widget.settings.tail {
widget.View.ScrollToEnd()
// Try to run the command. If the command is still running, let it keep
// running and do a refresh instead. Otherwise, the widget will redraw when
// the command completes.
select {
case widget.runChan <- true:
default:
widget.redrawChan <- true
}
}
@ -86,45 +82,11 @@ func (widget *Widget) Write(p []byte) (n int, err error) {
widget.drainLines(lines - widget.settings.maxLines)
}
// Redraw the widget
widget.Redraw(widget.content)
return
return n, err
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) execute() {
// Make sure the command is not already running
if widget.running {
return
}
// Reset the buffer
widget.buffer.Reset()
// Indicate that the command is running
widget.running = true
// Setup the command to run
cmd := exec.Command(widget.settings.cmd, widget.settings.args...)
cmd.Stdout = widget
cmd.Env = widget.environment()
// Run the command and wait for it to exit in another Go-routine
go func() {
err := cmd.Run()
// The command has exited, print any error messages
widget.m.Lock()
if err != nil {
widget.buffer.WriteString(err.Error())
}
widget.running = false
widget.m.Unlock()
}()
}
// countLines counts the lines of data in the buffer
func (widget *Widget) countLines() int {
return bytes.Count(widget.buffer.Bytes(), []byte{'\n'})
@ -146,3 +108,55 @@ func (widget *Widget) environment() []string {
)
return envs
}
func runCommandLoop(widget *Widget) {
// Run the command forever in a loop. Refresh() will put a value into the
// channel to signal the loop to continue.
for {
<-widget.runChan
widget.resetBuffer()
cmd := exec.Command(widget.settings.cmd, widget.settings.args...)
cmd.Stdout = widget
cmd.Env = widget.environment()
err := cmd.Run()
// The command has exited, print any error messages
if err != nil {
widget.m.Lock()
widget.buffer.WriteString(err.Error())
widget.m.Unlock()
}
widget.redrawChan <- true
}
}
func redrawLoop(widget *Widget) {
for {
widget.Redraw(widget.content)
if widget.settings.tail {
widget.View.ScrollToEnd()
}
<-widget.redrawChan
}
}
func (widget *Widget) content() (string, string, bool) {
widget.m.Lock()
result := widget.buffer.String()
widget.m.Unlock()
ansiTitle := tview.TranslateANSI(widget.CommonSettings().Title)
if ansiTitle == defaultTitle {
ansiTitle = tview.TranslateANSI(widget.String())
}
ansiResult := tview.TranslateANSI(result)
return ansiTitle, ansiResult, false
}
func (widget *Widget) resetBuffer() {
widget.m.Lock()
defer widget.m.Unlock()
widget.buffer.Reset()
}