fix(daemon): preserve batched notification yank selection
- commit
- 89abebd99d4767d215af44faf680938548012316
- author
- brice · · 2026-07-21 10:52 UTC
- parent
- 23819c3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +41 -5
M internal/usecase/daemon/notices_overlay.go +3 -5
| @@ -171,17 +171,15 @@ func (d *Daemon) handleNoticesInput(ac *attachedClient, data []byte) { | |||
| 171 | 171 | rt.noticeMu.Unlock() | |
| 172 | 172 | return | |
| 173 | 173 | } | |
| 174 | + | var yankTarget domain.Notification | |
| 175 | + | var yank bool | |
| 174 | 176 | result := handleListInputLocked(d.clock, data, d.noticesListInputState(ac), func(b byte) listInputResult { | |
| 175 | 177 | if b == 'y' { | |
| 178 | + | yankTarget, yank = rt.noticesOverlay.Selected() | |
| 176 | 179 | return listInputResult{action: b} | |
| 177 | 180 | } | |
| 178 | 181 | return listInputResult{} | |
| 179 | 182 | }) | |
| 180 | - | var yankTarget domain.Notification | |
| 181 | - | yank := result.action == 'y' | |
| 182 | - | if yank { | |
| 183 | - | yankTarget, yank = rt.noticesOverlay.Selected() | |
| 184 | - | } | |
| 185 | 183 | rt.noticeMu.Unlock() | |
| 186 | 184 | ||
| 187 | 185 | if result.exit { | |
| @@ -171,17 +171,15 @@ func (d *Daemon) handleNoticesInput(ac *attachedClient, data []byte) { | |||||
| 171 | rt.noticeMu.Unlock() | 171 | rt.noticeMu.Unlock() | ||
| 172 | return | 172 | return | ||
| 173 | } | 173 | } | ||
| 174 | + | var yankTarget domain.Notification | |||
| 175 | + | var yank bool | |||
| 174 | result := handleListInputLocked(d.clock, data, d.noticesListInputState(ac), func(b byte) listInputResult { | 176 | result := handleListInputLocked(d.clock, data, d.noticesListInputState(ac), func(b byte) listInputResult { | ||
| 175 | if b == 'y' { | 177 | if b == 'y' { | ||
| 178 | + | yankTarget, yank = rt.noticesOverlay.Selected() | |||
| 176 | return listInputResult{action: b} | 179 | return listInputResult{action: b} | ||
| 177 | } | 180 | } | ||
| 178 | return listInputResult{} | 181 | return listInputResult{} | ||
| 179 | }) | 182 | }) | ||
| 180 | - | var yankTarget domain.Notification | |||
| 181 | - | yank := result.action == 'y' | |||
| 182 | - | if yank { | |||
| 183 | - | yankTarget, yank = rt.noticesOverlay.Selected() | |||
| 184 | - | } | |||
| 185 | rt.noticeMu.Unlock() | 183 | rt.noticeMu.Unlock() | ||
| 186 | 184 | ||||
| 187 | if result.exit { | 185 | if result.exit { | ||
M internal/usecase/daemon/notices_overlay_test.go +38 -0
| @@ -271,6 +271,44 @@ func TestNoticesYKeyYanksSelectedNotification(t *testing.T) { | |||
| 271 | 271 | require.True(t, ac.overlays.noticesActive()) | |
| 272 | 272 | } | |
| 273 | 273 | ||
| 274 | + | func TestNoticesBatchedYankCapturesSelectionAtY(t *testing.T) { | |
| 275 | + | tests := []struct { | |
| 276 | + | name string | |
| 277 | + | input []byte | |
| 278 | + | wantYanked string | |
| 279 | + | wantSelected string | |
| 280 | + | }{ | |
| 281 | + | {name: "yank then down", input: []byte("yj"), wantYanked: "third", wantSelected: "second"}, | |
| 282 | + | {name: "down then yank", input: []byte("jy"), wantYanked: "second", wantSelected: "second"}, | |
| 283 | + | } | |
| 284 | + | for _, tt := range tests { | |
| 285 | + | t.Run(tt.name, func(t *testing.T) { | |
| 286 | + | p, release := newBlockingPTY(t) | |
| 287 | + | defer release() | |
| 288 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 289 | + | d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "first", Time: time.Unix(1, 0)}) | |
| 290 | + | d.notices.record(domain.Notification{Code: domain.NoticeTabSpawn, Message: "second", Time: time.Unix(2, 0)}) | |
| 291 | + | d.notices.record(domain.Notification{Code: domain.NoticeInternal, Message: "third", Time: time.Unix(3, 0)}) | |
| 292 | + | ||
| 293 | + | d.enterNotices(sess, ac) | |
| 294 | + | awaitFrame(t, sends, ports.MsgOutput) | |
| 295 | + | d.handleInput(sess, ac, tt.input) | |
| 296 | + | ||
| 297 | + | out := awaitFrame(t, sends, ports.MsgOutput) | |
| 298 | + | msg, err := ports.UnmarshalOutput(out.Payload) | |
| 299 | + | require.NoError(t, err) | |
| 300 | + | payload := strings.TrimSuffix(strings.TrimPrefix(string(msg.Data), "\x1b]52;c;"), "\a") | |
| 301 | + | decoded, err := base64.StdEncoding.DecodeString(payload) | |
| 302 | + | require.NoError(t, err) | |
| 303 | + | require.Contains(t, string(decoded), "\n"+tt.wantYanked+"\n") | |
| 304 | + | ||
| 305 | + | selected, ok := ac.overlays.noticesOverlay.Selected() | |
| 306 | + | require.True(t, ok) | |
| 307 | + | require.Equal(t, tt.wantSelected, selected.Message) | |
| 308 | + | }) | |
| 309 | + | } | |
| 310 | + | } | |
| 311 | + | ||
| 274 | 312 | func TestNoticesYKeyWithNoSelectionIsNoOp(t *testing.T) { | |
| 275 | 313 | p, release := newBlockingPTY(t) | |
| 276 | 314 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| @@ -271,6 +271,44 @@ func TestNoticesYKeyYanksSelectedNotification(t *testing.T) { | |||||
| 271 | require.True(t, ac.overlays.noticesActive()) | 271 | require.True(t, ac.overlays.noticesActive()) | ||
| 272 | } | 272 | } | ||
| 273 | 273 | ||||
| 274 | + | func TestNoticesBatchedYankCapturesSelectionAtY(t *testing.T) { | |||
| 275 | + | tests := []struct { | |||
| 276 | + | name string | |||
| 277 | + | input []byte | |||
| 278 | + | wantYanked string | |||
| 279 | + | wantSelected string | |||
| 280 | + | }{ | |||
| 281 | + | {name: "yank then down", input: []byte("yj"), wantYanked: "third", wantSelected: "second"}, | |||
| 282 | + | {name: "down then yank", input: []byte("jy"), wantYanked: "second", wantSelected: "second"}, | |||
| 283 | + | } | |||
| 284 | + | for _, tt := range tests { | |||
| 285 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 286 | + | p, release := newBlockingPTY(t) | |||
| 287 | + | defer release() | |||
| 288 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |||
| 289 | + | d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "first", Time: time.Unix(1, 0)}) | |||
| 290 | + | d.notices.record(domain.Notification{Code: domain.NoticeTabSpawn, Message: "second", Time: time.Unix(2, 0)}) | |||
| 291 | + | d.notices.record(domain.Notification{Code: domain.NoticeInternal, Message: "third", Time: time.Unix(3, 0)}) | |||
| 292 | + | ||||
| 293 | + | d.enterNotices(sess, ac) | |||
| 294 | + | awaitFrame(t, sends, ports.MsgOutput) | |||
| 295 | + | d.handleInput(sess, ac, tt.input) | |||
| 296 | + | ||||
| 297 | + | out := awaitFrame(t, sends, ports.MsgOutput) | |||
| 298 | + | msg, err := ports.UnmarshalOutput(out.Payload) | |||
| 299 | + | require.NoError(t, err) | |||
| 300 | + | payload := strings.TrimSuffix(strings.TrimPrefix(string(msg.Data), "\x1b]52;c;"), "\a") | |||
| 301 | + | decoded, err := base64.StdEncoding.DecodeString(payload) | |||
| 302 | + | require.NoError(t, err) | |||
| 303 | + | require.Contains(t, string(decoded), "\n"+tt.wantYanked+"\n") | |||
| 304 | + | ||||
| 305 | + | selected, ok := ac.overlays.noticesOverlay.Selected() | |||
| 306 | + | require.True(t, ok) | |||
| 307 | + | require.Equal(t, tt.wantSelected, selected.Message) | |||
| 308 | + | }) | |||
| 309 | + | } | |||
| 310 | + | } | |||
| 311 | + | ||||
| 274 | func TestNoticesYKeyWithNoSelectionIsNoOp(t *testing.T) { | 312 | func TestNoticesYKeyWithNoSelectionIsNoOp(t *testing.T) { | ||
| 275 | p, release := newBlockingPTY(t) | 313 | p, release := newBlockingPTY(t) | ||
| 276 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 314 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||