a49caf96b5
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>
175 lines
3.4 KiB
Go
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)
|
|
}
|