mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-02-22 23:23:57 +01:00

Add a drop-down (quake-mode) terminal which is a persistent terminal session that overlays aerc at the top and can be toggled on or off. Enable quake mode by setting [General].enable-quake-mode=true. The height of the drop-down terminal can be set with [ui].quake-terminal-height (default: 20). Toggling is hardcoded to the F1 key. Note that this key should not be used in your key bindings when you enable Quake mode. Signed-off-by: Koni Marti <koni.marti@gmail.com> Tested-by: Inwit <inwit@sindominio.net> Acked-by: Robin Jarry <robin@jarry.cc>
177 lines
3.3 KiB
Go
177 lines
3.3 KiB
Go
package app
|
|
|
|
import (
|
|
"os/exec"
|
|
"sync/atomic"
|
|
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
"git.sr.ht/~rockorager/vaxis/widgets/term"
|
|
)
|
|
|
|
type HasTerminal interface {
|
|
Terminal() *Terminal
|
|
}
|
|
|
|
type Terminal struct {
|
|
closed int32
|
|
visible int32 // visible if >0
|
|
cmd *exec.Cmd
|
|
ctx *ui.Context
|
|
focus bool
|
|
vterm *term.Model
|
|
running bool
|
|
|
|
OnClose func(err error)
|
|
OnEvent func(event vaxis.Event) bool
|
|
OnStart func()
|
|
OnTitle func(title string)
|
|
}
|
|
|
|
func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
|
|
term := &Terminal{
|
|
cmd: cmd,
|
|
vterm: term.New(),
|
|
visible: 1,
|
|
}
|
|
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.ctx != nil {
|
|
term.ctx.HideCursor()
|
|
}
|
|
}
|
|
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) {
|
|
if ctx.Width() == 0 || ctx.Height() == 0 {
|
|
return
|
|
}
|
|
term.ctx = ctx
|
|
if !term.running && term.cmd != nil {
|
|
term.vterm.Attach(term.HandleEvent)
|
|
w, h := ctx.Window().Size()
|
|
if err := term.vterm.StartWithSize(term.cmd, w, h); 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(ctx.Window())
|
|
}
|
|
|
|
func (term *Terminal) Show(visible bool) {
|
|
if visible {
|
|
atomic.StoreInt32(&term.visible, 1)
|
|
} else {
|
|
atomic.StoreInt32(&term.visible, 0)
|
|
}
|
|
}
|
|
|
|
func (term *Terminal) Terminal() *Terminal {
|
|
return term
|
|
}
|
|
|
|
func (term *Terminal) MouseEvent(localX int, localY int, event vaxis.Event) {
|
|
ev, ok := event.(vaxis.Mouse)
|
|
if !ok {
|
|
return
|
|
}
|
|
if term.OnEvent != nil {
|
|
term.OnEvent(ev)
|
|
}
|
|
if term.isClosed() {
|
|
return
|
|
}
|
|
ev.Row = localY
|
|
ev.Col = localX
|
|
term.vterm.Update(ev)
|
|
}
|
|
|
|
func (term *Terminal) Focus(focus bool) {
|
|
if term.isClosed() {
|
|
return
|
|
}
|
|
term.focus = focus
|
|
if term.focus {
|
|
term.vterm.Focus()
|
|
} else {
|
|
term.vterm.Blur()
|
|
}
|
|
}
|
|
|
|
// HandleEvent is used to watch the underlying terminal events
|
|
func (t *Terminal) HandleEvent(ev vaxis.Event) {
|
|
if t.isClosed() {
|
|
return
|
|
}
|
|
switch ev := ev.(type) {
|
|
case vaxis.Redraw:
|
|
if atomic.LoadInt32(&t.visible) > 0 {
|
|
ui.Invalidate()
|
|
}
|
|
case term.EventTitle:
|
|
if t.OnTitle != nil {
|
|
t.OnTitle(string(ev))
|
|
}
|
|
case term.EventClosed:
|
|
t.Close()
|
|
ui.Invalidate()
|
|
case term.EventBell:
|
|
aerc.Beep()
|
|
}
|
|
}
|
|
|
|
func (term *Terminal) Event(event vaxis.Event) bool {
|
|
if term.OnEvent != nil {
|
|
if term.OnEvent(event) {
|
|
return true
|
|
}
|
|
}
|
|
if term.isClosed() {
|
|
return false
|
|
}
|
|
term.vterm.Update(event)
|
|
return true
|
|
}
|