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:
parent
5f23a0c11f
commit
f1ed15a8e4
@ -18,9 +18,10 @@ type Widget struct {
|
|||||||
|
|
||||||
settings *Settings
|
settings *Settings
|
||||||
|
|
||||||
m sync.Mutex
|
m sync.Mutex
|
||||||
buffer *bytes.Buffer
|
buffer *bytes.Buffer
|
||||||
running bool
|
runChan chan bool
|
||||||
|
redrawChan chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWidget creates a new instance of the widget
|
// 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.SetWrap(true)
|
||||||
widget.View.SetScrollable(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
|
return &widget
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *Widget) content() (string, string, bool) {
|
// Refresh signals the runCommandLoop to continue, or triggers a re-draw if the
|
||||||
result := widget.buffer.String()
|
// command is still running.
|
||||||
|
|
||||||
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
|
|
||||||
func (widget *Widget) Refresh() {
|
func (widget *Widget) Refresh() {
|
||||||
widget.m.Lock()
|
// Try to run the command. If the command is still running, let it keep
|
||||||
defer widget.m.Unlock()
|
// running and do a refresh instead. Otherwise, the widget will redraw when
|
||||||
|
// the command completes.
|
||||||
widget.execute()
|
select {
|
||||||
widget.Redraw(widget.content)
|
case widget.runChan <- true:
|
||||||
if widget.settings.tail {
|
default:
|
||||||
widget.View.ScrollToEnd()
|
widget.redrawChan <- true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,45 +82,11 @@ func (widget *Widget) Write(p []byte) (n int, err error) {
|
|||||||
widget.drainLines(lines - widget.settings.maxLines)
|
widget.drainLines(lines - widget.settings.maxLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redraw the widget
|
return n, err
|
||||||
widget.Redraw(widget.content)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------- Unexported Functions -------------------- */
|
/* -------------------- 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
|
// countLines counts the lines of data in the buffer
|
||||||
func (widget *Widget) countLines() int {
|
func (widget *Widget) countLines() int {
|
||||||
return bytes.Count(widget.buffer.Bytes(), []byte{'\n'})
|
return bytes.Count(widget.buffer.Bytes(), []byte{'\n'})
|
||||||
@ -146,3 +108,55 @@ func (widget *Widget) environment() []string {
|
|||||||
)
|
)
|
||||||
return envs
|
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()
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user