aerc: use single event loop

Combine tcell events with WorkerMessages to better synchronize state
with IO and UI. Remove Tick loop for rendering. Use events to trigger
renders.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Tim Culverhouse 2022-10-06 11:46:41 -05:00 committed by Robin Jarry
parent d847073bdf
commit bb1249164d
12 changed files with 75 additions and 66 deletions

View file

@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Running the same command multiple times only adds one entry to the command - Running the same command multiple times only adds one entry to the command
history. history.
- Embedded terminal backend (libvterm was replaced by a pure go implementation). - Embedded terminal backend (libvterm was replaced by a pure go implementation).
- Use event driven loop instead of Tick based
### Fixed ### Fixed
@ -35,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
can be disabled by setting `outgoing-cred-cmd-cache=false` in can be disabled by setting `outgoing-cred-cmd-cache=false` in
`accounts.conf`. `accounts.conf`.
- Mouse support for embedded editors when `mouse-enabled=true`. - Mouse support for embedded editors when `mouse-enabled=true`.
- Numerous race conditions related to event handling order
## [0.12.0](https://git.sr.ht/~rjarry/aerc/refs/0.12.0) - 2022-09-01 ## [0.12.0](https://git.sr.ht/~rjarry/aerc/refs/0.12.0) - 2022-09-01

20
aerc.go
View file

@ -9,9 +9,9 @@ import (
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
"time"
"git.sr.ht/~sircmpwn/getopt" "git.sr.ht/~sircmpwn/getopt"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/xo/terminfo" "github.com/xo/terminfo"
@ -28,6 +28,7 @@ import (
libui "git.sr.ht/~rjarry/aerc/lib/ui" libui "git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~rjarry/aerc/worker/types"
) )
func getCommands(selected libui.Drawable) []*commands.Commands { func getCommands(selected libui.Drawable) []*commands.Commands {
@ -241,15 +242,18 @@ func main() {
setWindowTitle() setWindowTitle()
} }
go ui.ProcessEvents() ui.ChannelEvents()
for !ui.ShouldExit() { for event := range libui.MsgChannel {
for aerc.Tick() { switch event := event.(type) {
// Continue updating our internal state case tcell.Event:
ui.HandleEvent(event)
case types.WorkerMessage:
aerc.HandleMessage(event)
} }
if !ui.Render() { if ui.ShouldExit() {
// ~60 FPS break
time.Sleep(16 * time.Millisecond)
} }
ui.Render()
} }
err = aerc.CloseBackends() err = aerc.CloseBackends()
if err != nil { if err != nil {

View file

@ -7,6 +7,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/marker" "git.sr.ht/~rjarry/aerc/lib/marker"
"git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types" "git.sr.ht/~rjarry/aerc/worker/types"
@ -394,6 +395,7 @@ func (store *MessageStore) runThreadBuilder() {
} }
store.threadBuilderDebounce = time.AfterFunc(store.threadBuilderDelay, func() { store.threadBuilderDebounce = time.AfterFunc(store.threadBuilderDelay, func() {
store.runThreadBuilderNow() store.runThreadBuilderNow()
ui.QueueRedraw()
}) })
} }

View file

@ -314,6 +314,7 @@ func (ti *TextInput) showCompletions() {
ti.completions, ti.prefix = ti.tabcomplete(ti.StringLeft()) ti.completions, ti.prefix = ti.tabcomplete(ti.StringLeft())
ti.completeIndex = -1 ti.completeIndex = -1
ti.Invalidate() ti.Invalidate()
QueueRedraw()
} }
func (ti *TextInput) OnChange(onChange func(ti *TextInput)) { func (ti *TextInput) OnChange(onChange func(ti *TextInput)) {

View file

@ -3,7 +3,6 @@ package ui
import ( import (
"sync/atomic" "sync/atomic"
"git.sr.ht/~rjarry/aerc/logging"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
@ -108,21 +107,24 @@ func (state *UI) EnableMouse() {
state.screen.EnableMouse() state.screen.EnableMouse()
} }
func (state *UI) ProcessEvents() { func (state *UI) ChannelEvents() {
defer logging.PanicHandler() go func() {
for {
MsgChannel <- state.screen.PollEvent()
}
}()
}
for !state.ShouldExit() { func (state *UI) HandleEvent(event tcell.Event) {
event := state.screen.PollEvent() if event, ok := event.(*tcell.EventResize); ok {
if event, ok := event.(*tcell.EventResize); ok { state.screen.Clear()
state.screen.Clear() width, height := event.Size()
width, height := event.Size() state.ctx = NewContext(width, height, state.screen, state.onPopover)
state.ctx = NewContext(width, height, state.screen, state.onPopover) state.Content.Invalidate()
state.Content.Invalidate() }
} // if we have a popover, and it can handle the event, it does so
// if we have a popover, and it can handle the event, it does so if state.popover == nil || !state.popover.Event(event) {
if state.popover == nil || !state.popover.Event(event) { // otherwise, we send the event to the main content
// otherwise, we send the event to the main content state.Content.Event(event)
state.Content.Event(event)
}
} }
} }

View file

@ -74,7 +74,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}) })
worker, err := worker.NewWorker(acct.Source) worker, err := worker.NewWorker(acct.Source, acct.Name)
if err != nil { if err != nil {
host.SetError(fmt.Sprintf("%s: %s", acct.Name, err)) host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
logging.Errorf("%s: %v", acct.Name, err) logging.Errorf("%s: %v", acct.Name, err)
@ -110,20 +110,6 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
return view, nil return view, nil
} }
func (acct *AccountView) Tick() bool {
if acct.worker == nil {
return false
}
select {
case msg := <-acct.worker.Messages:
msg = acct.worker.ProcessMessage(msg)
acct.onMessage(msg)
return true
default:
return false
}
}
func (acct *AccountView) SetStatus(setters ...statusline.SetStateFunc) { func (acct *AccountView) SetStatus(setters ...statusline.SetStateFunc) {
for _, fn := range setters { for _, fn := range setters {
fn(acct.state, acct.SelectedDirectory()) fn(acct.state, acct.SelectedDirectory())
@ -236,6 +222,7 @@ func (acct *AccountView) isSelected() bool {
} }
func (acct *AccountView) onMessage(msg types.WorkerMessage) { func (acct *AccountView) onMessage(msg types.WorkerMessage) {
msg = acct.worker.ProcessMessage(msg)
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.Done: case *types.Done:
switch msg.InResponseTo().(type) { switch msg.InResponseTo().(type) {

View file

@ -20,6 +20,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
) )
type Aerc struct { type Aerc struct {
@ -145,26 +146,10 @@ func (aerc *Aerc) Beep() {
} }
} }
func (aerc *Aerc) Tick() bool { func (aerc *Aerc) HandleMessage(msg types.WorkerMessage) {
more := false if acct, ok := aerc.accounts[msg.Account()]; ok {
for _, acct := range aerc.accounts { acct.onMessage(msg)
more = acct.Tick() || more
} }
if len(aerc.prompts.Children()) > 0 {
more = true
previous := aerc.focused
prompt := aerc.prompts.Pop().(*ExLine)
prompt.finish = func() {
aerc.statusbar.Pop()
aerc.focus(previous)
}
aerc.statusbar.Push(prompt)
aerc.focus(prompt)
}
return more
} }
func (aerc *Aerc) OnInvalidate(onInvalidate func(d ui.Drawable)) { func (aerc *Aerc) OnInvalidate(onInvalidate func(d ui.Drawable)) {
@ -182,6 +167,17 @@ func (aerc *Aerc) Focus(focus bool) {
} }
func (aerc *Aerc) Draw(ctx *ui.Context) { func (aerc *Aerc) Draw(ctx *ui.Context) {
if len(aerc.prompts.Children()) > 0 {
previous := aerc.focused
prompt := aerc.prompts.Pop().(*ExLine)
prompt.finish = func() {
aerc.statusbar.Pop()
aerc.focus(previous)
}
aerc.statusbar.Push(prompt)
aerc.focus(prompt)
}
aerc.grid.Draw(ctx) aerc.grid.Draw(ctx)
if aerc.dialog != nil { if aerc.dialog != nil {
if w, h := ctx.Width(), ctx.Height(); w > 8 && h > 4 { if w, h := ctx.Width(), ctx.Height(); w > 8 && h > 4 {

View file

@ -49,6 +49,7 @@ func (s *Spinner) Start() {
case <-time.After(200 * time.Millisecond): case <-time.After(200 * time.Millisecond):
atomic.AddInt64(&s.frame, 1) atomic.AddInt64(&s.frame, 1)
s.Invalidate() s.Invalidate()
ui.QueueRedraw()
} }
} }
}() }()

View file

@ -169,6 +169,7 @@ func (term *Terminal) HandleEvent(ev tcell.Event) bool {
} }
case *tcellterm.EventClosed: case *tcellterm.EventClosed:
term.Close(nil) term.Close(nil)
ui.QueueRedraw()
} }
return false return false
} }

View file

@ -12,11 +12,14 @@ type WorkerMessage interface {
InResponseTo() WorkerMessage InResponseTo() WorkerMessage
getId() int64 getId() int64
setId(id int64) setId(id int64)
Account() string
setAccount(string)
} }
type Message struct { type Message struct {
inResponseTo WorkerMessage inResponseTo WorkerMessage
id int64 id int64
acct string
} }
func RespondTo(msg WorkerMessage) Message { func RespondTo(msg WorkerMessage) Message {
@ -37,6 +40,14 @@ func (m *Message) setId(id int64) {
m.id = id m.id = id
} }
func (m *Message) Account() string {
return m.acct
}
func (m *Message) setAccount(name string) {
m.acct = name
}
// Meta-messages // Meta-messages
type Done struct { type Done struct {

View file

@ -5,6 +5,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
) )
@ -16,9 +17,9 @@ type Backend interface {
} }
type Worker struct { type Worker struct {
Backend Backend Backend Backend
Actions chan WorkerMessage Actions chan WorkerMessage
Messages chan WorkerMessage Name string
actionCallbacks map[int64]func(msg WorkerMessage) actionCallbacks map[int64]func(msg WorkerMessage)
messageCallbacks map[int64]func(msg WorkerMessage) messageCallbacks map[int64]func(msg WorkerMessage)
@ -28,10 +29,10 @@ type Worker struct {
sync.Mutex sync.Mutex
} }
func NewWorker() *Worker { func NewWorker(name string) *Worker {
return &Worker{ return &Worker{
Actions: make(chan WorkerMessage), Actions: make(chan WorkerMessage),
Messages: make(chan WorkerMessage, 50), Name: name,
actionCallbacks: make(map[int64]func(msg WorkerMessage)), actionCallbacks: make(map[int64]func(msg WorkerMessage)),
messageCallbacks: make(map[int64]func(msg WorkerMessage)), messageCallbacks: make(map[int64]func(msg WorkerMessage)),
actionQueue: list.New(), actionQueue: list.New(),
@ -103,13 +104,14 @@ func (worker *Worker) PostMessage(msg WorkerMessage,
cb func(msg WorkerMessage), cb func(msg WorkerMessage),
) { ) {
worker.setId(msg) worker.setId(msg)
msg.setAccount(worker.Name)
if resp := msg.InResponseTo(); resp != nil { if resp := msg.InResponseTo(); resp != nil {
logging.Debugf("PostMessage %T:%T", msg, resp) logging.Debugf("PostMessage %T:%T", msg, resp)
} else { } else {
logging.Debugf("PostMessage %T", msg) logging.Debugf("PostMessage %T", msg)
} }
worker.Messages <- msg ui.MsgChannel <- msg
if cb != nil { if cb != nil {
worker.Lock() worker.Lock()

View file

@ -9,12 +9,12 @@ import (
) )
// Guesses the appropriate worker type based on the given source string // Guesses the appropriate worker type based on the given source string
func NewWorker(source string) (*types.Worker, error) { func NewWorker(source string, name string) (*types.Worker, error) {
u, err := url.Parse(source) u, err := url.Parse(source)
if err != nil { if err != nil {
return nil, err return nil, err
} }
worker := types.NewWorker() worker := types.NewWorker(name)
scheme := u.Scheme scheme := u.Scheme
if strings.ContainsRune(scheme, '+') { if strings.ContainsRune(scheme, '+') {
scheme = scheme[:strings.IndexRune(scheme, '+')] scheme = scheme[:strings.IndexRune(scheme, '+')]