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:
parent
d847073bdf
commit
bb1249164d
12 changed files with 75 additions and 66 deletions
|
@ -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
20
aerc.go
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
14
lib/ui/ui.go
14
lib/ui/ui.go
|
@ -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,11 +107,15 @@ 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()
|
||||||
|
@ -125,4 +128,3 @@ func (state *UI) ProcessEvents() {
|
||||||
state.Content.Event(event)
|
state.Content.Event(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -18,7 +19,7 @@ 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()
|
||||||
|
|
|
@ -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, '+')]
|
||||||
|
|
Loading…
Reference in a new issue