aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2024-08-06 21:55:50 +0200
committerRobin Jarry <robin@jarry.cc>2024-10-08 12:45:21 +0200
commitbc8698e1f088cf144a797d2d0b8f875138a79967 (patch)
tree2cab0a76ae4898b264490716dbb665f923feec22 /app
parent1c54bb3a9d11185d86745c64f2c28655385c9146 (diff)
downloadaerc-bc8698e1f088cf144a797d2d0b8f875138a79967.tar.gz
aerc: add quake-mode terminal
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>
Diffstat (limited to 'app')
-rw-r--r--app/aerc.go26
-rw-r--r--app/dialog.go2
-rw-r--r--app/quake.go243
-rw-r--r--app/terminal.go15
4 files changed, 271 insertions, 15 deletions
diff --git a/app/aerc.go b/app/aerc.go
index beef6328..107fc1f3 100644
--- a/app/aerc.go
+++ b/app/aerc.go
@@ -190,16 +190,15 @@ func (aerc *Aerc) Draw(ctx *ui.Context) {
}
aerc.grid.Draw(ctx)
if aerc.dialog != nil {
- if w, h := ctx.Width(), ctx.Height(); w > 8 && h > 4 {
- if d, ok := aerc.dialog.(Dialog); ok {
- xstart, width := d.ContextWidth()
- ystart, height := d.ContextHeight()
- aerc.dialog.Draw(
- ctx.Subcontext(xstart(w), ystart(h),
- width(w), height(h)))
- } else {
- aerc.dialog.Draw(ctx.Subcontext(4, h/2-2, w-8, 4))
- }
+ w, h := ctx.Width(), ctx.Height()
+ if d, ok := aerc.dialog.(Dialog); ok {
+ xstart, width := d.ContextWidth()
+ ystart, height := d.ContextHeight()
+ aerc.dialog.Draw(
+ ctx.Subcontext(xstart(w), ystart(h),
+ width(w), height(h)))
+ } else if w > 8 && h > 4 {
+ aerc.dialog.Draw(ctx.Subcontext(4, h/2-2, w-8, 4))
}
}
}
@@ -329,6 +328,13 @@ func (aerc *Aerc) simulate(strokes []config.KeyStroke) {
}
func (aerc *Aerc) Event(event vaxis.Event) bool {
+ if config.General.QuakeMode {
+ if e, ok := event.(vaxis.Key); ok && e.MatchString("F1") {
+ ToggleQuake()
+ return true
+ }
+ }
+
if aerc.dialog != nil {
return aerc.dialog.Event(event)
}
diff --git a/app/dialog.go b/app/dialog.go
index fba0fd7b..64dbf125 100644
--- a/app/dialog.go
+++ b/app/dialog.go
@@ -30,7 +30,7 @@ func NewDialog(
d ui.DrawableInteractive,
x func(int) int, y func(int) int,
w func(int) int, h func(int) int,
-) Dialog {
+) *dialog {
return &dialog{DrawableInteractive: d, x: x, y: y, w: w, h: h}
}
diff --git a/app/quake.go b/app/quake.go
new file mode 100644
index 00000000..9ceb6457
--- /dev/null
+++ b/app/quake.go
@@ -0,0 +1,243 @@
+package app
+
+import (
+ "os/exec"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "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"
+ "github.com/riywo/loginshell"
+)
+
+var qt quakeTerminal
+
+type quakeTerminal struct {
+ mu sync.Mutex
+ rolling int32
+ visible bool
+ term *Terminal
+}
+
+func ToggleQuake() {
+ handleErr := func(err error) {
+ log.Errorf("quake-terminal: %v", err)
+ }
+ if !qt.HasTerm() {
+ shell, err := loginshell.Shell()
+ if err != nil {
+ handleErr(err)
+ return
+ }
+ args := []string{shell}
+ cmd := exec.Command(args[0], args[1:]...)
+ term, err := NewTerminal(cmd)
+ if err != nil {
+ handleErr(err)
+ return
+ }
+ term.OnClose = func(err error) {
+ if err != nil {
+ aerc.PushError(err.Error())
+ }
+ qt.Hide()
+ qt.SetTerm(nil)
+ }
+ qt.SetTerm(term)
+ }
+
+ if qt.Rolling() {
+ return
+ }
+
+ if qt.Visible() {
+ qt.Hide()
+ } else {
+ qt.Show()
+ }
+}
+
+func (q *quakeTerminal) Rolling() bool {
+ return atomic.LoadInt32(&q.rolling) > 0
+}
+
+func (q *quakeTerminal) SetTerm(t *Terminal) {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+
+ q.term = t
+}
+
+func (q *quakeTerminal) HasTerm() bool {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+
+ return q.term != nil
+}
+
+func (q *quakeTerminal) Visible() bool {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+
+ return q.visible
+}
+
+// inputReturn is helper function to create dialog boxes.
+func inputReturn() func(int) int {
+ return func(x int) int { return x }
+}
+
+// fixReturn is helper function to create dialog boxes.
+func fixReturn(x int) func(int) int {
+ return func(_ int) int { return x }
+}
+
+func (q *quakeTerminal) Show() {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+
+ if q.term == nil {
+ return
+ }
+
+ uiConfig := SelectedAccountUiConfig()
+ h := uiConfig.QuakeHeight
+
+ termBox := NewDialog(
+ ui.NewBox(q.term, "", "", uiConfig),
+ fixReturn(0),
+ fixReturn(0),
+ inputReturn(),
+ fixReturn(h),
+ )
+
+ f := Roller{
+ span: 100 * time.Millisecond,
+ done: func() {
+ log.Tracef("restore after show")
+ atomic.StoreInt32(&q.rolling, 0)
+ ui.QueueFunc(func() {
+ CloseDialog()
+ AddDialog(termBox)
+ })
+ },
+ }
+
+ atomic.StoreInt32(&q.rolling, 1)
+ emptyBox := NewDialog(
+ ui.NewBox(&EmptyInteractive{}, "", "", uiConfig),
+ fixReturn(0),
+ fixReturn(0),
+ inputReturn(),
+ f.Roll(1, h),
+ )
+
+ q.visible = true
+ if q.term != nil {
+ q.term.Show(q.visible)
+ q.term.Focus(q.visible)
+ }
+
+ CloseDialog()
+ AddDialog(emptyBox)
+}
+
+func (q *quakeTerminal) Hide() {
+ uiConfig := SelectedAccountUiConfig()
+ f := Roller{
+ span: 100 * time.Millisecond,
+ done: func() {
+ atomic.StoreInt32(&q.rolling, 0)
+ ui.QueueFunc(CloseDialog)
+ log.Tracef("restore after hide")
+ },
+ }
+
+ atomic.StoreInt32(&q.rolling, 1)
+ emptyBox := NewDialog(
+ ui.NewBox(&EmptyInteractive{}, "", "", uiConfig),
+ fixReturn(0),
+ fixReturn(0),
+ inputReturn(),
+ f.Roll(uiConfig.QuakeHeight, 2),
+ )
+
+ q.mu.Lock()
+ q.visible = false
+ if q.term != nil {
+ q.term.Focus(q.visible)
+ q.term.Show(q.visible)
+ }
+ q.mu.Unlock()
+
+ ui.QueueFunc(func() {
+ CloseDialog()
+ AddDialog(emptyBox)
+ })
+}
+
+type EmptyInteractive struct{}
+
+func (e *EmptyInteractive) Draw(ctx *ui.Context) {
+ w := ctx.Width()
+ h := ctx.Height()
+ if w == 0 || h == 0 {
+ return
+ }
+ style := SelectedAccountUiConfig().GetStyle(config.STYLE_DEFAULT)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
+}
+
+func (e *EmptyInteractive) Invalidate() {
+}
+
+func (e *EmptyInteractive) MouseEvent(_ int, _ int, _ vaxis.Event) {
+}
+
+func (e *EmptyInteractive) Event(_ vaxis.Event) bool {
+ return true
+}
+
+func (e *EmptyInteractive) Focus(_ bool) {
+}
+
+type Roller struct {
+ span time.Duration
+ done func()
+ value int64
+}
+
+func (f *Roller) Roll(start, end int) func(int) int {
+ nsteps := end - start
+
+ var step int64 = 1
+ if end < start {
+ step = -1
+ nsteps = -nsteps
+ }
+
+ span := f.span.Milliseconds() / int64(nsteps)
+ refresh := time.Duration(span) * time.Millisecond
+
+ atomic.StoreInt64(&f.value, int64(start))
+
+ go func() {
+ defer log.PanicHandler()
+ for i := 0; i < int(nsteps); i++ {
+ aerc.Invalidate()
+ time.Sleep(refresh)
+ atomic.AddInt64(&f.value, step)
+ }
+ if f.done != nil {
+ ui.QueueFunc(f.done)
+ }
+ }()
+
+ return func(_ int) int {
+ log.Tracef("in roller")
+ return int(atomic.LoadInt64(&f.value))
+ }
+}
diff --git a/app/terminal.go b/app/terminal.go
index a3109321..9bf4f869 100644
--- a/app/terminal.go
+++ b/app/terminal.go
@@ -17,10 +17,10 @@ type HasTerminal interface {
type Terminal struct {
closed int32
+ visible int32 // visible if >0
cmd *exec.Cmd
ctx *ui.Context
focus bool
- visible bool
vterm *term.Model
running bool
@@ -34,7 +34,7 @@ func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
term := &Terminal{
cmd: cmd,
vterm: term.New(),
- visible: true,
+ visible: 1,
}
term.vterm.OSC8 = config.General.EnableOSC8
term.vterm.TERM = config.General.Term
@@ -81,6 +81,9 @@ func (term *Terminal) 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)
@@ -99,7 +102,11 @@ func (term *Terminal) Draw(ctx *ui.Context) {
}
func (term *Terminal) Show(visible bool) {
- term.visible = visible
+ if visible {
+ atomic.StoreInt32(&term.visible, 1)
+ } else {
+ atomic.StoreInt32(&term.visible, 0)
+ }
}
func (term *Terminal) Terminal() *Terminal {
@@ -141,7 +148,7 @@ func (t *Terminal) HandleEvent(ev vaxis.Event) {
}
switch ev := ev.(type) {
case vaxis.Redraw:
- if t.visible {
+ if atomic.LoadInt32(&t.visible) > 0 {
ui.Invalidate()
}
case term.EventTitle: