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 } // Stop receiving events term.vterm.Unwatch(term) if term.cmd != nil && term.cmd.Process != nil { err := term.cmd.Process.Kill() if err != nil { logging.Warnf("failed to kill process: %v", err) } // Race condition here, check if cmd exists. If process exits // fast, this could by nil and panic if term.cmd != nil { err = term.cmd.Wait() } if err != nil { logging.Warnf("failed for wait for process to terminate: %v", err) } term.cmd = nil } if term.vterm != nil { term.vterm.Close() } if !term.closed && term.OnClose != nil { term.OnClose(err) } 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.invalidate() } 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 { go func() { defer logging.PanicHandler() term.vterm.Watch(term) attr := &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1} if err := term.vterm.RunWithAttrs(term.cmd, attr); err != nil { logging.Errorf("error running terminal: %w", err) term.Close(err) term.running = false return } term.running = false term.Close(nil) }() for { if term.cmd.Process != nil { term.running = true break } } if term.OnStart != nil { term.OnStart() } } term.draw() } func (term *Terminal) draw() { term.vterm.Draw() if term.focus && !term.closed { 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) { if event, ok := event.(*tcell.EventMouse); ok { if term.OnEvent != nil { if term.OnEvent(event) { return } } if term.closed { return } } } 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: // Draw here for performance improvement. We call draw again in // the main Draw, but tcell-term only draws dirty cells, so it // won't be too much extra CPU there. Drawing there is needed // for certain msgviews, particularly if the pager command // exits. term.draw() // Perform a tcell screen.Show() to show our updates // immediately term.ctx.Show() term.invalidate() return true case *tcellterm.EventTitle: if term.OnTitle != nil { term.OnTitle(ev.Title()) } } 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) }