fix(keys): cancel pending esc waiter
- commit
- d8fc8080cae16091f6401f1ef9e7dbd42ab7e76a
- author
- brice · · 2026-07-02 15:32 UTC
- parent
- 0309f6e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +42 -11
M internal/usecase/daemon/daemon.go +2 -5
| @@ -543,10 +543,7 @@ func windowSize(clientSize domain.Size) domain.Size { | |||
| 543 | 543 | if !clientSize.Valid() { | |
| 544 | 544 | clientSize = defaultSize | |
| 545 | 545 | } | |
| 546 | - | rows := clientSize.Rows - 1 | |
| 547 | - | if rows < 1 { | |
| 548 | - | rows = 1 | |
| 549 | - | } | |
| 546 | + | rows := max(clientSize.Rows-1, 1) | |
| 550 | 547 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | |
| 551 | 548 | } | |
| 552 | 549 | ||
| @@ -898,7 +895,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 898 | 895 | func composeClientFrame(sess *session, win *window, full bool) (renderer.Frame, []renderer.Damage) { | |
| 899 | 896 | width, screenRows := win.screen.Frame.Width, win.screen.Frame.Height | |
| 900 | 897 | frame := renderer.NewFrame(width, screenRows+1) | |
| 901 | - | for y := 0; y < screenRows; y++ { | |
| 898 | + | for y := range screenRows { | |
| 902 | 899 | copy(frame.Row(y), win.screen.Frame.Row(y)) | |
| 903 | 900 | } | |
| 904 | 901 | drawStatus(frame.Row(screenRows), sess) | |
| @@ -543,10 +543,7 @@ func windowSize(clientSize domain.Size) domain.Size { | |||||
| 543 | if !clientSize.Valid() { | 543 | if !clientSize.Valid() { | ||
| 544 | clientSize = defaultSize | 544 | clientSize = defaultSize | ||
| 545 | } | 545 | } | ||
| 546 | - | rows := clientSize.Rows - 1 | 546 | + | rows := max(clientSize.Rows-1, 1) |
| 547 | - | if rows < 1 { | |||
| 548 | - | rows = 1 | |||
| 549 | - | } | |||
| 550 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | 547 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | ||
| 551 | } | 548 | } | ||
| 552 | 549 | ||||
| @@ -898,7 +895,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 898 | func composeClientFrame(sess *session, win *window, full bool) (renderer.Frame, []renderer.Damage) { | 895 | func composeClientFrame(sess *session, win *window, full bool) (renderer.Frame, []renderer.Damage) { | ||
| 899 | width, screenRows := win.screen.Frame.Width, win.screen.Frame.Height | 896 | width, screenRows := win.screen.Frame.Width, win.screen.Frame.Height | ||
| 900 | frame := renderer.NewFrame(width, screenRows+1) | 897 | frame := renderer.NewFrame(width, screenRows+1) | ||
| 901 | - | for y := 0; y < screenRows; y++ { | 898 | + | for y := range screenRows { |
| 902 | copy(frame.Row(y), win.screen.Frame.Row(y)) | 899 | copy(frame.Row(y), win.screen.Frame.Row(y)) | ||
| 903 | } | 900 | } | ||
| 904 | drawStatus(frame.Row(screenRows), sess) | 901 | drawStatus(frame.Row(screenRows), sess) | ||
M internal/usecase/keys/router.go +16 -6
| @@ -51,9 +51,10 @@ type Router struct { | |||
| 51 | 51 | delay time.Duration | |
| 52 | 52 | h Handler | |
| 53 | 53 | ||
| 54 | - | mu sync.Mutex | |
| 55 | - | pending bool | |
| 56 | - | timer ports.Timer | |
| 54 | + | mu sync.Mutex | |
| 55 | + | pending bool | |
| 56 | + | timer ports.Timer | |
| 57 | + | pendingDone chan struct{} | |
| 57 | 58 | } | |
| 58 | 59 | ||
| 59 | 60 | func NewRouter(clock ports.Clock, h Handler) *Router { | |
| @@ -142,15 +143,20 @@ func (r *Router) routeAfterPendingESC(data []byte) int { | |||
| 142 | 143 | func (r *Router) retainESC() { | |
| 143 | 144 | r.pending = true | |
| 144 | 145 | r.timer = r.clock.NewTimer(r.delay) | |
| 145 | - | go func(timer ports.Timer) { | |
| 146 | - | <-timer.C() | |
| 146 | + | r.pendingDone = make(chan struct{}) | |
| 147 | + | go func(timer ports.Timer, done <-chan struct{}) { | |
| 148 | + | select { | |
| 149 | + | case <-timer.C(): | |
| 150 | + | case <-done: | |
| 151 | + | return | |
| 152 | + | } | |
| 147 | 153 | r.mu.Lock() | |
| 148 | 154 | defer r.mu.Unlock() | |
| 149 | 155 | if r.pending && r.timer == timer { | |
| 150 | 156 | r.pending = false | |
| 151 | 157 | r.forward([]byte{ESC}) | |
| 152 | 158 | } | |
| 153 | - | }(r.timer) | |
| 159 | + | }(r.timer, r.pendingDone) | |
| 154 | 160 | } | |
| 155 | 161 | ||
| 156 | 162 | func (r *Router) stopTimer() { | |
| @@ -158,6 +164,10 @@ func (r *Router) stopTimer() { | |||
| 158 | 164 | r.timer.Stop() | |
| 159 | 165 | r.timer = nil | |
| 160 | 166 | } | |
| 167 | + | if r.pendingDone != nil { | |
| 168 | + | close(r.pendingDone) | |
| 169 | + | r.pendingDone = nil | |
| 170 | + | } | |
| 161 | 171 | } | |
| 162 | 172 | ||
| 163 | 173 | func (r *Router) forward(data []byte) { | |
| @@ -51,9 +51,10 @@ type Router struct { | |||||
| 51 | delay time.Duration | 51 | delay time.Duration | ||
| 52 | h Handler | 52 | h Handler | ||
| 53 | 53 | ||||
| 54 | - | mu sync.Mutex | 54 | + | mu sync.Mutex |
| 55 | - | pending bool | 55 | + | pending bool |
| 56 | - | timer ports.Timer | 56 | + | timer ports.Timer |
| 57 | + | pendingDone chan struct{} | |||
| 57 | } | 58 | } | ||
| 58 | 59 | ||||
| 59 | func NewRouter(clock ports.Clock, h Handler) *Router { | 60 | func NewRouter(clock ports.Clock, h Handler) *Router { | ||
| @@ -142,15 +143,20 @@ func (r *Router) routeAfterPendingESC(data []byte) int { | |||||
| 142 | func (r *Router) retainESC() { | 143 | func (r *Router) retainESC() { | ||
| 143 | r.pending = true | 144 | r.pending = true | ||
| 144 | r.timer = r.clock.NewTimer(r.delay) | 145 | r.timer = r.clock.NewTimer(r.delay) | ||
| 145 | - | go func(timer ports.Timer) { | 146 | + | r.pendingDone = make(chan struct{}) |
| 146 | - | <-timer.C() | 147 | + | go func(timer ports.Timer, done <-chan struct{}) { |
| 148 | + | select { | |||
| 149 | + | case <-timer.C(): | |||
| 150 | + | case <-done: | |||
| 151 | + | return | |||
| 152 | + | } | |||
| 147 | r.mu.Lock() | 153 | r.mu.Lock() | ||
| 148 | defer r.mu.Unlock() | 154 | defer r.mu.Unlock() | ||
| 149 | if r.pending && r.timer == timer { | 155 | if r.pending && r.timer == timer { | ||
| 150 | r.pending = false | 156 | r.pending = false | ||
| 151 | r.forward([]byte{ESC}) | 157 | r.forward([]byte{ESC}) | ||
| 152 | } | 158 | } | ||
| 153 | - | }(r.timer) | 159 | + | }(r.timer, r.pendingDone) |
| 154 | } | 160 | } | ||
| 155 | 161 | ||||
| 156 | func (r *Router) stopTimer() { | 162 | func (r *Router) stopTimer() { | ||
| @@ -158,6 +164,10 @@ func (r *Router) stopTimer() { | |||||
| 158 | r.timer.Stop() | 164 | r.timer.Stop() | ||
| 159 | r.timer = nil | 165 | r.timer = nil | ||
| 160 | } | 166 | } | ||
| 167 | + | if r.pendingDone != nil { | |||
| 168 | + | close(r.pendingDone) | |||
| 169 | + | r.pendingDone = nil | |||
| 170 | + | } | |||
| 161 | } | 171 | } | ||
| 162 | 172 | ||||
| 163 | func (r *Router) forward(data []byte) { | 173 | func (r *Router) forward(data []byte) { | ||
M internal/usecase/keys/router_test.go +24 -0
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package keys | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "runtime" | |
| 5 | + | "strings" | |
| 4 | 6 | "testing" | |
| 5 | 7 | "time" | |
| 6 | 8 | ||
| @@ -119,6 +121,28 @@ func TestRouterRetainsSplitESCAndInterceptsNextBoundByte(t *testing.T) { | |||
| 119 | 121 | require.Empty(t, h.forwards) | |
| 120 | 122 | } | |
| 121 | 123 | ||
| 124 | + | func TestRouterCancelsPendingESCWaiterWhenNextReadConsumesESC(t *testing.T) { | |
| 125 | + | clk := &fakeClock{} | |
| 126 | + | h := &captureHandler{} | |
| 127 | + | r := NewRouter(clk, h) | |
| 128 | + | ||
| 129 | + | before := retainESCWaiters() | |
| 130 | + | r.Route([]byte{ESC}) | |
| 131 | + | require.Eventually(t, func() bool { return retainESCWaiters() > before }, time.Second, time.Millisecond) | |
| 132 | + | ||
| 133 | + | r.Route([]byte{'n'}) | |
| 134 | + | require.Eventually(t, func() bool { return retainESCWaiters() == before }, time.Second, time.Millisecond) | |
| 135 | + | require.True(t, clk.timers[0].stopped) | |
| 136 | + | require.Equal(t, []Action{ActionNextWindow}, h.actions) | |
| 137 | + | require.Empty(t, h.forwards) | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func retainESCWaiters() int { | |
| 141 | + | buf := make([]byte, 1<<20) | |
| 142 | + | n := runtime.Stack(buf, true) | |
| 143 | + | return strings.Count(string(buf[:n]), "github.com/bnema/vev/internal/usecase/keys.(*Router).retainESC.func1") | |
| 144 | + | } | |
| 145 | + | ||
| 122 | 146 | func TestRouterFlushesLoneESCAfterTimer(t *testing.T) { | |
| 123 | 147 | clk := &fakeClock{} | |
| 124 | 148 | h := &captureHandler{notify: make(chan struct{}, 1)} | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package keys | 1 | package keys | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "runtime" | |||
| 5 | + | "strings" | |||
| 4 | "testing" | 6 | "testing" | ||
| 5 | "time" | 7 | "time" | ||
| 6 | 8 | ||||
| @@ -119,6 +121,28 @@ func TestRouterRetainsSplitESCAndInterceptsNextBoundByte(t *testing.T) { | |||||
| 119 | require.Empty(t, h.forwards) | 121 | require.Empty(t, h.forwards) | ||
| 120 | } | 122 | } | ||
| 121 | 123 | ||||
| 124 | + | func TestRouterCancelsPendingESCWaiterWhenNextReadConsumesESC(t *testing.T) { | |||
| 125 | + | clk := &fakeClock{} | |||
| 126 | + | h := &captureHandler{} | |||
| 127 | + | r := NewRouter(clk, h) | |||
| 128 | + | ||||
| 129 | + | before := retainESCWaiters() | |||
| 130 | + | r.Route([]byte{ESC}) | |||
| 131 | + | require.Eventually(t, func() bool { return retainESCWaiters() > before }, time.Second, time.Millisecond) | |||
| 132 | + | ||||
| 133 | + | r.Route([]byte{'n'}) | |||
| 134 | + | require.Eventually(t, func() bool { return retainESCWaiters() == before }, time.Second, time.Millisecond) | |||
| 135 | + | require.True(t, clk.timers[0].stopped) | |||
| 136 | + | require.Equal(t, []Action{ActionNextWindow}, h.actions) | |||
| 137 | + | require.Empty(t, h.forwards) | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func retainESCWaiters() int { | |||
| 141 | + | buf := make([]byte, 1<<20) | |||
| 142 | + | n := runtime.Stack(buf, true) | |||
| 143 | + | return strings.Count(string(buf[:n]), "github.com/bnema/vev/internal/usecase/keys.(*Router).retainESC.func1") | |||
| 144 | + | } | |||
| 145 | + | ||||
| 122 | func TestRouterFlushesLoneESCAfterTimer(t *testing.T) { | 146 | func TestRouterFlushesLoneESCAfterTimer(t *testing.T) { | ||
| 123 | clk := &fakeClock{} | 147 | clk := &fakeClock{} | ||
| 124 | h := &captureHandler{notify: make(chan struct{}, 1)} | 148 | h := &captureHandler{notify: make(chan struct{}, 1)} | ||