mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
* 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.
163 lines
3.6 KiB
Go
163 lines
3.6 KiB
Go
package cmdrunner
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/rivo/tview"
|
|
"github.com/wtfutil/wtf/view"
|
|
)
|
|
|
|
// Widget contains the data for this widget
|
|
type Widget struct {
|
|
view.TextWidget
|
|
|
|
settings *Settings
|
|
|
|
m sync.Mutex
|
|
buffer *bytes.Buffer
|
|
runChan chan bool
|
|
redrawChan chan bool
|
|
}
|
|
|
|
// NewWidget creates a new instance of the widget
|
|
func NewWidget(app *tview.Application, settings *Settings) *Widget {
|
|
widget := Widget{
|
|
TextWidget: view.NewTextWidget(app, settings.common),
|
|
|
|
settings: settings,
|
|
buffer: &bytes.Buffer{},
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Refresh signals the runCommandLoop to continue, or triggers a re-draw if the
|
|
// command is still running.
|
|
func (widget *Widget) Refresh() {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// String returns the string representation of the widget
|
|
func (widget *Widget) String() string {
|
|
args := strings.Join(widget.settings.args, " ")
|
|
|
|
if args != "" {
|
|
return fmt.Sprintf(" %s %s ", widget.settings.cmd, args)
|
|
}
|
|
|
|
return fmt.Sprintf(" %s ", widget.settings.cmd)
|
|
}
|
|
|
|
func (widget *Widget) Write(p []byte) (n int, err error) {
|
|
widget.m.Lock()
|
|
defer widget.m.Unlock()
|
|
|
|
// Write the new data into the buffer
|
|
n, err = widget.buffer.Write(p)
|
|
|
|
// Remove lines that exceed maxLines
|
|
lines := widget.countLines()
|
|
if lines > widget.settings.maxLines {
|
|
widget.drainLines(lines - widget.settings.maxLines)
|
|
}
|
|
|
|
return n, err
|
|
}
|
|
|
|
/* -------------------- Unexported Functions -------------------- */
|
|
|
|
// countLines counts the lines of data in the buffer
|
|
func (widget *Widget) countLines() int {
|
|
return bytes.Count(widget.buffer.Bytes(), []byte{'\n'})
|
|
}
|
|
|
|
// drainLines removed the first n lines from the buffer
|
|
func (widget *Widget) drainLines(n int) {
|
|
for i := 0; i < n; i++ {
|
|
widget.buffer.ReadBytes('\n')
|
|
}
|
|
}
|
|
|
|
func (widget *Widget) environment() []string {
|
|
envs := os.Environ()
|
|
envs = append(
|
|
envs,
|
|
fmt.Sprintf("WTF_WIDGET_WIDTH=%d", widget.settings.width),
|
|
fmt.Sprintf("WTF_WIDGET_HEIGHT=%d", widget.settings.height),
|
|
)
|
|
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()
|
|
}
|