aerc/widgets/terminal.go
Tim Culverhouse a49caf96b5 terminal: use Invalidate and QueueRedraw
The terminal widget uses it's own redraw logic to improve performance.
With the addition of a main event loop, the redraw logic can happen in
the main loop via the standard Invalidate logic.

Use the Invalidate method to mark aerc invalid, and immediately trigger
a redraw with ui.QueueRedraw. The follow up call to QueueRedraw is
needed because the terminal update happens in a separate goroutine. This
can result in the main event loop finishing it's process of the current
event, redrawing the screen, and the terminal having additional updates
to be drawn.

This fixes race conditions by drawing and calling screen.Show in a
separate goroutine.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-10-07 10:51:53 +02:00

175 lines
3.4 KiB
Go

package widgets
import (
"os/exec"
"syscall"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging"
tcellterm "git.sr.ht/~rockorager/tcell-term"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
type Terminal struct {
ui.Invalidatable
closed bool
cmd *exec.Cmd
ctx *ui.Context
cursorShown bool
destroyed bool
focus bool
vterm *tcellterm.Terminal
running bool
OnClose func(err error)
OnEvent func(event tcell.Event) bool
OnStart func()
OnTitle func(title string)
}
func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
term := &Terminal{
cursorShown: true,
}
term.cmd = cmd
term.vterm = tcellterm.New()
return term, nil
}
func (term *Terminal) Close(err error) {
if term.closed {
return
}
if term.vterm != nil {
// Stop receiving events
term.vterm.Unwatch(term)
term.vterm.Close()
}
if !term.closed && term.OnClose != nil {
term.OnClose(err)
}
if term.ctx != nil {
term.ctx.HideCursor()
}
term.closed = true
}
func (term *Terminal) Destroy() {
if term.destroyed {
return
}
if term.ctx != nil {
term.ctx.HideCursor()
}
// If we destroy, we don't want to call the OnClose callback
term.OnClose = nil
term.Close(nil)
term.vterm = nil
term.destroyed = true
}
func (term *Terminal) Invalidate() {
term.DoInvalidate(term)
}
func (term *Terminal) Draw(ctx *ui.Context) {
if term.destroyed {
return
}
term.ctx = ctx // gross
term.vterm.SetView(ctx.View())
if !term.running && !term.closed && term.cmd != nil {
term.vterm.Watch(term)
attr := &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1}
if err := term.vterm.StartWithAttrs(term.cmd, attr); err != nil {
logging.Errorf("error running terminal: %v", err)
term.Close(err)
return
}
term.running = true
if term.OnStart != nil {
term.OnStart()
}
}
term.draw()
}
func (term *Terminal) draw() {
term.vterm.Draw()
if term.focus && !term.closed && term.ctx != nil {
if !term.cursorShown {
term.ctx.HideCursor()
} else {
_, x, y, style := term.vterm.GetCursor()
term.ctx.SetCursor(x, y)
term.ctx.SetCursorStyle(style)
}
}
}
func (term *Terminal) MouseEvent(localX int, localY int, event tcell.Event) {
ev, ok := event.(*tcell.EventMouse)
if !ok {
return
}
if term.OnEvent != nil {
term.OnEvent(ev)
}
if term.closed {
return
}
e := tcell.NewEventMouse(localX, localY, ev.Buttons(), ev.Modifiers())
term.vterm.HandleEvent(e)
}
func (term *Terminal) Focus(focus bool) {
if term.closed {
return
}
term.focus = focus
if term.ctx != nil {
if !term.focus {
term.ctx.HideCursor()
} else {
_, x, y, style := term.vterm.GetCursor()
term.ctx.SetCursor(x, y)
term.ctx.SetCursorStyle(style)
term.Invalidate()
}
}
}
// HandleEvent is used to watch the underlying terminal events
func (term *Terminal) HandleEvent(ev tcell.Event) bool {
if term.closed || term.destroyed {
return false
}
switch ev := ev.(type) {
case *views.EventWidgetContent:
term.Invalidate()
ui.QueueRedraw()
return true
case *tcellterm.EventTitle:
if term.OnTitle != nil {
term.OnTitle(ev.Title())
}
case *tcellterm.EventClosed:
term.Close(nil)
ui.QueueRedraw()
}
return false
}
func (term *Terminal) Event(event tcell.Event) bool {
if term.OnEvent != nil {
if term.OnEvent(event) {
return true
}
}
if term.closed {
return false
}
return term.vterm.HandleEvent(event)
}