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)) } }