aboutsummaryrefslogtreecommitdiffstats
path: root/app/terminal.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/terminal.go')
-rw-r--r--app/terminal.go178
1 files changed, 178 insertions, 0 deletions
diff --git a/app/terminal.go b/app/terminal.go
new file mode 100644
index 00000000..1b177f3e
--- /dev/null
+++ b/app/terminal.go
@@ -0,0 +1,178 @@
+package app
+
+import (
+ "os/exec"
+ "sync/atomic"
+
+ "git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/ui"
+ "git.sr.ht/~rjarry/aerc/log"
+ tcellterm "git.sr.ht/~rockorager/tcell-term"
+
+ "github.com/gdamore/tcell/v2"
+)
+
+type Terminal struct {
+ closed int32
+ cmd *exec.Cmd
+ ctx *ui.Context
+ focus bool
+ visible bool
+ vterm *tcellterm.VT
+ 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{
+ cmd: cmd,
+ vterm: tcellterm.New(),
+ visible: true,
+ }
+ term.vterm.OSC8 = config.General.EnableOSC8
+ term.vterm.TERM = config.General.Term
+ return term, nil
+}
+
+func (term *Terminal) Close() {
+ term.closeErr(nil)
+}
+
+// TODO: replace with atomic.Bool when min go version will have it (1.19+)
+const closed int32 = 1
+
+func (term *Terminal) isClosed() bool {
+ return atomic.LoadInt32(&term.closed) == closed
+}
+
+func (term *Terminal) closeErr(err error) {
+ if atomic.SwapInt32(&term.closed, closed) == closed {
+ return
+ }
+ if term.vterm != nil {
+ // Stop receiving events
+ term.vterm.Detach()
+ term.vterm.Close()
+ }
+ if term.OnClose != nil {
+ term.OnClose(err)
+ }
+ ui.Invalidate()
+}
+
+func (term *Terminal) Destroy() {
+ // If we destroy, we don't want to call the OnClose callback
+ term.OnClose = nil
+ term.closeErr(nil)
+}
+
+func (term *Terminal) Invalidate() {
+ ui.Invalidate()
+}
+
+func (term *Terminal) Draw(ctx *ui.Context) {
+ term.vterm.SetSurface(ctx.View())
+
+ w, h := ctx.View().Size()
+ if !term.isClosed() && term.ctx != nil {
+ ow, oh := term.ctx.View().Size()
+ if w != ow || h != oh {
+ term.vterm.Resize(w, h)
+ }
+ }
+ term.ctx = ctx
+ if !term.running && term.cmd != nil {
+ term.vterm.Attach(term.HandleEvent)
+ if err := term.vterm.Start(term.cmd); err != nil {
+ log.Errorf("error running terminal: %v", err)
+ term.closeErr(err)
+ return
+ }
+ term.running = true
+ if term.OnStart != nil {
+ term.OnStart()
+ }
+ }
+ term.vterm.Draw()
+ if term.focus {
+ y, x, style, vis := term.vterm.Cursor()
+ if vis && !term.isClosed() {
+ ctx.SetCursor(x, y)
+ ctx.SetCursorStyle(style)
+ } else {
+ ctx.HideCursor()
+ }
+ }
+}
+
+func (term *Terminal) Show(visible bool) {
+ term.visible = visible
+}
+
+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.isClosed() {
+ return
+ }
+ e := tcell.NewEventMouse(localX, localY, ev.Buttons(), ev.Modifiers())
+ term.vterm.HandleEvent(e)
+}
+
+func (term *Terminal) Focus(focus bool) {
+ if term.isClosed() {
+ return
+ }
+ term.focus = focus
+ if term.ctx != nil {
+ if !term.focus {
+ term.ctx.HideCursor()
+ } else {
+ y, x, style, _ := term.vterm.Cursor()
+ 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) {
+ if term.isClosed() {
+ return
+ }
+ switch ev := ev.(type) {
+ case *tcellterm.EventRedraw:
+ if term.visible {
+ ui.Invalidate()
+ }
+ case *tcellterm.EventTitle:
+ if term.OnTitle != nil {
+ term.OnTitle(ev.Title())
+ }
+ case *tcellterm.EventClosed:
+ term.Close()
+ ui.Invalidate()
+ }
+}
+
+func (term *Terminal) Event(event tcell.Event) bool {
+ if term.OnEvent != nil {
+ if term.OnEvent(event) {
+ return true
+ }
+ }
+ if term.isClosed() {
+ return false
+ }
+ return term.vterm.HandleEvent(event)
+}