refactor(daemon): deepen overlay runtime
- commit
- ae77a3124679c4ad936efed3268c1e21518a55b8
- author
- brice · · 2026-07-04 18:08 UTC
- committer
- brice · · 2026-07-05 04:22 UTC
- parent
- 604f123
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
24 files · +1015 -519
M internal/usecase/daemon/attention_test.go +2 -0
| @@ -101,6 +101,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) { | |||
| 101 | 101 | }).Maybe() | |
| 102 | 102 | trW.EXPECT().Close().Return(nil).Maybe() | |
| 103 | 103 | acW := &attachedClient{tr: trW, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 104 | + | acW.initOverlays() | |
| 104 | 105 | sctxW, cancelW := context.WithCancel(d.serveCtx) | |
| 105 | 106 | t.Cleanup(cancelW) | |
| 106 | 107 | tabW := newTestTabWithContext(newScriptPTY(nil), sctxW, cancelW) | |
| @@ -348,6 +349,7 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) { | |||
| 348 | 349 | ||
| 349 | 350 | trB, sendsB := newCapturingTransport(t) | |
| 350 | 351 | acB := &attachedClient{tr: trB, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 352 | + | acB.initOverlays() | |
| 351 | 353 | sctxB, cancelB := context.WithCancel(d.serveCtx) | |
| 352 | 354 | t.Cleanup(cancelB) | |
| 353 | 355 | tbB := newTestTabWithContext(pB, sctxB, cancelB) | |
| @@ -101,6 +101,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) { | |||||
| 101 | }).Maybe() | 101 | }).Maybe() | ||
| 102 | trW.EXPECT().Close().Return(nil).Maybe() | 102 | trW.EXPECT().Close().Return(nil).Maybe() | ||
| 103 | acW := &attachedClient{tr: trW, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 103 | acW := &attachedClient{tr: trW, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 104 | + | acW.initOverlays() | |||
| 104 | sctxW, cancelW := context.WithCancel(d.serveCtx) | 105 | sctxW, cancelW := context.WithCancel(d.serveCtx) | ||
| 105 | t.Cleanup(cancelW) | 106 | t.Cleanup(cancelW) | ||
| 106 | tabW := newTestTabWithContext(newScriptPTY(nil), sctxW, cancelW) | 107 | tabW := newTestTabWithContext(newScriptPTY(nil), sctxW, cancelW) | ||
| @@ -348,6 +349,7 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) { | |||||
| 348 | 349 | ||||
| 349 | trB, sendsB := newCapturingTransport(t) | 350 | trB, sendsB := newCapturingTransport(t) | ||
| 350 | acB := &attachedClient{tr: trB, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 351 | acB := &attachedClient{tr: trB, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 352 | + | acB.initOverlays() | |||
| 351 | sctxB, cancelB := context.WithCancel(d.serveCtx) | 353 | sctxB, cancelB := context.WithCancel(d.serveCtx) | ||
| 352 | t.Cleanup(cancelB) | 354 | t.Cleanup(cancelB) | ||
| 353 | tbB := newTestTabWithContext(pB, sctxB, cancelB) | 355 | tbB := newTestTabWithContext(pB, sctxB, cancelB) | ||
M internal/usecase/daemon/client.go +26 -69
| @@ -32,58 +32,32 @@ import ( | |||
| 32 | 32 | ||
| 33 | 33 | "github.com/bnema/vev/internal/domain" | |
| 34 | 34 | "github.com/bnema/vev/internal/ports" | |
| 35 | - | scopy "github.com/bnema/vev/internal/usecase/copy" | |
| 36 | 35 | "github.com/bnema/vev/internal/usecase/keys" | |
| 37 | 36 | "github.com/bnema/vev/internal/usecase/mouse" | |
| 38 | - | "github.com/bnema/vev/internal/usecase/palette" | |
| 39 | - | "github.com/bnema/vev/internal/usecase/picker" | |
| 40 | - | promptui "github.com/bnema/vev/internal/usecase/prompt" | |
| 41 | 37 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 42 | 38 | "github.com/bnema/vev/pkg/renderer" | |
| 43 | 39 | ) | |
| 44 | 40 | ||
| 45 | 41 | type attachedClient struct { | |
| 46 | - | tr ports.Transport | |
| 47 | - | rend *renderer.Renderer | |
| 48 | - | clientID [16]byte | |
| 49 | - | resumeCapable bool | |
| 50 | - | resumeToken uint64 | |
| 51 | - | parked bool | |
| 52 | - | nextStateNum uint64 | |
| 53 | - | echoAck atomic.Uint64 | |
| 54 | - | bars barCache // only touched while sendMu is held | |
| 55 | - | size domain.Size | |
| 56 | - | keys *keys.Router | |
| 57 | - | sess Guarded[*session] | |
| 58 | - | copyMu sync.Mutex | |
| 59 | - | copyMode *scopy.Mode | |
| 60 | - | copyPending []byte | |
| 61 | - | copyESC pendingByteTimer | |
| 62 | - | copyFeedback string | |
| 63 | - | copyPressRow int | |
| 64 | - | copyPressRowValid bool | |
| 65 | - | copyDragging bool | |
| 66 | - | normalMousePressRow int | |
| 67 | - | normalMousePressTop int | |
| 68 | - | normalMousePressValid bool | |
| 69 | - | pickerMu sync.Mutex | |
| 70 | - | picker *picker.Model | |
| 71 | - | pickerPreview *tab | |
| 72 | - | pickerPending []byte | |
| 73 | - | pickerESC pendingByteTimer | |
| 74 | - | paletteMu sync.Mutex | |
| 75 | - | palette *palette.Model | |
| 76 | - | palettePending []byte | |
| 77 | - | promptMu sync.Mutex | |
| 78 | - | prompt *promptui.Model | |
| 79 | - | promptSubmit func(string) error | |
| 80 | - | promptPending []byte | |
| 81 | - | mouseScan mouse.Scanner | |
| 82 | - | themeMu sync.Mutex | |
| 83 | - | theme themeui.Theme | |
| 84 | - | lastCursor cursorOut | |
| 85 | - | linkMu sync.Mutex | |
| 86 | - | sendMu sync.Mutex | |
| 42 | + | tr ports.Transport | |
| 43 | + | rend *renderer.Renderer | |
| 44 | + | overlays *overlayRuntime | |
| 45 | + | clientID [16]byte | |
| 46 | + | resumeCapable bool | |
| 47 | + | resumeToken uint64 | |
| 48 | + | parked bool | |
| 49 | + | nextStateNum uint64 | |
| 50 | + | echoAck atomic.Uint64 | |
| 51 | + | bars barCache // only touched while sendMu is held | |
| 52 | + | size domain.Size | |
| 53 | + | keys *keys.Router | |
| 54 | + | sess Guarded[*session] | |
| 55 | + | mouseScan mouse.Scanner | |
| 56 | + | themeMu sync.Mutex | |
| 57 | + | theme themeui.Theme | |
| 58 | + | lastCursor cursorOut | |
| 59 | + | linkMu sync.Mutex | |
| 60 | + | sendMu sync.Mutex | |
| 87 | 61 | } | |
| 88 | 62 | ||
| 89 | 63 | type cursorOut struct { | |
| @@ -124,6 +98,12 @@ func (p *pendingByteTimer) stop() { | |||
| 124 | 98 | } | |
| 125 | 99 | } | |
| 126 | 100 | ||
| 101 | + | func (ac *attachedClient) initOverlays() { | |
| 102 | + | if ac.overlays == nil { | |
| 103 | + | ac.overlays = newOverlayRuntime(ac) | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | ||
| 127 | 107 | func (ac *attachedClient) currentSession() *session { return ac.sess.Get() } | |
| 128 | 108 | ||
| 129 | 109 | func (ac *attachedClient) setSession(sess *session) { ac.sess.Set(sess) } | |
| @@ -140,30 +120,6 @@ func (ac *attachedClient) setTheme(t themeui.Theme) { | |||
| 140 | 120 | ac.themeMu.Unlock() | |
| 141 | 121 | } | |
| 142 | 122 | ||
| 143 | - | func (ac *attachedClient) copyModeActive() bool { | |
| 144 | - | ac.copyMu.Lock() | |
| 145 | - | defer ac.copyMu.Unlock() | |
| 146 | - | return ac.copyMode != nil | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | func (ac *attachedClient) pickerActive() bool { | |
| 150 | - | ac.pickerMu.Lock() | |
| 151 | - | defer ac.pickerMu.Unlock() | |
| 152 | - | return ac.picker != nil | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | func (ac *attachedClient) paletteActive() bool { | |
| 156 | - | ac.paletteMu.Lock() | |
| 157 | - | defer ac.paletteMu.Unlock() | |
| 158 | - | return ac.palette != nil | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | func (ac *attachedClient) promptActive() bool { | |
| 162 | - | ac.promptMu.Lock() | |
| 163 | - | defer ac.promptMu.Unlock() | |
| 164 | - | return ac.prompt != nil | |
| 165 | - | } | |
| 166 | - | ||
| 167 | 123 | func (ac *attachedClient) transport() ports.Transport { | |
| 168 | 124 | ac.linkMu.Lock() | |
| 169 | 125 | defer ac.linkMu.Unlock() | |
| @@ -362,6 +318,7 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||
| 362 | 318 | resumeCapable: opts.resumeCapable, | |
| 363 | 319 | resumeToken: resumeToken, | |
| 364 | 320 | } | |
| 321 | + | ac.initOverlays() | |
| 365 | 322 | ac.setSession(sess) | |
| 366 | 323 | ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}) | |
| 367 | 324 | sess.mu.Lock() | |
| @@ -32,58 +32,32 @@ import ( | |||||
| 32 | 32 | ||||
| 33 | "github.com/bnema/vev/internal/domain" | 33 | "github.com/bnema/vev/internal/domain" | ||
| 34 | "github.com/bnema/vev/internal/ports" | 34 | "github.com/bnema/vev/internal/ports" | ||
| 35 | - | scopy "github.com/bnema/vev/internal/usecase/copy" | |||
| 36 | "github.com/bnema/vev/internal/usecase/keys" | 35 | "github.com/bnema/vev/internal/usecase/keys" | ||
| 37 | "github.com/bnema/vev/internal/usecase/mouse" | 36 | "github.com/bnema/vev/internal/usecase/mouse" | ||
| 38 | - | "github.com/bnema/vev/internal/usecase/palette" | |||
| 39 | - | "github.com/bnema/vev/internal/usecase/picker" | |||
| 40 | - | promptui "github.com/bnema/vev/internal/usecase/prompt" | |||
| 41 | themeui "github.com/bnema/vev/internal/usecase/theme" | 37 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 42 | "github.com/bnema/vev/pkg/renderer" | 38 | "github.com/bnema/vev/pkg/renderer" | ||
| 43 | ) | 39 | ) | ||
| 44 | 40 | ||||
| 45 | type attachedClient struct { | 41 | type attachedClient struct { | ||
| 46 | - | tr ports.Transport | 42 | + | tr ports.Transport |
| 47 | - | rend *renderer.Renderer | 43 | + | rend *renderer.Renderer |
| 48 | - | clientID [16]byte | 44 | + | overlays *overlayRuntime |
| 49 | - | resumeCapable bool | 45 | + | clientID [16]byte |
| 50 | - | resumeToken uint64 | 46 | + | resumeCapable bool |
| 51 | - | parked bool | 47 | + | resumeToken uint64 |
| 52 | - | nextStateNum uint64 | 48 | + | parked bool |
| 53 | - | echoAck atomic.Uint64 | 49 | + | nextStateNum uint64 |
| 54 | - | bars barCache // only touched while sendMu is held | 50 | + | echoAck atomic.Uint64 |
| 55 | - | size domain.Size | 51 | + | bars barCache // only touched while sendMu is held |
| 56 | - | keys *keys.Router | 52 | + | size domain.Size |
| 57 | - | sess Guarded[*session] | 53 | + | keys *keys.Router |
| 58 | - | copyMu sync.Mutex | 54 | + | sess Guarded[*session] |
| 59 | - | copyMode *scopy.Mode | 55 | + | mouseScan mouse.Scanner |
| 60 | - | copyPending []byte | 56 | + | themeMu sync.Mutex |
| 61 | - | copyESC pendingByteTimer | 57 | + | theme themeui.Theme |
| 62 | - | copyFeedback string | 58 | + | lastCursor cursorOut |
| 63 | - | copyPressRow int | 59 | + | linkMu sync.Mutex |
| 64 | - | copyPressRowValid bool | 60 | + | sendMu sync.Mutex |
| 65 | - | copyDragging bool | |||
| 66 | - | normalMousePressRow int | |||
| 67 | - | normalMousePressTop int | |||
| 68 | - | normalMousePressValid bool | |||
| 69 | - | pickerMu sync.Mutex | |||
| 70 | - | picker *picker.Model | |||
| 71 | - | pickerPreview *tab | |||
| 72 | - | pickerPending []byte | |||
| 73 | - | pickerESC pendingByteTimer | |||
| 74 | - | paletteMu sync.Mutex | |||
| 75 | - | palette *palette.Model | |||
| 76 | - | palettePending []byte | |||
| 77 | - | promptMu sync.Mutex | |||
| 78 | - | prompt *promptui.Model | |||
| 79 | - | promptSubmit func(string) error | |||
| 80 | - | promptPending []byte | |||
| 81 | - | mouseScan mouse.Scanner | |||
| 82 | - | themeMu sync.Mutex | |||
| 83 | - | theme themeui.Theme | |||
| 84 | - | lastCursor cursorOut | |||
| 85 | - | linkMu sync.Mutex | |||
| 86 | - | sendMu sync.Mutex | |||
| 87 | } | 61 | } | ||
| 88 | 62 | ||||
| 89 | type cursorOut struct { | 63 | type cursorOut struct { | ||
| @@ -124,6 +98,12 @@ func (p *pendingByteTimer) stop() { | |||||
| 124 | } | 98 | } | ||
| 125 | } | 99 | } | ||
| 126 | 100 | ||||
| 101 | + | func (ac *attachedClient) initOverlays() { | |||
| 102 | + | if ac.overlays == nil { | |||
| 103 | + | ac.overlays = newOverlayRuntime(ac) | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | ||||
| 127 | func (ac *attachedClient) currentSession() *session { return ac.sess.Get() } | 107 | func (ac *attachedClient) currentSession() *session { return ac.sess.Get() } | ||
| 128 | 108 | ||||
| 129 | func (ac *attachedClient) setSession(sess *session) { ac.sess.Set(sess) } | 109 | func (ac *attachedClient) setSession(sess *session) { ac.sess.Set(sess) } | ||
| @@ -140,30 +120,6 @@ func (ac *attachedClient) setTheme(t themeui.Theme) { | |||||
| 140 | ac.themeMu.Unlock() | 120 | ac.themeMu.Unlock() | ||
| 141 | } | 121 | } | ||
| 142 | 122 | ||||
| 143 | - | func (ac *attachedClient) copyModeActive() bool { | |||
| 144 | - | ac.copyMu.Lock() | |||
| 145 | - | defer ac.copyMu.Unlock() | |||
| 146 | - | return ac.copyMode != nil | |||
| 147 | - | } | |||
| 148 | - | ||||
| 149 | - | func (ac *attachedClient) pickerActive() bool { | |||
| 150 | - | ac.pickerMu.Lock() | |||
| 151 | - | defer ac.pickerMu.Unlock() | |||
| 152 | - | return ac.picker != nil | |||
| 153 | - | } | |||
| 154 | - | ||||
| 155 | - | func (ac *attachedClient) paletteActive() bool { | |||
| 156 | - | ac.paletteMu.Lock() | |||
| 157 | - | defer ac.paletteMu.Unlock() | |||
| 158 | - | return ac.palette != nil | |||
| 159 | - | } | |||
| 160 | - | ||||
| 161 | - | func (ac *attachedClient) promptActive() bool { | |||
| 162 | - | ac.promptMu.Lock() | |||
| 163 | - | defer ac.promptMu.Unlock() | |||
| 164 | - | return ac.prompt != nil | |||
| 165 | - | } | |||
| 166 | - | ||||
| 167 | func (ac *attachedClient) transport() ports.Transport { | 123 | func (ac *attachedClient) transport() ports.Transport { | ||
| 168 | ac.linkMu.Lock() | 124 | ac.linkMu.Lock() | ||
| 169 | defer ac.linkMu.Unlock() | 125 | defer ac.linkMu.Unlock() | ||
| @@ -362,6 +318,7 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 362 | resumeCapable: opts.resumeCapable, | 318 | resumeCapable: opts.resumeCapable, | ||
| 363 | resumeToken: resumeToken, | 319 | resumeToken: resumeToken, | ||
| 364 | } | 320 | } | ||
| 321 | + | ac.initOverlays() | |||
| 365 | ac.setSession(sess) | 322 | ac.setSession(sess) | ||
| 366 | ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}) | 323 | ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}) | ||
| 367 | sess.mu.Lock() | 324 | sess.mu.Lock() | ||
M internal/usecase/daemon/clipboard_test.go +4 -0
| @@ -191,6 +191,7 @@ func TestPTYReaderForwardsOSC52ClipboardToAttachedClient(t *testing.T) { | |||
| 191 | 191 | defer cancel() | |
| 192 | 192 | win := newTestTabWithContext(p, sctx, cancel) | |
| 193 | 193 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 194 | + | ac.initOverlays() | |
| 194 | 195 | sess := &session{id: "clip", name: "clip", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 195 | 196 | ac.setSession(sess) | |
| 196 | 197 | d.sessions[sess.id] = sess | |
| @@ -215,6 +216,7 @@ func TestPTYReaderDropsOversizedClipboardPayload(t *testing.T) { | |||
| 215 | 216 | defer cancel() | |
| 216 | 217 | win := newTestTabWithContext(p, sctx, cancel) | |
| 217 | 218 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 219 | + | ac.initOverlays() | |
| 218 | 220 | sess := &session{id: "clip-big", name: "clip-big", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 219 | 221 | ac.setSession(sess) | |
| 220 | 222 | d.sessions[sess.id] = sess | |
| @@ -238,6 +240,7 @@ func TestPTYReaderDropsInvalidBase64Clipboard(t *testing.T) { | |||
| 238 | 240 | defer cancel() | |
| 239 | 241 | win := newTestTabWithContext(p, sctx, cancel) | |
| 240 | 242 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 243 | + | ac.initOverlays() | |
| 241 | 244 | sess := &session{id: "clip-bad", name: "clip-bad", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 242 | 245 | ac.setSession(sess) | |
| 243 | 246 | d.sessions[sess.id] = sess | |
| @@ -272,6 +275,7 @@ func TestForwardClipboardAsyncSerializesClipboardWrites(t *testing.T) { | |||
| 272 | 275 | d := newTestDaemon(t, nil, stubClock{}) | |
| 273 | 276 | tr := newBlockingClipboardTransport() | |
| 274 | 277 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 278 | + | ac.initOverlays() | |
| 275 | 279 | sctx, cancel := context.WithCancel(context.Background()) | |
| 276 | 280 | defer cancel() | |
| 277 | 281 | sess := &session{id: "clip-order", name: "clip-order", ctx: sctx, cancel: cancel, client: ac} | |
| @@ -191,6 +191,7 @@ func TestPTYReaderForwardsOSC52ClipboardToAttachedClient(t *testing.T) { | |||||
| 191 | defer cancel() | 191 | defer cancel() | ||
| 192 | win := newTestTabWithContext(p, sctx, cancel) | 192 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 193 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 193 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 194 | + | ac.initOverlays() | |||
| 194 | sess := &session{id: "clip", name: "clip", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 195 | sess := &session{id: "clip", name: "clip", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 195 | ac.setSession(sess) | 196 | ac.setSession(sess) | ||
| 196 | d.sessions[sess.id] = sess | 197 | d.sessions[sess.id] = sess | ||
| @@ -215,6 +216,7 @@ func TestPTYReaderDropsOversizedClipboardPayload(t *testing.T) { | |||||
| 215 | defer cancel() | 216 | defer cancel() | ||
| 216 | win := newTestTabWithContext(p, sctx, cancel) | 217 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 217 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 218 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 219 | + | ac.initOverlays() | |||
| 218 | sess := &session{id: "clip-big", name: "clip-big", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 220 | sess := &session{id: "clip-big", name: "clip-big", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 219 | ac.setSession(sess) | 221 | ac.setSession(sess) | ||
| 220 | d.sessions[sess.id] = sess | 222 | d.sessions[sess.id] = sess | ||
| @@ -238,6 +240,7 @@ func TestPTYReaderDropsInvalidBase64Clipboard(t *testing.T) { | |||||
| 238 | defer cancel() | 240 | defer cancel() | ||
| 239 | win := newTestTabWithContext(p, sctx, cancel) | 241 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 240 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 242 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 243 | + | ac.initOverlays() | |||
| 241 | sess := &session{id: "clip-bad", name: "clip-bad", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 244 | sess := &session{id: "clip-bad", name: "clip-bad", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 242 | ac.setSession(sess) | 245 | ac.setSession(sess) | ||
| 243 | d.sessions[sess.id] = sess | 246 | d.sessions[sess.id] = sess | ||
| @@ -272,6 +275,7 @@ func TestForwardClipboardAsyncSerializesClipboardWrites(t *testing.T) { | |||||
| 272 | d := newTestDaemon(t, nil, stubClock{}) | 275 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 273 | tr := newBlockingClipboardTransport() | 276 | tr := newBlockingClipboardTransport() | ||
| 274 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 277 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 278 | + | ac.initOverlays() | |||
| 275 | sctx, cancel := context.WithCancel(context.Background()) | 279 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 276 | defer cancel() | 280 | defer cancel() | ||
| 277 | sess := &session{id: "clip-order", name: "clip-order", ctx: sctx, cancel: cancel, client: ac} | 281 | sess := &session{id: "clip-order", name: "clip-order", ctx: sctx, cancel: cancel, client: ac} | ||
M internal/usecase/daemon/copymode.go +76 -70
| @@ -37,6 +37,7 @@ import ( | |||
| 37 | 37 | ) | |
| 38 | 38 | ||
| 39 | 39 | func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |
| 40 | + | rt := ac.overlays | |
| 40 | 41 | tb := sess.activeTab() | |
| 41 | 42 | if tb == nil { | |
| 42 | 43 | return | |
| @@ -49,32 +50,33 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||
| 49 | 50 | return | |
| 50 | 51 | } | |
| 51 | 52 | p.mu.Lock() | |
| 52 | - | ac.copyMu.Lock() | |
| 53 | - | if ac.copyMode == nil { | |
| 54 | - | ac.copyMu.Unlock() | |
| 53 | + | rt.copyMu.Lock() | |
| 54 | + | if rt.copyMode == nil { | |
| 55 | + | rt.copyMu.Unlock() | |
| 55 | 56 | p.mu.Unlock() | |
| 56 | 57 | return | |
| 57 | 58 | } | |
| 58 | 59 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 59 | - | if delta > 0 && ac.copyMode.AtBottom(snap) { | |
| 60 | - | ac.copyMode = nil | |
| 61 | - | ac.copyMu.Unlock() | |
| 60 | + | if delta > 0 && rt.copyMode.AtBottom(snap) { | |
| 61 | + | rt.copyMode = nil | |
| 62 | + | rt.copyMu.Unlock() | |
| 62 | 63 | p.mu.Unlock() | |
| 63 | 64 | d.paint(sess, ac, true) | |
| 64 | 65 | return | |
| 65 | 66 | } | |
| 66 | - | ac.copyMode.Move(snap, delta) | |
| 67 | - | exit := delta > 0 && ac.copyMode.AtBottom(snap) | |
| 67 | + | rt.copyMode.Move(snap, delta) | |
| 68 | + | exit := delta > 0 && rt.copyMode.AtBottom(snap) | |
| 68 | 69 | if exit { | |
| 69 | - | ac.copyMode = nil | |
| 70 | + | rt.copyMode = nil | |
| 70 | 71 | } | |
| 71 | - | ac.copyMu.Unlock() | |
| 72 | + | rt.copyMu.Unlock() | |
| 72 | 73 | p.mu.Unlock() | |
| 73 | 74 | ||
| 74 | 75 | d.paint(sess, ac, true) | |
| 75 | 76 | } | |
| 76 | 77 | ||
| 77 | 78 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |
| 79 | + | rt := ac.overlays | |
| 78 | 80 | tb := sess.activeTab() | |
| 79 | 81 | if tb == nil { | |
| 80 | 82 | return | |
| @@ -88,16 +90,17 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||
| 88 | 90 | p.mu.Lock() | |
| 89 | 91 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 90 | 92 | p.mu.Unlock() | |
| 91 | - | ac.copyMu.Lock() | |
| 92 | - | ac.copyMode = scopy.NewMode(snap) | |
| 93 | - | ac.copyPressRowValid = false | |
| 94 | - | ac.copyDragging = false | |
| 95 | - | ac.normalMousePressValid = false | |
| 96 | - | ac.copyMu.Unlock() | |
| 93 | + | rt.copyMu.Lock() | |
| 94 | + | rt.copyMode = scopy.NewMode(snap) | |
| 95 | + | rt.copyPressRowValid = false | |
| 96 | + | rt.copyDragging = false | |
| 97 | + | rt.normalMousePressValid = false | |
| 98 | + | rt.copyMu.Unlock() | |
| 97 | 99 | d.paint(sess, ac, true) | |
| 98 | 100 | } | |
| 99 | 101 | ||
| 100 | 102 | func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |
| 103 | + | rt := ac.overlays | |
| 101 | 104 | if ev.Button != mouse.Left { | |
| 102 | 105 | return | |
| 103 | 106 | } | |
| @@ -115,44 +118,44 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||
| 115 | 118 | p.mu.Lock() | |
| 116 | 119 | if ev.Row >= p.screen.Frame.Height { | |
| 117 | 120 | if ev.Type == mouse.Press { | |
| 118 | - | ac.copyMu.Lock() | |
| 119 | - | ac.copyPressRowValid = false | |
| 120 | - | ac.copyDragging = false | |
| 121 | - | ac.copyMu.Unlock() | |
| 121 | + | rt.copyMu.Lock() | |
| 122 | + | rt.copyPressRowValid = false | |
| 123 | + | rt.copyDragging = false | |
| 124 | + | rt.copyMu.Unlock() | |
| 122 | 125 | } | |
| 123 | 126 | p.mu.Unlock() | |
| 124 | 127 | return | |
| 125 | 128 | } | |
| 126 | - | ac.copyMu.Lock() | |
| 127 | - | if ac.copyMode == nil { | |
| 128 | - | ac.copyMu.Unlock() | |
| 129 | + | rt.copyMu.Lock() | |
| 130 | + | if rt.copyMode == nil { | |
| 131 | + | rt.copyMu.Unlock() | |
| 129 | 132 | p.mu.Unlock() | |
| 130 | 133 | return | |
| 131 | 134 | } | |
| 132 | 135 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 133 | - | absRow := ac.copyMode.ViewportTop + ev.Row | |
| 136 | + | absRow := rt.copyMode.ViewportTop + ev.Row | |
| 134 | 137 | changed := false | |
| 135 | 138 | switch ev.Type { | |
| 136 | 139 | case mouse.Press: | |
| 137 | - | ac.copyMode.SetCursor(snap, absRow) | |
| 138 | - | ac.copyPressRow = ac.copyMode.Cursor | |
| 139 | - | ac.copyPressRowValid = true | |
| 140 | - | ac.copyDragging = false | |
| 140 | + | rt.copyMode.SetCursor(snap, absRow) | |
| 141 | + | rt.copyPressRow = rt.copyMode.Cursor | |
| 142 | + | rt.copyPressRowValid = true | |
| 143 | + | rt.copyDragging = false | |
| 141 | 144 | changed = true | |
| 142 | 145 | case mouse.Motion: | |
| 143 | - | if !ac.copyPressRowValid { | |
| 146 | + | if !rt.copyPressRowValid { | |
| 144 | 147 | break | |
| 145 | 148 | } | |
| 146 | - | if !ac.copyDragging { | |
| 147 | - | ac.copyMode.StartSelectionAt(snap, ac.copyPressRow) | |
| 148 | - | ac.copyDragging = true | |
| 149 | + | if !rt.copyDragging { | |
| 150 | + | rt.copyMode.StartSelectionAt(snap, rt.copyPressRow) | |
| 151 | + | rt.copyDragging = true | |
| 149 | 152 | } | |
| 150 | - | ac.copyMode.ExtendTo(snap, absRow) | |
| 153 | + | rt.copyMode.ExtendTo(snap, absRow) | |
| 151 | 154 | changed = true | |
| 152 | 155 | case mouse.Release: | |
| 153 | 156 | // Button release intentionally has no visual effect. | |
| 154 | 157 | } | |
| 155 | - | ac.copyMu.Unlock() | |
| 158 | + | rt.copyMu.Unlock() | |
| 156 | 159 | p.mu.Unlock() | |
| 157 | 160 | ||
| 158 | 161 | if changed { | |
| @@ -161,6 +164,7 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||
| 161 | 164 | } | |
| 162 | 165 | ||
| 163 | 166 | func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |
| 167 | + | rt := ac.overlays | |
| 164 | 168 | sess := ac.currentSession() | |
| 165 | 169 | if sess == nil { | |
| 166 | 170 | return | |
| @@ -177,21 +181,21 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 177 | 181 | return | |
| 178 | 182 | } | |
| 179 | 183 | p.mu.Lock() | |
| 180 | - | ac.copyMu.Lock() | |
| 181 | - | if ac.copyMode == nil { | |
| 182 | - | ac.copyPending = nil | |
| 184 | + | rt.copyMu.Lock() | |
| 185 | + | if rt.copyMode == nil { | |
| 186 | + | rt.copyPending = nil | |
| 183 | 187 | d.stopCopyPendingTimerLocked(ac) | |
| 184 | - | ac.copyMu.Unlock() | |
| 188 | + | rt.copyMu.Unlock() | |
| 185 | 189 | p.mu.Unlock() | |
| 186 | 190 | return | |
| 187 | 191 | } | |
| 188 | - | if len(ac.copyPending) > 0 { | |
| 192 | + | if len(rt.copyPending) > 0 { | |
| 189 | 193 | d.stopCopyPendingTimerLocked(ac) | |
| 190 | - | combined := make([]byte, 0, len(ac.copyPending)+len(data)) | |
| 191 | - | combined = append(combined, ac.copyPending...) | |
| 194 | + | combined := make([]byte, 0, len(rt.copyPending)+len(data)) | |
| 195 | + | combined = append(combined, rt.copyPending...) | |
| 192 | 196 | combined = append(combined, data...) | |
| 193 | 197 | data = combined | |
| 194 | - | ac.copyPending = nil | |
| 198 | + | rt.copyPending = nil | |
| 195 | 199 | } | |
| 196 | 200 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 197 | 201 | changed := false | |
| @@ -200,19 +204,19 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 200 | 204 | for i := 0; i < len(data); i++ { | |
| 201 | 205 | switch data[i] { | |
| 202 | 206 | case 'j': | |
| 203 | - | ac.copyMode.Move(snap, 1) | |
| 207 | + | rt.copyMode.Move(snap, 1) | |
| 204 | 208 | changed = true | |
| 205 | 209 | case 'k': | |
| 206 | - | ac.copyMode.Move(snap, -1) | |
| 210 | + | rt.copyMode.Move(snap, -1) | |
| 207 | 211 | changed = true | |
| 208 | 212 | case 'g': | |
| 209 | - | ac.copyMode.Top(snap) | |
| 213 | + | rt.copyMode.Top(snap) | |
| 210 | 214 | changed = true | |
| 211 | 215 | case 'G': | |
| 212 | - | ac.copyMode.Bottom(snap) | |
| 216 | + | rt.copyMode.Bottom(snap) | |
| 213 | 217 | changed = true | |
| 214 | 218 | case ' ', 'v': | |
| 215 | - | ac.copyMode.ToggleSelection() | |
| 219 | + | rt.copyMode.ToggleSelection() | |
| 216 | 220 | changed = true | |
| 217 | 221 | case '\r', '\n', 'y': | |
| 218 | 222 | copyOut = true | |
| @@ -220,7 +224,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 220 | 224 | case 'q', 0x03, 0x1b: | |
| 221 | 225 | if data[i] == 0x1b { | |
| 222 | 226 | tail := data[i:] | |
| 223 | - | consumed, ok := routeCopyEscape(ac.copyMode, snap, tail) | |
| 227 | + | consumed, ok := routeCopyEscape(rt.copyMode, snap, tail) | |
| 224 | 228 | if ok { | |
| 225 | 229 | i += consumed - 1 | |
| 226 | 230 | changed = true | |
| @@ -231,7 +235,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 231 | 235 | break | |
| 232 | 236 | } | |
| 233 | 237 | if isCopyEscapePrefix(tail) { | |
| 234 | - | ac.copyPending = append(ac.copyPending[:0], tail...) | |
| 238 | + | rt.copyPending = append(rt.copyPending[:0], tail...) | |
| 235 | 239 | break | |
| 236 | 240 | } | |
| 237 | 241 | } | |
| @@ -240,13 +244,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 240 | 244 | } | |
| 241 | 245 | text := "" | |
| 242 | 246 | if copyOut { | |
| 243 | - | text = ac.copyMode.SelectedText(snap) | |
| 247 | + | text = rt.copyMode.SelectedText(snap) | |
| 244 | 248 | } | |
| 245 | 249 | if exit { | |
| 246 | 250 | d.stopCopyPendingTimerLocked(ac) | |
| 247 | - | ac.copyMode = nil | |
| 251 | + | rt.copyMode = nil | |
| 248 | 252 | } | |
| 249 | - | ac.copyMu.Unlock() | |
| 253 | + | rt.copyMu.Unlock() | |
| 250 | 254 | p.mu.Unlock() | |
| 251 | 255 | ||
| 252 | 256 | if copyOut && text != "" { | |
| @@ -258,13 +262,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 258 | 262 | return | |
| 259 | 263 | } | |
| 260 | 264 | } | |
| 261 | - | ac.copyMu.Lock() | |
| 265 | + | rt.copyMu.Lock() | |
| 262 | 266 | if len(chunks) > 0 { | |
| 263 | - | ac.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard" | |
| 267 | + | rt.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard" | |
| 264 | 268 | } else { | |
| 265 | - | ac.copyFeedback = "selection too large to copy" | |
| 269 | + | rt.copyFeedback = "selection too large to copy" | |
| 266 | 270 | } | |
| 267 | - | ac.copyMu.Unlock() | |
| 271 | + | rt.copyMu.Unlock() | |
| 268 | 272 | } | |
| 269 | 273 | if exit { | |
| 270 | 274 | d.paint(sess, ac, true) | |
| @@ -276,23 +280,24 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 276 | 280 | } | |
| 277 | 281 | ||
| 278 | 282 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |
| 279 | - | mode := ac.copyMode | |
| 280 | - | ac.copyPending = append(ac.copyPending[:0], keys.ESC) | |
| 281 | - | ac.copyESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | |
| 282 | - | ac.copyMu.Lock() | |
| 283 | - | if ac.copyESC.timer != timer { | |
| 284 | - | ac.copyMu.Unlock() | |
| 283 | + | rt := ac.overlays | |
| 284 | + | mode := rt.copyMode | |
| 285 | + | rt.copyPending = append(rt.copyPending[:0], keys.ESC) | |
| 286 | + | rt.copyESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | |
| 287 | + | rt.copyMu.Lock() | |
| 288 | + | if rt.copyESC.timer != timer { | |
| 289 | + | rt.copyMu.Unlock() | |
| 285 | 290 | return | |
| 286 | 291 | } | |
| 287 | - | ac.copyPending = nil | |
| 288 | - | ac.copyESC.timer = nil | |
| 289 | - | ac.copyESC.done = nil | |
| 290 | - | if ac.copyMode != mode || ac.copyMode == nil { | |
| 291 | - | ac.copyMu.Unlock() | |
| 292 | + | rt.copyPending = nil | |
| 293 | + | rt.copyESC.timer = nil | |
| 294 | + | rt.copyESC.done = nil | |
| 295 | + | if rt.copyMode != mode || rt.copyMode == nil { | |
| 296 | + | rt.copyMu.Unlock() | |
| 292 | 297 | return | |
| 293 | 298 | } | |
| 294 | - | ac.copyMode = nil | |
| 295 | - | ac.copyMu.Unlock() | |
| 299 | + | rt.copyMode = nil | |
| 300 | + | rt.copyMu.Unlock() | |
| 296 | 301 | ||
| 297 | 302 | if sess := ac.currentSession(); sess != nil { | |
| 298 | 303 | d.paint(sess, ac, true) | |
| @@ -301,7 +306,8 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||
| 301 | 306 | } | |
| 302 | 307 | ||
| 303 | 308 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { | |
| 304 | - | ac.copyESC.stop() | |
| 309 | + | rt := ac.overlays | |
| 310 | + | rt.copyESC.stop() | |
| 305 | 311 | } | |
| 306 | 312 | ||
| 307 | 313 | func routeCopyEscape(m *scopy.Mode, snap scopy.Snapshot, data []byte) (int, bool) { | |
| @@ -37,6 +37,7 @@ import ( | |||||
| 37 | ) | 37 | ) | ||
| 38 | 38 | ||||
| 39 | func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | 39 | func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | ||
| 40 | + | rt := ac.overlays | |||
| 40 | tb := sess.activeTab() | 41 | tb := sess.activeTab() | ||
| 41 | if tb == nil { | 42 | if tb == nil { | ||
| 42 | return | 43 | return | ||
| @@ -49,32 +50,33 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||||
| 49 | return | 50 | return | ||
| 50 | } | 51 | } | ||
| 51 | p.mu.Lock() | 52 | p.mu.Lock() | ||
| 52 | - | ac.copyMu.Lock() | 53 | + | rt.copyMu.Lock() |
| 53 | - | if ac.copyMode == nil { | 54 | + | if rt.copyMode == nil { |
| 54 | - | ac.copyMu.Unlock() | 55 | + | rt.copyMu.Unlock() |
| 55 | p.mu.Unlock() | 56 | p.mu.Unlock() | ||
| 56 | return | 57 | return | ||
| 57 | } | 58 | } | ||
| 58 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 59 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | ||
| 59 | - | if delta > 0 && ac.copyMode.AtBottom(snap) { | 60 | + | if delta > 0 && rt.copyMode.AtBottom(snap) { |
| 60 | - | ac.copyMode = nil | 61 | + | rt.copyMode = nil |
| 61 | - | ac.copyMu.Unlock() | 62 | + | rt.copyMu.Unlock() |
| 62 | p.mu.Unlock() | 63 | p.mu.Unlock() | ||
| 63 | d.paint(sess, ac, true) | 64 | d.paint(sess, ac, true) | ||
| 64 | return | 65 | return | ||
| 65 | } | 66 | } | ||
| 66 | - | ac.copyMode.Move(snap, delta) | 67 | + | rt.copyMode.Move(snap, delta) |
| 67 | - | exit := delta > 0 && ac.copyMode.AtBottom(snap) | 68 | + | exit := delta > 0 && rt.copyMode.AtBottom(snap) |
| 68 | if exit { | 69 | if exit { | ||
| 69 | - | ac.copyMode = nil | 70 | + | rt.copyMode = nil |
| 70 | } | 71 | } | ||
| 71 | - | ac.copyMu.Unlock() | 72 | + | rt.copyMu.Unlock() |
| 72 | p.mu.Unlock() | 73 | p.mu.Unlock() | ||
| 73 | 74 | ||||
| 74 | d.paint(sess, ac, true) | 75 | d.paint(sess, ac, true) | ||
| 75 | } | 76 | } | ||
| 76 | 77 | ||||
| 77 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | 78 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | ||
| 79 | + | rt := ac.overlays | |||
| 78 | tb := sess.activeTab() | 80 | tb := sess.activeTab() | ||
| 79 | if tb == nil { | 81 | if tb == nil { | ||
| 80 | return | 82 | return | ||
| @@ -88,16 +90,17 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||||
| 88 | p.mu.Lock() | 90 | p.mu.Lock() | ||
| 89 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 91 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | ||
| 90 | p.mu.Unlock() | 92 | p.mu.Unlock() | ||
| 91 | - | ac.copyMu.Lock() | 93 | + | rt.copyMu.Lock() |
| 92 | - | ac.copyMode = scopy.NewMode(snap) | 94 | + | rt.copyMode = scopy.NewMode(snap) |
| 93 | - | ac.copyPressRowValid = false | 95 | + | rt.copyPressRowValid = false |
| 94 | - | ac.copyDragging = false | 96 | + | rt.copyDragging = false |
| 95 | - | ac.normalMousePressValid = false | 97 | + | rt.normalMousePressValid = false |
| 96 | - | ac.copyMu.Unlock() | 98 | + | rt.copyMu.Unlock() |
| 97 | d.paint(sess, ac, true) | 99 | d.paint(sess, ac, true) | ||
| 98 | } | 100 | } | ||
| 99 | 101 | ||||
| 100 | func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | 102 | func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | ||
| 103 | + | rt := ac.overlays | |||
| 101 | if ev.Button != mouse.Left { | 104 | if ev.Button != mouse.Left { | ||
| 102 | return | 105 | return | ||
| 103 | } | 106 | } | ||
| @@ -115,44 +118,44 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||||
| 115 | p.mu.Lock() | 118 | p.mu.Lock() | ||
| 116 | if ev.Row >= p.screen.Frame.Height { | 119 | if ev.Row >= p.screen.Frame.Height { | ||
| 117 | if ev.Type == mouse.Press { | 120 | if ev.Type == mouse.Press { | ||
| 118 | - | ac.copyMu.Lock() | 121 | + | rt.copyMu.Lock() |
| 119 | - | ac.copyPressRowValid = false | 122 | + | rt.copyPressRowValid = false |
| 120 | - | ac.copyDragging = false | 123 | + | rt.copyDragging = false |
| 121 | - | ac.copyMu.Unlock() | 124 | + | rt.copyMu.Unlock() |
| 122 | } | 125 | } | ||
| 123 | p.mu.Unlock() | 126 | p.mu.Unlock() | ||
| 124 | return | 127 | return | ||
| 125 | } | 128 | } | ||
| 126 | - | ac.copyMu.Lock() | 129 | + | rt.copyMu.Lock() |
| 127 | - | if ac.copyMode == nil { | 130 | + | if rt.copyMode == nil { |
| 128 | - | ac.copyMu.Unlock() | 131 | + | rt.copyMu.Unlock() |
| 129 | p.mu.Unlock() | 132 | p.mu.Unlock() | ||
| 130 | return | 133 | return | ||
| 131 | } | 134 | } | ||
| 132 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 135 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | ||
| 133 | - | absRow := ac.copyMode.ViewportTop + ev.Row | 136 | + | absRow := rt.copyMode.ViewportTop + ev.Row |
| 134 | changed := false | 137 | changed := false | ||
| 135 | switch ev.Type { | 138 | switch ev.Type { | ||
| 136 | case mouse.Press: | 139 | case mouse.Press: | ||
| 137 | - | ac.copyMode.SetCursor(snap, absRow) | 140 | + | rt.copyMode.SetCursor(snap, absRow) |
| 138 | - | ac.copyPressRow = ac.copyMode.Cursor | 141 | + | rt.copyPressRow = rt.copyMode.Cursor |
| 139 | - | ac.copyPressRowValid = true | 142 | + | rt.copyPressRowValid = true |
| 140 | - | ac.copyDragging = false | 143 | + | rt.copyDragging = false |
| 141 | changed = true | 144 | changed = true | ||
| 142 | case mouse.Motion: | 145 | case mouse.Motion: | ||
| 143 | - | if !ac.copyPressRowValid { | 146 | + | if !rt.copyPressRowValid { |
| 144 | break | 147 | break | ||
| 145 | } | 148 | } | ||
| 146 | - | if !ac.copyDragging { | 149 | + | if !rt.copyDragging { |
| 147 | - | ac.copyMode.StartSelectionAt(snap, ac.copyPressRow) | 150 | + | rt.copyMode.StartSelectionAt(snap, rt.copyPressRow) |
| 148 | - | ac.copyDragging = true | 151 | + | rt.copyDragging = true |
| 149 | } | 152 | } | ||
| 150 | - | ac.copyMode.ExtendTo(snap, absRow) | 153 | + | rt.copyMode.ExtendTo(snap, absRow) |
| 151 | changed = true | 154 | changed = true | ||
| 152 | case mouse.Release: | 155 | case mouse.Release: | ||
| 153 | // Button release intentionally has no visual effect. | 156 | // Button release intentionally has no visual effect. | ||
| 154 | } | 157 | } | ||
| 155 | - | ac.copyMu.Unlock() | 158 | + | rt.copyMu.Unlock() |
| 156 | p.mu.Unlock() | 159 | p.mu.Unlock() | ||
| 157 | 160 | ||||
| 158 | if changed { | 161 | if changed { | ||
| @@ -161,6 +164,7 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||||
| 161 | } | 164 | } | ||
| 162 | 165 | ||||
| 163 | func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | 166 | func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | ||
| 167 | + | rt := ac.overlays | |||
| 164 | sess := ac.currentSession() | 168 | sess := ac.currentSession() | ||
| 165 | if sess == nil { | 169 | if sess == nil { | ||
| 166 | return | 170 | return | ||
| @@ -177,21 +181,21 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 177 | return | 181 | return | ||
| 178 | } | 182 | } | ||
| 179 | p.mu.Lock() | 183 | p.mu.Lock() | ||
| 180 | - | ac.copyMu.Lock() | 184 | + | rt.copyMu.Lock() |
| 181 | - | if ac.copyMode == nil { | 185 | + | if rt.copyMode == nil { |
| 182 | - | ac.copyPending = nil | 186 | + | rt.copyPending = nil |
| 183 | d.stopCopyPendingTimerLocked(ac) | 187 | d.stopCopyPendingTimerLocked(ac) | ||
| 184 | - | ac.copyMu.Unlock() | 188 | + | rt.copyMu.Unlock() |
| 185 | p.mu.Unlock() | 189 | p.mu.Unlock() | ||
| 186 | return | 190 | return | ||
| 187 | } | 191 | } | ||
| 188 | - | if len(ac.copyPending) > 0 { | 192 | + | if len(rt.copyPending) > 0 { |
| 189 | d.stopCopyPendingTimerLocked(ac) | 193 | d.stopCopyPendingTimerLocked(ac) | ||
| 190 | - | combined := make([]byte, 0, len(ac.copyPending)+len(data)) | 194 | + | combined := make([]byte, 0, len(rt.copyPending)+len(data)) |
| 191 | - | combined = append(combined, ac.copyPending...) | 195 | + | combined = append(combined, rt.copyPending...) |
| 192 | combined = append(combined, data...) | 196 | combined = append(combined, data...) | ||
| 193 | data = combined | 197 | data = combined | ||
| 194 | - | ac.copyPending = nil | 198 | + | rt.copyPending = nil |
| 195 | } | 199 | } | ||
| 196 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 200 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | ||
| 197 | changed := false | 201 | changed := false | ||
| @@ -200,19 +204,19 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 200 | for i := 0; i < len(data); i++ { | 204 | for i := 0; i < len(data); i++ { | ||
| 201 | switch data[i] { | 205 | switch data[i] { | ||
| 202 | case 'j': | 206 | case 'j': | ||
| 203 | - | ac.copyMode.Move(snap, 1) | 207 | + | rt.copyMode.Move(snap, 1) |
| 204 | changed = true | 208 | changed = true | ||
| 205 | case 'k': | 209 | case 'k': | ||
| 206 | - | ac.copyMode.Move(snap, -1) | 210 | + | rt.copyMode.Move(snap, -1) |
| 207 | changed = true | 211 | changed = true | ||
| 208 | case 'g': | 212 | case 'g': | ||
| 209 | - | ac.copyMode.Top(snap) | 213 | + | rt.copyMode.Top(snap) |
| 210 | changed = true | 214 | changed = true | ||
| 211 | case 'G': | 215 | case 'G': | ||
| 212 | - | ac.copyMode.Bottom(snap) | 216 | + | rt.copyMode.Bottom(snap) |
| 213 | changed = true | 217 | changed = true | ||
| 214 | case ' ', 'v': | 218 | case ' ', 'v': | ||
| 215 | - | ac.copyMode.ToggleSelection() | 219 | + | rt.copyMode.ToggleSelection() |
| 216 | changed = true | 220 | changed = true | ||
| 217 | case '\r', '\n', 'y': | 221 | case '\r', '\n', 'y': | ||
| 218 | copyOut = true | 222 | copyOut = true | ||
| @@ -220,7 +224,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 220 | case 'q', 0x03, 0x1b: | 224 | case 'q', 0x03, 0x1b: | ||
| 221 | if data[i] == 0x1b { | 225 | if data[i] == 0x1b { | ||
| 222 | tail := data[i:] | 226 | tail := data[i:] | ||
| 223 | - | consumed, ok := routeCopyEscape(ac.copyMode, snap, tail) | 227 | + | consumed, ok := routeCopyEscape(rt.copyMode, snap, tail) |
| 224 | if ok { | 228 | if ok { | ||
| 225 | i += consumed - 1 | 229 | i += consumed - 1 | ||
| 226 | changed = true | 230 | changed = true | ||
| @@ -231,7 +235,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 231 | break | 235 | break | ||
| 232 | } | 236 | } | ||
| 233 | if isCopyEscapePrefix(tail) { | 237 | if isCopyEscapePrefix(tail) { | ||
| 234 | - | ac.copyPending = append(ac.copyPending[:0], tail...) | 238 | + | rt.copyPending = append(rt.copyPending[:0], tail...) |
| 235 | break | 239 | break | ||
| 236 | } | 240 | } | ||
| 237 | } | 241 | } | ||
| @@ -240,13 +244,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 240 | } | 244 | } | ||
| 241 | text := "" | 245 | text := "" | ||
| 242 | if copyOut { | 246 | if copyOut { | ||
| 243 | - | text = ac.copyMode.SelectedText(snap) | 247 | + | text = rt.copyMode.SelectedText(snap) |
| 244 | } | 248 | } | ||
| 245 | if exit { | 249 | if exit { | ||
| 246 | d.stopCopyPendingTimerLocked(ac) | 250 | d.stopCopyPendingTimerLocked(ac) | ||
| 247 | - | ac.copyMode = nil | 251 | + | rt.copyMode = nil |
| 248 | } | 252 | } | ||
| 249 | - | ac.copyMu.Unlock() | 253 | + | rt.copyMu.Unlock() |
| 250 | p.mu.Unlock() | 254 | p.mu.Unlock() | ||
| 251 | 255 | ||||
| 252 | if copyOut && text != "" { | 256 | if copyOut && text != "" { | ||
| @@ -258,13 +262,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 258 | return | 262 | return | ||
| 259 | } | 263 | } | ||
| 260 | } | 264 | } | ||
| 261 | - | ac.copyMu.Lock() | 265 | + | rt.copyMu.Lock() |
| 262 | if len(chunks) > 0 { | 266 | if len(chunks) > 0 { | ||
| 263 | - | ac.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard" | 267 | + | rt.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard" |
| 264 | } else { | 268 | } else { | ||
| 265 | - | ac.copyFeedback = "selection too large to copy" | 269 | + | rt.copyFeedback = "selection too large to copy" |
| 266 | } | 270 | } | ||
| 267 | - | ac.copyMu.Unlock() | 271 | + | rt.copyMu.Unlock() |
| 268 | } | 272 | } | ||
| 269 | if exit { | 273 | if exit { | ||
| 270 | d.paint(sess, ac, true) | 274 | d.paint(sess, ac, true) | ||
| @@ -276,23 +280,24 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 276 | } | 280 | } | ||
| 277 | 281 | ||||
| 278 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | 282 | func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | ||
| 279 | - | mode := ac.copyMode | 283 | + | rt := ac.overlays |
| 280 | - | ac.copyPending = append(ac.copyPending[:0], keys.ESC) | 284 | + | mode := rt.copyMode |
| 281 | - | ac.copyESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | 285 | + | rt.copyPending = append(rt.copyPending[:0], keys.ESC) |
| 282 | - | ac.copyMu.Lock() | 286 | + | rt.copyESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { |
| 283 | - | if ac.copyESC.timer != timer { | 287 | + | rt.copyMu.Lock() |
| 284 | - | ac.copyMu.Unlock() | 288 | + | if rt.copyESC.timer != timer { |
| 289 | + | rt.copyMu.Unlock() | |||
| 285 | return | 290 | return | ||
| 286 | } | 291 | } | ||
| 287 | - | ac.copyPending = nil | 292 | + | rt.copyPending = nil |
| 288 | - | ac.copyESC.timer = nil | 293 | + | rt.copyESC.timer = nil |
| 289 | - | ac.copyESC.done = nil | 294 | + | rt.copyESC.done = nil |
| 290 | - | if ac.copyMode != mode || ac.copyMode == nil { | 295 | + | if rt.copyMode != mode || rt.copyMode == nil { |
| 291 | - | ac.copyMu.Unlock() | 296 | + | rt.copyMu.Unlock() |
| 292 | return | 297 | return | ||
| 293 | } | 298 | } | ||
| 294 | - | ac.copyMode = nil | 299 | + | rt.copyMode = nil |
| 295 | - | ac.copyMu.Unlock() | 300 | + | rt.copyMu.Unlock() |
| 296 | 301 | ||||
| 297 | if sess := ac.currentSession(); sess != nil { | 302 | if sess := ac.currentSession(); sess != nil { | ||
| 298 | d.paint(sess, ac, true) | 303 | d.paint(sess, ac, true) | ||
| @@ -301,7 +306,8 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||||
| 301 | } | 306 | } | ||
| 302 | 307 | ||||
| 303 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { | 308 | func (d *Daemon) stopCopyPendingTimerLocked(ac *attachedClient) { | ||
| 304 | - | ac.copyESC.stop() | 309 | + | rt := ac.overlays |
| 310 | + | rt.copyESC.stop() | |||
| 305 | } | 311 | } | ||
| 306 | 312 | ||||
| 307 | func routeCopyEscape(m *scopy.Mode, snap scopy.Snapshot, data []byte) (int, bool) { | 313 | func routeCopyEscape(m *scopy.Mode, snap scopy.Snapshot, data []byte) (int, bool) { | ||
M internal/usecase/daemon/copymode_test.go +21 -20
| @@ -26,6 +26,7 @@ import ( | |||
| 26 | 26 | func TestBoundedSendOutputErrTransportReturnsTransportUsedBySend(t *testing.T) { | |
| 27 | 27 | d := newTestDaemon(t, nil, stubClock{}) | |
| 28 | 28 | ac := &attachedClient{} | |
| 29 | + | ac.initOverlays() | |
| 29 | 30 | replacement := &closeTrackingTransport{} | |
| 30 | 31 | sendErr := errors.New("send failed") | |
| 31 | 32 | failed := &swapErrorTransport{ac: ac, replacement: replacement, err: sendErr} | |
| @@ -119,7 +120,7 @@ func TestCopyModePaletteCommandEntersAndDoesNotForward(t *testing.T) { | |||
| 119 | 120 | awaitFrame(t, sends, ports.MsgOutput) | |
| 120 | 121 | d.handleInput(sess, ac, []byte("VIS\r")) | |
| 121 | 122 | ||
| 122 | - | if ac.copyMode == nil { | |
| 123 | + | if ac.overlays.copyMode == nil { | |
| 123 | 124 | t.Fatal("scrollback mode not entered") | |
| 124 | 125 | } | |
| 125 | 126 | select { | |
| @@ -167,7 +168,7 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) { | |||
| 167 | 168 | if got, want := string(msg.Data), "\x1b]52;c;b2xkMQpvbGQy\x07"; got != want { | |
| 168 | 169 | t.Fatalf("OSC52 = %q, want %q", got, want) | |
| 169 | 170 | } | |
| 170 | - | if ac.copyMode != nil { | |
| 171 | + | if ac.overlays.copyMode != nil { | |
| 171 | 172 | t.Fatal("copy mode still active after yank") | |
| 172 | 173 | } | |
| 173 | 174 | live := awaitFrame(t, sends, ports.MsgOutput) | |
| @@ -280,7 +281,7 @@ func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) { | |||
| 280 | 281 | awaitFrame(t, sends, ports.MsgOutput) | |
| 281 | 282 | d.handleInput(sess, ac, []byte("q")) | |
| 282 | 283 | ||
| 283 | - | if ac.copyMode != nil { | |
| 284 | + | if ac.overlays.copyMode != nil { | |
| 284 | 285 | t.Fatal("copy mode still active after q") | |
| 285 | 286 | } | |
| 286 | 287 | out := awaitFrame(t, sends, ports.MsgOutput) | |
| @@ -318,8 +319,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||
| 318 | 319 | d.handleInput(sess, ac, input) | |
| 319 | 320 | } | |
| 320 | 321 | ||
| 321 | - | require.NotNil(t, ac.copyMode) | |
| 322 | - | require.Equal(t, tc.wantCursor, ac.copyMode.Cursor) | |
| 322 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 323 | + | require.Equal(t, tc.wantCursor, ac.overlays.copyMode.Cursor) | |
| 323 | 324 | }) | |
| 324 | 325 | } | |
| 325 | 326 | } | |
| @@ -359,14 +360,14 @@ func TestCopyModeLoneEscapeExitsAfterDelay(t *testing.T) { | |||
| 359 | 360 | awaitFrame(t, sends, ports.MsgOutput) | |
| 360 | 361 | d.handleInput(sess, ac, []byte("\x1b")) | |
| 361 | 362 | timer := <-clk.timers | |
| 362 | - | ac.copyMu.Lock() | |
| 363 | - | require.NotNil(t, ac.copyMode) | |
| 364 | - | ac.copyMu.Unlock() | |
| 363 | + | ac.overlays.copyMu.Lock() | |
| 364 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 365 | + | ac.overlays.copyMu.Unlock() | |
| 365 | 366 | timer.ch <- time.Now() | |
| 366 | 367 | require.Eventually(t, func() bool { | |
| 367 | - | ac.copyMu.Lock() | |
| 368 | - | defer ac.copyMu.Unlock() | |
| 369 | - | return ac.copyMode == nil | |
| 368 | + | ac.overlays.copyMu.Lock() | |
| 369 | + | defer ac.overlays.copyMu.Unlock() | |
| 370 | + | return ac.overlays.copyMode == nil | |
| 370 | 371 | }, time.Second, 5*time.Millisecond) | |
| 371 | 372 | } | |
| 372 | 373 | ||
| @@ -387,15 +388,15 @@ func TestCopyModePendingEscapeDoesNotCloseNewMode(t *testing.T) { | |||
| 387 | 388 | ||
| 388 | 389 | timer.ch <- time.Now() | |
| 389 | 390 | require.Never(t, func() bool { | |
| 390 | - | ac.copyMu.Lock() | |
| 391 | - | defer ac.copyMu.Unlock() | |
| 392 | - | return ac.copyMode == nil | |
| 391 | + | ac.overlays.copyMu.Lock() | |
| 392 | + | defer ac.overlays.copyMu.Unlock() | |
| 393 | + | return ac.overlays.copyMode == nil | |
| 393 | 394 | }, 50*time.Millisecond, 5*time.Millisecond) | |
| 394 | - | ac.copyMu.Lock() | |
| 395 | - | require.Nil(t, ac.copyESC.timer) | |
| 396 | - | require.Nil(t, ac.copyESC.done) | |
| 397 | - | require.Empty(t, ac.copyPending) | |
| 398 | - | ac.copyMu.Unlock() | |
| 395 | + | ac.overlays.copyMu.Lock() | |
| 396 | + | require.Nil(t, ac.overlays.copyESC.timer) | |
| 397 | + | require.Nil(t, ac.overlays.copyESC.done) | |
| 398 | + | require.Empty(t, ac.overlays.copyPending) | |
| 399 | + | ac.overlays.copyMu.Unlock() | |
| 399 | 400 | } | |
| 400 | 401 | ||
| 401 | 402 | func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | |
| @@ -417,7 +418,7 @@ func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | |||
| 417 | 418 | awaitFrame(t, sends, ports.MsgOutput) | |
| 418 | 419 | d.handleInput(sess, ac, tc.input) | |
| 419 | 420 | ||
| 420 | - | if ac.copyMode != nil { | |
| 421 | + | if ac.overlays.copyMode != nil { | |
| 421 | 422 | t.Fatal("copy mode still active after empty yank") | |
| 422 | 423 | } | |
| 423 | 424 | out := awaitFrame(t, sends, ports.MsgOutput) | |
| @@ -26,6 +26,7 @@ import ( | |||||
| 26 | func TestBoundedSendOutputErrTransportReturnsTransportUsedBySend(t *testing.T) { | 26 | func TestBoundedSendOutputErrTransportReturnsTransportUsedBySend(t *testing.T) { | ||
| 27 | d := newTestDaemon(t, nil, stubClock{}) | 27 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 28 | ac := &attachedClient{} | 28 | ac := &attachedClient{} | ||
| 29 | + | ac.initOverlays() | |||
| 29 | replacement := &closeTrackingTransport{} | 30 | replacement := &closeTrackingTransport{} | ||
| 30 | sendErr := errors.New("send failed") | 31 | sendErr := errors.New("send failed") | ||
| 31 | failed := &swapErrorTransport{ac: ac, replacement: replacement, err: sendErr} | 32 | failed := &swapErrorTransport{ac: ac, replacement: replacement, err: sendErr} | ||
| @@ -119,7 +120,7 @@ func TestCopyModePaletteCommandEntersAndDoesNotForward(t *testing.T) { | |||||
| 119 | awaitFrame(t, sends, ports.MsgOutput) | 120 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 120 | d.handleInput(sess, ac, []byte("VIS\r")) | 121 | d.handleInput(sess, ac, []byte("VIS\r")) | ||
| 121 | 122 | ||||
| 122 | - | if ac.copyMode == nil { | 123 | + | if ac.overlays.copyMode == nil { |
| 123 | t.Fatal("scrollback mode not entered") | 124 | t.Fatal("scrollback mode not entered") | ||
| 124 | } | 125 | } | ||
| 125 | select { | 126 | select { | ||
| @@ -167,7 +168,7 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) { | |||||
| 167 | if got, want := string(msg.Data), "\x1b]52;c;b2xkMQpvbGQy\x07"; got != want { | 168 | if got, want := string(msg.Data), "\x1b]52;c;b2xkMQpvbGQy\x07"; got != want { | ||
| 168 | t.Fatalf("OSC52 = %q, want %q", got, want) | 169 | t.Fatalf("OSC52 = %q, want %q", got, want) | ||
| 169 | } | 170 | } | ||
| 170 | - | if ac.copyMode != nil { | 171 | + | if ac.overlays.copyMode != nil { |
| 171 | t.Fatal("copy mode still active after yank") | 172 | t.Fatal("copy mode still active after yank") | ||
| 172 | } | 173 | } | ||
| 173 | live := awaitFrame(t, sends, ports.MsgOutput) | 174 | live := awaitFrame(t, sends, ports.MsgOutput) | ||
| @@ -280,7 +281,7 @@ func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) { | |||||
| 280 | awaitFrame(t, sends, ports.MsgOutput) | 281 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 281 | d.handleInput(sess, ac, []byte("q")) | 282 | d.handleInput(sess, ac, []byte("q")) | ||
| 282 | 283 | ||||
| 283 | - | if ac.copyMode != nil { | 284 | + | if ac.overlays.copyMode != nil { |
| 284 | t.Fatal("copy mode still active after q") | 285 | t.Fatal("copy mode still active after q") | ||
| 285 | } | 286 | } | ||
| 286 | out := awaitFrame(t, sends, ports.MsgOutput) | 287 | out := awaitFrame(t, sends, ports.MsgOutput) | ||
| @@ -318,8 +319,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||||
| 318 | d.handleInput(sess, ac, input) | 319 | d.handleInput(sess, ac, input) | ||
| 319 | } | 320 | } | ||
| 320 | 321 | ||||
| 321 | - | require.NotNil(t, ac.copyMode) | 322 | + | require.NotNil(t, ac.overlays.copyMode) |
| 322 | - | require.Equal(t, tc.wantCursor, ac.copyMode.Cursor) | 323 | + | require.Equal(t, tc.wantCursor, ac.overlays.copyMode.Cursor) |
| 323 | }) | 324 | }) | ||
| 324 | } | 325 | } | ||
| 325 | } | 326 | } | ||
| @@ -359,14 +360,14 @@ func TestCopyModeLoneEscapeExitsAfterDelay(t *testing.T) { | |||||
| 359 | awaitFrame(t, sends, ports.MsgOutput) | 360 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 360 | d.handleInput(sess, ac, []byte("\x1b")) | 361 | d.handleInput(sess, ac, []byte("\x1b")) | ||
| 361 | timer := <-clk.timers | 362 | timer := <-clk.timers | ||
| 362 | - | ac.copyMu.Lock() | 363 | + | ac.overlays.copyMu.Lock() |
| 363 | - | require.NotNil(t, ac.copyMode) | 364 | + | require.NotNil(t, ac.overlays.copyMode) |
| 364 | - | ac.copyMu.Unlock() | 365 | + | ac.overlays.copyMu.Unlock() |
| 365 | timer.ch <- time.Now() | 366 | timer.ch <- time.Now() | ||
| 366 | require.Eventually(t, func() bool { | 367 | require.Eventually(t, func() bool { | ||
| 367 | - | ac.copyMu.Lock() | 368 | + | ac.overlays.copyMu.Lock() |
| 368 | - | defer ac.copyMu.Unlock() | 369 | + | defer ac.overlays.copyMu.Unlock() |
| 369 | - | return ac.copyMode == nil | 370 | + | return ac.overlays.copyMode == nil |
| 370 | }, time.Second, 5*time.Millisecond) | 371 | }, time.Second, 5*time.Millisecond) | ||
| 371 | } | 372 | } | ||
| 372 | 373 | ||||
| @@ -387,15 +388,15 @@ func TestCopyModePendingEscapeDoesNotCloseNewMode(t *testing.T) { | |||||
| 387 | 388 | ||||
| 388 | timer.ch <- time.Now() | 389 | timer.ch <- time.Now() | ||
| 389 | require.Never(t, func() bool { | 390 | require.Never(t, func() bool { | ||
| 390 | - | ac.copyMu.Lock() | 391 | + | ac.overlays.copyMu.Lock() |
| 391 | - | defer ac.copyMu.Unlock() | 392 | + | defer ac.overlays.copyMu.Unlock() |
| 392 | - | return ac.copyMode == nil | 393 | + | return ac.overlays.copyMode == nil |
| 393 | }, 50*time.Millisecond, 5*time.Millisecond) | 394 | }, 50*time.Millisecond, 5*time.Millisecond) | ||
| 394 | - | ac.copyMu.Lock() | 395 | + | ac.overlays.copyMu.Lock() |
| 395 | - | require.Nil(t, ac.copyESC.timer) | 396 | + | require.Nil(t, ac.overlays.copyESC.timer) |
| 396 | - | require.Nil(t, ac.copyESC.done) | 397 | + | require.Nil(t, ac.overlays.copyESC.done) |
| 397 | - | require.Empty(t, ac.copyPending) | 398 | + | require.Empty(t, ac.overlays.copyPending) |
| 398 | - | ac.copyMu.Unlock() | 399 | + | ac.overlays.copyMu.Unlock() |
| 399 | } | 400 | } | ||
| 400 | 401 | ||||
| 401 | func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | 402 | func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | ||
| @@ -417,7 +418,7 @@ func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | |||||
| 417 | awaitFrame(t, sends, ports.MsgOutput) | 418 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 418 | d.handleInput(sess, ac, tc.input) | 419 | d.handleInput(sess, ac, tc.input) | ||
| 419 | 420 | ||||
| 420 | - | if ac.copyMode != nil { | 421 | + | if ac.overlays.copyMode != nil { |
| 421 | t.Fatal("copy mode still active after empty yank") | 422 | t.Fatal("copy mode still active after empty yank") | ||
| 422 | } | 423 | } | ||
| 423 | out := awaitFrame(t, sends, ports.MsgOutput) | 424 | out := awaitFrame(t, sends, ports.MsgOutput) | ||
M internal/usecase/daemon/daemon_test.go +1 -0
| @@ -239,6 +239,7 @@ func newManualSessionWithPTYs(t *testing.T, ptys ...ports.PTY) (*Daemon, *sessio | |||
| 239 | 239 | d := newTestDaemon(t, nil, stubClock{}) | |
| 240 | 240 | tr, sends := newCapturingTransport(t) | |
| 241 | 241 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 242 | + | ac.initOverlays() | |
| 242 | 243 | sctx, cancel := context.WithCancel(d.serveCtx) | |
| 243 | 244 | tabs := make([]*tab, 0, len(ptys)) | |
| 244 | 245 | for _, p := range ptys { | |
| @@ -239,6 +239,7 @@ func newManualSessionWithPTYs(t *testing.T, ptys ...ports.PTY) (*Daemon, *sessio | |||||
| 239 | d := newTestDaemon(t, nil, stubClock{}) | 239 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 240 | tr, sends := newCapturingTransport(t) | 240 | tr, sends := newCapturingTransport(t) | ||
| 241 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 241 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 242 | + | ac.initOverlays() | |||
| 242 | sctx, cancel := context.WithCancel(d.serveCtx) | 243 | sctx, cancel := context.WithCancel(d.serveCtx) | ||
| 243 | tabs := make([]*tab, 0, len(ptys)) | 244 | tabs := make([]*tab, 0, len(ptys)) | ||
| 244 | for _, p := range ptys { | 245 | for _, p := range ptys { | ||
M internal/usecase/daemon/input.go +29 -39
| @@ -43,23 +43,11 @@ func (d *Daemon) handleSequencedInput(sess *session, ac *attachedClient, _ uint6 | |||
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | |
| 46 | + | ac.initOverlays() | |
| 46 | 47 | ac.mouseScan.Scan(data, | |
| 47 | 48 | func(ev mouse.Event) { d.handleMouse(ac, ev) }, | |
| 48 | 49 | func(b []byte) { | |
| 49 | - | if ac.promptActive() { | |
| 50 | - | d.handlePromptInput(ac, b) | |
| 51 | - | return | |
| 52 | - | } | |
| 53 | - | if ac.paletteActive() { | |
| 54 | - | d.handlePaletteInput(ac, b) | |
| 55 | - | return | |
| 56 | - | } | |
| 57 | - | if ac.pickerActive() { | |
| 58 | - | d.handlePickerInput(ac, b) | |
| 59 | - | return | |
| 60 | - | } | |
| 61 | - | if ac.copyModeActive() { | |
| 62 | - | d.handleCopyInput(ac, b) | |
| 50 | + | if ac.overlays.HandleInput(d, b) { | |
| 63 | 51 | return | |
| 64 | 52 | } | |
| 65 | 53 | ac.keys.Route(b) | |
| @@ -68,7 +56,9 @@ func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | |||
| 68 | 56 | } | |
| 69 | 57 | ||
| 70 | 58 | func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |
| 71 | - | if ac.promptActive() || ac.paletteActive() || ac.pickerActive() { | |
| 59 | + | ac.initOverlays() | |
| 60 | + | rt := ac.overlays | |
| 61 | + | if rt.promptActive() || rt.paletteActive() || rt.pickerActive() { | |
| 72 | 62 | return | |
| 73 | 63 | } | |
| 74 | 64 | sess := ac.currentSession() | |
| @@ -80,7 +70,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 80 | 70 | return | |
| 81 | 71 | } | |
| 82 | 72 | ||
| 83 | - | if ac.copyModeActive() { | |
| 73 | + | if rt.copyActive() { | |
| 84 | 74 | if ev.Button == mouse.Left { | |
| 85 | 75 | contentRow := ev.Row - 1 | |
| 86 | 76 | tb.mu.Lock() | |
| @@ -211,25 +201,25 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 211 | 201 | switch ev.Type { | |
| 212 | 202 | case mouse.Press: | |
| 213 | 203 | if altScreen || ev.Row >= childRows { | |
| 214 | - | ac.copyMu.Lock() | |
| 215 | - | ac.normalMousePressValid = false | |
| 216 | - | ac.copyMu.Unlock() | |
| 204 | + | rt.copyMu.Lock() | |
| 205 | + | rt.normalMousePressValid = false | |
| 206 | + | rt.copyMu.Unlock() | |
| 217 | 207 | return | |
| 218 | 208 | } | |
| 219 | - | ac.copyMu.Lock() | |
| 220 | - | ac.normalMousePressRow = ev.Row | |
| 221 | - | ac.normalMousePressTop = scrollbackRows | |
| 222 | - | ac.normalMousePressValid = true | |
| 223 | - | ac.copyMu.Unlock() | |
| 209 | + | rt.copyMu.Lock() | |
| 210 | + | rt.normalMousePressRow = ev.Row | |
| 211 | + | rt.normalMousePressTop = scrollbackRows | |
| 212 | + | rt.normalMousePressValid = true | |
| 213 | + | rt.copyMu.Unlock() | |
| 224 | 214 | case mouse.Motion: | |
| 225 | 215 | if altScreen || ev.Row >= childRows { | |
| 226 | 216 | return | |
| 227 | 217 | } | |
| 228 | - | ac.copyMu.Lock() | |
| 229 | - | pressValid := ac.normalMousePressValid | |
| 230 | - | pressRow := ac.normalMousePressRow | |
| 231 | - | pressTop := ac.normalMousePressTop | |
| 232 | - | ac.copyMu.Unlock() | |
| 218 | + | rt.copyMu.Lock() | |
| 219 | + | pressValid := rt.normalMousePressValid | |
| 220 | + | pressRow := rt.normalMousePressRow | |
| 221 | + | pressTop := rt.normalMousePressTop | |
| 222 | + | rt.copyMu.Unlock() | |
| 233 | 223 | if !pressValid { | |
| 234 | 224 | return | |
| 235 | 225 | } | |
| @@ -238,21 +228,21 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 238 | 228 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 239 | 229 | p.mu.Unlock() | |
| 240 | 230 | ||
| 241 | - | ac.copyMu.Lock() | |
| 231 | + | rt.copyMu.Lock() | |
| 242 | 232 | mode := scopy.NewMode(snap) | |
| 243 | 233 | mode.StartSelectionAt(snap, pressTop+pressRow) | |
| 244 | 234 | mode.ExtendTo(snap, len(snap.Rows)-snap.Height+ev.Row) | |
| 245 | - | ac.copyMode = mode | |
| 246 | - | ac.copyPressRow = pressTop + pressRow | |
| 247 | - | ac.copyPressRowValid = true | |
| 248 | - | ac.copyDragging = true | |
| 249 | - | ac.normalMousePressValid = false | |
| 250 | - | ac.copyMu.Unlock() | |
| 235 | + | rt.copyMode = mode | |
| 236 | + | rt.copyPressRow = pressTop + pressRow | |
| 237 | + | rt.copyPressRowValid = true | |
| 238 | + | rt.copyDragging = true | |
| 239 | + | rt.normalMousePressValid = false | |
| 240 | + | rt.copyMu.Unlock() | |
| 251 | 241 | d.paint(sess, ac, true) | |
| 252 | 242 | case mouse.Release: | |
| 253 | - | ac.copyMu.Lock() | |
| 254 | - | ac.normalMousePressValid = false | |
| 255 | - | ac.copyMu.Unlock() | |
| 243 | + | rt.copyMu.Lock() | |
| 244 | + | rt.normalMousePressValid = false | |
| 245 | + | rt.copyMu.Unlock() | |
| 256 | 246 | } | |
| 257 | 247 | case mouse.WheelUp: | |
| 258 | 248 | if altScreen { | |
| @@ -43,23 +43,11 @@ func (d *Daemon) handleSequencedInput(sess *session, ac *attachedClient, _ uint6 | |||||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | 45 | func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | ||
| 46 | + | ac.initOverlays() | |||
| 46 | ac.mouseScan.Scan(data, | 47 | ac.mouseScan.Scan(data, | ||
| 47 | func(ev mouse.Event) { d.handleMouse(ac, ev) }, | 48 | func(ev mouse.Event) { d.handleMouse(ac, ev) }, | ||
| 48 | func(b []byte) { | 49 | func(b []byte) { | ||
| 49 | - | if ac.promptActive() { | 50 | + | if ac.overlays.HandleInput(d, b) { |
| 50 | - | d.handlePromptInput(ac, b) | |||
| 51 | - | return | |||
| 52 | - | } | |||
| 53 | - | if ac.paletteActive() { | |||
| 54 | - | d.handlePaletteInput(ac, b) | |||
| 55 | - | return | |||
| 56 | - | } | |||
| 57 | - | if ac.pickerActive() { | |||
| 58 | - | d.handlePickerInput(ac, b) | |||
| 59 | - | return | |||
| 60 | - | } | |||
| 61 | - | if ac.copyModeActive() { | |||
| 62 | - | d.handleCopyInput(ac, b) | |||
| 63 | return | 51 | return | ||
| 64 | } | 52 | } | ||
| 65 | ac.keys.Route(b) | 53 | ac.keys.Route(b) | ||
| @@ -68,7 +56,9 @@ func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | |||||
| 68 | } | 56 | } | ||
| 69 | 57 | ||||
| 70 | func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | 58 | func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | ||
| 71 | - | if ac.promptActive() || ac.paletteActive() || ac.pickerActive() { | 59 | + | ac.initOverlays() |
| 60 | + | rt := ac.overlays | |||
| 61 | + | if rt.promptActive() || rt.paletteActive() || rt.pickerActive() { | |||
| 72 | return | 62 | return | ||
| 73 | } | 63 | } | ||
| 74 | sess := ac.currentSession() | 64 | sess := ac.currentSession() | ||
| @@ -80,7 +70,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 80 | return | 70 | return | ||
| 81 | } | 71 | } | ||
| 82 | 72 | ||||
| 83 | - | if ac.copyModeActive() { | 73 | + | if rt.copyActive() { |
| 84 | if ev.Button == mouse.Left { | 74 | if ev.Button == mouse.Left { | ||
| 85 | contentRow := ev.Row - 1 | 75 | contentRow := ev.Row - 1 | ||
| 86 | tb.mu.Lock() | 76 | tb.mu.Lock() | ||
| @@ -211,25 +201,25 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 211 | switch ev.Type { | 201 | switch ev.Type { | ||
| 212 | case mouse.Press: | 202 | case mouse.Press: | ||
| 213 | if altScreen || ev.Row >= childRows { | 203 | if altScreen || ev.Row >= childRows { | ||
| 214 | - | ac.copyMu.Lock() | 204 | + | rt.copyMu.Lock() |
| 215 | - | ac.normalMousePressValid = false | 205 | + | rt.normalMousePressValid = false |
| 216 | - | ac.copyMu.Unlock() | 206 | + | rt.copyMu.Unlock() |
| 217 | return | 207 | return | ||
| 218 | } | 208 | } | ||
| 219 | - | ac.copyMu.Lock() | 209 | + | rt.copyMu.Lock() |
| 220 | - | ac.normalMousePressRow = ev.Row | 210 | + | rt.normalMousePressRow = ev.Row |
| 221 | - | ac.normalMousePressTop = scrollbackRows | 211 | + | rt.normalMousePressTop = scrollbackRows |
| 222 | - | ac.normalMousePressValid = true | 212 | + | rt.normalMousePressValid = true |
| 223 | - | ac.copyMu.Unlock() | 213 | + | rt.copyMu.Unlock() |
| 224 | case mouse.Motion: | 214 | case mouse.Motion: | ||
| 225 | if altScreen || ev.Row >= childRows { | 215 | if altScreen || ev.Row >= childRows { | ||
| 226 | return | 216 | return | ||
| 227 | } | 217 | } | ||
| 228 | - | ac.copyMu.Lock() | 218 | + | rt.copyMu.Lock() |
| 229 | - | pressValid := ac.normalMousePressValid | 219 | + | pressValid := rt.normalMousePressValid |
| 230 | - | pressRow := ac.normalMousePressRow | 220 | + | pressRow := rt.normalMousePressRow |
| 231 | - | pressTop := ac.normalMousePressTop | 221 | + | pressTop := rt.normalMousePressTop |
| 232 | - | ac.copyMu.Unlock() | 222 | + | rt.copyMu.Unlock() |
| 233 | if !pressValid { | 223 | if !pressValid { | ||
| 234 | return | 224 | return | ||
| 235 | } | 225 | } | ||
| @@ -238,21 +228,21 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 238 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 228 | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | ||
| 239 | p.mu.Unlock() | 229 | p.mu.Unlock() | ||
| 240 | 230 | ||||
| 241 | - | ac.copyMu.Lock() | 231 | + | rt.copyMu.Lock() |
| 242 | mode := scopy.NewMode(snap) | 232 | mode := scopy.NewMode(snap) | ||
| 243 | mode.StartSelectionAt(snap, pressTop+pressRow) | 233 | mode.StartSelectionAt(snap, pressTop+pressRow) | ||
| 244 | mode.ExtendTo(snap, len(snap.Rows)-snap.Height+ev.Row) | 234 | mode.ExtendTo(snap, len(snap.Rows)-snap.Height+ev.Row) | ||
| 245 | - | ac.copyMode = mode | 235 | + | rt.copyMode = mode |
| 246 | - | ac.copyPressRow = pressTop + pressRow | 236 | + | rt.copyPressRow = pressTop + pressRow |
| 247 | - | ac.copyPressRowValid = true | 237 | + | rt.copyPressRowValid = true |
| 248 | - | ac.copyDragging = true | 238 | + | rt.copyDragging = true |
| 249 | - | ac.normalMousePressValid = false | 239 | + | rt.normalMousePressValid = false |
| 250 | - | ac.copyMu.Unlock() | 240 | + | rt.copyMu.Unlock() |
| 251 | d.paint(sess, ac, true) | 241 | d.paint(sess, ac, true) | ||
| 252 | case mouse.Release: | 242 | case mouse.Release: | ||
| 253 | - | ac.copyMu.Lock() | 243 | + | rt.copyMu.Lock() |
| 254 | - | ac.normalMousePressValid = false | 244 | + | rt.normalMousePressValid = false |
| 255 | - | ac.copyMu.Unlock() | 245 | + | rt.copyMu.Unlock() |
| 256 | } | 246 | } | ||
| 257 | case mouse.WheelUp: | 247 | case mouse.WheelUp: | ||
| 258 | if altScreen { | 248 | if altScreen { | ||
M internal/usecase/daemon/input_test.go +135 -24
| @@ -94,6 +94,117 @@ func TestBracketedMultilinePasteForwardsDelimitersAndNewlines(t *testing.T) { | |||
| 94 | 94 | releasePTY() | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | func TestOverlayInputPrecedence(t *testing.T) { | |
| 98 | + | cases := []struct { | |
| 99 | + | name string | |
| 100 | + | setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) | |
| 101 | + | input []byte | |
| 102 | + | check func(t *testing.T, ac *attachedClient, writes chan []byte) | |
| 103 | + | }{ | |
| 104 | + | { | |
| 105 | + | name: "prompt before palette picker copy and normal", | |
| 106 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |
| 107 | + | t.Helper() | |
| 108 | + | d.enterCopyMode(sess, ac) | |
| 109 | + | d.enterPicker(sess, ac) | |
| 110 | + | d.enterPalette(sess, ac) | |
| 111 | + | d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil }) | |
| 112 | + | }, | |
| 113 | + | input: []byte("x"), | |
| 114 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |
| 115 | + | t.Helper() | |
| 116 | + | require.Equal(t, "x", ac.overlays.prompt.Value()) | |
| 117 | + | require.Equal(t, "", ac.overlays.palette.Query()) | |
| 118 | + | require.True(t, ac.overlays.pickerActive()) | |
| 119 | + | require.True(t, ac.overlays.copyActive()) | |
| 120 | + | requireNoPTYWrite(t, writes) | |
| 121 | + | }, | |
| 122 | + | }, | |
| 123 | + | { | |
| 124 | + | name: "palette before picker copy and normal", | |
| 125 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |
| 126 | + | t.Helper() | |
| 127 | + | d.enterCopyMode(sess, ac) | |
| 128 | + | d.enterPicker(sess, ac) | |
| 129 | + | d.enterPalette(sess, ac) | |
| 130 | + | }, | |
| 131 | + | input: []byte("x"), | |
| 132 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |
| 133 | + | t.Helper() | |
| 134 | + | require.Equal(t, "x", ac.overlays.palette.Query()) | |
| 135 | + | require.True(t, ac.overlays.pickerActive()) | |
| 136 | + | require.True(t, ac.overlays.copyActive()) | |
| 137 | + | requireNoPTYWrite(t, writes) | |
| 138 | + | }, | |
| 139 | + | }, | |
| 140 | + | { | |
| 141 | + | name: "picker before copy and normal", | |
| 142 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |
| 143 | + | t.Helper() | |
| 144 | + | d.enterCopyMode(sess, ac) | |
| 145 | + | d.enterPicker(sess, ac) | |
| 146 | + | }, | |
| 147 | + | input: []byte("j"), | |
| 148 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |
| 149 | + | t.Helper() | |
| 150 | + | target, ok := ac.overlays.picker.Selected() | |
| 151 | + | require.True(t, ok) | |
| 152 | + | require.Equal(t, 1, target.TabIndex) | |
| 153 | + | require.True(t, ac.overlays.copyActive()) | |
| 154 | + | requireNoPTYWrite(t, writes) | |
| 155 | + | }, | |
| 156 | + | }, | |
| 157 | + | { | |
| 158 | + | name: "copy before normal", | |
| 159 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |
| 160 | + | t.Helper() | |
| 161 | + | d.enterCopyMode(sess, ac) | |
| 162 | + | }, | |
| 163 | + | input: []byte("q"), | |
| 164 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |
| 165 | + | t.Helper() | |
| 166 | + | require.False(t, ac.overlays.copyActive()) | |
| 167 | + | requireNoPTYWrite(t, writes) | |
| 168 | + | }, | |
| 169 | + | }, | |
| 170 | + | { | |
| 171 | + | name: "normal keys when no overlay is active", | |
| 172 | + | input: []byte("Z"), | |
| 173 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |
| 174 | + | t.Helper() | |
| 175 | + | require.Equal(t, []byte("Z"), <-writes) | |
| 176 | + | }, | |
| 177 | + | }, | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | for _, tc := range cases { | |
| 181 | + | t.Run(tc.name, func(t *testing.T) { | |
| 182 | + | writes := make(chan []byte, 1) | |
| 183 | + | p, releasePTY := newBlockingPTYWithWrites(t, writes) | |
| 184 | + | p2, releasePTY2 := newBlockingPTY(t) | |
| 185 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p, p2) | |
| 186 | + | defer releasePTY() | |
| 187 | + | defer releasePTY2() | |
| 188 | + | if tc.setup != nil { | |
| 189 | + | tc.setup(t, d, sess, ac, sends) | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | d.handleInput(sess, ac, tc.input) | |
| 193 | + | ||
| 194 | + | tc.check(t, ac, writes) | |
| 195 | + | }) | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | func requireNoPTYWrite(t *testing.T, writes chan []byte) { | |
| 200 | + | t.Helper() | |
| 201 | + | select { | |
| 202 | + | case got := <-writes: | |
| 203 | + | t.Fatalf("input forwarded to PTY: %q", got) | |
| 204 | + | default: | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 97 | 208 | func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) { | |
| 98 | 209 | cases := []struct { | |
| 99 | 210 | name string | |
| @@ -172,12 +283,12 @@ func TestRNSOpensPromptAndEnterPromotesEphemeralSession(t *testing.T) { | |||
| 172 | 283 | awaitFrame(t, sends, ports.MsgOutput) | |
| 173 | 284 | d.handleInput(sess, ac, []byte("RNS\r")) | |
| 174 | 285 | awaitFrame(t, sends, ports.MsgOutput) | |
| 175 | - | require.True(t, ac.promptActive()) | |
| 286 | + | require.True(t, ac.overlays.promptActive()) | |
| 176 | 287 | ||
| 177 | 288 | d.handleInput(sess, ac, []byte("\r")) | |
| 178 | 289 | awaitFrame(t, sends, ports.MsgOutput) | |
| 179 | 290 | ||
| 180 | - | require.False(t, ac.promptActive()) | |
| 291 | + | require.False(t, ac.overlays.promptActive()) | |
| 181 | 292 | require.False(t, sess.ephemeral) | |
| 182 | 293 | require.Equal(t, "0", sess.name) | |
| 183 | 294 | } | |
| @@ -191,18 +302,18 @@ func TestMouseWheelEntersScrollbackModeAndExitsAtBottom(t *testing.T) { | |||
| 191 | 302 | ||
| 192 | 303 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1M")) | |
| 193 | 304 | data := mustOutputData(t, sends) | |
| 194 | - | require.NotNil(t, ac.copyMode) | |
| 195 | - | require.Equal(t, 19, ac.copyMode.Cursor) | |
| 305 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 306 | + | require.Equal(t, 19, ac.overlays.copyMode.Cursor) | |
| 196 | 307 | require.Contains(t, string(data), "[VISUAL]") | |
| 197 | 308 | ||
| 198 | 309 | d.handleInput(sess, ac, []byte("\x1b[<65;1;1M")) | |
| 199 | 310 | mustOutputData(t, sends) | |
| 200 | - | require.Nil(t, ac.copyMode) | |
| 311 | + | require.Nil(t, ac.overlays.copyMode) | |
| 201 | 312 | ||
| 202 | 313 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1Mq")) | |
| 203 | 314 | mustOutputData(t, sends) | |
| 204 | 315 | mustOutputData(t, sends) | |
| 205 | - | require.Nil(t, ac.copyMode, "q after wheel in same input must be routed after copy mode is entered") | |
| 316 | + | require.Nil(t, ac.overlays.copyMode, "q after wheel in same input must be routed after copy mode is entered") | |
| 206 | 317 | select { | |
| 207 | 318 | case got := <-writes: | |
| 208 | 319 | t.Fatalf("mouse/copy input forwarded to PTY: %q", got) | |
| @@ -221,7 +332,7 @@ func TestMouseAltScreenWheelMapsToArrows(t *testing.T) { | |||
| 221 | 332 | ||
| 222 | 333 | require.Equal(t, []byte("\x1b[A\x1b[A\x1b[A"), <-writes) | |
| 223 | 334 | require.Equal(t, []byte("\x1b[B\x1b[B\x1b[B"), <-writes) | |
| 224 | - | require.Nil(t, ac.copyMode) | |
| 335 | + | require.Nil(t, ac.overlays.copyMode) | |
| 225 | 336 | } | |
| 226 | 337 | ||
| 227 | 338 | func TestSGRRowOffset(t *testing.T) { | |
| @@ -262,7 +373,7 @@ func TestMouseChildForwardingStatusDropAndPressDrop(t *testing.T) { | |||
| 262 | 373 | t.Fatalf("press without child mouse mode forwarded: %q", got) | |
| 263 | 374 | default: | |
| 264 | 375 | } | |
| 265 | - | require.Nil(t, ac.copyMode, "press alone must not enter visual mode") | |
| 376 | + | require.Nil(t, ac.overlays.copyMode, "press alone must not enter visual mode") | |
| 266 | 377 | ||
| 267 | 378 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1000h")) | |
| 268 | 379 | raw := []byte("\x1b[<0;2;3M") | |
| @@ -294,11 +405,11 @@ func TestMouseChildForwardingStatusDropAndPressDrop(t *testing.T) { | |||
| 294 | 405 | ||
| 295 | 406 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1006l\x1b[?1000l")) | |
| 296 | 407 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | |
| 297 | - | require.Nil(t, ac.copyMode, "press alone must still not enter visual mode") | |
| 408 | + | require.Nil(t, ac.overlays.copyMode, "press alone must still not enter visual mode") | |
| 298 | 409 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | |
| 299 | 410 | mustOutputData(t, sends) | |
| 300 | - | require.NotNil(t, ac.copyMode) | |
| 301 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | |
| 411 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 412 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() | |
| 302 | 413 | require.True(t, ok) | |
| 303 | 414 | require.Equal(t, 0, lo) | |
| 304 | 415 | require.Equal(t, 2, hi) | |
| @@ -315,8 +426,8 @@ func TestCopyModeMouseDragYanksOSC52AndExits(t *testing.T) { | |||
| 315 | 426 | mustOutputData(t, sends) | |
| 316 | 427 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | |
| 317 | 428 | mustOutputData(t, sends) | |
| 318 | - | require.NotNil(t, ac.copyMode) | |
| 319 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | |
| 429 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 430 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() | |
| 320 | 431 | require.True(t, ok) | |
| 321 | 432 | require.Equal(t, 0, lo) | |
| 322 | 433 | require.Equal(t, 1, hi) | |
| @@ -330,7 +441,7 @@ func TestCopyModeMouseDragYanksOSC52AndExits(t *testing.T) { | |||
| 330 | 441 | decoded, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(strings.TrimPrefix(data, "\x1b]52;c;"), "\a")) | |
| 331 | 442 | require.NoError(t, err) | |
| 332 | 443 | require.Equal(t, "alpha\nbravo", string(decoded)) | |
| 333 | - | require.Nil(t, ac.copyMode) | |
| 444 | + | require.Nil(t, ac.overlays.copyMode) | |
| 334 | 445 | ||
| 335 | 446 | exitPaint := string(mustOutputData(t, sends)) | |
| 336 | 447 | require.NotContains(t, exitPaint, "[SELECT]") | |
| @@ -350,7 +461,7 @@ func TestMouseNormalScreenStatusRowClearsStalePressState(t *testing.T) { | |||
| 350 | 461 | ||
| 351 | 462 | // Press on a content row establishes a (soon to be stale) anchor. | |
| 352 | 463 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | |
| 353 | - | require.Nil(t, ac.copyMode) | |
| 464 | + | require.Nil(t, ac.overlays.copyMode) | |
| 354 | 465 | ||
| 355 | 466 | // Release lands on the status row: must still clear the press state, | |
| 356 | 467 | // regardless of the row it landed on. | |
| @@ -363,7 +474,7 @@ func TestMouseNormalScreenStatusRowClearsStalePressState(t *testing.T) { | |||
| 363 | 474 | // Motion onto a content row must not resurrect a stale anchor. | |
| 364 | 475 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | |
| 365 | 476 | ||
| 366 | - | require.Nil(t, ac.copyMode, "stale press state must not resurrect a selection") | |
| 477 | + | require.Nil(t, ac.overlays.copyMode, "stale press state must not resurrect a selection") | |
| 367 | 478 | } | |
| 368 | 479 | ||
| 369 | 480 | // TestCopyModeStatusRowPressClearsDragState covers the copy-mode counterpart | |
| @@ -386,8 +497,8 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) { | |||
| 386 | 497 | // Full drag: press row0, motion to row1 -> selection [0,1]. | |
| 387 | 498 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | |
| 388 | 499 | mustOutputData(t, sends) | |
| 389 | - | require.NotNil(t, ac.copyMode) | |
| 390 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | |
| 500 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 501 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() | |
| 391 | 502 | require.True(t, ok) | |
| 392 | 503 | require.Equal(t, 0, lo) | |
| 393 | 504 | require.Equal(t, 1, hi) | |
| @@ -400,8 +511,8 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) { | |||
| 400 | 511 | // have cleared copyPressRowValid/copyDragging. | |
| 401 | 512 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | |
| 402 | 513 | ||
| 403 | - | require.NotNil(t, ac.copyMode) | |
| 404 | - | lo, hi, ok = ac.copyMode.SelectedBounds() | |
| 514 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 515 | + | lo, hi, ok = ac.overlays.copyMode.SelectedBounds() | |
| 405 | 516 | require.True(t, ok) | |
| 406 | 517 | require.Equal(t, 0, lo, "anchor must not have moved") | |
| 407 | 518 | require.Equal(t, 1, hi, "status-row press must invalidate the drag so the motion is a no-op") | |
| @@ -434,8 +545,8 @@ func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) { | |||
| 434 | 545 | // pressTop(0)+ev.Row(2) = 2. | |
| 435 | 546 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | |
| 436 | 547 | ||
| 437 | - | require.NotNil(t, ac.copyMode) | |
| 438 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | |
| 548 | + | require.NotNil(t, ac.overlays.copyMode) | |
| 549 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() | |
| 439 | 550 | require.True(t, ok) | |
| 440 | 551 | require.Equal(t, 0, lo, "anchor stays content-stable at the row under the pointer at press time") | |
| 441 | 552 | require.Equal(t, 7, hi, "extend target must track the pointer's current content row, not a stale scrollback offset") | |
| @@ -452,7 +563,7 @@ func TestMouseSplitReportPreservesOrder(t *testing.T) { | |||
| 452 | 563 | ||
| 453 | 564 | mustOutputData(t, sends) | |
| 454 | 565 | mustOutputData(t, sends) | |
| 455 | - | require.Nil(t, ac.copyMode) | |
| 566 | + | require.Nil(t, ac.overlays.copyMode) | |
| 456 | 567 | select { | |
| 457 | 568 | case got := <-writes: | |
| 458 | 569 | t.Fatalf("split mouse/copy bytes forwarded to PTY: %q", got) | |
| @@ -550,7 +661,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | |||
| 550 | 661 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;12M")) | |
| 551 | 662 | mustOutputData(t, sends) | |
| 552 | 663 | ||
| 553 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | |
| 664 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() | |
| 554 | 665 | require.True(t, ok) | |
| 555 | 666 | require.Equal(t, 0, lo) | |
| 556 | 667 | require.Equal(t, 9, hi) | |
| @@ -94,6 +94,117 @@ func TestBracketedMultilinePasteForwardsDelimitersAndNewlines(t *testing.T) { | |||||
| 94 | releasePTY() | 94 | releasePTY() | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | + | func TestOverlayInputPrecedence(t *testing.T) { | |||
| 98 | + | cases := []struct { | |||
| 99 | + | name string | |||
| 100 | + | setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) | |||
| 101 | + | input []byte | |||
| 102 | + | check func(t *testing.T, ac *attachedClient, writes chan []byte) | |||
| 103 | + | }{ | |||
| 104 | + | { | |||
| 105 | + | name: "prompt before palette picker copy and normal", | |||
| 106 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |||
| 107 | + | t.Helper() | |||
| 108 | + | d.enterCopyMode(sess, ac) | |||
| 109 | + | d.enterPicker(sess, ac) | |||
| 110 | + | d.enterPalette(sess, ac) | |||
| 111 | + | d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil }) | |||
| 112 | + | }, | |||
| 113 | + | input: []byte("x"), | |||
| 114 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |||
| 115 | + | t.Helper() | |||
| 116 | + | require.Equal(t, "x", ac.overlays.prompt.Value()) | |||
| 117 | + | require.Equal(t, "", ac.overlays.palette.Query()) | |||
| 118 | + | require.True(t, ac.overlays.pickerActive()) | |||
| 119 | + | require.True(t, ac.overlays.copyActive()) | |||
| 120 | + | requireNoPTYWrite(t, writes) | |||
| 121 | + | }, | |||
| 122 | + | }, | |||
| 123 | + | { | |||
| 124 | + | name: "palette before picker copy and normal", | |||
| 125 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |||
| 126 | + | t.Helper() | |||
| 127 | + | d.enterCopyMode(sess, ac) | |||
| 128 | + | d.enterPicker(sess, ac) | |||
| 129 | + | d.enterPalette(sess, ac) | |||
| 130 | + | }, | |||
| 131 | + | input: []byte("x"), | |||
| 132 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |||
| 133 | + | t.Helper() | |||
| 134 | + | require.Equal(t, "x", ac.overlays.palette.Query()) | |||
| 135 | + | require.True(t, ac.overlays.pickerActive()) | |||
| 136 | + | require.True(t, ac.overlays.copyActive()) | |||
| 137 | + | requireNoPTYWrite(t, writes) | |||
| 138 | + | }, | |||
| 139 | + | }, | |||
| 140 | + | { | |||
| 141 | + | name: "picker before copy and normal", | |||
| 142 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |||
| 143 | + | t.Helper() | |||
| 144 | + | d.enterCopyMode(sess, ac) | |||
| 145 | + | d.enterPicker(sess, ac) | |||
| 146 | + | }, | |||
| 147 | + | input: []byte("j"), | |||
| 148 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |||
| 149 | + | t.Helper() | |||
| 150 | + | target, ok := ac.overlays.picker.Selected() | |||
| 151 | + | require.True(t, ok) | |||
| 152 | + | require.Equal(t, 1, target.TabIndex) | |||
| 153 | + | require.True(t, ac.overlays.copyActive()) | |||
| 154 | + | requireNoPTYWrite(t, writes) | |||
| 155 | + | }, | |||
| 156 | + | }, | |||
| 157 | + | { | |||
| 158 | + | name: "copy before normal", | |||
| 159 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, sends chan ports.Frame) { | |||
| 160 | + | t.Helper() | |||
| 161 | + | d.enterCopyMode(sess, ac) | |||
| 162 | + | }, | |||
| 163 | + | input: []byte("q"), | |||
| 164 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |||
| 165 | + | t.Helper() | |||
| 166 | + | require.False(t, ac.overlays.copyActive()) | |||
| 167 | + | requireNoPTYWrite(t, writes) | |||
| 168 | + | }, | |||
| 169 | + | }, | |||
| 170 | + | { | |||
| 171 | + | name: "normal keys when no overlay is active", | |||
| 172 | + | input: []byte("Z"), | |||
| 173 | + | check: func(t *testing.T, ac *attachedClient, writes chan []byte) { | |||
| 174 | + | t.Helper() | |||
| 175 | + | require.Equal(t, []byte("Z"), <-writes) | |||
| 176 | + | }, | |||
| 177 | + | }, | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | for _, tc := range cases { | |||
| 181 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 182 | + | writes := make(chan []byte, 1) | |||
| 183 | + | p, releasePTY := newBlockingPTYWithWrites(t, writes) | |||
| 184 | + | p2, releasePTY2 := newBlockingPTY(t) | |||
| 185 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p, p2) | |||
| 186 | + | defer releasePTY() | |||
| 187 | + | defer releasePTY2() | |||
| 188 | + | if tc.setup != nil { | |||
| 189 | + | tc.setup(t, d, sess, ac, sends) | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | d.handleInput(sess, ac, tc.input) | |||
| 193 | + | ||||
| 194 | + | tc.check(t, ac, writes) | |||
| 195 | + | }) | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | func requireNoPTYWrite(t *testing.T, writes chan []byte) { | |||
| 200 | + | t.Helper() | |||
| 201 | + | select { | |||
| 202 | + | case got := <-writes: | |||
| 203 | + | t.Fatalf("input forwarded to PTY: %q", got) | |||
| 204 | + | default: | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | ||||
| 97 | func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) { | 208 | func TestPaletteNextPreviousSwitchActiveTab(t *testing.T) { | ||
| 98 | cases := []struct { | 209 | cases := []struct { | ||
| 99 | name string | 210 | name string | ||
| @@ -172,12 +283,12 @@ func TestRNSOpensPromptAndEnterPromotesEphemeralSession(t *testing.T) { | |||||
| 172 | awaitFrame(t, sends, ports.MsgOutput) | 283 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 173 | d.handleInput(sess, ac, []byte("RNS\r")) | 284 | d.handleInput(sess, ac, []byte("RNS\r")) | ||
| 174 | awaitFrame(t, sends, ports.MsgOutput) | 285 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 175 | - | require.True(t, ac.promptActive()) | 286 | + | require.True(t, ac.overlays.promptActive()) |
| 176 | 287 | ||||
| 177 | d.handleInput(sess, ac, []byte("\r")) | 288 | d.handleInput(sess, ac, []byte("\r")) | ||
| 178 | awaitFrame(t, sends, ports.MsgOutput) | 289 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 179 | 290 | ||||
| 180 | - | require.False(t, ac.promptActive()) | 291 | + | require.False(t, ac.overlays.promptActive()) |
| 181 | require.False(t, sess.ephemeral) | 292 | require.False(t, sess.ephemeral) | ||
| 182 | require.Equal(t, "0", sess.name) | 293 | require.Equal(t, "0", sess.name) | ||
| 183 | } | 294 | } | ||
| @@ -191,18 +302,18 @@ func TestMouseWheelEntersScrollbackModeAndExitsAtBottom(t *testing.T) { | |||||
| 191 | 302 | ||||
| 192 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1M")) | 303 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1M")) | ||
| 193 | data := mustOutputData(t, sends) | 304 | data := mustOutputData(t, sends) | ||
| 194 | - | require.NotNil(t, ac.copyMode) | 305 | + | require.NotNil(t, ac.overlays.copyMode) |
| 195 | - | require.Equal(t, 19, ac.copyMode.Cursor) | 306 | + | require.Equal(t, 19, ac.overlays.copyMode.Cursor) |
| 196 | require.Contains(t, string(data), "[VISUAL]") | 307 | require.Contains(t, string(data), "[VISUAL]") | ||
| 197 | 308 | ||||
| 198 | d.handleInput(sess, ac, []byte("\x1b[<65;1;1M")) | 309 | d.handleInput(sess, ac, []byte("\x1b[<65;1;1M")) | ||
| 199 | mustOutputData(t, sends) | 310 | mustOutputData(t, sends) | ||
| 200 | - | require.Nil(t, ac.copyMode) | 311 | + | require.Nil(t, ac.overlays.copyMode) |
| 201 | 312 | ||||
| 202 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1Mq")) | 313 | d.handleInput(sess, ac, []byte("\x1b[<64;1;1Mq")) | ||
| 203 | mustOutputData(t, sends) | 314 | mustOutputData(t, sends) | ||
| 204 | mustOutputData(t, sends) | 315 | mustOutputData(t, sends) | ||
| 205 | - | require.Nil(t, ac.copyMode, "q after wheel in same input must be routed after copy mode is entered") | 316 | + | require.Nil(t, ac.overlays.copyMode, "q after wheel in same input must be routed after copy mode is entered") |
| 206 | select { | 317 | select { | ||
| 207 | case got := <-writes: | 318 | case got := <-writes: | ||
| 208 | t.Fatalf("mouse/copy input forwarded to PTY: %q", got) | 319 | t.Fatalf("mouse/copy input forwarded to PTY: %q", got) | ||
| @@ -221,7 +332,7 @@ func TestMouseAltScreenWheelMapsToArrows(t *testing.T) { | |||||
| 221 | 332 | ||||
| 222 | require.Equal(t, []byte("\x1b[A\x1b[A\x1b[A"), <-writes) | 333 | require.Equal(t, []byte("\x1b[A\x1b[A\x1b[A"), <-writes) | ||
| 223 | require.Equal(t, []byte("\x1b[B\x1b[B\x1b[B"), <-writes) | 334 | require.Equal(t, []byte("\x1b[B\x1b[B\x1b[B"), <-writes) | ||
| 224 | - | require.Nil(t, ac.copyMode) | 335 | + | require.Nil(t, ac.overlays.copyMode) |
| 225 | } | 336 | } | ||
| 226 | 337 | ||||
| 227 | func TestSGRRowOffset(t *testing.T) { | 338 | func TestSGRRowOffset(t *testing.T) { | ||
| @@ -262,7 +373,7 @@ func TestMouseChildForwardingStatusDropAndPressDrop(t *testing.T) { | |||||
| 262 | t.Fatalf("press without child mouse mode forwarded: %q", got) | 373 | t.Fatalf("press without child mouse mode forwarded: %q", got) | ||
| 263 | default: | 374 | default: | ||
| 264 | } | 375 | } | ||
| 265 | - | require.Nil(t, ac.copyMode, "press alone must not enter visual mode") | 376 | + | require.Nil(t, ac.overlays.copyMode, "press alone must not enter visual mode") |
| 266 | 377 | ||||
| 267 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1000h")) | 378 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1000h")) | ||
| 268 | raw := []byte("\x1b[<0;2;3M") | 379 | raw := []byte("\x1b[<0;2;3M") | ||
| @@ -294,11 +405,11 @@ func TestMouseChildForwardingStatusDropAndPressDrop(t *testing.T) { | |||||
| 294 | 405 | ||||
| 295 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1006l\x1b[?1000l")) | 406 | sess.tabs[0].focusedPane().screen.Write([]byte("\x1b[?1006l\x1b[?1000l")) | ||
| 296 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | 407 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | ||
| 297 | - | require.Nil(t, ac.copyMode, "press alone must still not enter visual mode") | 408 | + | require.Nil(t, ac.overlays.copyMode, "press alone must still not enter visual mode") |
| 298 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | 409 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | ||
| 299 | mustOutputData(t, sends) | 410 | mustOutputData(t, sends) | ||
| 300 | - | require.NotNil(t, ac.copyMode) | 411 | + | require.NotNil(t, ac.overlays.copyMode) |
| 301 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | 412 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() |
| 302 | require.True(t, ok) | 413 | require.True(t, ok) | ||
| 303 | require.Equal(t, 0, lo) | 414 | require.Equal(t, 0, lo) | ||
| 304 | require.Equal(t, 2, hi) | 415 | require.Equal(t, 2, hi) | ||
| @@ -315,8 +426,8 @@ func TestCopyModeMouseDragYanksOSC52AndExits(t *testing.T) { | |||||
| 315 | mustOutputData(t, sends) | 426 | mustOutputData(t, sends) | ||
| 316 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | 427 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | ||
| 317 | mustOutputData(t, sends) | 428 | mustOutputData(t, sends) | ||
| 318 | - | require.NotNil(t, ac.copyMode) | 429 | + | require.NotNil(t, ac.overlays.copyMode) |
| 319 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | 430 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() |
| 320 | require.True(t, ok) | 431 | require.True(t, ok) | ||
| 321 | require.Equal(t, 0, lo) | 432 | require.Equal(t, 0, lo) | ||
| 322 | require.Equal(t, 1, hi) | 433 | require.Equal(t, 1, hi) | ||
| @@ -330,7 +441,7 @@ func TestCopyModeMouseDragYanksOSC52AndExits(t *testing.T) { | |||||
| 330 | decoded, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(strings.TrimPrefix(data, "\x1b]52;c;"), "\a")) | 441 | decoded, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(strings.TrimPrefix(data, "\x1b]52;c;"), "\a")) | ||
| 331 | require.NoError(t, err) | 442 | require.NoError(t, err) | ||
| 332 | require.Equal(t, "alpha\nbravo", string(decoded)) | 443 | require.Equal(t, "alpha\nbravo", string(decoded)) | ||
| 333 | - | require.Nil(t, ac.copyMode) | 444 | + | require.Nil(t, ac.overlays.copyMode) |
| 334 | 445 | ||||
| 335 | exitPaint := string(mustOutputData(t, sends)) | 446 | exitPaint := string(mustOutputData(t, sends)) | ||
| 336 | require.NotContains(t, exitPaint, "[SELECT]") | 447 | require.NotContains(t, exitPaint, "[SELECT]") | ||
| @@ -350,7 +461,7 @@ func TestMouseNormalScreenStatusRowClearsStalePressState(t *testing.T) { | |||||
| 350 | 461 | ||||
| 351 | // Press on a content row establishes a (soon to be stale) anchor. | 462 | // Press on a content row establishes a (soon to be stale) anchor. | ||
| 352 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | 463 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M")) | ||
| 353 | - | require.Nil(t, ac.copyMode) | 464 | + | require.Nil(t, ac.overlays.copyMode) |
| 354 | 465 | ||||
| 355 | // Release lands on the status row: must still clear the press state, | 466 | // Release lands on the status row: must still clear the press state, | ||
| 356 | // regardless of the row it landed on. | 467 | // regardless of the row it landed on. | ||
| @@ -363,7 +474,7 @@ func TestMouseNormalScreenStatusRowClearsStalePressState(t *testing.T) { | |||||
| 363 | // Motion onto a content row must not resurrect a stale anchor. | 474 | // Motion onto a content row must not resurrect a stale anchor. | ||
| 364 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | 475 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | ||
| 365 | 476 | ||||
| 366 | - | require.Nil(t, ac.copyMode, "stale press state must not resurrect a selection") | 477 | + | require.Nil(t, ac.overlays.copyMode, "stale press state must not resurrect a selection") |
| 367 | } | 478 | } | ||
| 368 | 479 | ||||
| 369 | // TestCopyModeStatusRowPressClearsDragState covers the copy-mode counterpart | 480 | // TestCopyModeStatusRowPressClearsDragState covers the copy-mode counterpart | ||
| @@ -386,8 +497,8 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) { | |||||
| 386 | // Full drag: press row0, motion to row1 -> selection [0,1]. | 497 | // Full drag: press row0, motion to row1 -> selection [0,1]. | ||
| 387 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | 498 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;2M")) | ||
| 388 | mustOutputData(t, sends) | 499 | mustOutputData(t, sends) | ||
| 389 | - | require.NotNil(t, ac.copyMode) | 500 | + | require.NotNil(t, ac.overlays.copyMode) |
| 390 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | 501 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() |
| 391 | require.True(t, ok) | 502 | require.True(t, ok) | ||
| 392 | require.Equal(t, 0, lo) | 503 | require.Equal(t, 0, lo) | ||
| 393 | require.Equal(t, 1, hi) | 504 | require.Equal(t, 1, hi) | ||
| @@ -400,8 +511,8 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) { | |||||
| 400 | // have cleared copyPressRowValid/copyDragging. | 511 | // have cleared copyPressRowValid/copyDragging. | ||
| 401 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | 512 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | ||
| 402 | 513 | ||||
| 403 | - | require.NotNil(t, ac.copyMode) | 514 | + | require.NotNil(t, ac.overlays.copyMode) |
| 404 | - | lo, hi, ok = ac.copyMode.SelectedBounds() | 515 | + | lo, hi, ok = ac.overlays.copyMode.SelectedBounds() |
| 405 | require.True(t, ok) | 516 | require.True(t, ok) | ||
| 406 | require.Equal(t, 0, lo, "anchor must not have moved") | 517 | require.Equal(t, 0, lo, "anchor must not have moved") | ||
| 407 | require.Equal(t, 1, hi, "status-row press must invalidate the drag so the motion is a no-op") | 518 | require.Equal(t, 1, hi, "status-row press must invalidate the drag so the motion is a no-op") | ||
| @@ -434,8 +545,8 @@ func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) { | |||||
| 434 | // pressTop(0)+ev.Row(2) = 2. | 545 | // pressTop(0)+ev.Row(2) = 2. | ||
| 435 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | 546 | d.handleInput(sess, ac, []byte("\x1b[<32;1;3M")) | ||
| 436 | 547 | ||||
| 437 | - | require.NotNil(t, ac.copyMode) | 548 | + | require.NotNil(t, ac.overlays.copyMode) |
| 438 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | 549 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() |
| 439 | require.True(t, ok) | 550 | require.True(t, ok) | ||
| 440 | require.Equal(t, 0, lo, "anchor stays content-stable at the row under the pointer at press time") | 551 | require.Equal(t, 0, lo, "anchor stays content-stable at the row under the pointer at press time") | ||
| 441 | require.Equal(t, 7, hi, "extend target must track the pointer's current content row, not a stale scrollback offset") | 552 | require.Equal(t, 7, hi, "extend target must track the pointer's current content row, not a stale scrollback offset") | ||
| @@ -452,7 +563,7 @@ func TestMouseSplitReportPreservesOrder(t *testing.T) { | |||||
| 452 | 563 | ||||
| 453 | mustOutputData(t, sends) | 564 | mustOutputData(t, sends) | ||
| 454 | mustOutputData(t, sends) | 565 | mustOutputData(t, sends) | ||
| 455 | - | require.Nil(t, ac.copyMode) | 566 | + | require.Nil(t, ac.overlays.copyMode) |
| 456 | select { | 567 | select { | ||
| 457 | case got := <-writes: | 568 | case got := <-writes: | ||
| 458 | t.Fatalf("split mouse/copy bytes forwarded to PTY: %q", got) | 569 | t.Fatalf("split mouse/copy bytes forwarded to PTY: %q", got) | ||
| @@ -550,7 +661,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | |||||
| 550 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;12M")) | 661 | d.handleInput(sess, ac, []byte("\x1b[<0;1;1M\x1b[<32;1;12M")) | ||
| 551 | mustOutputData(t, sends) | 662 | mustOutputData(t, sends) | ||
| 552 | 663 | ||||
| 553 | - | lo, hi, ok := ac.copyMode.SelectedBounds() | 664 | + | lo, hi, ok := ac.overlays.copyMode.SelectedBounds() |
| 554 | require.True(t, ok) | 665 | require.True(t, ok) | ||
| 555 | require.Equal(t, 0, lo) | 666 | require.Equal(t, 0, lo) | ||
| 556 | require.Equal(t, 9, hi) | 667 | require.Equal(t, 9, hi) | ||
M internal/usecase/daemon/mouse_panes.go +7 -6
| @@ -142,10 +142,11 @@ func (d *Daemon) exitCopyMode(ac *attachedClient) { | |||
| 142 | 142 | if ac == nil { | |
| 143 | 143 | return | |
| 144 | 144 | } | |
| 145 | - | ac.copyMu.Lock() | |
| 146 | - | ac.copyMode = nil | |
| 147 | - | ac.copyPressRowValid = false | |
| 148 | - | ac.copyDragging = false | |
| 149 | - | ac.normalMousePressValid = false | |
| 150 | - | ac.copyMu.Unlock() | |
| 145 | + | rt := ac.overlays | |
| 146 | + | rt.copyMu.Lock() | |
| 147 | + | rt.copyMode = nil | |
| 148 | + | rt.copyPressRowValid = false | |
| 149 | + | rt.copyDragging = false | |
| 150 | + | rt.normalMousePressValid = false | |
| 151 | + | rt.copyMu.Unlock() | |
| 151 | 152 | } | |
| @@ -142,10 +142,11 @@ func (d *Daemon) exitCopyMode(ac *attachedClient) { | |||||
| 142 | if ac == nil { | 142 | if ac == nil { | ||
| 143 | return | 143 | return | ||
| 144 | } | 144 | } | ||
| 145 | - | ac.copyMu.Lock() | 145 | + | rt := ac.overlays |
| 146 | - | ac.copyMode = nil | 146 | + | rt.copyMu.Lock() |
| 147 | - | ac.copyPressRowValid = false | 147 | + | rt.copyMode = nil |
| 148 | - | ac.copyDragging = false | 148 | + | rt.copyPressRowValid = false |
| 149 | - | ac.normalMousePressValid = false | 149 | + | rt.copyDragging = false |
| 150 | - | ac.copyMu.Unlock() | 150 | + | rt.normalMousePressValid = false |
| 151 | + | rt.copyMu.Unlock() | |||
| 151 | } | 152 | } | ||
M internal/usecase/daemon/nil_pty_test.go +1 -0
| @@ -58,6 +58,7 @@ func TestNilPTYInputPathsDoNotPanic(t *testing.T) { | |||
| 58 | 58 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | |
| 59 | 59 | sess := &session{id: "s", name: "s", ctx: ctx, cancel: cancel, tabs: []*tab{tb}} | |
| 60 | 60 | ac := &attachedClient{} | |
| 61 | + | ac.initOverlays() | |
| 61 | 62 | ac.setSession(sess) | |
| 62 | 63 | ||
| 63 | 64 | require.NotPanics(t, func() { d.writeToPane(sess, tb.focusedPane(), []byte("x")) }) | |
| @@ -58,6 +58,7 @@ func TestNilPTYInputPathsDoNotPanic(t *testing.T) { | |||||
| 58 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | 58 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | ||
| 59 | sess := &session{id: "s", name: "s", ctx: ctx, cancel: cancel, tabs: []*tab{tb}} | 59 | sess := &session{id: "s", name: "s", ctx: ctx, cancel: cancel, tabs: []*tab{tb}} | ||
| 60 | ac := &attachedClient{} | 60 | ac := &attachedClient{} | ||
| 61 | + | ac.initOverlays() | |||
| 61 | ac.setSession(sess) | 62 | ac.setSession(sess) | ||
| 62 | 63 | ||||
| 63 | require.NotPanics(t, func() { d.writeToPane(sess, tb.focusedPane(), []byte("x")) }) | 64 | require.NotPanics(t, func() { d.writeToPane(sess, tb.focusedPane(), []byte("x")) }) | ||
A internal/usecase/daemon/overlay_runtime.go +204 -0
| @@ -0,0 +1,204 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | ||
| 6 | + | scopy "github.com/bnema/vev/internal/usecase/copy" | |
| 7 | + | "github.com/bnema/vev/internal/usecase/palette" | |
| 8 | + | "github.com/bnema/vev/internal/usecase/picker" | |
| 9 | + | promptui "github.com/bnema/vev/internal/usecase/prompt" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type overlayRuntime struct { | |
| 13 | + | ac *attachedClient | |
| 14 | + | ||
| 15 | + | pickerMu sync.Mutex | |
| 16 | + | picker *picker.Model | |
| 17 | + | pickerPreview *tab | |
| 18 | + | pickerPending []byte | |
| 19 | + | pickerESC pendingByteTimer | |
| 20 | + | ||
| 21 | + | paletteMu sync.Mutex | |
| 22 | + | palette *palette.Model | |
| 23 | + | palettePending []byte | |
| 24 | + | ||
| 25 | + | promptMu sync.Mutex | |
| 26 | + | prompt *promptui.Model | |
| 27 | + | promptSubmit func(string) error | |
| 28 | + | promptPending []byte | |
| 29 | + | ||
| 30 | + | copyMu sync.Mutex | |
| 31 | + | copyMode *scopy.Mode | |
| 32 | + | copyPending []byte | |
| 33 | + | copyESC pendingByteTimer | |
| 34 | + | copyFeedback string | |
| 35 | + | copyPressRow int | |
| 36 | + | copyPressRowValid bool | |
| 37 | + | copyDragging bool | |
| 38 | + | normalMousePressRow int | |
| 39 | + | normalMousePressTop int | |
| 40 | + | normalMousePressValid bool | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func newOverlayRuntime(ac *attachedClient) *overlayRuntime { | |
| 44 | + | return &overlayRuntime{ac: ac} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func (rt *overlayRuntime) Active() bool { | |
| 48 | + | if rt == nil || rt.ac == nil { | |
| 49 | + | return false | |
| 50 | + | } | |
| 51 | + | return rt.promptActive() || rt.paletteActive() || rt.pickerActive() || rt.copyActive() | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | func (rt *overlayRuntime) promptActive() bool { | |
| 55 | + | if rt == nil { | |
| 56 | + | return false | |
| 57 | + | } | |
| 58 | + | rt.promptMu.Lock() | |
| 59 | + | defer rt.promptMu.Unlock() | |
| 60 | + | return rt.prompt != nil | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (rt *overlayRuntime) paletteActive() bool { | |
| 64 | + | if rt == nil { | |
| 65 | + | return false | |
| 66 | + | } | |
| 67 | + | rt.paletteMu.Lock() | |
| 68 | + | defer rt.paletteMu.Unlock() | |
| 69 | + | return rt.palette != nil | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (rt *overlayRuntime) pickerActive() bool { | |
| 73 | + | if rt == nil { | |
| 74 | + | return false | |
| 75 | + | } | |
| 76 | + | rt.pickerMu.Lock() | |
| 77 | + | defer rt.pickerMu.Unlock() | |
| 78 | + | return rt.picker != nil | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (rt *overlayRuntime) copyActive() bool { | |
| 82 | + | if rt == nil { | |
| 83 | + | return false | |
| 84 | + | } | |
| 85 | + | rt.copyMu.Lock() | |
| 86 | + | defer rt.copyMu.Unlock() | |
| 87 | + | return rt.copyMode != nil | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (rt *overlayRuntime) HandleInput(d *Daemon, data []byte) bool { | |
| 91 | + | if rt == nil || rt.ac == nil { | |
| 92 | + | return false | |
| 93 | + | } | |
| 94 | + | ac := rt.ac | |
| 95 | + | if rt.promptActive() { | |
| 96 | + | d.handlePromptInput(ac, data) | |
| 97 | + | return true | |
| 98 | + | } | |
| 99 | + | if rt.paletteActive() { | |
| 100 | + | d.handlePaletteInput(ac, data) | |
| 101 | + | return true | |
| 102 | + | } | |
| 103 | + | if rt.pickerActive() { | |
| 104 | + | d.handlePickerInput(ac, data) | |
| 105 | + | return true | |
| 106 | + | } | |
| 107 | + | if rt.copyActive() { | |
| 108 | + | d.handleCopyInput(ac, data) | |
| 109 | + | return true | |
| 110 | + | } | |
| 111 | + | return false | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | type overlayRenderSnapshot struct { | |
| 115 | + | rt *overlayRuntime | |
| 116 | + | ||
| 117 | + | copyActive bool | |
| 118 | + | copyMode *scopy.Mode | |
| 119 | + | copyFeedback string | |
| 120 | + | ||
| 121 | + | pickerActive bool | |
| 122 | + | pickerModel *picker.Model | |
| 123 | + | previewTab *tab | |
| 124 | + | ||
| 125 | + | paletteActive bool | |
| 126 | + | paletteModel *palette.Model | |
| 127 | + | paletteLocked bool | |
| 128 | + | ||
| 129 | + | promptActive bool | |
| 130 | + | promptModel *promptui.Model | |
| 131 | + | promptLocked bool | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // SnapshotForRender captures the overlay state needed by paint. | |
| 135 | + | // | |
| 136 | + | // The snapshot owns any prompt or palette locks it had to keep held so the | |
| 137 | + | // returned model pointers stay stable during composition. Callers must release | |
| 138 | + | // those locks with overlayRenderSnapshot.Unlock, usually with defer immediately | |
| 139 | + | // after acquisition and before any path that may re-enter rendering. | |
| 140 | + | func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot { | |
| 141 | + | snap := &overlayRenderSnapshot{rt: rt} | |
| 142 | + | if rt == nil { | |
| 143 | + | return snap | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | rt.copyMu.Lock() | |
| 147 | + | snap.copyActive = rt.copyMode != nil | |
| 148 | + | if rt.copyMode != nil { | |
| 149 | + | copyModeValue := *rt.copyMode | |
| 150 | + | snap.copyMode = ©ModeValue | |
| 151 | + | } | |
| 152 | + | snap.copyFeedback = rt.copyFeedback | |
| 153 | + | if snap.copyFeedback != "" && !snap.copyActive { | |
| 154 | + | rt.copyFeedback = "" | |
| 155 | + | } | |
| 156 | + | rt.copyMu.Unlock() | |
| 157 | + | ||
| 158 | + | rt.pickerMu.Lock() | |
| 159 | + | snap.pickerActive = rt.picker != nil | |
| 160 | + | snap.pickerModel = rt.picker.Clone() | |
| 161 | + | snap.previewTab = rt.pickerPreview | |
| 162 | + | rt.pickerMu.Unlock() | |
| 163 | + | ||
| 164 | + | rt.paletteMu.Lock() | |
| 165 | + | snap.paletteModel = rt.palette | |
| 166 | + | snap.paletteActive = snap.paletteModel != nil | |
| 167 | + | if snap.paletteActive { | |
| 168 | + | snap.paletteLocked = true | |
| 169 | + | } else { | |
| 170 | + | rt.paletteMu.Unlock() | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | rt.promptMu.Lock() | |
| 174 | + | snap.promptModel = rt.prompt | |
| 175 | + | snap.promptActive = snap.promptModel != nil | |
| 176 | + | if snap.promptActive { | |
| 177 | + | snap.promptLocked = true | |
| 178 | + | } else { | |
| 179 | + | rt.promptMu.Unlock() | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | return snap | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func (rt *overlayRuntime) UnlockRenderSnapshot(snap *overlayRenderSnapshot) { | |
| 186 | + | if snap == nil { | |
| 187 | + | return | |
| 188 | + | } | |
| 189 | + | snap.Unlock() | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (snap *overlayRenderSnapshot) Unlock() { | |
| 193 | + | if snap == nil || snap.rt == nil { | |
| 194 | + | return | |
| 195 | + | } | |
| 196 | + | if snap.promptLocked { | |
| 197 | + | snap.rt.promptMu.Unlock() | |
| 198 | + | snap.promptLocked = false | |
| 199 | + | } | |
| 200 | + | if snap.paletteLocked { | |
| 201 | + | snap.rt.paletteMu.Unlock() | |
| 202 | + | snap.paletteLocked = false | |
| 203 | + | } | |
| 204 | + | } | |
| @@ -0,0 +1,204 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | ||||
| 6 | + | scopy "github.com/bnema/vev/internal/usecase/copy" | |||
| 7 | + | "github.com/bnema/vev/internal/usecase/palette" | |||
| 8 | + | "github.com/bnema/vev/internal/usecase/picker" | |||
| 9 | + | promptui "github.com/bnema/vev/internal/usecase/prompt" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type overlayRuntime struct { | |||
| 13 | + | ac *attachedClient | |||
| 14 | + | ||||
| 15 | + | pickerMu sync.Mutex | |||
| 16 | + | picker *picker.Model | |||
| 17 | + | pickerPreview *tab | |||
| 18 | + | pickerPending []byte | |||
| 19 | + | pickerESC pendingByteTimer | |||
| 20 | + | ||||
| 21 | + | paletteMu sync.Mutex | |||
| 22 | + | palette *palette.Model | |||
| 23 | + | palettePending []byte | |||
| 24 | + | ||||
| 25 | + | promptMu sync.Mutex | |||
| 26 | + | prompt *promptui.Model | |||
| 27 | + | promptSubmit func(string) error | |||
| 28 | + | promptPending []byte | |||
| 29 | + | ||||
| 30 | + | copyMu sync.Mutex | |||
| 31 | + | copyMode *scopy.Mode | |||
| 32 | + | copyPending []byte | |||
| 33 | + | copyESC pendingByteTimer | |||
| 34 | + | copyFeedback string | |||
| 35 | + | copyPressRow int | |||
| 36 | + | copyPressRowValid bool | |||
| 37 | + | copyDragging bool | |||
| 38 | + | normalMousePressRow int | |||
| 39 | + | normalMousePressTop int | |||
| 40 | + | normalMousePressValid bool | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func newOverlayRuntime(ac *attachedClient) *overlayRuntime { | |||
| 44 | + | return &overlayRuntime{ac: ac} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func (rt *overlayRuntime) Active() bool { | |||
| 48 | + | if rt == nil || rt.ac == nil { | |||
| 49 | + | return false | |||
| 50 | + | } | |||
| 51 | + | return rt.promptActive() || rt.paletteActive() || rt.pickerActive() || rt.copyActive() | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func (rt *overlayRuntime) promptActive() bool { | |||
| 55 | + | if rt == nil { | |||
| 56 | + | return false | |||
| 57 | + | } | |||
| 58 | + | rt.promptMu.Lock() | |||
| 59 | + | defer rt.promptMu.Unlock() | |||
| 60 | + | return rt.prompt != nil | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func (rt *overlayRuntime) paletteActive() bool { | |||
| 64 | + | if rt == nil { | |||
| 65 | + | return false | |||
| 66 | + | } | |||
| 67 | + | rt.paletteMu.Lock() | |||
| 68 | + | defer rt.paletteMu.Unlock() | |||
| 69 | + | return rt.palette != nil | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func (rt *overlayRuntime) pickerActive() bool { | |||
| 73 | + | if rt == nil { | |||
| 74 | + | return false | |||
| 75 | + | } | |||
| 76 | + | rt.pickerMu.Lock() | |||
| 77 | + | defer rt.pickerMu.Unlock() | |||
| 78 | + | return rt.picker != nil | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | func (rt *overlayRuntime) copyActive() bool { | |||
| 82 | + | if rt == nil { | |||
| 83 | + | return false | |||
| 84 | + | } | |||
| 85 | + | rt.copyMu.Lock() | |||
| 86 | + | defer rt.copyMu.Unlock() | |||
| 87 | + | return rt.copyMode != nil | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func (rt *overlayRuntime) HandleInput(d *Daemon, data []byte) bool { | |||
| 91 | + | if rt == nil || rt.ac == nil { | |||
| 92 | + | return false | |||
| 93 | + | } | |||
| 94 | + | ac := rt.ac | |||
| 95 | + | if rt.promptActive() { | |||
| 96 | + | d.handlePromptInput(ac, data) | |||
| 97 | + | return true | |||
| 98 | + | } | |||
| 99 | + | if rt.paletteActive() { | |||
| 100 | + | d.handlePaletteInput(ac, data) | |||
| 101 | + | return true | |||
| 102 | + | } | |||
| 103 | + | if rt.pickerActive() { | |||
| 104 | + | d.handlePickerInput(ac, data) | |||
| 105 | + | return true | |||
| 106 | + | } | |||
| 107 | + | if rt.copyActive() { | |||
| 108 | + | d.handleCopyInput(ac, data) | |||
| 109 | + | return true | |||
| 110 | + | } | |||
| 111 | + | return false | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | type overlayRenderSnapshot struct { | |||
| 115 | + | rt *overlayRuntime | |||
| 116 | + | ||||
| 117 | + | copyActive bool | |||
| 118 | + | copyMode *scopy.Mode | |||
| 119 | + | copyFeedback string | |||
| 120 | + | ||||
| 121 | + | pickerActive bool | |||
| 122 | + | pickerModel *picker.Model | |||
| 123 | + | previewTab *tab | |||
| 124 | + | ||||
| 125 | + | paletteActive bool | |||
| 126 | + | paletteModel *palette.Model | |||
| 127 | + | paletteLocked bool | |||
| 128 | + | ||||
| 129 | + | promptActive bool | |||
| 130 | + | promptModel *promptui.Model | |||
| 131 | + | promptLocked bool | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // SnapshotForRender captures the overlay state needed by paint. | |||
| 135 | + | // | |||
| 136 | + | // The snapshot owns any prompt or palette locks it had to keep held so the | |||
| 137 | + | // returned model pointers stay stable during composition. Callers must release | |||
| 138 | + | // those locks with overlayRenderSnapshot.Unlock, usually with defer immediately | |||
| 139 | + | // after acquisition and before any path that may re-enter rendering. | |||
| 140 | + | func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot { | |||
| 141 | + | snap := &overlayRenderSnapshot{rt: rt} | |||
| 142 | + | if rt == nil { | |||
| 143 | + | return snap | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | rt.copyMu.Lock() | |||
| 147 | + | snap.copyActive = rt.copyMode != nil | |||
| 148 | + | if rt.copyMode != nil { | |||
| 149 | + | copyModeValue := *rt.copyMode | |||
| 150 | + | snap.copyMode = ©ModeValue | |||
| 151 | + | } | |||
| 152 | + | snap.copyFeedback = rt.copyFeedback | |||
| 153 | + | if snap.copyFeedback != "" && !snap.copyActive { | |||
| 154 | + | rt.copyFeedback = "" | |||
| 155 | + | } | |||
| 156 | + | rt.copyMu.Unlock() | |||
| 157 | + | ||||
| 158 | + | rt.pickerMu.Lock() | |||
| 159 | + | snap.pickerActive = rt.picker != nil | |||
| 160 | + | snap.pickerModel = rt.picker.Clone() | |||
| 161 | + | snap.previewTab = rt.pickerPreview | |||
| 162 | + | rt.pickerMu.Unlock() | |||
| 163 | + | ||||
| 164 | + | rt.paletteMu.Lock() | |||
| 165 | + | snap.paletteModel = rt.palette | |||
| 166 | + | snap.paletteActive = snap.paletteModel != nil | |||
| 167 | + | if snap.paletteActive { | |||
| 168 | + | snap.paletteLocked = true | |||
| 169 | + | } else { | |||
| 170 | + | rt.paletteMu.Unlock() | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | rt.promptMu.Lock() | |||
| 174 | + | snap.promptModel = rt.prompt | |||
| 175 | + | snap.promptActive = snap.promptModel != nil | |||
| 176 | + | if snap.promptActive { | |||
| 177 | + | snap.promptLocked = true | |||
| 178 | + | } else { | |||
| 179 | + | rt.promptMu.Unlock() | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | return snap | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (rt *overlayRuntime) UnlockRenderSnapshot(snap *overlayRenderSnapshot) { | |||
| 186 | + | if snap == nil { | |||
| 187 | + | return | |||
| 188 | + | } | |||
| 189 | + | snap.Unlock() | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (snap *overlayRenderSnapshot) Unlock() { | |||
| 193 | + | if snap == nil || snap.rt == nil { | |||
| 194 | + | return | |||
| 195 | + | } | |||
| 196 | + | if snap.promptLocked { | |||
| 197 | + | snap.rt.promptMu.Unlock() | |||
| 198 | + | snap.promptLocked = false | |||
| 199 | + | } | |||
| 200 | + | if snap.paletteLocked { | |||
| 201 | + | snap.rt.paletteMu.Unlock() | |||
| 202 | + | snap.paletteLocked = false | |||
| 203 | + | } | |||
| 204 | + | } | |||
A internal/usecase/daemon/overlay_runtime_test.go +113 -0
| @@ -0,0 +1,113 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | promptui "github.com/bnema/vev/internal/usecase/prompt" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestOverlayRuntimeActive(t *testing.T) { | |
| 11 | + | ac := &attachedClient{} | |
| 12 | + | ac.initOverlays() | |
| 13 | + | require.False(t, ac.overlays.Active()) | |
| 14 | + | ||
| 15 | + | ac.overlays.promptMu.Lock() | |
| 16 | + | ac.overlays.prompt = promptui.New(" Test ", "") | |
| 17 | + | ac.overlays.promptMu.Unlock() | |
| 18 | + | require.True(t, ac.overlays.Active()) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func TestOverlayRuntimeHandleInputPrecedence(t *testing.T) { | |
| 22 | + | cases := []struct { | |
| 23 | + | name string | |
| 24 | + | setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) | |
| 25 | + | input []byte | |
| 26 | + | check func(t *testing.T, ac *attachedClient) | |
| 27 | + | }{ | |
| 28 | + | { | |
| 29 | + | name: "prompt before palette picker copy", | |
| 30 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |
| 31 | + | t.Helper() | |
| 32 | + | d.enterCopyMode(sess, ac) | |
| 33 | + | d.enterPicker(sess, ac) | |
| 34 | + | d.enterPalette(sess, ac) | |
| 35 | + | d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil }) | |
| 36 | + | }, | |
| 37 | + | input: []byte("x"), | |
| 38 | + | check: func(t *testing.T, ac *attachedClient) { | |
| 39 | + | t.Helper() | |
| 40 | + | require.Equal(t, "x", ac.overlays.prompt.Value()) | |
| 41 | + | require.Equal(t, "", ac.overlays.palette.Query()) | |
| 42 | + | require.True(t, ac.overlays.pickerActive()) | |
| 43 | + | require.True(t, ac.overlays.copyActive()) | |
| 44 | + | }, | |
| 45 | + | }, | |
| 46 | + | { | |
| 47 | + | name: "palette before picker copy", | |
| 48 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |
| 49 | + | t.Helper() | |
| 50 | + | d.enterCopyMode(sess, ac) | |
| 51 | + | d.enterPicker(sess, ac) | |
| 52 | + | d.enterPalette(sess, ac) | |
| 53 | + | }, | |
| 54 | + | input: []byte("x"), | |
| 55 | + | check: func(t *testing.T, ac *attachedClient) { | |
| 56 | + | t.Helper() | |
| 57 | + | require.Equal(t, "x", ac.overlays.palette.Query()) | |
| 58 | + | require.True(t, ac.overlays.pickerActive()) | |
| 59 | + | require.True(t, ac.overlays.copyActive()) | |
| 60 | + | }, | |
| 61 | + | }, | |
| 62 | + | { | |
| 63 | + | name: "picker before copy", | |
| 64 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |
| 65 | + | t.Helper() | |
| 66 | + | d.enterCopyMode(sess, ac) | |
| 67 | + | d.enterPicker(sess, ac) | |
| 68 | + | }, | |
| 69 | + | input: []byte("j"), | |
| 70 | + | check: func(t *testing.T, ac *attachedClient) { | |
| 71 | + | t.Helper() | |
| 72 | + | target, ok := ac.overlays.picker.Selected() | |
| 73 | + | require.True(t, ok) | |
| 74 | + | require.Equal(t, 1, target.TabIndex) | |
| 75 | + | require.True(t, ac.overlays.copyActive()) | |
| 76 | + | }, | |
| 77 | + | }, | |
| 78 | + | { | |
| 79 | + | name: "copy", | |
| 80 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |
| 81 | + | t.Helper() | |
| 82 | + | d.enterCopyMode(sess, ac) | |
| 83 | + | }, | |
| 84 | + | input: []byte("q"), | |
| 85 | + | check: func(t *testing.T, ac *attachedClient) { | |
| 86 | + | t.Helper() | |
| 87 | + | require.False(t, ac.overlays.copyActive()) | |
| 88 | + | }, | |
| 89 | + | }, | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | for _, tc := range cases { | |
| 93 | + | t.Run(tc.name, func(t *testing.T) { | |
| 94 | + | p1, releasePTY1 := newBlockingPTY(t) | |
| 95 | + | p2, releasePTY2 := newBlockingPTY(t) | |
| 96 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2) | |
| 97 | + | defer releasePTY1() | |
| 98 | + | defer releasePTY2() | |
| 99 | + | tc.setup(t, d, sess, ac) | |
| 100 | + | ||
| 101 | + | require.True(t, ac.overlays.HandleInput(d, tc.input)) | |
| 102 | + | ||
| 103 | + | tc.check(t, ac) | |
| 104 | + | }) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func TestOverlayRuntimeHandleInputInactive(t *testing.T) { | |
| 109 | + | ac := &attachedClient{} | |
| 110 | + | ac.initOverlays() | |
| 111 | + | ||
| 112 | + | require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x"))) | |
| 113 | + | } | |
| @@ -0,0 +1,113 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | promptui "github.com/bnema/vev/internal/usecase/prompt" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestOverlayRuntimeActive(t *testing.T) { | |||
| 11 | + | ac := &attachedClient{} | |||
| 12 | + | ac.initOverlays() | |||
| 13 | + | require.False(t, ac.overlays.Active()) | |||
| 14 | + | ||||
| 15 | + | ac.overlays.promptMu.Lock() | |||
| 16 | + | ac.overlays.prompt = promptui.New(" Test ", "") | |||
| 17 | + | ac.overlays.promptMu.Unlock() | |||
| 18 | + | require.True(t, ac.overlays.Active()) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func TestOverlayRuntimeHandleInputPrecedence(t *testing.T) { | |||
| 22 | + | cases := []struct { | |||
| 23 | + | name string | |||
| 24 | + | setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) | |||
| 25 | + | input []byte | |||
| 26 | + | check func(t *testing.T, ac *attachedClient) | |||
| 27 | + | }{ | |||
| 28 | + | { | |||
| 29 | + | name: "prompt before palette picker copy", | |||
| 30 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |||
| 31 | + | t.Helper() | |||
| 32 | + | d.enterCopyMode(sess, ac) | |||
| 33 | + | d.enterPicker(sess, ac) | |||
| 34 | + | d.enterPalette(sess, ac) | |||
| 35 | + | d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil }) | |||
| 36 | + | }, | |||
| 37 | + | input: []byte("x"), | |||
| 38 | + | check: func(t *testing.T, ac *attachedClient) { | |||
| 39 | + | t.Helper() | |||
| 40 | + | require.Equal(t, "x", ac.overlays.prompt.Value()) | |||
| 41 | + | require.Equal(t, "", ac.overlays.palette.Query()) | |||
| 42 | + | require.True(t, ac.overlays.pickerActive()) | |||
| 43 | + | require.True(t, ac.overlays.copyActive()) | |||
| 44 | + | }, | |||
| 45 | + | }, | |||
| 46 | + | { | |||
| 47 | + | name: "palette before picker copy", | |||
| 48 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |||
| 49 | + | t.Helper() | |||
| 50 | + | d.enterCopyMode(sess, ac) | |||
| 51 | + | d.enterPicker(sess, ac) | |||
| 52 | + | d.enterPalette(sess, ac) | |||
| 53 | + | }, | |||
| 54 | + | input: []byte("x"), | |||
| 55 | + | check: func(t *testing.T, ac *attachedClient) { | |||
| 56 | + | t.Helper() | |||
| 57 | + | require.Equal(t, "x", ac.overlays.palette.Query()) | |||
| 58 | + | require.True(t, ac.overlays.pickerActive()) | |||
| 59 | + | require.True(t, ac.overlays.copyActive()) | |||
| 60 | + | }, | |||
| 61 | + | }, | |||
| 62 | + | { | |||
| 63 | + | name: "picker before copy", | |||
| 64 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |||
| 65 | + | t.Helper() | |||
| 66 | + | d.enterCopyMode(sess, ac) | |||
| 67 | + | d.enterPicker(sess, ac) | |||
| 68 | + | }, | |||
| 69 | + | input: []byte("j"), | |||
| 70 | + | check: func(t *testing.T, ac *attachedClient) { | |||
| 71 | + | t.Helper() | |||
| 72 | + | target, ok := ac.overlays.picker.Selected() | |||
| 73 | + | require.True(t, ok) | |||
| 74 | + | require.Equal(t, 1, target.TabIndex) | |||
| 75 | + | require.True(t, ac.overlays.copyActive()) | |||
| 76 | + | }, | |||
| 77 | + | }, | |||
| 78 | + | { | |||
| 79 | + | name: "copy", | |||
| 80 | + | setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |||
| 81 | + | t.Helper() | |||
| 82 | + | d.enterCopyMode(sess, ac) | |||
| 83 | + | }, | |||
| 84 | + | input: []byte("q"), | |||
| 85 | + | check: func(t *testing.T, ac *attachedClient) { | |||
| 86 | + | t.Helper() | |||
| 87 | + | require.False(t, ac.overlays.copyActive()) | |||
| 88 | + | }, | |||
| 89 | + | }, | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | for _, tc := range cases { | |||
| 93 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 94 | + | p1, releasePTY1 := newBlockingPTY(t) | |||
| 95 | + | p2, releasePTY2 := newBlockingPTY(t) | |||
| 96 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2) | |||
| 97 | + | defer releasePTY1() | |||
| 98 | + | defer releasePTY2() | |||
| 99 | + | tc.setup(t, d, sess, ac) | |||
| 100 | + | ||||
| 101 | + | require.True(t, ac.overlays.HandleInput(d, tc.input)) | |||
| 102 | + | ||||
| 103 | + | tc.check(t, ac) | |||
| 104 | + | }) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func TestOverlayRuntimeHandleInputInactive(t *testing.T) { | |||
| 109 | + | ac := &attachedClient{} | |||
| 110 | + | ac.initOverlays() | |||
| 111 | + | ||||
| 112 | + | require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x"))) | |||
| 113 | + | } | |||
M internal/usecase/daemon/overlay_test.go +96 -89
| @@ -6,96 +6,103 @@ import ( | |||
| 6 | 6 | "github.com/stretchr/testify/require" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | func TestRouteOverlayBytesHandlesTextAndSplitUTF8(t *testing.T) { | |
| 10 | - | var pending []byte | |
| 11 | - | var got []rune | |
| 12 | - | ev := overlayEvents{rune: func(r rune) { got = append(got, r) }} | |
| 13 | - | ||
| 14 | - | routeOverlayBytes([]byte{'w', 0xc3}, &pending, ev) | |
| 15 | - | require.Equal(t, []rune{'w'}, got) | |
| 16 | - | require.Equal(t, []byte{0xc3}, pending) | |
| 17 | - | ||
| 18 | - | routeOverlayBytes([]byte{0xb8, 'k'}, &pending, ev) | |
| 19 | - | require.Equal(t, []rune{'w', 'ø', 'k'}, got) | |
| 20 | - | require.Empty(t, pending) | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | func TestRouteOverlayBytesHandlesSplitArrowsAndCtrlNavigation(t *testing.T) { | |
| 24 | - | var pending []byte | |
| 25 | - | var up, down int | |
| 26 | - | ev := overlayEvents{ | |
| 27 | - | up: func() { up++ }, | |
| 28 | - | down: func() { down++ }, | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | routeOverlayBytes([]byte("\x1b["), &pending, ev) | |
| 32 | - | require.Equal(t, []byte("\x1b["), pending) | |
| 33 | - | routeOverlayBytes([]byte("B"), &pending, ev) | |
| 34 | - | require.Equal(t, 1, down) | |
| 35 | - | require.Empty(t, pending) | |
| 36 | - | ||
| 37 | - | routeOverlayBytes([]byte("\x1b[A\x0e\x10"), &pending, ev) | |
| 38 | - | require.Equal(t, 2, up) | |
| 39 | - | require.Equal(t, 2, down) | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | func TestRouteOverlayBytesHandlesSS3Arrows(t *testing.T) { | |
| 43 | - | var pending []byte | |
| 44 | - | var up, down int | |
| 45 | - | ev := overlayEvents{ | |
| 46 | - | up: func() { up++ }, | |
| 47 | - | down: func() { down++ }, | |
| 9 | + | func TestRouteOverlayBytes(t *testing.T) { | |
| 10 | + | t.Parallel() | |
| 11 | + | ||
| 12 | + | tests := []struct { | |
| 13 | + | name string | |
| 14 | + | chunks [][]byte | |
| 15 | + | wantRunes []rune | |
| 16 | + | wantPending []byte | |
| 17 | + | wantBackspace int | |
| 18 | + | wantEnter int | |
| 19 | + | wantCancel int | |
| 20 | + | wantUp int | |
| 21 | + | wantDown int | |
| 22 | + | }{ | |
| 23 | + | { | |
| 24 | + | name: "text and split utf8", | |
| 25 | + | chunks: [][]byte{{'w', 0xc3}, {0xb8, 'k'}}, | |
| 26 | + | wantRunes: []rune{'w', 'ø', 'k'}, | |
| 27 | + | wantPending: nil, | |
| 28 | + | }, | |
| 29 | + | { | |
| 30 | + | name: "incomplete utf8 remains pending", | |
| 31 | + | chunks: [][]byte{{'w', 0xc3}}, | |
| 32 | + | wantRunes: []rune{'w'}, | |
| 33 | + | wantPending: []byte{0xc3}, | |
| 34 | + | }, | |
| 35 | + | { | |
| 36 | + | name: "incomplete csi resumes as arrow", | |
| 37 | + | chunks: [][]byte{[]byte("\x1b["), []byte("B")}, | |
| 38 | + | wantDown: 1, | |
| 39 | + | }, | |
| 40 | + | { | |
| 41 | + | name: "incomplete csi remains pending", | |
| 42 | + | chunks: [][]byte{[]byte("\x1b[")}, | |
| 43 | + | wantPending: []byte("\x1b["), | |
| 44 | + | }, | |
| 45 | + | { | |
| 46 | + | name: "incomplete ss3 resumes as arrow", | |
| 47 | + | chunks: [][]byte{[]byte("\x1bO"), []byte("A\x1bOB")}, | |
| 48 | + | wantUp: 1, | |
| 49 | + | wantDown: 1, | |
| 50 | + | }, | |
| 51 | + | { | |
| 52 | + | name: "incomplete ss3 remains pending", | |
| 53 | + | chunks: [][]byte{[]byte("\x1bO")}, | |
| 54 | + | wantPending: []byte("\x1bO"), | |
| 55 | + | }, | |
| 56 | + | { | |
| 57 | + | name: "arrows and ctrl navigation", | |
| 58 | + | chunks: [][]byte{[]byte("\x1b[A\x1b[B\x0e\x10")}, | |
| 59 | + | wantUp: 2, | |
| 60 | + | wantDown: 2, | |
| 61 | + | }, | |
| 62 | + | { | |
| 63 | + | name: "unhandled escapes are consumed", | |
| 64 | + | chunks: [][]byte{[]byte("a\x1b[D\x1b[3~b\x1bOQ")}, | |
| 65 | + | wantRunes: []rune{'a', 'b'}, | |
| 66 | + | }, | |
| 67 | + | { | |
| 68 | + | name: "submit cancel and editing", | |
| 69 | + | chunks: [][]byte{{0x7f, 0x08, '\r', '\n', 0x1b, 0x03}}, | |
| 70 | + | wantBackspace: 2, | |
| 71 | + | wantEnter: 2, | |
| 72 | + | wantCancel: 2, | |
| 73 | + | }, | |
| 74 | + | { | |
| 75 | + | name: "controls and invalid utf8 ignored", | |
| 76 | + | chunks: [][]byte{{'a', 0x01, 0xff, 'b'}}, | |
| 77 | + | wantRunes: []rune{'a', 'b'}, | |
| 78 | + | }, | |
| 48 | 79 | } | |
| 49 | 80 | ||
| 50 | - | routeOverlayBytes([]byte("\x1bO"), &pending, ev) | |
| 51 | - | require.Equal(t, []byte("\x1bO"), pending) | |
| 52 | - | routeOverlayBytes([]byte("A\x1bOB"), &pending, ev) | |
| 53 | - | ||
| 54 | - | require.Equal(t, 1, up) | |
| 55 | - | require.Equal(t, 1, down) | |
| 56 | - | require.Empty(t, pending) | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | func TestRouteOverlayBytesConsumesUnhandledEscapeSequences(t *testing.T) { | |
| 60 | - | var pending []byte | |
| 61 | - | var got []rune | |
| 62 | - | var cancel int | |
| 63 | - | ev := overlayEvents{ | |
| 64 | - | rune: func(r rune) { got = append(got, r) }, | |
| 65 | - | cancel: func() { cancel++ }, | |
| 81 | + | for _, tt := range tests { | |
| 82 | + | t.Run(tt.name, func(t *testing.T) { | |
| 83 | + | var pending []byte | |
| 84 | + | var gotRunes []rune | |
| 85 | + | var gotBackspace, gotEnter, gotCancel, gotUp, gotDown int | |
| 86 | + | ev := overlayEvents{ | |
| 87 | + | rune: func(r rune) { gotRunes = append(gotRunes, r) }, | |
| 88 | + | backspace: func() { gotBackspace++ }, | |
| 89 | + | enter: func() { gotEnter++ }, | |
| 90 | + | cancel: func() { gotCancel++ }, | |
| 91 | + | up: func() { gotUp++ }, | |
| 92 | + | down: func() { gotDown++ }, | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | for _, chunk := range tt.chunks { | |
| 96 | + | routeOverlayBytes(chunk, &pending, ev) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | require.Equal(t, tt.wantRunes, gotRunes) | |
| 100 | + | require.Equal(t, tt.wantPending, pending) | |
| 101 | + | require.Equal(t, tt.wantBackspace, gotBackspace) | |
| 102 | + | require.Equal(t, tt.wantEnter, gotEnter) | |
| 103 | + | require.Equal(t, tt.wantCancel, gotCancel) | |
| 104 | + | require.Equal(t, tt.wantUp, gotUp) | |
| 105 | + | require.Equal(t, tt.wantDown, gotDown) | |
| 106 | + | }) | |
| 66 | 107 | } | |
| 67 | - | ||
| 68 | - | routeOverlayBytes([]byte("a\x1b[D\x1b[3~b\x1bOQ"), &pending, ev) | |
| 69 | - | ||
| 70 | - | require.Equal(t, []rune{'a', 'b'}, got) | |
| 71 | - | require.Equal(t, 0, cancel) | |
| 72 | - | require.Empty(t, pending) | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | func TestRouteOverlayBytesHandlesEditingSubmitAndCancel(t *testing.T) { | |
| 76 | - | var pending []byte | |
| 77 | - | var backspace, enter, cancel int | |
| 78 | - | ev := overlayEvents{ | |
| 79 | - | backspace: func() { backspace++ }, | |
| 80 | - | enter: func() { enter++ }, | |
| 81 | - | cancel: func() { cancel++ }, | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | routeOverlayBytes([]byte{0x7f, 0x08, '\r', '\n', 0x1b, 0x03}, &pending, ev) | |
| 85 | - | ||
| 86 | - | require.Equal(t, 2, backspace) | |
| 87 | - | require.Equal(t, 2, enter) | |
| 88 | - | require.Equal(t, 2, cancel) | |
| 89 | - | require.Empty(t, pending) | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | func TestRouteOverlayBytesIgnoresControlsAndInvalidUTF8(t *testing.T) { | |
| 93 | - | var pending []byte | |
| 94 | - | var got []rune | |
| 95 | - | ev := overlayEvents{rune: func(r rune) { got = append(got, r) }} | |
| 96 | - | ||
| 97 | - | routeOverlayBytes([]byte{'a', 0x01, 0xff, 'b'}, &pending, ev) | |
| 98 | - | ||
| 99 | - | require.Equal(t, []rune{'a', 'b'}, got) | |
| 100 | - | require.Empty(t, pending) | |
| 101 | 108 | } | |
| @@ -6,96 +6,103 @@ import ( | |||||
| 6 | "github.com/stretchr/testify/require" | 6 | "github.com/stretchr/testify/require" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | func TestRouteOverlayBytesHandlesTextAndSplitUTF8(t *testing.T) { | 9 | + | func TestRouteOverlayBytes(t *testing.T) { |
| 10 | - | var pending []byte | 10 | + | t.Parallel() |
| 11 | - | var got []rune | 11 | + | |
| 12 | - | ev := overlayEvents{rune: func(r rune) { got = append(got, r) }} | 12 | + | tests := []struct { |
| 13 | - | 13 | + | name string | |
| 14 | - | routeOverlayBytes([]byte{'w', 0xc3}, &pending, ev) | 14 | + | chunks [][]byte |
| 15 | - | require.Equal(t, []rune{'w'}, got) | 15 | + | wantRunes []rune |
| 16 | - | require.Equal(t, []byte{0xc3}, pending) | 16 | + | wantPending []byte |
| 17 | - | 17 | + | wantBackspace int | |
| 18 | - | routeOverlayBytes([]byte{0xb8, 'k'}, &pending, ev) | 18 | + | wantEnter int |
| 19 | - | require.Equal(t, []rune{'w', 'ø', 'k'}, got) | 19 | + | wantCancel int |
| 20 | - | require.Empty(t, pending) | 20 | + | wantUp int |
| 21 | - | } | 21 | + | wantDown int |
| 22 | - | 22 | + | }{ | |
| 23 | - | func TestRouteOverlayBytesHandlesSplitArrowsAndCtrlNavigation(t *testing.T) { | 23 | + | { |
| 24 | - | var pending []byte | 24 | + | name: "text and split utf8", |
| 25 | - | var up, down int | 25 | + | chunks: [][]byte{{'w', 0xc3}, {0xb8, 'k'}}, |
| 26 | - | ev := overlayEvents{ | 26 | + | wantRunes: []rune{'w', 'ø', 'k'}, |
| 27 | - | up: func() { up++ }, | 27 | + | wantPending: nil, |
| 28 | - | down: func() { down++ }, | 28 | + | }, |
| 29 | - | } | 29 | + | { |
| 30 | - | 30 | + | name: "incomplete utf8 remains pending", | |
| 31 | - | routeOverlayBytes([]byte("\x1b["), &pending, ev) | 31 | + | chunks: [][]byte{{'w', 0xc3}}, |
| 32 | - | require.Equal(t, []byte("\x1b["), pending) | 32 | + | wantRunes: []rune{'w'}, |
| 33 | - | routeOverlayBytes([]byte("B"), &pending, ev) | 33 | + | wantPending: []byte{0xc3}, |
| 34 | - | require.Equal(t, 1, down) | 34 | + | }, |
| 35 | - | require.Empty(t, pending) | 35 | + | { |
| 36 | - | 36 | + | name: "incomplete csi resumes as arrow", | |
| 37 | - | routeOverlayBytes([]byte("\x1b[A\x0e\x10"), &pending, ev) | 37 | + | chunks: [][]byte{[]byte("\x1b["), []byte("B")}, |
| 38 | - | require.Equal(t, 2, up) | 38 | + | wantDown: 1, |
| 39 | - | require.Equal(t, 2, down) | 39 | + | }, |
| 40 | - | } | 40 | + | { |
| 41 | - | 41 | + | name: "incomplete csi remains pending", | |
| 42 | - | func TestRouteOverlayBytesHandlesSS3Arrows(t *testing.T) { | 42 | + | chunks: [][]byte{[]byte("\x1b[")}, |
| 43 | - | var pending []byte | 43 | + | wantPending: []byte("\x1b["), |
| 44 | - | var up, down int | 44 | + | }, |
| 45 | - | ev := overlayEvents{ | 45 | + | { |
| 46 | - | up: func() { up++ }, | 46 | + | name: "incomplete ss3 resumes as arrow", |
| 47 | - | down: func() { down++ }, | 47 | + | chunks: [][]byte{[]byte("\x1bO"), []byte("A\x1bOB")}, |
| 48 | + | wantUp: 1, | |||
| 49 | + | wantDown: 1, | |||
| 50 | + | }, | |||
| 51 | + | { | |||
| 52 | + | name: "incomplete ss3 remains pending", | |||
| 53 | + | chunks: [][]byte{[]byte("\x1bO")}, | |||
| 54 | + | wantPending: []byte("\x1bO"), | |||
| 55 | + | }, | |||
| 56 | + | { | |||
| 57 | + | name: "arrows and ctrl navigation", | |||
| 58 | + | chunks: [][]byte{[]byte("\x1b[A\x1b[B\x0e\x10")}, | |||
| 59 | + | wantUp: 2, | |||
| 60 | + | wantDown: 2, | |||
| 61 | + | }, | |||
| 62 | + | { | |||
| 63 | + | name: "unhandled escapes are consumed", | |||
| 64 | + | chunks: [][]byte{[]byte("a\x1b[D\x1b[3~b\x1bOQ")}, | |||
| 65 | + | wantRunes: []rune{'a', 'b'}, | |||
| 66 | + | }, | |||
| 67 | + | { | |||
| 68 | + | name: "submit cancel and editing", | |||
| 69 | + | chunks: [][]byte{{0x7f, 0x08, '\r', '\n', 0x1b, 0x03}}, | |||
| 70 | + | wantBackspace: 2, | |||
| 71 | + | wantEnter: 2, | |||
| 72 | + | wantCancel: 2, | |||
| 73 | + | }, | |||
| 74 | + | { | |||
| 75 | + | name: "controls and invalid utf8 ignored", | |||
| 76 | + | chunks: [][]byte{{'a', 0x01, 0xff, 'b'}}, | |||
| 77 | + | wantRunes: []rune{'a', 'b'}, | |||
| 78 | + | }, | |||
| 48 | } | 79 | } | ||
| 49 | 80 | ||||
| 50 | - | routeOverlayBytes([]byte("\x1bO"), &pending, ev) | 81 | + | for _, tt := range tests { |
| 51 | - | require.Equal(t, []byte("\x1bO"), pending) | 82 | + | t.Run(tt.name, func(t *testing.T) { |
| 52 | - | routeOverlayBytes([]byte("A\x1bOB"), &pending, ev) | 83 | + | var pending []byte |
| 53 | - | 84 | + | var gotRunes []rune | |
| 54 | - | require.Equal(t, 1, up) | 85 | + | var gotBackspace, gotEnter, gotCancel, gotUp, gotDown int |
| 55 | - | require.Equal(t, 1, down) | 86 | + | ev := overlayEvents{ |
| 56 | - | require.Empty(t, pending) | 87 | + | rune: func(r rune) { gotRunes = append(gotRunes, r) }, |
| 57 | - | } | 88 | + | backspace: func() { gotBackspace++ }, |
| 58 | - | 89 | + | enter: func() { gotEnter++ }, | |
| 59 | - | func TestRouteOverlayBytesConsumesUnhandledEscapeSequences(t *testing.T) { | 90 | + | cancel: func() { gotCancel++ }, |
| 60 | - | var pending []byte | 91 | + | up: func() { gotUp++ }, |
| 61 | - | var got []rune | 92 | + | down: func() { gotDown++ }, |
| 62 | - | var cancel int | 93 | + | } |
| 63 | - | ev := overlayEvents{ | 94 | + | |
| 64 | - | rune: func(r rune) { got = append(got, r) }, | 95 | + | for _, chunk := range tt.chunks { |
| 65 | - | cancel: func() { cancel++ }, | 96 | + | routeOverlayBytes(chunk, &pending, ev) |
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | require.Equal(t, tt.wantRunes, gotRunes) | |||
| 100 | + | require.Equal(t, tt.wantPending, pending) | |||
| 101 | + | require.Equal(t, tt.wantBackspace, gotBackspace) | |||
| 102 | + | require.Equal(t, tt.wantEnter, gotEnter) | |||
| 103 | + | require.Equal(t, tt.wantCancel, gotCancel) | |||
| 104 | + | require.Equal(t, tt.wantUp, gotUp) | |||
| 105 | + | require.Equal(t, tt.wantDown, gotDown) | |||
| 106 | + | }) | |||
| 66 | } | 107 | } | ||
| 67 | - | ||||
| 68 | - | routeOverlayBytes([]byte("a\x1b[D\x1b[3~b\x1bOQ"), &pending, ev) | |||
| 69 | - | ||||
| 70 | - | require.Equal(t, []rune{'a', 'b'}, got) | |||
| 71 | - | require.Equal(t, 0, cancel) | |||
| 72 | - | require.Empty(t, pending) | |||
| 73 | - | } | |||
| 74 | - | ||||
| 75 | - | func TestRouteOverlayBytesHandlesEditingSubmitAndCancel(t *testing.T) { | |||
| 76 | - | var pending []byte | |||
| 77 | - | var backspace, enter, cancel int | |||
| 78 | - | ev := overlayEvents{ | |||
| 79 | - | backspace: func() { backspace++ }, | |||
| 80 | - | enter: func() { enter++ }, | |||
| 81 | - | cancel: func() { cancel++ }, | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | routeOverlayBytes([]byte{0x7f, 0x08, '\r', '\n', 0x1b, 0x03}, &pending, ev) | |||
| 85 | - | ||||
| 86 | - | require.Equal(t, 2, backspace) | |||
| 87 | - | require.Equal(t, 2, enter) | |||
| 88 | - | require.Equal(t, 2, cancel) | |||
| 89 | - | require.Empty(t, pending) | |||
| 90 | - | } | |||
| 91 | - | ||||
| 92 | - | func TestRouteOverlayBytesIgnoresControlsAndInvalidUTF8(t *testing.T) { | |||
| 93 | - | var pending []byte | |||
| 94 | - | var got []rune | |||
| 95 | - | ev := overlayEvents{rune: func(r rune) { got = append(got, r) }} | |||
| 96 | - | ||||
| 97 | - | routeOverlayBytes([]byte{'a', 0x01, 0xff, 'b'}, &pending, ev) | |||
| 98 | - | ||||
| 99 | - | require.Equal(t, []rune{'a', 'b'}, got) | |||
| 100 | - | require.Empty(t, pending) | |||
| 101 | } | 108 | } | ||
M internal/usecase/daemon/palette.go +21 -21
| @@ -13,18 +13,18 @@ var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: | |||
| 13 | 13 | ||
| 14 | 14 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | |
| 15 | 15 | d.closePalette(ac) | |
| 16 | - | ac.paletteMu.Lock() | |
| 17 | - | ac.palette = palette.New(d.paletteCommands()) | |
| 18 | - | ac.palettePending = nil | |
| 19 | - | ac.paletteMu.Unlock() | |
| 16 | + | ac.overlays.paletteMu.Lock() | |
| 17 | + | ac.overlays.palette = palette.New(d.paletteCommands()) | |
| 18 | + | ac.overlays.palettePending = nil | |
| 19 | + | ac.overlays.paletteMu.Unlock() | |
| 20 | 20 | d.paint(sess, ac, true) | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | func (d *Daemon) closePalette(ac *attachedClient) { | |
| 24 | - | ac.paletteMu.Lock() | |
| 25 | - | ac.palette = nil | |
| 26 | - | ac.palettePending = nil | |
| 27 | - | ac.paletteMu.Unlock() | |
| 24 | + | ac.overlays.paletteMu.Lock() | |
| 25 | + | ac.overlays.palette = nil | |
| 26 | + | ac.overlays.palettePending = nil | |
| 27 | + | ac.overlays.paletteMu.Unlock() | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | 30 | func (d *Daemon) paletteCommands() []command.Command { | |
| @@ -75,10 +75,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 75 | 75 | return | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | ac.paletteMu.Lock() | |
| 79 | - | if ac.palette == nil { | |
| 80 | - | ac.palettePending = nil | |
| 81 | - | ac.paletteMu.Unlock() | |
| 78 | + | ac.overlays.paletteMu.Lock() | |
| 79 | + | if ac.overlays.palette == nil { | |
| 80 | + | ac.overlays.palettePending = nil | |
| 81 | + | ac.overlays.paletteMu.Unlock() | |
| 82 | 82 | return | |
| 83 | 83 | } | |
| 84 | 84 | changed := false | |
| @@ -87,17 +87,17 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 87 | 87 | var cmd command.Command | |
| 88 | 88 | var ok bool | |
| 89 | 89 | ||
| 90 | - | routeOverlayBytes(data, &ac.palettePending, overlayEvents{ | |
| 90 | + | routeOverlayBytes(data, &ac.overlays.palettePending, overlayEvents{ | |
| 91 | 91 | rune: func(r rune) { | |
| 92 | - | ac.palette.Insert(r) | |
| 92 | + | ac.overlays.palette.Insert(r) | |
| 93 | 93 | changed = true | |
| 94 | 94 | }, | |
| 95 | 95 | backspace: func() { | |
| 96 | - | ac.palette.Backspace() | |
| 96 | + | ac.overlays.palette.Backspace() | |
| 97 | 97 | changed = true | |
| 98 | 98 | }, | |
| 99 | 99 | enter: func() { | |
| 100 | - | cmd, ok = ac.palette.Selected() | |
| 100 | + | cmd, ok = ac.overlays.palette.Selected() | |
| 101 | 101 | if ok { | |
| 102 | 102 | run = true | |
| 103 | 103 | exit = true | |
| @@ -105,19 +105,19 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 105 | 105 | }, | |
| 106 | 106 | cancel: func() { exit = true }, | |
| 107 | 107 | up: func() { | |
| 108 | - | ac.palette.Up() | |
| 108 | + | ac.overlays.palette.Up() | |
| 109 | 109 | changed = true | |
| 110 | 110 | }, | |
| 111 | 111 | down: func() { | |
| 112 | - | ac.palette.Down() | |
| 112 | + | ac.overlays.palette.Down() | |
| 113 | 113 | changed = true | |
| 114 | 114 | }, | |
| 115 | 115 | }) | |
| 116 | 116 | if exit { | |
| 117 | - | ac.palette = nil | |
| 118 | - | ac.palettePending = nil | |
| 117 | + | ac.overlays.palette = nil | |
| 118 | + | ac.overlays.palettePending = nil | |
| 119 | 119 | } | |
| 120 | - | ac.paletteMu.Unlock() | |
| 120 | + | ac.overlays.paletteMu.Unlock() | |
| 121 | 121 | ||
| 122 | 122 | if run && ok { | |
| 123 | 123 | if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil { | |
| @@ -13,18 +13,18 @@ var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: | |||||
| 13 | 13 | ||||
| 14 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | 14 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | ||
| 15 | d.closePalette(ac) | 15 | d.closePalette(ac) | ||
| 16 | - | ac.paletteMu.Lock() | 16 | + | ac.overlays.paletteMu.Lock() |
| 17 | - | ac.palette = palette.New(d.paletteCommands()) | 17 | + | ac.overlays.palette = palette.New(d.paletteCommands()) |
| 18 | - | ac.palettePending = nil | 18 | + | ac.overlays.palettePending = nil |
| 19 | - | ac.paletteMu.Unlock() | 19 | + | ac.overlays.paletteMu.Unlock() |
| 20 | d.paint(sess, ac, true) | 20 | d.paint(sess, ac, true) | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | func (d *Daemon) closePalette(ac *attachedClient) { | 23 | func (d *Daemon) closePalette(ac *attachedClient) { | ||
| 24 | - | ac.paletteMu.Lock() | 24 | + | ac.overlays.paletteMu.Lock() |
| 25 | - | ac.palette = nil | 25 | + | ac.overlays.palette = nil |
| 26 | - | ac.palettePending = nil | 26 | + | ac.overlays.palettePending = nil |
| 27 | - | ac.paletteMu.Unlock() | 27 | + | ac.overlays.paletteMu.Unlock() |
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | func (d *Daemon) paletteCommands() []command.Command { | 30 | func (d *Daemon) paletteCommands() []command.Command { | ||
| @@ -75,10 +75,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 75 | return | 75 | return | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | ac.paletteMu.Lock() | 78 | + | ac.overlays.paletteMu.Lock() |
| 79 | - | if ac.palette == nil { | 79 | + | if ac.overlays.palette == nil { |
| 80 | - | ac.palettePending = nil | 80 | + | ac.overlays.palettePending = nil |
| 81 | - | ac.paletteMu.Unlock() | 81 | + | ac.overlays.paletteMu.Unlock() |
| 82 | return | 82 | return | ||
| 83 | } | 83 | } | ||
| 84 | changed := false | 84 | changed := false | ||
| @@ -87,17 +87,17 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 87 | var cmd command.Command | 87 | var cmd command.Command | ||
| 88 | var ok bool | 88 | var ok bool | ||
| 89 | 89 | ||||
| 90 | - | routeOverlayBytes(data, &ac.palettePending, overlayEvents{ | 90 | + | routeOverlayBytes(data, &ac.overlays.palettePending, overlayEvents{ |
| 91 | rune: func(r rune) { | 91 | rune: func(r rune) { | ||
| 92 | - | ac.palette.Insert(r) | 92 | + | ac.overlays.palette.Insert(r) |
| 93 | changed = true | 93 | changed = true | ||
| 94 | }, | 94 | }, | ||
| 95 | backspace: func() { | 95 | backspace: func() { | ||
| 96 | - | ac.palette.Backspace() | 96 | + | ac.overlays.palette.Backspace() |
| 97 | changed = true | 97 | changed = true | ||
| 98 | }, | 98 | }, | ||
| 99 | enter: func() { | 99 | enter: func() { | ||
| 100 | - | cmd, ok = ac.palette.Selected() | 100 | + | cmd, ok = ac.overlays.palette.Selected() |
| 101 | if ok { | 101 | if ok { | ||
| 102 | run = true | 102 | run = true | ||
| 103 | exit = true | 103 | exit = true | ||
| @@ -105,19 +105,19 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 105 | }, | 105 | }, | ||
| 106 | cancel: func() { exit = true }, | 106 | cancel: func() { exit = true }, | ||
| 107 | up: func() { | 107 | up: func() { | ||
| 108 | - | ac.palette.Up() | 108 | + | ac.overlays.palette.Up() |
| 109 | changed = true | 109 | changed = true | ||
| 110 | }, | 110 | }, | ||
| 111 | down: func() { | 111 | down: func() { | ||
| 112 | - | ac.palette.Down() | 112 | + | ac.overlays.palette.Down() |
| 113 | changed = true | 113 | changed = true | ||
| 114 | }, | 114 | }, | ||
| 115 | }) | 115 | }) | ||
| 116 | if exit { | 116 | if exit { | ||
| 117 | - | ac.palette = nil | 117 | + | ac.overlays.palette = nil |
| 118 | - | ac.palettePending = nil | 118 | + | ac.overlays.palettePending = nil |
| 119 | } | 119 | } | ||
| 120 | - | ac.paletteMu.Unlock() | 120 | + | ac.overlays.paletteMu.Unlock() |
| 121 | 121 | ||||
| 122 | if run && ok { | 122 | if run && ok { | ||
| 123 | if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil { | 123 | if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil { | ||
M internal/usecase/daemon/palette_test.go +24 -26
| @@ -22,21 +22,21 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) { | |||
| 22 | 22 | defer release2() | |
| 23 | 23 | ||
| 24 | 24 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 25 | - | require.True(t, ac.paletteActive()) | |
| 25 | + | require.True(t, ac.overlays.paletteActive()) | |
| 26 | 26 | out := awaitFrame(t, sends, ports.MsgOutput) | |
| 27 | 27 | msg, err := ports.UnmarshalOutput(out.Payload) | |
| 28 | 28 | require.NoError(t, err) | |
| 29 | 29 | require.Contains(t, string(msg.Data), "Commands") | |
| 30 | 30 | ||
| 31 | 31 | d.handleInput(sess, ac, []byte("NXT\r")) | |
| 32 | - | require.False(t, ac.paletteActive()) | |
| 32 | + | require.False(t, ac.overlays.paletteActive()) | |
| 33 | 33 | require.Equal(t, 1, activeTabIndex(sess)) | |
| 34 | 34 | awaitFrame(t, sends, ports.MsgOutput) | |
| 35 | 35 | ||
| 36 | 36 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 37 | 37 | awaitFrame(t, sends, ports.MsgOutput) | |
| 38 | 38 | d.handleInput(sess, ac, []byte("\x1b")) | |
| 39 | - | require.False(t, ac.paletteActive()) | |
| 39 | + | require.False(t, ac.overlays.paletteActive()) | |
| 40 | 40 | awaitFrame(t, sends, ports.MsgOutput) | |
| 41 | 41 | } | |
| 42 | 42 | ||
| @@ -56,8 +56,8 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||
| 56 | 56 | promptFrame := awaitFrame(t, sends, ports.MsgOutput) | |
| 57 | 57 | promptOutput, err := ports.UnmarshalOutput(promptFrame.Payload) | |
| 58 | 58 | require.NoError(t, err) | |
| 59 | - | require.False(t, ac.paletteActive()) | |
| 60 | - | require.True(t, ac.promptActive()) | |
| 59 | + | require.False(t, ac.overlays.paletteActive()) | |
| 60 | + | require.True(t, ac.overlays.promptActive()) | |
| 61 | 61 | require.Contains(t, string(promptOutput.Data), "Create session") | |
| 62 | 62 | ||
| 63 | 63 | d.handleInput(sess, ac, []byte("scratch\r")) | |
| @@ -68,7 +68,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||
| 68 | 68 | finalRepaint := awaitFrame(t, sends, ports.MsgOutput) | |
| 69 | 69 | finalOutput, err := ports.UnmarshalOutput(finalRepaint.Payload) | |
| 70 | 70 | require.NoError(t, err) | |
| 71 | - | require.False(t, ac.promptActive()) | |
| 71 | + | require.False(t, ac.overlays.promptActive()) | |
| 72 | 72 | require.Equal(t, 2, sessionCount(d)) | |
| 73 | 73 | require.Nil(t, sess.client) | |
| 74 | 74 | newSess := ac.currentSession() | |
| @@ -89,12 +89,12 @@ func TestPaletteReopensWithSuccessfulCommandFirst(t *testing.T) { | |||
| 89 | 89 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 90 | 90 | awaitFrame(t, sends, ports.MsgOutput) | |
| 91 | 91 | d.handleInput(sess, ac, []byte("NXT\r")) | |
| 92 | - | require.False(t, ac.paletteActive()) | |
| 92 | + | require.False(t, ac.overlays.paletteActive()) | |
| 93 | 93 | awaitFrame(t, sends, ports.MsgOutput) | |
| 94 | 94 | ||
| 95 | 95 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 96 | 96 | awaitFrame(t, sends, ports.MsgOutput) | |
| 97 | - | cmd, ok := ac.palette.Selected() | |
| 97 | + | cmd, ok := ac.overlays.palette.Selected() | |
| 98 | 98 | require.True(t, ok) | |
| 99 | 99 | require.Equal(t, "NXT", cmd.Code) | |
| 100 | 100 | } | |
| @@ -152,7 +152,7 @@ func TestPaletteCommandNoopRepaintsAfterClose(t *testing.T) { | |||
| 152 | 152 | require.Contains(t, string(paletteOutput.Data), "Commands") | |
| 153 | 153 | ||
| 154 | 154 | d.handleInput(sess, ac, []byte("NXT\r")) | |
| 155 | - | require.False(t, ac.paletteActive()) | |
| 155 | + | require.False(t, ac.overlays.paletteActive()) | |
| 156 | 156 | repaint := awaitFrame(t, sends, ports.MsgOutput) | |
| 157 | 157 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | |
| 158 | 158 | require.NoError(t, err) | |
| @@ -169,7 +169,7 @@ func TestPaletteCreateTabErrorRepaintsAfterClose(t *testing.T) { | |||
| 169 | 169 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 170 | 170 | awaitFrame(t, sends, ports.MsgOutput) | |
| 171 | 171 | d.handleInput(sess, ac, []byte("CNT\r")) | |
| 172 | - | require.False(t, ac.paletteActive()) | |
| 172 | + | require.False(t, ac.overlays.paletteActive()) | |
| 173 | 173 | repaint := awaitFrame(t, sends, ports.MsgOutput) | |
| 174 | 174 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | |
| 175 | 175 | require.NoError(t, err) | |
| @@ -184,15 +184,15 @@ func TestPaletteEnterNoMatchKeepsOpenAndEscapeSplit(t *testing.T) { | |||
| 184 | 184 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 185 | 185 | awaitFrame(t, sends, ports.MsgOutput) | |
| 186 | 186 | d.handleInput(sess, ac, []byte("zzzz\r")) | |
| 187 | - | require.True(t, ac.paletteActive()) | |
| 187 | + | require.True(t, ac.overlays.paletteActive()) | |
| 188 | 188 | awaitFrame(t, sends, ports.MsgOutput) | |
| 189 | 189 | ||
| 190 | 190 | d.handlePaletteInput(ac, []byte{0x1b, '['}) | |
| 191 | - | require.True(t, ac.paletteActive()) | |
| 192 | - | require.Equal(t, []byte{0x1b, '['}, ac.palettePending) | |
| 191 | + | require.True(t, ac.overlays.paletteActive()) | |
| 192 | + | require.Equal(t, []byte{0x1b, '['}, ac.overlays.palettePending) | |
| 193 | 193 | d.handlePaletteInput(ac, []byte{'A'}) | |
| 194 | - | require.True(t, ac.paletteActive()) | |
| 195 | - | require.Empty(t, ac.palettePending) | |
| 194 | + | require.True(t, ac.overlays.paletteActive()) | |
| 195 | + | require.Empty(t, ac.overlays.palettePending) | |
| 196 | 196 | awaitFrame(t, sends, ports.MsgOutput) | |
| 197 | 197 | } | |
| 198 | 198 | ||
| @@ -205,13 +205,13 @@ func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) { | |||
| 205 | 205 | ||
| 206 | 206 | d.handlePaletteInput(ac, []byte{0x0e}) | |
| 207 | 207 | awaitFrame(t, sends, ports.MsgOutput) | |
| 208 | - | cmd, ok := ac.palette.Selected() | |
| 208 | + | cmd, ok := ac.overlays.palette.Selected() | |
| 209 | 209 | require.True(t, ok) | |
| 210 | 210 | require.Equal(t, "CNS", cmd.Code) | |
| 211 | 211 | ||
| 212 | 212 | d.handlePaletteInput(ac, []byte{0x10}) | |
| 213 | 213 | awaitFrame(t, sends, ports.MsgOutput) | |
| 214 | - | cmd, ok = ac.palette.Selected() | |
| 214 | + | cmd, ok = ac.overlays.palette.Selected() | |
| 215 | 215 | require.True(t, ok) | |
| 216 | 216 | require.Equal(t, "CNT", cmd.Code) | |
| 217 | 217 | } | |
| @@ -234,10 +234,10 @@ func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | |||
| 234 | 234 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 235 | 235 | awaitFrame(t, sends, ports.MsgOutput) | |
| 236 | 236 | d.handlePaletteInput(ac, []byte{0xc3}) | |
| 237 | - | require.Equal(t, []byte{0xc3}, ac.palettePending) | |
| 237 | + | require.Equal(t, []byte{0xc3}, ac.overlays.palettePending) | |
| 238 | 238 | d.handlePaletteInput(ac, []byte{0xa9}) | |
| 239 | - | require.Empty(t, ac.palettePending) | |
| 240 | - | require.Equal(t, "é", ac.palette.Query()) | |
| 239 | + | require.Empty(t, ac.overlays.palettePending) | |
| 240 | + | require.Equal(t, "é", ac.overlays.palette.Query()) | |
| 241 | 241 | } | |
| 242 | 242 | ||
| 243 | 243 | func TestPaletteRenderAndInputCanRunConcurrently(t *testing.T) { | |
| @@ -296,17 +296,15 @@ func TestPaletteExecMethods(t *testing.T) { | |||
| 296 | 296 | require.Equal(t, 0, activeTabIndex(sess)) | |
| 297 | 297 | sess.ephemeral = true | |
| 298 | 298 | require.NoError(t, exec.RenameSession()) | |
| 299 | - | require.True(t, ac.promptActive()) | |
| 299 | + | require.True(t, ac.overlays.promptActive()) | |
| 300 | 300 | d.closePrompt(ac) | |
| 301 | 301 | require.True(t, sess.ephemeral) | |
| 302 | 302 | require.NoError(t, exec.OpenSessionPicker()) | |
| 303 | - | require.True(t, ac.pickerActive()) | |
| 303 | + | require.True(t, ac.overlays.pickerActive()) | |
| 304 | 304 | d.closePicker(ac) | |
| 305 | 305 | require.NoError(t, exec.EnterVisualMode()) | |
| 306 | - | require.True(t, ac.copyModeActive()) | |
| 307 | - | ac.copyMu.Lock() | |
| 308 | - | ac.copyMode = nil | |
| 309 | - | ac.copyMu.Unlock() | |
| 306 | + | require.True(t, ac.overlays.copyActive()) | |
| 307 | + | d.exitCopyMode(ac) | |
| 310 | 308 | require.NoError(t, exec.SplitRight()) | |
| 311 | 309 | require.NoError(t, exec.SplitLeft()) | |
| 312 | 310 | require.NoError(t, exec.SplitUp()) | |
| @@ -22,21 +22,21 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) { | |||||
| 22 | defer release2() | 22 | defer release2() | ||
| 23 | 23 | ||||
| 24 | d.handleInput(sess, ac, []byte("\x1b ")) | 24 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 25 | - | require.True(t, ac.paletteActive()) | 25 | + | require.True(t, ac.overlays.paletteActive()) |
| 26 | out := awaitFrame(t, sends, ports.MsgOutput) | 26 | out := awaitFrame(t, sends, ports.MsgOutput) | ||
| 27 | msg, err := ports.UnmarshalOutput(out.Payload) | 27 | msg, err := ports.UnmarshalOutput(out.Payload) | ||
| 28 | require.NoError(t, err) | 28 | require.NoError(t, err) | ||
| 29 | require.Contains(t, string(msg.Data), "Commands") | 29 | require.Contains(t, string(msg.Data), "Commands") | ||
| 30 | 30 | ||||
| 31 | d.handleInput(sess, ac, []byte("NXT\r")) | 31 | d.handleInput(sess, ac, []byte("NXT\r")) | ||
| 32 | - | require.False(t, ac.paletteActive()) | 32 | + | require.False(t, ac.overlays.paletteActive()) |
| 33 | require.Equal(t, 1, activeTabIndex(sess)) | 33 | require.Equal(t, 1, activeTabIndex(sess)) | ||
| 34 | awaitFrame(t, sends, ports.MsgOutput) | 34 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 35 | 35 | ||||
| 36 | d.handleInput(sess, ac, []byte("\x1b ")) | 36 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 37 | awaitFrame(t, sends, ports.MsgOutput) | 37 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 38 | d.handleInput(sess, ac, []byte("\x1b")) | 38 | d.handleInput(sess, ac, []byte("\x1b")) | ||
| 39 | - | require.False(t, ac.paletteActive()) | 39 | + | require.False(t, ac.overlays.paletteActive()) |
| 40 | awaitFrame(t, sends, ports.MsgOutput) | 40 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 41 | } | 41 | } | ||
| 42 | 42 | ||||
| @@ -56,8 +56,8 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||||
| 56 | promptFrame := awaitFrame(t, sends, ports.MsgOutput) | 56 | promptFrame := awaitFrame(t, sends, ports.MsgOutput) | ||
| 57 | promptOutput, err := ports.UnmarshalOutput(promptFrame.Payload) | 57 | promptOutput, err := ports.UnmarshalOutput(promptFrame.Payload) | ||
| 58 | require.NoError(t, err) | 58 | require.NoError(t, err) | ||
| 59 | - | require.False(t, ac.paletteActive()) | 59 | + | require.False(t, ac.overlays.paletteActive()) |
| 60 | - | require.True(t, ac.promptActive()) | 60 | + | require.True(t, ac.overlays.promptActive()) |
| 61 | require.Contains(t, string(promptOutput.Data), "Create session") | 61 | require.Contains(t, string(promptOutput.Data), "Create session") | ||
| 62 | 62 | ||||
| 63 | d.handleInput(sess, ac, []byte("scratch\r")) | 63 | d.handleInput(sess, ac, []byte("scratch\r")) | ||
| @@ -68,7 +68,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||||
| 68 | finalRepaint := awaitFrame(t, sends, ports.MsgOutput) | 68 | finalRepaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 69 | finalOutput, err := ports.UnmarshalOutput(finalRepaint.Payload) | 69 | finalOutput, err := ports.UnmarshalOutput(finalRepaint.Payload) | ||
| 70 | require.NoError(t, err) | 70 | require.NoError(t, err) | ||
| 71 | - | require.False(t, ac.promptActive()) | 71 | + | require.False(t, ac.overlays.promptActive()) |
| 72 | require.Equal(t, 2, sessionCount(d)) | 72 | require.Equal(t, 2, sessionCount(d)) | ||
| 73 | require.Nil(t, sess.client) | 73 | require.Nil(t, sess.client) | ||
| 74 | newSess := ac.currentSession() | 74 | newSess := ac.currentSession() | ||
| @@ -89,12 +89,12 @@ func TestPaletteReopensWithSuccessfulCommandFirst(t *testing.T) { | |||||
| 89 | d.handleInput(sess, ac, []byte("\x1b ")) | 89 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 90 | awaitFrame(t, sends, ports.MsgOutput) | 90 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 91 | d.handleInput(sess, ac, []byte("NXT\r")) | 91 | d.handleInput(sess, ac, []byte("NXT\r")) | ||
| 92 | - | require.False(t, ac.paletteActive()) | 92 | + | require.False(t, ac.overlays.paletteActive()) |
| 93 | awaitFrame(t, sends, ports.MsgOutput) | 93 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 94 | 94 | ||||
| 95 | d.handleInput(sess, ac, []byte("\x1b ")) | 95 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 96 | awaitFrame(t, sends, ports.MsgOutput) | 96 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 97 | - | cmd, ok := ac.palette.Selected() | 97 | + | cmd, ok := ac.overlays.palette.Selected() |
| 98 | require.True(t, ok) | 98 | require.True(t, ok) | ||
| 99 | require.Equal(t, "NXT", cmd.Code) | 99 | require.Equal(t, "NXT", cmd.Code) | ||
| 100 | } | 100 | } | ||
| @@ -152,7 +152,7 @@ func TestPaletteCommandNoopRepaintsAfterClose(t *testing.T) { | |||||
| 152 | require.Contains(t, string(paletteOutput.Data), "Commands") | 152 | require.Contains(t, string(paletteOutput.Data), "Commands") | ||
| 153 | 153 | ||||
| 154 | d.handleInput(sess, ac, []byte("NXT\r")) | 154 | d.handleInput(sess, ac, []byte("NXT\r")) | ||
| 155 | - | require.False(t, ac.paletteActive()) | 155 | + | require.False(t, ac.overlays.paletteActive()) |
| 156 | repaint := awaitFrame(t, sends, ports.MsgOutput) | 156 | repaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 157 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | 157 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | ||
| 158 | require.NoError(t, err) | 158 | require.NoError(t, err) | ||
| @@ -169,7 +169,7 @@ func TestPaletteCreateTabErrorRepaintsAfterClose(t *testing.T) { | |||||
| 169 | d.handleInput(sess, ac, []byte("\x1b ")) | 169 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 170 | awaitFrame(t, sends, ports.MsgOutput) | 170 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 171 | d.handleInput(sess, ac, []byte("CNT\r")) | 171 | d.handleInput(sess, ac, []byte("CNT\r")) | ||
| 172 | - | require.False(t, ac.paletteActive()) | 172 | + | require.False(t, ac.overlays.paletteActive()) |
| 173 | repaint := awaitFrame(t, sends, ports.MsgOutput) | 173 | repaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 174 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | 174 | repaintOutput, err := ports.UnmarshalOutput(repaint.Payload) | ||
| 175 | require.NoError(t, err) | 175 | require.NoError(t, err) | ||
| @@ -184,15 +184,15 @@ func TestPaletteEnterNoMatchKeepsOpenAndEscapeSplit(t *testing.T) { | |||||
| 184 | d.handleInput(sess, ac, []byte("\x1b ")) | 184 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 185 | awaitFrame(t, sends, ports.MsgOutput) | 185 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 186 | d.handleInput(sess, ac, []byte("zzzz\r")) | 186 | d.handleInput(sess, ac, []byte("zzzz\r")) | ||
| 187 | - | require.True(t, ac.paletteActive()) | 187 | + | require.True(t, ac.overlays.paletteActive()) |
| 188 | awaitFrame(t, sends, ports.MsgOutput) | 188 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 189 | 189 | ||||
| 190 | d.handlePaletteInput(ac, []byte{0x1b, '['}) | 190 | d.handlePaletteInput(ac, []byte{0x1b, '['}) | ||
| 191 | - | require.True(t, ac.paletteActive()) | 191 | + | require.True(t, ac.overlays.paletteActive()) |
| 192 | - | require.Equal(t, []byte{0x1b, '['}, ac.palettePending) | 192 | + | require.Equal(t, []byte{0x1b, '['}, ac.overlays.palettePending) |
| 193 | d.handlePaletteInput(ac, []byte{'A'}) | 193 | d.handlePaletteInput(ac, []byte{'A'}) | ||
| 194 | - | require.True(t, ac.paletteActive()) | 194 | + | require.True(t, ac.overlays.paletteActive()) |
| 195 | - | require.Empty(t, ac.palettePending) | 195 | + | require.Empty(t, ac.overlays.palettePending) |
| 196 | awaitFrame(t, sends, ports.MsgOutput) | 196 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 197 | } | 197 | } | ||
| 198 | 198 | ||||
| @@ -205,13 +205,13 @@ func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) { | |||||
| 205 | 205 | ||||
| 206 | d.handlePaletteInput(ac, []byte{0x0e}) | 206 | d.handlePaletteInput(ac, []byte{0x0e}) | ||
| 207 | awaitFrame(t, sends, ports.MsgOutput) | 207 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 208 | - | cmd, ok := ac.palette.Selected() | 208 | + | cmd, ok := ac.overlays.palette.Selected() |
| 209 | require.True(t, ok) | 209 | require.True(t, ok) | ||
| 210 | require.Equal(t, "CNS", cmd.Code) | 210 | require.Equal(t, "CNS", cmd.Code) | ||
| 211 | 211 | ||||
| 212 | d.handlePaletteInput(ac, []byte{0x10}) | 212 | d.handlePaletteInput(ac, []byte{0x10}) | ||
| 213 | awaitFrame(t, sends, ports.MsgOutput) | 213 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 214 | - | cmd, ok = ac.palette.Selected() | 214 | + | cmd, ok = ac.overlays.palette.Selected() |
| 215 | require.True(t, ok) | 215 | require.True(t, ok) | ||
| 216 | require.Equal(t, "CNT", cmd.Code) | 216 | require.Equal(t, "CNT", cmd.Code) | ||
| 217 | } | 217 | } | ||
| @@ -234,10 +234,10 @@ func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | |||||
| 234 | d.handleInput(sess, ac, []byte("\x1b ")) | 234 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 235 | awaitFrame(t, sends, ports.MsgOutput) | 235 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 236 | d.handlePaletteInput(ac, []byte{0xc3}) | 236 | d.handlePaletteInput(ac, []byte{0xc3}) | ||
| 237 | - | require.Equal(t, []byte{0xc3}, ac.palettePending) | 237 | + | require.Equal(t, []byte{0xc3}, ac.overlays.palettePending) |
| 238 | d.handlePaletteInput(ac, []byte{0xa9}) | 238 | d.handlePaletteInput(ac, []byte{0xa9}) | ||
| 239 | - | require.Empty(t, ac.palettePending) | 239 | + | require.Empty(t, ac.overlays.palettePending) |
| 240 | - | require.Equal(t, "é", ac.palette.Query()) | 240 | + | require.Equal(t, "é", ac.overlays.palette.Query()) |
| 241 | } | 241 | } | ||
| 242 | 242 | ||||
| 243 | func TestPaletteRenderAndInputCanRunConcurrently(t *testing.T) { | 243 | func TestPaletteRenderAndInputCanRunConcurrently(t *testing.T) { | ||
| @@ -296,17 +296,15 @@ func TestPaletteExecMethods(t *testing.T) { | |||||
| 296 | require.Equal(t, 0, activeTabIndex(sess)) | 296 | require.Equal(t, 0, activeTabIndex(sess)) | ||
| 297 | sess.ephemeral = true | 297 | sess.ephemeral = true | ||
| 298 | require.NoError(t, exec.RenameSession()) | 298 | require.NoError(t, exec.RenameSession()) | ||
| 299 | - | require.True(t, ac.promptActive()) | 299 | + | require.True(t, ac.overlays.promptActive()) |
| 300 | d.closePrompt(ac) | 300 | d.closePrompt(ac) | ||
| 301 | require.True(t, sess.ephemeral) | 301 | require.True(t, sess.ephemeral) | ||
| 302 | require.NoError(t, exec.OpenSessionPicker()) | 302 | require.NoError(t, exec.OpenSessionPicker()) | ||
| 303 | - | require.True(t, ac.pickerActive()) | 303 | + | require.True(t, ac.overlays.pickerActive()) |
| 304 | d.closePicker(ac) | 304 | d.closePicker(ac) | ||
| 305 | require.NoError(t, exec.EnterVisualMode()) | 305 | require.NoError(t, exec.EnterVisualMode()) | ||
| 306 | - | require.True(t, ac.copyModeActive()) | 306 | + | require.True(t, ac.overlays.copyActive()) |
| 307 | - | ac.copyMu.Lock() | 307 | + | d.exitCopyMode(ac) |
| 308 | - | ac.copyMode = nil | |||
| 309 | - | ac.copyMu.Unlock() | |||
| 310 | require.NoError(t, exec.SplitRight()) | 308 | require.NoError(t, exec.SplitRight()) | ||
| 311 | require.NoError(t, exec.SplitLeft()) | 309 | require.NoError(t, exec.SplitLeft()) | ||
| 312 | require.NoError(t, exec.SplitUp()) | 310 | require.NoError(t, exec.SplitUp()) | ||
M internal/usecase/daemon/pane_actions_test.go +5 -2
| @@ -150,12 +150,14 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) { | |||
| 150 | 150 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 151 | 151 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 152 | 152 | tr, _ := newCapturingTransport(t) | |
| 153 | - | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 41, Rows: 12}, copyMode: &scopy.Mode{}} | |
| 153 | + | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 41, Rows: 12}} | |
| 154 | + | ac.initOverlays() | |
| 155 | + | ac.overlays.copyMode = &scopy.Mode{} | |
| 154 | 156 | ||
| 155 | 157 | require.NoError(t, d.focusDir(sess, ac, layout.Right)) | |
| 156 | 158 | ||
| 157 | 159 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus) | |
| 158 | - | require.Nil(t, ac.copyMode) | |
| 160 | + | require.Nil(t, ac.overlays.copyMode) | |
| 159 | 161 | oldPTY.AssertNotCalled(t, "Resize", mock.Anything) | |
| 160 | 162 | newPTY.AssertNotCalled(t, "Resize", mock.Anything) | |
| 161 | 163 | } | |
| @@ -301,6 +303,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||
| 301 | 303 | require.Equal(t, layout.PaneID("pane-2"), tb.focusedPane().id) | |
| 302 | 304 | ||
| 303 | 305 | ac := &attachedClient{} | |
| 306 | + | ac.initOverlays() | |
| 304 | 307 | ac.setSession(sess) | |
| 305 | 308 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("Z")) | |
| 306 | 309 | require.Equal(t, []byte("Z"), <-writes) | |
| @@ -150,12 +150,14 @@ func TestFocusDirMovesFocusAndExitsCopyMode(t *testing.T) { | |||||
| 150 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 150 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 151 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 151 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 152 | tr, _ := newCapturingTransport(t) | 152 | tr, _ := newCapturingTransport(t) | ||
| 153 | - | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 41, Rows: 12}, copyMode: &scopy.Mode{}} | 153 | + | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 41, Rows: 12}} |
| 154 | + | ac.initOverlays() | |||
| 155 | + | ac.overlays.copyMode = &scopy.Mode{} | |||
| 154 | 156 | ||||
| 155 | require.NoError(t, d.focusDir(sess, ac, layout.Right)) | 157 | require.NoError(t, d.focusDir(sess, ac, layout.Right)) | ||
| 156 | 158 | ||||
| 157 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus) | 159 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Focus) | ||
| 158 | - | require.Nil(t, ac.copyMode) | 160 | + | require.Nil(t, ac.overlays.copyMode) |
| 159 | oldPTY.AssertNotCalled(t, "Resize", mock.Anything) | 161 | oldPTY.AssertNotCalled(t, "Resize", mock.Anything) | ||
| 160 | newPTY.AssertNotCalled(t, "Resize", mock.Anything) | 162 | newPTY.AssertNotCalled(t, "Resize", mock.Anything) | ||
| 161 | } | 163 | } | ||
| @@ -301,6 +303,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||||
| 301 | require.Equal(t, layout.PaneID("pane-2"), tb.focusedPane().id) | 303 | require.Equal(t, layout.PaneID("pane-2"), tb.focusedPane().id) | ||
| 302 | 304 | ||||
| 303 | ac := &attachedClient{} | 305 | ac := &attachedClient{} | ||
| 306 | + | ac.initOverlays() | |||
| 304 | ac.setSession(sess) | 307 | ac.setSession(sess) | ||
| 305 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("Z")) | 308 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("Z")) | ||
| 306 | require.Equal(t, []byte("Z"), <-writes) | 309 | require.Equal(t, []byte("Z"), <-writes) | ||
M internal/usecase/daemon/picker.go +60 -60
| @@ -43,11 +43,11 @@ var pickerModal = ui.Modal{WidthPct: 80, HeightPct: 80, MinWidth: 24, MinHeight: | |||
| 43 | 43 | ||
| 44 | 44 | func (d *Daemon) enterPicker(sess *session, ac *attachedClient) { | |
| 45 | 45 | views, curTab := d.pickerViews(sess) | |
| 46 | - | ac.pickerMu.Lock() | |
| 47 | - | ac.picker = picker.New(views, sess.id, curTab) | |
| 48 | - | ac.pickerPending = nil | |
| 49 | - | ac.pickerPreview = nil | |
| 50 | - | ac.pickerMu.Unlock() | |
| 46 | + | ac.overlays.pickerMu.Lock() | |
| 47 | + | ac.overlays.picker = picker.New(views, sess.id, curTab) | |
| 48 | + | ac.overlays.pickerPending = nil | |
| 49 | + | ac.overlays.pickerPreview = nil | |
| 50 | + | ac.overlays.pickerMu.Unlock() | |
| 51 | 51 | d.registerPreviewForSelection(ac) | |
| 52 | 52 | d.paint(sess, ac, true) | |
| 53 | 53 | } | |
| @@ -108,20 +108,20 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 108 | 108 | if sess == nil { | |
| 109 | 109 | return | |
| 110 | 110 | } | |
| 111 | - | ac.pickerMu.Lock() | |
| 112 | - | if ac.picker == nil { | |
| 113 | - | ac.pickerPending = nil | |
| 111 | + | ac.overlays.pickerMu.Lock() | |
| 112 | + | if ac.overlays.picker == nil { | |
| 113 | + | ac.overlays.pickerPending = nil | |
| 114 | 114 | d.stopPickerPendingTimerLocked(ac) | |
| 115 | - | ac.pickerMu.Unlock() | |
| 115 | + | ac.overlays.pickerMu.Unlock() | |
| 116 | 116 | return | |
| 117 | 117 | } | |
| 118 | - | if len(ac.pickerPending) > 0 { | |
| 118 | + | if len(ac.overlays.pickerPending) > 0 { | |
| 119 | 119 | d.stopPickerPendingTimerLocked(ac) | |
| 120 | - | combined := make([]byte, 0, len(ac.pickerPending)+len(data)) | |
| 121 | - | combined = append(combined, ac.pickerPending...) | |
| 120 | + | combined := make([]byte, 0, len(ac.overlays.pickerPending)+len(data)) | |
| 121 | + | combined = append(combined, ac.overlays.pickerPending...) | |
| 122 | 122 | combined = append(combined, data...) | |
| 123 | 123 | data = combined | |
| 124 | - | ac.pickerPending = nil | |
| 124 | + | ac.overlays.pickerPending = nil | |
| 125 | 125 | } | |
| 126 | 126 | changed := false | |
| 127 | 127 | exit := false | |
| @@ -129,8 +129,8 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 129 | 129 | for i := 0; i < len(data); i++ { | |
| 130 | 130 | switch data[i] { | |
| 131 | 131 | case 'x': | |
| 132 | - | target, ok := ac.picker.Selected() | |
| 133 | - | ac.pickerMu.Unlock() | |
| 132 | + | target, ok := ac.overlays.picker.Selected() | |
| 133 | + | ac.overlays.pickerMu.Unlock() | |
| 134 | 134 | if ok { | |
| 135 | 135 | d.killPickerTarget(target) | |
| 136 | 136 | } | |
| @@ -138,10 +138,10 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 138 | 138 | d.paint(sess, ac, true) | |
| 139 | 139 | return | |
| 140 | 140 | case 'j': | |
| 141 | - | ac.picker.Down() | |
| 141 | + | ac.overlays.picker.Down() | |
| 142 | 142 | changed = true | |
| 143 | 143 | case 'k': | |
| 144 | - | ac.picker.Up() | |
| 144 | + | ac.overlays.picker.Up() | |
| 145 | 145 | changed = true | |
| 146 | 146 | case '\r', '\n': | |
| 147 | 147 | switchTarget = true | |
| @@ -149,7 +149,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 149 | 149 | case 'q', 0x03, 0x1b: | |
| 150 | 150 | if data[i] == 0x1b { | |
| 151 | 151 | tail := data[i:] | |
| 152 | - | consumed, ok := routePickerEscape(ac.picker, tail) | |
| 152 | + | consumed, ok := routePickerEscape(ac.overlays.picker, tail) | |
| 153 | 153 | if ok { | |
| 154 | 154 | i += consumed - 1 | |
| 155 | 155 | changed = true | |
| @@ -160,7 +160,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 160 | 160 | break | |
| 161 | 161 | } | |
| 162 | 162 | if isPickerEscapePrefix(tail) { | |
| 163 | - | ac.pickerPending = append(ac.pickerPending[:0], tail...) | |
| 163 | + | ac.overlays.pickerPending = append(ac.overlays.pickerPending[:0], tail...) | |
| 164 | 164 | break | |
| 165 | 165 | } | |
| 166 | 166 | } | |
| @@ -170,9 +170,9 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 170 | 170 | var target picker.Target | |
| 171 | 171 | var ok bool | |
| 172 | 172 | if switchTarget { | |
| 173 | - | target, ok = ac.picker.Selected() | |
| 173 | + | target, ok = ac.overlays.picker.Selected() | |
| 174 | 174 | } | |
| 175 | - | ac.pickerMu.Unlock() | |
| 175 | + | ac.overlays.pickerMu.Unlock() | |
| 176 | 176 | ||
| 177 | 177 | if changed { | |
| 178 | 178 | d.registerPreviewForSelection(ac) | |
| @@ -190,18 +190,18 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 190 | 190 | } | |
| 191 | 191 | ||
| 192 | 192 | func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | |
| 193 | - | ac.pickerPending = append(ac.pickerPending[:0], keys.ESC) | |
| 194 | - | ac.pickerESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | |
| 195 | - | ac.pickerMu.Lock() | |
| 196 | - | if ac.pickerESC.timer != timer || len(ac.pickerPending) != 1 || ac.pickerPending[0] != keys.ESC || ac.picker == nil { | |
| 197 | - | ac.pickerMu.Unlock() | |
| 193 | + | ac.overlays.pickerPending = append(ac.overlays.pickerPending[:0], keys.ESC) | |
| 194 | + | ac.overlays.pickerESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | |
| 195 | + | ac.overlays.pickerMu.Lock() | |
| 196 | + | if ac.overlays.pickerESC.timer != timer || len(ac.overlays.pickerPending) != 1 || ac.overlays.pickerPending[0] != keys.ESC || ac.overlays.picker == nil { | |
| 197 | + | ac.overlays.pickerMu.Unlock() | |
| 198 | 198 | return | |
| 199 | 199 | } | |
| 200 | - | ac.pickerPending = nil | |
| 201 | - | ac.pickerESC.timer = nil | |
| 202 | - | ac.pickerESC.done = nil | |
| 203 | - | ac.picker = nil | |
| 204 | - | ac.pickerMu.Unlock() | |
| 200 | + | ac.overlays.pickerPending = nil | |
| 201 | + | ac.overlays.pickerESC.timer = nil | |
| 202 | + | ac.overlays.pickerESC.done = nil | |
| 203 | + | ac.overlays.picker = nil | |
| 204 | + | ac.overlays.pickerMu.Unlock() | |
| 205 | 205 | ||
| 206 | 206 | d.unregisterPreview(ac) | |
| 207 | 207 | if sess := ac.currentSession(); sess != nil { | |
| @@ -211,7 +211,7 @@ func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | |||
| 211 | 211 | } | |
| 212 | 212 | ||
| 213 | 213 | func (d *Daemon) stopPickerPendingTimerLocked(ac *attachedClient) { | |
| 214 | - | ac.pickerESC.stop() | |
| 214 | + | ac.overlays.pickerESC.stop() | |
| 215 | 215 | } | |
| 216 | 216 | ||
| 217 | 217 | func routePickerEscape(m *picker.Model, data []byte) (int, bool) { | |
| @@ -233,26 +233,26 @@ func isPickerEscapePrefix(data []byte) bool { | |||
| 233 | 233 | } | |
| 234 | 234 | ||
| 235 | 235 | func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |
| 236 | - | ac.pickerMu.Lock() | |
| 236 | + | ac.overlays.pickerMu.Lock() | |
| 237 | 237 | var target picker.Target | |
| 238 | 238 | var ok bool | |
| 239 | - | if ac.picker != nil { | |
| 240 | - | target, ok = ac.picker.Selected() | |
| 239 | + | if ac.overlays.picker != nil { | |
| 240 | + | target, ok = ac.overlays.picker.Selected() | |
| 241 | 241 | } | |
| 242 | - | ac.pickerMu.Unlock() | |
| 242 | + | ac.overlays.pickerMu.Unlock() | |
| 243 | 243 | ||
| 244 | 244 | var next *tab | |
| 245 | 245 | if ok { | |
| 246 | 246 | next = d.tabByTarget(target) | |
| 247 | 247 | } | |
| 248 | 248 | ||
| 249 | - | ac.pickerMu.Lock() | |
| 250 | - | old := ac.pickerPreview | |
| 251 | - | if ac.picker == nil { | |
| 249 | + | ac.overlays.pickerMu.Lock() | |
| 250 | + | old := ac.overlays.pickerPreview | |
| 251 | + | if ac.overlays.picker == nil { | |
| 252 | 252 | next = nil | |
| 253 | 253 | } | |
| 254 | - | ac.pickerPreview = next | |
| 255 | - | ac.pickerMu.Unlock() | |
| 254 | + | ac.overlays.pickerPreview = next | |
| 255 | + | ac.overlays.pickerMu.Unlock() | |
| 256 | 256 | ||
| 257 | 257 | if old != nil && old != next { | |
| 258 | 258 | old.mu.Lock() | |
| @@ -266,9 +266,9 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||
| 266 | 266 | next.previewClient = ac | |
| 267 | 267 | next.mu.Unlock() | |
| 268 | 268 | ||
| 269 | - | ac.pickerMu.Lock() | |
| 270 | - | keep := ac.pickerPreview == next | |
| 271 | - | ac.pickerMu.Unlock() | |
| 269 | + | ac.overlays.pickerMu.Lock() | |
| 270 | + | keep := ac.overlays.pickerPreview == next | |
| 271 | + | ac.overlays.pickerMu.Unlock() | |
| 272 | 272 | if !keep { | |
| 273 | 273 | next.mu.Lock() | |
| 274 | 274 | if next.previewClient == ac { | |
| @@ -280,10 +280,10 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||
| 280 | 280 | } | |
| 281 | 281 | ||
| 282 | 282 | func (d *Daemon) unregisterPreview(ac *attachedClient) { | |
| 283 | - | ac.pickerMu.Lock() | |
| 284 | - | old := ac.pickerPreview | |
| 285 | - | ac.pickerPreview = nil | |
| 286 | - | ac.pickerMu.Unlock() | |
| 283 | + | ac.overlays.pickerMu.Lock() | |
| 284 | + | old := ac.overlays.pickerPreview | |
| 285 | + | ac.overlays.pickerPreview = nil | |
| 286 | + | ac.overlays.pickerMu.Unlock() | |
| 287 | 287 | ||
| 288 | 288 | if old != nil { | |
| 289 | 289 | old.mu.Lock() | |
| @@ -302,12 +302,12 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||
| 302 | 302 | return | |
| 303 | 303 | } | |
| 304 | 304 | cleared := false | |
| 305 | - | previewer.pickerMu.Lock() | |
| 306 | - | if previewer.pickerPreview == tb { | |
| 307 | - | previewer.pickerPreview = nil | |
| 305 | + | previewer.overlays.pickerMu.Lock() | |
| 306 | + | if previewer.overlays.pickerPreview == tb { | |
| 307 | + | previewer.overlays.pickerPreview = nil | |
| 308 | 308 | cleared = true | |
| 309 | 309 | } | |
| 310 | - | previewer.pickerMu.Unlock() | |
| 310 | + | previewer.overlays.pickerMu.Unlock() | |
| 311 | 311 | tb.mu.Lock() | |
| 312 | 312 | if tb.previewClient == previewer { | |
| 313 | 313 | tb.previewClient = nil | |
| @@ -321,11 +321,11 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||
| 321 | 321 | } | |
| 322 | 322 | ||
| 323 | 323 | func (d *Daemon) closePicker(ac *attachedClient) { | |
| 324 | - | ac.pickerMu.Lock() | |
| 325 | - | ac.picker = nil | |
| 326 | - | ac.pickerPending = nil | |
| 324 | + | ac.overlays.pickerMu.Lock() | |
| 325 | + | ac.overlays.picker = nil | |
| 326 | + | ac.overlays.pickerPending = nil | |
| 327 | 327 | d.stopPickerPendingTimerLocked(ac) | |
| 328 | - | ac.pickerMu.Unlock() | |
| 328 | + | ac.overlays.pickerMu.Unlock() | |
| 329 | 329 | d.unregisterPreview(ac) | |
| 330 | 330 | } | |
| 331 | 331 | ||
| @@ -471,11 +471,11 @@ func (d *Daemon) refreshPicker(ac *attachedClient) { | |||
| 471 | 471 | return | |
| 472 | 472 | } | |
| 473 | 473 | views, curTab := d.pickerViews(sess) | |
| 474 | - | ac.pickerMu.Lock() | |
| 475 | - | if ac.picker != nil { | |
| 476 | - | ac.picker = picker.New(views, sess.id, curTab) | |
| 474 | + | ac.overlays.pickerMu.Lock() | |
| 475 | + | if ac.overlays.picker != nil { | |
| 476 | + | ac.overlays.picker = picker.New(views, sess.id, curTab) | |
| 477 | 477 | } | |
| 478 | - | ac.pickerMu.Unlock() | |
| 478 | + | ac.overlays.pickerMu.Unlock() | |
| 479 | 479 | d.registerPreviewForSelection(ac) | |
| 480 | 480 | } | |
| 481 | 481 | ||
| @@ -43,11 +43,11 @@ var pickerModal = ui.Modal{WidthPct: 80, HeightPct: 80, MinWidth: 24, MinHeight: | |||||
| 43 | 43 | ||||
| 44 | func (d *Daemon) enterPicker(sess *session, ac *attachedClient) { | 44 | func (d *Daemon) enterPicker(sess *session, ac *attachedClient) { | ||
| 45 | views, curTab := d.pickerViews(sess) | 45 | views, curTab := d.pickerViews(sess) | ||
| 46 | - | ac.pickerMu.Lock() | 46 | + | ac.overlays.pickerMu.Lock() |
| 47 | - | ac.picker = picker.New(views, sess.id, curTab) | 47 | + | ac.overlays.picker = picker.New(views, sess.id, curTab) |
| 48 | - | ac.pickerPending = nil | 48 | + | ac.overlays.pickerPending = nil |
| 49 | - | ac.pickerPreview = nil | 49 | + | ac.overlays.pickerPreview = nil |
| 50 | - | ac.pickerMu.Unlock() | 50 | + | ac.overlays.pickerMu.Unlock() |
| 51 | d.registerPreviewForSelection(ac) | 51 | d.registerPreviewForSelection(ac) | ||
| 52 | d.paint(sess, ac, true) | 52 | d.paint(sess, ac, true) | ||
| 53 | } | 53 | } | ||
| @@ -108,20 +108,20 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 108 | if sess == nil { | 108 | if sess == nil { | ||
| 109 | return | 109 | return | ||
| 110 | } | 110 | } | ||
| 111 | - | ac.pickerMu.Lock() | 111 | + | ac.overlays.pickerMu.Lock() |
| 112 | - | if ac.picker == nil { | 112 | + | if ac.overlays.picker == nil { |
| 113 | - | ac.pickerPending = nil | 113 | + | ac.overlays.pickerPending = nil |
| 114 | d.stopPickerPendingTimerLocked(ac) | 114 | d.stopPickerPendingTimerLocked(ac) | ||
| 115 | - | ac.pickerMu.Unlock() | 115 | + | ac.overlays.pickerMu.Unlock() |
| 116 | return | 116 | return | ||
| 117 | } | 117 | } | ||
| 118 | - | if len(ac.pickerPending) > 0 { | 118 | + | if len(ac.overlays.pickerPending) > 0 { |
| 119 | d.stopPickerPendingTimerLocked(ac) | 119 | d.stopPickerPendingTimerLocked(ac) | ||
| 120 | - | combined := make([]byte, 0, len(ac.pickerPending)+len(data)) | 120 | + | combined := make([]byte, 0, len(ac.overlays.pickerPending)+len(data)) |
| 121 | - | combined = append(combined, ac.pickerPending...) | 121 | + | combined = append(combined, ac.overlays.pickerPending...) |
| 122 | combined = append(combined, data...) | 122 | combined = append(combined, data...) | ||
| 123 | data = combined | 123 | data = combined | ||
| 124 | - | ac.pickerPending = nil | 124 | + | ac.overlays.pickerPending = nil |
| 125 | } | 125 | } | ||
| 126 | changed := false | 126 | changed := false | ||
| 127 | exit := false | 127 | exit := false | ||
| @@ -129,8 +129,8 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 129 | for i := 0; i < len(data); i++ { | 129 | for i := 0; i < len(data); i++ { | ||
| 130 | switch data[i] { | 130 | switch data[i] { | ||
| 131 | case 'x': | 131 | case 'x': | ||
| 132 | - | target, ok := ac.picker.Selected() | 132 | + | target, ok := ac.overlays.picker.Selected() |
| 133 | - | ac.pickerMu.Unlock() | 133 | + | ac.overlays.pickerMu.Unlock() |
| 134 | if ok { | 134 | if ok { | ||
| 135 | d.killPickerTarget(target) | 135 | d.killPickerTarget(target) | ||
| 136 | } | 136 | } | ||
| @@ -138,10 +138,10 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 138 | d.paint(sess, ac, true) | 138 | d.paint(sess, ac, true) | ||
| 139 | return | 139 | return | ||
| 140 | case 'j': | 140 | case 'j': | ||
| 141 | - | ac.picker.Down() | 141 | + | ac.overlays.picker.Down() |
| 142 | changed = true | 142 | changed = true | ||
| 143 | case 'k': | 143 | case 'k': | ||
| 144 | - | ac.picker.Up() | 144 | + | ac.overlays.picker.Up() |
| 145 | changed = true | 145 | changed = true | ||
| 146 | case '\r', '\n': | 146 | case '\r', '\n': | ||
| 147 | switchTarget = true | 147 | switchTarget = true | ||
| @@ -149,7 +149,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 149 | case 'q', 0x03, 0x1b: | 149 | case 'q', 0x03, 0x1b: | ||
| 150 | if data[i] == 0x1b { | 150 | if data[i] == 0x1b { | ||
| 151 | tail := data[i:] | 151 | tail := data[i:] | ||
| 152 | - | consumed, ok := routePickerEscape(ac.picker, tail) | 152 | + | consumed, ok := routePickerEscape(ac.overlays.picker, tail) |
| 153 | if ok { | 153 | if ok { | ||
| 154 | i += consumed - 1 | 154 | i += consumed - 1 | ||
| 155 | changed = true | 155 | changed = true | ||
| @@ -160,7 +160,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 160 | break | 160 | break | ||
| 161 | } | 161 | } | ||
| 162 | if isPickerEscapePrefix(tail) { | 162 | if isPickerEscapePrefix(tail) { | ||
| 163 | - | ac.pickerPending = append(ac.pickerPending[:0], tail...) | 163 | + | ac.overlays.pickerPending = append(ac.overlays.pickerPending[:0], tail...) |
| 164 | break | 164 | break | ||
| 165 | } | 165 | } | ||
| 166 | } | 166 | } | ||
| @@ -170,9 +170,9 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 170 | var target picker.Target | 170 | var target picker.Target | ||
| 171 | var ok bool | 171 | var ok bool | ||
| 172 | if switchTarget { | 172 | if switchTarget { | ||
| 173 | - | target, ok = ac.picker.Selected() | 173 | + | target, ok = ac.overlays.picker.Selected() |
| 174 | } | 174 | } | ||
| 175 | - | ac.pickerMu.Unlock() | 175 | + | ac.overlays.pickerMu.Unlock() |
| 176 | 176 | ||||
| 177 | if changed { | 177 | if changed { | ||
| 178 | d.registerPreviewForSelection(ac) | 178 | d.registerPreviewForSelection(ac) | ||
| @@ -190,18 +190,18 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 190 | } | 190 | } | ||
| 191 | 191 | ||||
| 192 | func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | 192 | func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | ||
| 193 | - | ac.pickerPending = append(ac.pickerPending[:0], keys.ESC) | 193 | + | ac.overlays.pickerPending = append(ac.overlays.pickerPending[:0], keys.ESC) |
| 194 | - | ac.pickerESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { | 194 | + | ac.overlays.pickerESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) { |
| 195 | - | ac.pickerMu.Lock() | 195 | + | ac.overlays.pickerMu.Lock() |
| 196 | - | if ac.pickerESC.timer != timer || len(ac.pickerPending) != 1 || ac.pickerPending[0] != keys.ESC || ac.picker == nil { | 196 | + | if ac.overlays.pickerESC.timer != timer || len(ac.overlays.pickerPending) != 1 || ac.overlays.pickerPending[0] != keys.ESC || ac.overlays.picker == nil { |
| 197 | - | ac.pickerMu.Unlock() | 197 | + | ac.overlays.pickerMu.Unlock() |
| 198 | return | 198 | return | ||
| 199 | } | 199 | } | ||
| 200 | - | ac.pickerPending = nil | 200 | + | ac.overlays.pickerPending = nil |
| 201 | - | ac.pickerESC.timer = nil | 201 | + | ac.overlays.pickerESC.timer = nil |
| 202 | - | ac.pickerESC.done = nil | 202 | + | ac.overlays.pickerESC.done = nil |
| 203 | - | ac.picker = nil | 203 | + | ac.overlays.picker = nil |
| 204 | - | ac.pickerMu.Unlock() | 204 | + | ac.overlays.pickerMu.Unlock() |
| 205 | 205 | ||||
| 206 | d.unregisterPreview(ac) | 206 | d.unregisterPreview(ac) | ||
| 207 | if sess := ac.currentSession(); sess != nil { | 207 | if sess := ac.currentSession(); sess != nil { | ||
| @@ -211,7 +211,7 @@ func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | |||||
| 211 | } | 211 | } | ||
| 212 | 212 | ||||
| 213 | func (d *Daemon) stopPickerPendingTimerLocked(ac *attachedClient) { | 213 | func (d *Daemon) stopPickerPendingTimerLocked(ac *attachedClient) { | ||
| 214 | - | ac.pickerESC.stop() | 214 | + | ac.overlays.pickerESC.stop() |
| 215 | } | 215 | } | ||
| 216 | 216 | ||||
| 217 | func routePickerEscape(m *picker.Model, data []byte) (int, bool) { | 217 | func routePickerEscape(m *picker.Model, data []byte) (int, bool) { | ||
| @@ -233,26 +233,26 @@ func isPickerEscapePrefix(data []byte) bool { | |||||
| 233 | } | 233 | } | ||
| 234 | 234 | ||||
| 235 | func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | 235 | func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | ||
| 236 | - | ac.pickerMu.Lock() | 236 | + | ac.overlays.pickerMu.Lock() |
| 237 | var target picker.Target | 237 | var target picker.Target | ||
| 238 | var ok bool | 238 | var ok bool | ||
| 239 | - | if ac.picker != nil { | 239 | + | if ac.overlays.picker != nil { |
| 240 | - | target, ok = ac.picker.Selected() | 240 | + | target, ok = ac.overlays.picker.Selected() |
| 241 | } | 241 | } | ||
| 242 | - | ac.pickerMu.Unlock() | 242 | + | ac.overlays.pickerMu.Unlock() |
| 243 | 243 | ||||
| 244 | var next *tab | 244 | var next *tab | ||
| 245 | if ok { | 245 | if ok { | ||
| 246 | next = d.tabByTarget(target) | 246 | next = d.tabByTarget(target) | ||
| 247 | } | 247 | } | ||
| 248 | 248 | ||||
| 249 | - | ac.pickerMu.Lock() | 249 | + | ac.overlays.pickerMu.Lock() |
| 250 | - | old := ac.pickerPreview | 250 | + | old := ac.overlays.pickerPreview |
| 251 | - | if ac.picker == nil { | 251 | + | if ac.overlays.picker == nil { |
| 252 | next = nil | 252 | next = nil | ||
| 253 | } | 253 | } | ||
| 254 | - | ac.pickerPreview = next | 254 | + | ac.overlays.pickerPreview = next |
| 255 | - | ac.pickerMu.Unlock() | 255 | + | ac.overlays.pickerMu.Unlock() |
| 256 | 256 | ||||
| 257 | if old != nil && old != next { | 257 | if old != nil && old != next { | ||
| 258 | old.mu.Lock() | 258 | old.mu.Lock() | ||
| @@ -266,9 +266,9 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||||
| 266 | next.previewClient = ac | 266 | next.previewClient = ac | ||
| 267 | next.mu.Unlock() | 267 | next.mu.Unlock() | ||
| 268 | 268 | ||||
| 269 | - | ac.pickerMu.Lock() | 269 | + | ac.overlays.pickerMu.Lock() |
| 270 | - | keep := ac.pickerPreview == next | 270 | + | keep := ac.overlays.pickerPreview == next |
| 271 | - | ac.pickerMu.Unlock() | 271 | + | ac.overlays.pickerMu.Unlock() |
| 272 | if !keep { | 272 | if !keep { | ||
| 273 | next.mu.Lock() | 273 | next.mu.Lock() | ||
| 274 | if next.previewClient == ac { | 274 | if next.previewClient == ac { | ||
| @@ -280,10 +280,10 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||||
| 280 | } | 280 | } | ||
| 281 | 281 | ||||
| 282 | func (d *Daemon) unregisterPreview(ac *attachedClient) { | 282 | func (d *Daemon) unregisterPreview(ac *attachedClient) { | ||
| 283 | - | ac.pickerMu.Lock() | 283 | + | ac.overlays.pickerMu.Lock() |
| 284 | - | old := ac.pickerPreview | 284 | + | old := ac.overlays.pickerPreview |
| 285 | - | ac.pickerPreview = nil | 285 | + | ac.overlays.pickerPreview = nil |
| 286 | - | ac.pickerMu.Unlock() | 286 | + | ac.overlays.pickerMu.Unlock() |
| 287 | 287 | ||||
| 288 | if old != nil { | 288 | if old != nil { | ||
| 289 | old.mu.Lock() | 289 | old.mu.Lock() | ||
| @@ -302,12 +302,12 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||||
| 302 | return | 302 | return | ||
| 303 | } | 303 | } | ||
| 304 | cleared := false | 304 | cleared := false | ||
| 305 | - | previewer.pickerMu.Lock() | 305 | + | previewer.overlays.pickerMu.Lock() |
| 306 | - | if previewer.pickerPreview == tb { | 306 | + | if previewer.overlays.pickerPreview == tb { |
| 307 | - | previewer.pickerPreview = nil | 307 | + | previewer.overlays.pickerPreview = nil |
| 308 | cleared = true | 308 | cleared = true | ||
| 309 | } | 309 | } | ||
| 310 | - | previewer.pickerMu.Unlock() | 310 | + | previewer.overlays.pickerMu.Unlock() |
| 311 | tb.mu.Lock() | 311 | tb.mu.Lock() | ||
| 312 | if tb.previewClient == previewer { | 312 | if tb.previewClient == previewer { | ||
| 313 | tb.previewClient = nil | 313 | tb.previewClient = nil | ||
| @@ -321,11 +321,11 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| 323 | func (d *Daemon) closePicker(ac *attachedClient) { | 323 | func (d *Daemon) closePicker(ac *attachedClient) { | ||
| 324 | - | ac.pickerMu.Lock() | 324 | + | ac.overlays.pickerMu.Lock() |
| 325 | - | ac.picker = nil | 325 | + | ac.overlays.picker = nil |
| 326 | - | ac.pickerPending = nil | 326 | + | ac.overlays.pickerPending = nil |
| 327 | d.stopPickerPendingTimerLocked(ac) | 327 | d.stopPickerPendingTimerLocked(ac) | ||
| 328 | - | ac.pickerMu.Unlock() | 328 | + | ac.overlays.pickerMu.Unlock() |
| 329 | d.unregisterPreview(ac) | 329 | d.unregisterPreview(ac) | ||
| 330 | } | 330 | } | ||
| 331 | 331 | ||||
| @@ -471,11 +471,11 @@ func (d *Daemon) refreshPicker(ac *attachedClient) { | |||||
| 471 | return | 471 | return | ||
| 472 | } | 472 | } | ||
| 473 | views, curTab := d.pickerViews(sess) | 473 | views, curTab := d.pickerViews(sess) | ||
| 474 | - | ac.pickerMu.Lock() | 474 | + | ac.overlays.pickerMu.Lock() |
| 475 | - | if ac.picker != nil { | 475 | + | if ac.overlays.picker != nil { |
| 476 | - | ac.picker = picker.New(views, sess.id, curTab) | 476 | + | ac.overlays.picker = picker.New(views, sess.id, curTab) |
| 477 | } | 477 | } | ||
| 478 | - | ac.pickerMu.Unlock() | 478 | + | ac.overlays.pickerMu.Unlock() |
| 479 | d.registerPreviewForSelection(ac) | 479 | d.registerPreviewForSelection(ac) | ||
| 480 | } | 480 | } | ||
| 481 | 481 | ||||
M internal/usecase/daemon/picker_test.go +10 -6
| @@ -37,7 +37,7 @@ func TestAltTForwardsToPTY(t *testing.T) { | |||
| 37 | 37 | ||
| 38 | 38 | d.handleInput(sess, ac, []byte("\x1bt")) | |
| 39 | 39 | ||
| 40 | - | require.False(t, ac.pickerActive()) | |
| 40 | + | require.False(t, ac.overlays.pickerActive()) | |
| 41 | 41 | require.Equal(t, []byte("\x1bt"), <-writes) | |
| 42 | 42 | } | |
| 43 | 43 | ||
| @@ -86,9 +86,9 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) { | |||
| 86 | 86 | d.clock = clk | |
| 87 | 87 | d.handleInput(sess, ac, []byte("\x1b")) | |
| 88 | 88 | timer := <-clk.timers | |
| 89 | - | require.True(t, ac.pickerActive()) | |
| 89 | + | require.True(t, ac.overlays.pickerActive()) | |
| 90 | 90 | timer.ch <- time.Now() | |
| 91 | - | require.Eventually(t, func() bool { return !ac.pickerActive() }, time.Second, 5*time.Millisecond) | |
| 91 | + | require.Eventually(t, func() bool { return !ac.overlays.pickerActive() }, time.Second, 5*time.Millisecond) | |
| 92 | 92 | awaitFrame(t, sends, ports.MsgOutput) | |
| 93 | 93 | } | |
| 94 | 94 | ||
| @@ -117,7 +117,7 @@ func TestPickerSplitArrowNavigatesWithoutExiting(t *testing.T) { | |||
| 117 | 117 | for _, input := range tc.input { | |
| 118 | 118 | d.handleInput(sess, ac, input) | |
| 119 | 119 | } | |
| 120 | - | require.True(t, ac.pickerActive()) | |
| 120 | + | require.True(t, ac.overlays.pickerActive()) | |
| 121 | 121 | d.handleInput(sess, ac, []byte("\r")) | |
| 122 | 122 | require.Equal(t, tc.wantActive, activeTabIndex(sess)) | |
| 123 | 123 | }) | |
| @@ -134,9 +134,9 @@ func TestPickerLoneEscapeExitsAfterDelay(t *testing.T) { | |||
| 134 | 134 | d.clock = clk | |
| 135 | 135 | d.handleInput(sess, ac, []byte("\x1b")) | |
| 136 | 136 | timer := <-clk.timers | |
| 137 | - | require.True(t, ac.pickerActive()) | |
| 137 | + | require.True(t, ac.overlays.pickerActive()) | |
| 138 | 138 | timer.ch <- time.Now() | |
| 139 | - | require.Eventually(t, func() bool { return !ac.pickerActive() }, time.Second, 5*time.Millisecond) | |
| 139 | + | require.Eventually(t, func() bool { return !ac.overlays.pickerActive() }, time.Second, 5*time.Millisecond) | |
| 140 | 140 | } | |
| 141 | 141 | ||
| 142 | 142 | func TestPickerCrossSessionSwitchDetachesExistingClient(t *testing.T) { | |
| @@ -148,7 +148,9 @@ func TestPickerCrossSessionSwitchDetachesExistingClient(t *testing.T) { | |||
| 148 | 148 | tr1, sends1 := newCapturingTransport(t) | |
| 149 | 149 | tr2, sends2 := newCapturingTransport(t) | |
| 150 | 150 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 151 | + | ac1.initOverlays() | |
| 151 | 152 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 153 | + | ac2.initOverlays() | |
| 152 | 154 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | |
| 153 | 155 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | |
| 154 | 156 | defer cancel1() | |
| @@ -258,7 +260,9 @@ func TestPickerLivePreviewRepaintsCrossSessionTab(t *testing.T) { | |||
| 258 | 260 | tr1, sends1 := newCapturingTransport(t) | |
| 259 | 261 | tr2, _ := newCapturingTransport(t) | |
| 260 | 262 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 263 | + | ac1.initOverlays() | |
| 261 | 264 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | |
| 265 | + | ac2.initOverlays() | |
| 262 | 266 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | |
| 263 | 267 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | |
| 264 | 268 | defer cancel1() | |
| @@ -37,7 +37,7 @@ func TestAltTForwardsToPTY(t *testing.T) { | |||||
| 37 | 37 | ||||
| 38 | d.handleInput(sess, ac, []byte("\x1bt")) | 38 | d.handleInput(sess, ac, []byte("\x1bt")) | ||
| 39 | 39 | ||||
| 40 | - | require.False(t, ac.pickerActive()) | 40 | + | require.False(t, ac.overlays.pickerActive()) |
| 41 | require.Equal(t, []byte("\x1bt"), <-writes) | 41 | require.Equal(t, []byte("\x1bt"), <-writes) | ||
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| @@ -86,9 +86,9 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) { | |||||
| 86 | d.clock = clk | 86 | d.clock = clk | ||
| 87 | d.handleInput(sess, ac, []byte("\x1b")) | 87 | d.handleInput(sess, ac, []byte("\x1b")) | ||
| 88 | timer := <-clk.timers | 88 | timer := <-clk.timers | ||
| 89 | - | require.True(t, ac.pickerActive()) | 89 | + | require.True(t, ac.overlays.pickerActive()) |
| 90 | timer.ch <- time.Now() | 90 | timer.ch <- time.Now() | ||
| 91 | - | require.Eventually(t, func() bool { return !ac.pickerActive() }, time.Second, 5*time.Millisecond) | 91 | + | require.Eventually(t, func() bool { return !ac.overlays.pickerActive() }, time.Second, 5*time.Millisecond) |
| 92 | awaitFrame(t, sends, ports.MsgOutput) | 92 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 93 | } | 93 | } | ||
| 94 | 94 | ||||
| @@ -117,7 +117,7 @@ func TestPickerSplitArrowNavigatesWithoutExiting(t *testing.T) { | |||||
| 117 | for _, input := range tc.input { | 117 | for _, input := range tc.input { | ||
| 118 | d.handleInput(sess, ac, input) | 118 | d.handleInput(sess, ac, input) | ||
| 119 | } | 119 | } | ||
| 120 | - | require.True(t, ac.pickerActive()) | 120 | + | require.True(t, ac.overlays.pickerActive()) |
| 121 | d.handleInput(sess, ac, []byte("\r")) | 121 | d.handleInput(sess, ac, []byte("\r")) | ||
| 122 | require.Equal(t, tc.wantActive, activeTabIndex(sess)) | 122 | require.Equal(t, tc.wantActive, activeTabIndex(sess)) | ||
| 123 | }) | 123 | }) | ||
| @@ -134,9 +134,9 @@ func TestPickerLoneEscapeExitsAfterDelay(t *testing.T) { | |||||
| 134 | d.clock = clk | 134 | d.clock = clk | ||
| 135 | d.handleInput(sess, ac, []byte("\x1b")) | 135 | d.handleInput(sess, ac, []byte("\x1b")) | ||
| 136 | timer := <-clk.timers | 136 | timer := <-clk.timers | ||
| 137 | - | require.True(t, ac.pickerActive()) | 137 | + | require.True(t, ac.overlays.pickerActive()) |
| 138 | timer.ch <- time.Now() | 138 | timer.ch <- time.Now() | ||
| 139 | - | require.Eventually(t, func() bool { return !ac.pickerActive() }, time.Second, 5*time.Millisecond) | 139 | + | require.Eventually(t, func() bool { return !ac.overlays.pickerActive() }, time.Second, 5*time.Millisecond) |
| 140 | } | 140 | } | ||
| 141 | 141 | ||||
| 142 | func TestPickerCrossSessionSwitchDetachesExistingClient(t *testing.T) { | 142 | func TestPickerCrossSessionSwitchDetachesExistingClient(t *testing.T) { | ||
| @@ -148,7 +148,9 @@ func TestPickerCrossSessionSwitchDetachesExistingClient(t *testing.T) { | |||||
| 148 | tr1, sends1 := newCapturingTransport(t) | 148 | tr1, sends1 := newCapturingTransport(t) | ||
| 149 | tr2, sends2 := newCapturingTransport(t) | 149 | tr2, sends2 := newCapturingTransport(t) | ||
| 150 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 150 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 151 | + | ac1.initOverlays() | |||
| 151 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 152 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 153 | + | ac2.initOverlays() | |||
| 152 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | 154 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | ||
| 153 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | 155 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | ||
| 154 | defer cancel1() | 156 | defer cancel1() | ||
| @@ -258,7 +260,9 @@ func TestPickerLivePreviewRepaintsCrossSessionTab(t *testing.T) { | |||||
| 258 | tr1, sends1 := newCapturingTransport(t) | 260 | tr1, sends1 := newCapturingTransport(t) | ||
| 259 | tr2, _ := newCapturingTransport(t) | 261 | tr2, _ := newCapturingTransport(t) | ||
| 260 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 262 | ac1 := &attachedClient{tr: tr1, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 263 | + | ac1.initOverlays() | |||
| 261 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | 264 | ac2 := &attachedClient{tr: tr2, rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 80, Rows: 24}} | ||
| 265 | + | ac2.initOverlays() | |||
| 262 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | 266 | sctx1, cancel1 := context.WithCancel(d.serveCtx) | ||
| 263 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | 267 | sctx2, cancel2 := context.WithCancel(d.serveCtx) | ||
| 264 | defer cancel1() | 268 | defer cancel1() | ||
M internal/usecase/daemon/prompt.go +26 -24
| @@ -15,20 +15,20 @@ func promptModalFor(title string) ui.Modal { | |||
| 15 | 15 | ||
| 16 | 16 | func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string) error) { | |
| 17 | 17 | d.closePrompt(ac) | |
| 18 | - | ac.promptMu.Lock() | |
| 19 | - | ac.prompt = promptui.New(title, initial) | |
| 20 | - | ac.promptSubmit = submit | |
| 21 | - | ac.promptPending = nil | |
| 22 | - | ac.promptMu.Unlock() | |
| 18 | + | ac.overlays.promptMu.Lock() | |
| 19 | + | ac.overlays.prompt = promptui.New(title, initial) | |
| 20 | + | ac.overlays.promptSubmit = submit | |
| 21 | + | ac.overlays.promptPending = nil | |
| 22 | + | ac.overlays.promptMu.Unlock() | |
| 23 | 23 | d.paint(sess, ac, true) | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | |
| 27 | - | ac.promptMu.Lock() | |
| 28 | - | ac.prompt = nil | |
| 29 | - | ac.promptSubmit = nil | |
| 30 | - | ac.promptPending = nil | |
| 31 | - | ac.promptMu.Unlock() | |
| 27 | + | ac.overlays.promptMu.Lock() | |
| 28 | + | ac.overlays.prompt = nil | |
| 29 | + | ac.overlays.promptSubmit = nil | |
| 30 | + | ac.overlays.promptPending = nil | |
| 31 | + | ac.overlays.promptMu.Unlock() | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | 34 | func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | |
| @@ -37,44 +37,46 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | |||
| 37 | 37 | return | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | - | ac.promptMu.Lock() | |
| 41 | - | if ac.prompt == nil { | |
| 42 | - | ac.promptPending = nil | |
| 43 | - | ac.promptMu.Unlock() | |
| 40 | + | ac.overlays.promptMu.Lock() | |
| 41 | + | if ac.overlays.prompt == nil { | |
| 42 | + | ac.overlays.promptPending = nil | |
| 43 | + | ac.overlays.promptMu.Unlock() | |
| 44 | 44 | return | |
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | 47 | changed := false | |
| 48 | 48 | exit := false | |
| 49 | 49 | var submit func(string) error | |
| 50 | + | var submittedPrompt *promptui.Model | |
| 50 | 51 | var value string | |
| 51 | 52 | ||
| 52 | - | routeOverlayBytes(data, &ac.promptPending, overlayEvents{ | |
| 53 | + | routeOverlayBytes(data, &ac.overlays.promptPending, overlayEvents{ | |
| 53 | 54 | rune: func(r rune) { | |
| 54 | - | ac.prompt.Insert(r) | |
| 55 | + | ac.overlays.prompt.Insert(r) | |
| 55 | 56 | changed = true | |
| 56 | 57 | }, | |
| 57 | 58 | backspace: func() { | |
| 58 | - | ac.prompt.Backspace() | |
| 59 | + | ac.overlays.prompt.Backspace() | |
| 59 | 60 | changed = true | |
| 60 | 61 | }, | |
| 61 | 62 | enter: func() { | |
| 62 | - | submit = ac.promptSubmit | |
| 63 | - | value = strings.TrimSpace(ac.prompt.Value()) | |
| 63 | + | submit = ac.overlays.promptSubmit | |
| 64 | + | submittedPrompt = ac.overlays.prompt | |
| 65 | + | value = strings.TrimSpace(ac.overlays.prompt.Value()) | |
| 64 | 66 | }, | |
| 65 | 67 | cancel: func() { exit = true }, | |
| 66 | 68 | up: func() {}, | |
| 67 | 69 | down: func() {}, | |
| 68 | 70 | }) | |
| 69 | - | ac.promptMu.Unlock() | |
| 71 | + | ac.overlays.promptMu.Unlock() | |
| 70 | 72 | ||
| 71 | 73 | if submit != nil { | |
| 72 | 74 | if err := submit(value); err != nil { | |
| 73 | - | ac.promptMu.Lock() | |
| 74 | - | if ac.prompt != nil { | |
| 75 | - | ac.prompt.SetError(err.Error()) | |
| 75 | + | ac.overlays.promptMu.Lock() | |
| 76 | + | if ac.overlays.prompt == submittedPrompt { | |
| 77 | + | ac.overlays.prompt.SetError(err.Error()) | |
| 76 | 78 | } | |
| 77 | - | ac.promptMu.Unlock() | |
| 79 | + | ac.overlays.promptMu.Unlock() | |
| 78 | 80 | d.paint(sess, ac, true) | |
| 79 | 81 | return | |
| 80 | 82 | } | |
| @@ -15,20 +15,20 @@ func promptModalFor(title string) ui.Modal { | |||||
| 15 | 15 | ||||
| 16 | func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string) error) { | 16 | func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string) error) { | ||
| 17 | d.closePrompt(ac) | 17 | d.closePrompt(ac) | ||
| 18 | - | ac.promptMu.Lock() | 18 | + | ac.overlays.promptMu.Lock() |
| 19 | - | ac.prompt = promptui.New(title, initial) | 19 | + | ac.overlays.prompt = promptui.New(title, initial) |
| 20 | - | ac.promptSubmit = submit | 20 | + | ac.overlays.promptSubmit = submit |
| 21 | - | ac.promptPending = nil | 21 | + | ac.overlays.promptPending = nil |
| 22 | - | ac.promptMu.Unlock() | 22 | + | ac.overlays.promptMu.Unlock() |
| 23 | d.paint(sess, ac, true) | 23 | d.paint(sess, ac, true) | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | ||
| 27 | - | ac.promptMu.Lock() | 27 | + | ac.overlays.promptMu.Lock() |
| 28 | - | ac.prompt = nil | 28 | + | ac.overlays.prompt = nil |
| 29 | - | ac.promptSubmit = nil | 29 | + | ac.overlays.promptSubmit = nil |
| 30 | - | ac.promptPending = nil | 30 | + | ac.overlays.promptPending = nil |
| 31 | - | ac.promptMu.Unlock() | 31 | + | ac.overlays.promptMu.Unlock() |
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | 34 | func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | ||
| @@ -37,44 +37,46 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | |||||
| 37 | return | 37 | return | ||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | - | ac.promptMu.Lock() | 40 | + | ac.overlays.promptMu.Lock() |
| 41 | - | if ac.prompt == nil { | 41 | + | if ac.overlays.prompt == nil { |
| 42 | - | ac.promptPending = nil | 42 | + | ac.overlays.promptPending = nil |
| 43 | - | ac.promptMu.Unlock() | 43 | + | ac.overlays.promptMu.Unlock() |
| 44 | return | 44 | return | ||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | changed := false | 47 | changed := false | ||
| 48 | exit := false | 48 | exit := false | ||
| 49 | var submit func(string) error | 49 | var submit func(string) error | ||
| 50 | + | var submittedPrompt *promptui.Model | |||
| 50 | var value string | 51 | var value string | ||
| 51 | 52 | ||||
| 52 | - | routeOverlayBytes(data, &ac.promptPending, overlayEvents{ | 53 | + | routeOverlayBytes(data, &ac.overlays.promptPending, overlayEvents{ |
| 53 | rune: func(r rune) { | 54 | rune: func(r rune) { | ||
| 54 | - | ac.prompt.Insert(r) | 55 | + | ac.overlays.prompt.Insert(r) |
| 55 | changed = true | 56 | changed = true | ||
| 56 | }, | 57 | }, | ||
| 57 | backspace: func() { | 58 | backspace: func() { | ||
| 58 | - | ac.prompt.Backspace() | 59 | + | ac.overlays.prompt.Backspace() |
| 59 | changed = true | 60 | changed = true | ||
| 60 | }, | 61 | }, | ||
| 61 | enter: func() { | 62 | enter: func() { | ||
| 62 | - | submit = ac.promptSubmit | 63 | + | submit = ac.overlays.promptSubmit |
| 63 | - | value = strings.TrimSpace(ac.prompt.Value()) | 64 | + | submittedPrompt = ac.overlays.prompt |
| 65 | + | value = strings.TrimSpace(ac.overlays.prompt.Value()) | |||
| 64 | }, | 66 | }, | ||
| 65 | cancel: func() { exit = true }, | 67 | cancel: func() { exit = true }, | ||
| 66 | up: func() {}, | 68 | up: func() {}, | ||
| 67 | down: func() {}, | 69 | down: func() {}, | ||
| 68 | }) | 70 | }) | ||
| 69 | - | ac.promptMu.Unlock() | 71 | + | ac.overlays.promptMu.Unlock() |
| 70 | 72 | ||||
| 71 | if submit != nil { | 73 | if submit != nil { | ||
| 72 | if err := submit(value); err != nil { | 74 | if err := submit(value); err != nil { | ||
| 73 | - | ac.promptMu.Lock() | 75 | + | ac.overlays.promptMu.Lock() |
| 74 | - | if ac.prompt != nil { | 76 | + | if ac.overlays.prompt == submittedPrompt { |
| 75 | - | ac.prompt.SetError(err.Error()) | 77 | + | ac.overlays.prompt.SetError(err.Error()) |
| 76 | } | 78 | } | ||
| 77 | - | ac.promptMu.Unlock() | 79 | + | ac.overlays.promptMu.Unlock() |
| 78 | d.paint(sess, ac, true) | 80 | d.paint(sess, ac, true) | ||
| 79 | return | 81 | return | ||
| 80 | } | 82 | } | ||
M internal/usecase/daemon/prompt_test.go +6 -6
| @@ -19,7 +19,7 @@ func TestEnterPromptRendersTitleAndPrefill(t *testing.T) { | |||
| 19 | 19 | require.NoError(t, err) | |
| 20 | 20 | require.Contains(t, string(msg.Data), "Rename session") | |
| 21 | 21 | require.Contains(t, string(msg.Data), "> 0") | |
| 22 | - | require.True(t, ac.promptActive()) | |
| 22 | + | require.True(t, ac.overlays.promptActive()) | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | 25 | func TestPromptSubmitRenamesAndPromotesSession(t *testing.T) { | |
| @@ -34,7 +34,7 @@ func TestPromptSubmitRenamesAndPromotesSession(t *testing.T) { | |||
| 34 | 34 | d.handlePromptInput(ac, []byte("work\r")) | |
| 35 | 35 | repaint := awaitFrame(t, sends, ports.MsgOutput) | |
| 36 | 36 | ||
| 37 | - | require.False(t, ac.promptActive()) | |
| 37 | + | require.False(t, ac.overlays.promptActive()) | |
| 38 | 38 | require.Equal(t, "0work", sess.name) | |
| 39 | 39 | require.False(t, sess.ephemeral) | |
| 40 | 40 | out, err := ports.UnmarshalOutput(repaint.Payload) | |
| @@ -58,7 +58,7 @@ func TestPromptSubmitErrorKeepsPromptOpen(t *testing.T) { | |||
| 58 | 58 | d.handlePromptInput(ac, []byte("taken\r")) | |
| 59 | 59 | repaint := awaitFrame(t, sends, ports.MsgOutput) | |
| 60 | 60 | ||
| 61 | - | require.True(t, ac.promptActive()) | |
| 61 | + | require.True(t, ac.overlays.promptActive()) | |
| 62 | 62 | require.NotEqual(t, "taken", sess.name) | |
| 63 | 63 | out, err := ports.UnmarshalOutput(repaint.Payload) | |
| 64 | 64 | require.NoError(t, err) | |
| @@ -76,7 +76,7 @@ func TestPromptEscapeCancelsWithoutRename(t *testing.T) { | |||
| 76 | 76 | d.handlePromptInput(ac, []byte("new\x1b")) | |
| 77 | 77 | awaitFrame(t, sends, ports.MsgOutput) | |
| 78 | 78 | ||
| 79 | - | require.False(t, ac.promptActive()) | |
| 79 | + | require.False(t, ac.overlays.promptActive()) | |
| 80 | 80 | require.Equal(t, oldName, sess.name) | |
| 81 | 81 | } | |
| 82 | 82 | ||
| @@ -92,8 +92,8 @@ func TestPaletteRNSOpensRenamePrompt(t *testing.T) { | |||
| 92 | 92 | d.handlePaletteInput(ac, []byte("RNS\r")) | |
| 93 | 93 | out := awaitFrame(t, sends, ports.MsgOutput) | |
| 94 | 94 | ||
| 95 | - | require.False(t, ac.paletteActive()) | |
| 96 | - | require.True(t, ac.promptActive()) | |
| 95 | + | require.False(t, ac.overlays.paletteActive()) | |
| 96 | + | require.True(t, ac.overlays.promptActive()) | |
| 97 | 97 | msg, err := ports.UnmarshalOutput(out.Payload) | |
| 98 | 98 | require.NoError(t, err) | |
| 99 | 99 | require.Contains(t, string(msg.Data), "Rename session") | |
| @@ -19,7 +19,7 @@ func TestEnterPromptRendersTitleAndPrefill(t *testing.T) { | |||||
| 19 | require.NoError(t, err) | 19 | require.NoError(t, err) | ||
| 20 | require.Contains(t, string(msg.Data), "Rename session") | 20 | require.Contains(t, string(msg.Data), "Rename session") | ||
| 21 | require.Contains(t, string(msg.Data), "> 0") | 21 | require.Contains(t, string(msg.Data), "> 0") | ||
| 22 | - | require.True(t, ac.promptActive()) | 22 | + | require.True(t, ac.overlays.promptActive()) |
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | func TestPromptSubmitRenamesAndPromotesSession(t *testing.T) { | 25 | func TestPromptSubmitRenamesAndPromotesSession(t *testing.T) { | ||
| @@ -34,7 +34,7 @@ func TestPromptSubmitRenamesAndPromotesSession(t *testing.T) { | |||||
| 34 | d.handlePromptInput(ac, []byte("work\r")) | 34 | d.handlePromptInput(ac, []byte("work\r")) | ||
| 35 | repaint := awaitFrame(t, sends, ports.MsgOutput) | 35 | repaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 36 | 36 | ||||
| 37 | - | require.False(t, ac.promptActive()) | 37 | + | require.False(t, ac.overlays.promptActive()) |
| 38 | require.Equal(t, "0work", sess.name) | 38 | require.Equal(t, "0work", sess.name) | ||
| 39 | require.False(t, sess.ephemeral) | 39 | require.False(t, sess.ephemeral) | ||
| 40 | out, err := ports.UnmarshalOutput(repaint.Payload) | 40 | out, err := ports.UnmarshalOutput(repaint.Payload) | ||
| @@ -58,7 +58,7 @@ func TestPromptSubmitErrorKeepsPromptOpen(t *testing.T) { | |||||
| 58 | d.handlePromptInput(ac, []byte("taken\r")) | 58 | d.handlePromptInput(ac, []byte("taken\r")) | ||
| 59 | repaint := awaitFrame(t, sends, ports.MsgOutput) | 59 | repaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 60 | 60 | ||||
| 61 | - | require.True(t, ac.promptActive()) | 61 | + | require.True(t, ac.overlays.promptActive()) |
| 62 | require.NotEqual(t, "taken", sess.name) | 62 | require.NotEqual(t, "taken", sess.name) | ||
| 63 | out, err := ports.UnmarshalOutput(repaint.Payload) | 63 | out, err := ports.UnmarshalOutput(repaint.Payload) | ||
| 64 | require.NoError(t, err) | 64 | require.NoError(t, err) | ||
| @@ -76,7 +76,7 @@ func TestPromptEscapeCancelsWithoutRename(t *testing.T) { | |||||
| 76 | d.handlePromptInput(ac, []byte("new\x1b")) | 76 | d.handlePromptInput(ac, []byte("new\x1b")) | ||
| 77 | awaitFrame(t, sends, ports.MsgOutput) | 77 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 78 | 78 | ||||
| 79 | - | require.False(t, ac.promptActive()) | 79 | + | require.False(t, ac.overlays.promptActive()) |
| 80 | require.Equal(t, oldName, sess.name) | 80 | require.Equal(t, oldName, sess.name) | ||
| 81 | } | 81 | } | ||
| 82 | 82 | ||||
| @@ -92,8 +92,8 @@ func TestPaletteRNSOpensRenamePrompt(t *testing.T) { | |||||
| 92 | d.handlePaletteInput(ac, []byte("RNS\r")) | 92 | d.handlePaletteInput(ac, []byte("RNS\r")) | ||
| 93 | out := awaitFrame(t, sends, ports.MsgOutput) | 93 | out := awaitFrame(t, sends, ports.MsgOutput) | ||
| 94 | 94 | ||||
| 95 | - | require.False(t, ac.paletteActive()) | 95 | + | require.False(t, ac.overlays.paletteActive()) |
| 96 | - | require.True(t, ac.promptActive()) | 96 | + | require.True(t, ac.overlays.promptActive()) |
| 97 | msg, err := ports.UnmarshalOutput(out.Payload) | 97 | msg, err := ports.UnmarshalOutput(out.Payload) | ||
| 98 | require.NoError(t, err) | 98 | require.NoError(t, err) | ||
| 99 | require.Contains(t, string(msg.Data), "Rename session") | 99 | require.Contains(t, string(msg.Data), "Rename session") | ||
M internal/usecase/daemon/render.go +26 -56
| @@ -32,7 +32,6 @@ import ( | |||
| 32 | 32 | ||
| 33 | 33 | "github.com/bnema/vev/internal/domain" | |
| 34 | 34 | "github.com/bnema/vev/internal/ports" | |
| 35 | - | scopy "github.com/bnema/vev/internal/usecase/copy" | |
| 36 | 35 | "github.com/bnema/vev/internal/usecase/layout" | |
| 37 | 36 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 38 | 37 | "github.com/bnema/vev/internal/usecase/ui" | |
| @@ -276,44 +275,22 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 276 | 275 | return | |
| 277 | 276 | } | |
| 278 | 277 | ||
| 278 | + | ac.initOverlays() | |
| 279 | 279 | ac.sendMu.Lock() | |
| 280 | - | ac.copyMu.Lock() | |
| 281 | - | copyActive := ac.copyMode != nil | |
| 282 | - | var copyMode *scopy.Mode | |
| 283 | - | if ac.copyMode != nil { | |
| 284 | - | copyModeValue := *ac.copyMode | |
| 285 | - | copyMode = ©ModeValue | |
| 286 | - | } | |
| 287 | - | copyFeedback := ac.copyFeedback | |
| 288 | - | if copyFeedback != "" && !copyActive { | |
| 289 | - | ac.copyFeedback = "" | |
| 290 | - | } | |
| 291 | - | ac.copyMu.Unlock() | |
| 292 | - | ac.pickerMu.Lock() | |
| 293 | - | pickerActive := ac.picker != nil | |
| 294 | - | pickerModel := ac.picker.Clone() | |
| 295 | - | previewTab := ac.pickerPreview | |
| 296 | - | ac.pickerMu.Unlock() | |
| 280 | + | overlays := ac.overlays.SnapshotForRender() | |
| 281 | + | repaintAttachedClients := false | |
| 282 | + | defer func() { | |
| 283 | + | if repaintAttachedClients { | |
| 284 | + | d.repaintAllAttachedClients() | |
| 285 | + | } | |
| 286 | + | }() | |
| 287 | + | defer overlays.Unlock() | |
| 297 | 288 | preview := snapshotPickerPreview(nil) | |
| 298 | - | if previewTab != tb { | |
| 299 | - | preview = snapshotPickerPreview(previewTab) | |
| 300 | - | } | |
| 301 | - | ac.paletteMu.Lock() | |
| 302 | - | paletteModel := ac.palette | |
| 303 | - | paletteActive := paletteModel != nil | |
| 304 | - | if !paletteActive { | |
| 305 | - | ac.paletteMu.Unlock() | |
| 306 | - | } | |
| 307 | - | ac.promptMu.Lock() | |
| 308 | - | promptModel := ac.prompt | |
| 309 | - | promptActive := promptModel != nil | |
| 310 | - | if !promptActive { | |
| 311 | - | ac.promptMu.Unlock() | |
| 289 | + | if overlays.previewTab != tb { | |
| 290 | + | preview = snapshotPickerPreview(overlays.previewTab) | |
| 312 | 291 | } | |
| 313 | - | if sess.ackAttention(tb) { | |
| 314 | - | defer d.repaintAllAttachedClients() | |
| 315 | - | } | |
| 316 | - | bars := d.barStateFor(sess, copyFeedback) | |
| 292 | + | repaintAttachedClients = sess.ackAttention(tb) | |
| 293 | + | bars := d.barStateFor(sess, overlays.copyFeedback) | |
| 317 | 294 | bars.theme = ac.getTheme() | |
| 318 | 295 | ||
| 319 | 296 | styles := newThemeStyles(ac.getTheme()) | |
| @@ -332,28 +309,22 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 332 | 309 | p := tb.focusedPane() | |
| 333 | 310 | if p == nil { | |
| 334 | 311 | tb.mu.Unlock() | |
| 335 | - | if paletteActive { | |
| 336 | - | ac.paletteMu.Unlock() | |
| 337 | - | } | |
| 338 | - | if promptActive { | |
| 339 | - | ac.promptMu.Unlock() | |
| 340 | - | } | |
| 341 | 312 | ac.sendMu.Unlock() | |
| 342 | 313 | return | |
| 343 | 314 | } | |
| 344 | - | if reset || copyActive || pickerActive || paletteActive || promptActive { | |
| 315 | + | if reset || overlays.copyActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive { | |
| 345 | 316 | ac.rend.Reset() | |
| 346 | 317 | ac.bars.Reset() | |
| 347 | 318 | } | |
| 348 | - | if reset || pickerActive || paletteActive || promptActive { | |
| 319 | + | if reset || overlays.pickerActive || overlays.paletteActive || overlays.promptActive { | |
| 349 | 320 | ac.lastCursor.valid = false | |
| 350 | 321 | } | |
| 351 | 322 | frame, damage := composeClientFrameWithLayout(bars, tb, reset, layoutSnap, &ac.bars) | |
| 352 | - | if copyActive { | |
| 353 | - | frame, damage = composeCopyClientFrame(copyMode, tb, bars) | |
| 323 | + | if overlays.copyActive { | |
| 324 | + | frame, damage = composeCopyClientFrame(overlays.copyMode, tb, bars) | |
| 354 | 325 | } | |
| 355 | - | if pickerActive { | |
| 356 | - | if previewTab == tb { | |
| 326 | + | if overlays.pickerActive { | |
| 327 | + | if overlays.previewTab == tb { | |
| 357 | 328 | if layoutSnap.ok && tb.tree != nil && tb.tree.Root != nil && tb.tree.Root.Kind != layout.Leaf { | |
| 358 | 329 | previewFrame, _ := composeTabFrameWithLayout(tb, layoutSnap.area, themeui.Theme{}, layoutSnap) | |
| 359 | 330 | preview = pickerPreviewFromFrame(previewFrame) | |
| @@ -361,19 +332,18 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 361 | 332 | preview = pickerPreviewFromLockedTab(tb) | |
| 362 | 333 | } | |
| 363 | 334 | } | |
| 364 | - | frame, damage = composePickerClientFrame(pickerModel, preview, frame, styles) | |
| 335 | + | frame, damage = composePickerClientFrame(overlays.pickerModel, preview, frame, styles) | |
| 365 | 336 | } | |
| 366 | - | if paletteActive { | |
| 367 | - | frame, damage = composePaletteClientFrame(paletteModel, frame, styles) | |
| 368 | - | ac.paletteMu.Unlock() | |
| 337 | + | if overlays.paletteActive { | |
| 338 | + | frame, damage = composePaletteClientFrame(overlays.paletteModel, frame, styles) | |
| 369 | 339 | } | |
| 370 | - | if promptActive { | |
| 371 | - | frame, damage = composePromptClientFrame(promptModel, frame, styles) | |
| 372 | - | ac.promptMu.Unlock() | |
| 340 | + | if overlays.promptActive { | |
| 341 | + | frame, damage = composePromptClientFrame(overlays.promptModel, frame, styles) | |
| 373 | 342 | } | |
| 343 | + | overlays.Unlock() | |
| 374 | 344 | cursorContent, cursorVisible := focusedPaneContentRect(layoutSnap, p.id) | |
| 375 | 345 | p.mu.Lock() | |
| 376 | - | desiredCursor := desiredCursorOut(p.screen, cursorContent, !cursorVisible || copyActive || pickerActive || paletteActive || promptActive) | |
| 346 | + | desiredCursor := desiredCursorOut(p.screen, cursorContent, !cursorVisible || overlays.copyActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive) | |
| 377 | 347 | p.mu.Unlock() | |
| 378 | 348 | data, err := ac.rend.Draw(frame, damage) | |
| 379 | 349 | var cursorTail []byte | |
| @@ -32,7 +32,6 @@ import ( | |||||
| 32 | 32 | ||||
| 33 | "github.com/bnema/vev/internal/domain" | 33 | "github.com/bnema/vev/internal/domain" | ||
| 34 | "github.com/bnema/vev/internal/ports" | 34 | "github.com/bnema/vev/internal/ports" | ||
| 35 | - | scopy "github.com/bnema/vev/internal/usecase/copy" | |||
| 36 | "github.com/bnema/vev/internal/usecase/layout" | 35 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 37 | themeui "github.com/bnema/vev/internal/usecase/theme" | 36 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 38 | "github.com/bnema/vev/internal/usecase/ui" | 37 | "github.com/bnema/vev/internal/usecase/ui" | ||
| @@ -276,44 +275,22 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 276 | return | 275 | return | ||
| 277 | } | 276 | } | ||
| 278 | 277 | ||||
| 278 | + | ac.initOverlays() | |||
| 279 | ac.sendMu.Lock() | 279 | ac.sendMu.Lock() | ||
| 280 | - | ac.copyMu.Lock() | 280 | + | overlays := ac.overlays.SnapshotForRender() |
| 281 | - | copyActive := ac.copyMode != nil | 281 | + | repaintAttachedClients := false |
| 282 | - | var copyMode *scopy.Mode | 282 | + | defer func() { |
| 283 | - | if ac.copyMode != nil { | 283 | + | if repaintAttachedClients { |
| 284 | - | copyModeValue := *ac.copyMode | 284 | + | d.repaintAllAttachedClients() |
| 285 | - | copyMode = ©ModeValue | 285 | + | } |
| 286 | - | } | 286 | + | }() |
| 287 | - | copyFeedback := ac.copyFeedback | 287 | + | defer overlays.Unlock() |
| 288 | - | if copyFeedback != "" && !copyActive { | |||
| 289 | - | ac.copyFeedback = "" | |||
| 290 | - | } | |||
| 291 | - | ac.copyMu.Unlock() | |||
| 292 | - | ac.pickerMu.Lock() | |||
| 293 | - | pickerActive := ac.picker != nil | |||
| 294 | - | pickerModel := ac.picker.Clone() | |||
| 295 | - | previewTab := ac.pickerPreview | |||
| 296 | - | ac.pickerMu.Unlock() | |||
| 297 | preview := snapshotPickerPreview(nil) | 288 | preview := snapshotPickerPreview(nil) | ||
| 298 | - | if previewTab != tb { | 289 | + | if overlays.previewTab != tb { |
| 299 | - | preview = snapshotPickerPreview(previewTab) | 290 | + | preview = snapshotPickerPreview(overlays.previewTab) |
| 300 | - | } | |||
| 301 | - | ac.paletteMu.Lock() | |||
| 302 | - | paletteModel := ac.palette | |||
| 303 | - | paletteActive := paletteModel != nil | |||
| 304 | - | if !paletteActive { | |||
| 305 | - | ac.paletteMu.Unlock() | |||
| 306 | - | } | |||
| 307 | - | ac.promptMu.Lock() | |||
| 308 | - | promptModel := ac.prompt | |||
| 309 | - | promptActive := promptModel != nil | |||
| 310 | - | if !promptActive { | |||
| 311 | - | ac.promptMu.Unlock() | |||
| 312 | } | 291 | } | ||
| 313 | - | if sess.ackAttention(tb) { | 292 | + | repaintAttachedClients = sess.ackAttention(tb) |
| 314 | - | defer d.repaintAllAttachedClients() | 293 | + | bars := d.barStateFor(sess, overlays.copyFeedback) |
| 315 | - | } | |||
| 316 | - | bars := d.barStateFor(sess, copyFeedback) | |||
| 317 | bars.theme = ac.getTheme() | 294 | bars.theme = ac.getTheme() | ||
| 318 | 295 | ||||
| 319 | styles := newThemeStyles(ac.getTheme()) | 296 | styles := newThemeStyles(ac.getTheme()) | ||
| @@ -332,28 +309,22 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 332 | p := tb.focusedPane() | 309 | p := tb.focusedPane() | ||
| 333 | if p == nil { | 310 | if p == nil { | ||
| 334 | tb.mu.Unlock() | 311 | tb.mu.Unlock() | ||
| 335 | - | if paletteActive { | |||
| 336 | - | ac.paletteMu.Unlock() | |||
| 337 | - | } | |||
| 338 | - | if promptActive { | |||
| 339 | - | ac.promptMu.Unlock() | |||
| 340 | - | } | |||
| 341 | ac.sendMu.Unlock() | 312 | ac.sendMu.Unlock() | ||
| 342 | return | 313 | return | ||
| 343 | } | 314 | } | ||
| 344 | - | if reset || copyActive || pickerActive || paletteActive || promptActive { | 315 | + | if reset || overlays.copyActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive { |
| 345 | ac.rend.Reset() | 316 | ac.rend.Reset() | ||
| 346 | ac.bars.Reset() | 317 | ac.bars.Reset() | ||
| 347 | } | 318 | } | ||
| 348 | - | if reset || pickerActive || paletteActive || promptActive { | 319 | + | if reset || overlays.pickerActive || overlays.paletteActive || overlays.promptActive { |
| 349 | ac.lastCursor.valid = false | 320 | ac.lastCursor.valid = false | ||
| 350 | } | 321 | } | ||
| 351 | frame, damage := composeClientFrameWithLayout(bars, tb, reset, layoutSnap, &ac.bars) | 322 | frame, damage := composeClientFrameWithLayout(bars, tb, reset, layoutSnap, &ac.bars) | ||
| 352 | - | if copyActive { | 323 | + | if overlays.copyActive { |
| 353 | - | frame, damage = composeCopyClientFrame(copyMode, tb, bars) | 324 | + | frame, damage = composeCopyClientFrame(overlays.copyMode, tb, bars) |
| 354 | } | 325 | } | ||
| 355 | - | if pickerActive { | 326 | + | if overlays.pickerActive { |
| 356 | - | if previewTab == tb { | 327 | + | if overlays.previewTab == tb { |
| 357 | if layoutSnap.ok && tb.tree != nil && tb.tree.Root != nil && tb.tree.Root.Kind != layout.Leaf { | 328 | if layoutSnap.ok && tb.tree != nil && tb.tree.Root != nil && tb.tree.Root.Kind != layout.Leaf { | ||
| 358 | previewFrame, _ := composeTabFrameWithLayout(tb, layoutSnap.area, themeui.Theme{}, layoutSnap) | 329 | previewFrame, _ := composeTabFrameWithLayout(tb, layoutSnap.area, themeui.Theme{}, layoutSnap) | ||
| 359 | preview = pickerPreviewFromFrame(previewFrame) | 330 | preview = pickerPreviewFromFrame(previewFrame) | ||
| @@ -361,19 +332,18 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 361 | preview = pickerPreviewFromLockedTab(tb) | 332 | preview = pickerPreviewFromLockedTab(tb) | ||
| 362 | } | 333 | } | ||
| 363 | } | 334 | } | ||
| 364 | - | frame, damage = composePickerClientFrame(pickerModel, preview, frame, styles) | 335 | + | frame, damage = composePickerClientFrame(overlays.pickerModel, preview, frame, styles) |
| 365 | } | 336 | } | ||
| 366 | - | if paletteActive { | 337 | + | if overlays.paletteActive { |
| 367 | - | frame, damage = composePaletteClientFrame(paletteModel, frame, styles) | 338 | + | frame, damage = composePaletteClientFrame(overlays.paletteModel, frame, styles) |
| 368 | - | ac.paletteMu.Unlock() | |||
| 369 | } | 339 | } | ||
| 370 | - | if promptActive { | 340 | + | if overlays.promptActive { |
| 371 | - | frame, damage = composePromptClientFrame(promptModel, frame, styles) | 341 | + | frame, damage = composePromptClientFrame(overlays.promptModel, frame, styles) |
| 372 | - | ac.promptMu.Unlock() | |||
| 373 | } | 342 | } | ||
| 343 | + | overlays.Unlock() | |||
| 374 | cursorContent, cursorVisible := focusedPaneContentRect(layoutSnap, p.id) | 344 | cursorContent, cursorVisible := focusedPaneContentRect(layoutSnap, p.id) | ||
| 375 | p.mu.Lock() | 345 | p.mu.Lock() | ||
| 376 | - | desiredCursor := desiredCursorOut(p.screen, cursorContent, !cursorVisible || copyActive || pickerActive || paletteActive || promptActive) | 346 | + | desiredCursor := desiredCursorOut(p.screen, cursorContent, !cursorVisible || overlays.copyActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive) |
| 377 | p.mu.Unlock() | 347 | p.mu.Unlock() | ||
| 378 | data, err := ac.rend.Draw(frame, damage) | 348 | data, err := ac.rend.Draw(frame, damage) | ||
| 379 | var cursorTail []byte | 349 | var cursorTail []byte | ||
M internal/usecase/daemon/render_test.go +120 -0
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | "github.com/bnema/vev/internal/domain" | |
| 19 | 19 | "github.com/bnema/vev/internal/ports" | |
| 20 | 20 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 21 | + | scopy "github.com/bnema/vev/internal/usecase/copy" | |
| 21 | 22 | "github.com/bnema/vev/internal/usecase/layout" | |
| 22 | 23 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 23 | 24 | "github.com/bnema/vev/pkg/renderer" | |
| @@ -37,6 +38,7 @@ func TestPTYWriteErrorIsLogged(t *testing.T) { | |||
| 37 | 38 | win := newTab(p, domain.Size{Cols: 80, Rows: 23}) | |
| 38 | 39 | sess := &session{id: "manual", name: "work", tabs: []*tab{win}} | |
| 39 | 40 | ac := &attachedClient{} | |
| 41 | + | ac.initOverlays() | |
| 40 | 42 | ac.setSession(sess) | |
| 41 | 43 | ||
| 42 | 44 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("input")) | |
| @@ -215,6 +217,7 @@ func TestSchedulerDebounceCoalesces(t *testing.T) { | |||
| 215 | 217 | sctx, cancel := context.WithCancel(context.Background()) | |
| 216 | 218 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 20, Rows: 5}) | |
| 217 | 219 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 220 | + | ac.initOverlays() | |
| 218 | 221 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 219 | 222 | ac.setSession(sess) | |
| 220 | 223 | ||
| @@ -278,6 +281,7 @@ func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) { | |||
| 278 | 281 | ||
| 279 | 282 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | |
| 280 | 283 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 284 | + | ac.initOverlays() | |
| 281 | 285 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | |
| 282 | 286 | ac.setSession(sess) | |
| 283 | 287 | ||
| @@ -388,6 +392,7 @@ func TestRenderWithNilTransportDoesNotAdvanceStateCounter(t *testing.T) { | |||
| 388 | 392 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | |
| 389 | 393 | sess := &session{id: "s", name: "work", tabs: []*tab{tb}, active: 0} | |
| 390 | 394 | ac := &attachedClient{rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 10, Rows: 4}} | |
| 395 | + | ac.initOverlays() | |
| 391 | 396 | sess.client = ac | |
| 392 | 397 | ac.setSession(sess) | |
| 393 | 398 | d := newTestDaemon(t, nil, stubClock{}) | |
| @@ -449,6 +454,115 @@ func TestComposeTabFrameStackDrawsTitleBarsAndDimsCollapsed(t *testing.T) { | |||
| 449 | 454 | require.True(t, frame.At(0, 1).Style.Inverse || frame.At(0, 1).Style.HasBackgroundRGB, "focused title bar should use accent chrome") | |
| 450 | 455 | } | |
| 451 | 456 | ||
| 457 | + | func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) { | |
| 458 | + | tests := []struct { | |
| 459 | + | name string | |
| 460 | + | open func(*Daemon, *session, *attachedClient) | |
| 461 | + | close func(*Daemon, *session, *attachedClient) | |
| 462 | + | visible string | |
| 463 | + | notVisible string | |
| 464 | + | wantRestored string | |
| 465 | + | prepareScreen func(*session) | |
| 466 | + | }{ | |
| 467 | + | { | |
| 468 | + | name: "prompt", | |
| 469 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |
| 470 | + | d.enterPrompt(sess, ac, " Rename session ", "work", func(string) error { return nil }) | |
| 471 | + | }, | |
| 472 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handlePromptInput(ac, []byte("\x1b")) }, | |
| 473 | + | visible: "Rename session", | |
| 474 | + | notVisible: "Rename session", | |
| 475 | + | }, | |
| 476 | + | { | |
| 477 | + | name: "palette", | |
| 478 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterPalette(sess, ac) }, | |
| 479 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handlePaletteInput(ac, []byte("\x1b")) }, | |
| 480 | + | visible: "Commands", | |
| 481 | + | notVisible: "Commands", | |
| 482 | + | }, | |
| 483 | + | { | |
| 484 | + | name: "picker", | |
| 485 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterPicker(sess, ac) }, | |
| 486 | + | close: func(d *Daemon, sess *session, ac *attachedClient) { d.closePicker(ac); d.paint(sess, ac, true) }, | |
| 487 | + | visible: "Sessions", | |
| 488 | + | notVisible: "Sessions", | |
| 489 | + | }, | |
| 490 | + | { | |
| 491 | + | name: "copy mode", | |
| 492 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) }, | |
| 493 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) }, | |
| 494 | + | prepareScreen: func(sess *session) { | |
| 495 | + | sess.tabs[0].focusedPane().scrollback = scopy.NewScrollback(4) | |
| 496 | + | }, | |
| 497 | + | visible: "[VISUAL]", | |
| 498 | + | notVisible: "[VISUAL]", | |
| 499 | + | wantRestored: "live", | |
| 500 | + | }, | |
| 501 | + | } | |
| 502 | + | for _, tt := range tests { | |
| 503 | + | t.Run(tt.name, func(t *testing.T) { | |
| 504 | + | p, release := newBlockingPTY(t) | |
| 505 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 506 | + | defer release() | |
| 507 | + | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 508 | + | if tt.prepareScreen != nil { | |
| 509 | + | tt.prepareScreen(sess) | |
| 510 | + | } | |
| 511 | + | if tt.wantRestored == "" { | |
| 512 | + | tt.wantRestored = "live" | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | tt.open(d, sess, ac) | |
| 516 | + | shown := string(mustOutputData(t, sends)) | |
| 517 | + | require.Contains(t, shown, tt.visible) | |
| 518 | + | ||
| 519 | + | tt.close(d, sess, ac) | |
| 520 | + | restored := string(mustOutputData(t, sends)) | |
| 521 | + | require.Contains(t, restored, tt.wantRestored) | |
| 522 | + | require.NotContains(t, restored, tt.notVisible) | |
| 523 | + | }) | |
| 524 | + | } | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | func TestPaintEarlyReturnReleasesOverlayRenderLocks(t *testing.T) { | |
| 528 | + | tests := []struct { | |
| 529 | + | name string | |
| 530 | + | open func(*Daemon, *session, *attachedClient) | |
| 531 | + | try func(*overlayRuntime) bool | |
| 532 | + | give func(*overlayRuntime) | |
| 533 | + | }{ | |
| 534 | + | { | |
| 535 | + | name: "prompt", | |
| 536 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |
| 537 | + | d.enterPrompt(sess, ac, " Rename session ", "work", func(string) error { return nil }) | |
| 538 | + | }, | |
| 539 | + | try: func(rt *overlayRuntime) bool { return rt.promptMu.TryLock() }, | |
| 540 | + | give: func(rt *overlayRuntime) { rt.promptMu.Unlock() }, | |
| 541 | + | }, | |
| 542 | + | { | |
| 543 | + | name: "palette", | |
| 544 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |
| 545 | + | d.enterPalette(sess, ac) | |
| 546 | + | }, | |
| 547 | + | try: func(rt *overlayRuntime) bool { return rt.paletteMu.TryLock() }, | |
| 548 | + | give: func(rt *overlayRuntime) { rt.paletteMu.Unlock() }, | |
| 549 | + | }, | |
| 550 | + | } | |
| 551 | + | for _, tt := range tests { | |
| 552 | + | t.Run(tt.name, func(t *testing.T) { | |
| 553 | + | p, release := newBlockingPTY(t) | |
| 554 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | |
| 555 | + | defer release() | |
| 556 | + | sess.tabs[0].tree.Focus = layout.PaneID("missing") | |
| 557 | + | ||
| 558 | + | tt.open(d, sess, ac) | |
| 559 | + | ||
| 560 | + | require.True(t, tt.try(ac.overlays), "overlay render lock should be released after paint returns early") | |
| 561 | + | tt.give(ac.overlays) | |
| 562 | + | }) | |
| 563 | + | } | |
| 564 | + | } | |
| 565 | + | ||
| 452 | 566 | func TestComposeClientFrameBarCacheSkipsUnchangedBars(t *testing.T) { | |
| 453 | 567 | sess, win := newBarCacheTestSession() | |
| 454 | 568 | var cache barCache | |
| @@ -524,6 +638,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) { | |||
| 524 | 638 | ||
| 525 | 639 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | |
| 526 | 640 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 641 | + | ac.initOverlays() | |
| 527 | 642 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | |
| 528 | 643 | ac.setSession(sess) | |
| 529 | 644 | ||
| @@ -549,6 +664,7 @@ func TestSendErrorKillsEphemeral(t *testing.T) { | |||
| 549 | 664 | win := newTab(p, domain.Size{Cols: 20, Rows: 5}) | |
| 550 | 665 | sctx, cancel := context.WithCancel(context.Background()) | |
| 551 | 666 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 667 | + | ac.initOverlays() | |
| 552 | 668 | sess := &session{id: "e", name: "0", ephemeral: true, tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 553 | 669 | ac.setSession(sess) | |
| 554 | 670 | d.sessions[sess.id] = sess | |
| @@ -594,6 +710,7 @@ func TestSchedulerDefersPendingDirtyTimerDuringSynchronizedUpdate(t *testing.T) | |||
| 594 | 710 | p := win.focusedPane() | |
| 595 | 711 | p.ctx, p.cancel = sctx, cancel | |
| 596 | 712 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 713 | + | ac.initOverlays() | |
| 597 | 714 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 598 | 715 | ac.setSession(sess) | |
| 599 | 716 | ||
| @@ -654,6 +771,7 @@ func TestPTYQueryGetsResponseWrittenBackToPTY(t *testing.T) { | |||
| 654 | 771 | sctx, cancel := context.WithCancel(context.Background()) | |
| 655 | 772 | win := newTestTabWithContext(p, sctx, cancel) | |
| 656 | 773 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 774 | + | ac.initOverlays() | |
| 657 | 775 | sess := &session{id: "query", name: "query", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 658 | 776 | ac.setSession(sess) | |
| 659 | 777 | d.sessions[sess.id] = sess | |
| @@ -759,6 +877,7 @@ func TestSyncUpdateWatchdogAbandonedSyncRenders(t *testing.T) { | |||
| 759 | 877 | sctx, cancel := context.WithCancel(context.Background()) | |
| 760 | 878 | win := newTestTabWithContext(p, sctx, cancel) | |
| 761 | 879 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 880 | + | ac.initOverlays() | |
| 762 | 881 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 763 | 882 | ac.setSession(sess) | |
| 764 | 883 | d.sessions[sess.id] = sess | |
| @@ -803,6 +922,7 @@ func TestSyncUpdateWatchdogStaleGenerationNoopAfterEnd(t *testing.T) { | |||
| 803 | 922 | sctx, cancel := context.WithCancel(context.Background()) | |
| 804 | 923 | win := newTestTabWithContext(p, sctx, cancel) | |
| 805 | 924 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | |
| 925 | + | ac.initOverlays() | |
| 806 | 926 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | |
| 807 | 927 | ac.setSession(sess) | |
| 808 | 928 | d.sessions[sess.id] = sess | |
| @@ -18,6 +18,7 @@ import ( | |||||
| 18 | "github.com/bnema/vev/internal/domain" | 18 | "github.com/bnema/vev/internal/domain" | ||
| 19 | "github.com/bnema/vev/internal/ports" | 19 | "github.com/bnema/vev/internal/ports" | ||
| 20 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | 20 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | ||
| 21 | + | scopy "github.com/bnema/vev/internal/usecase/copy" | |||
| 21 | "github.com/bnema/vev/internal/usecase/layout" | 22 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 22 | themeui "github.com/bnema/vev/internal/usecase/theme" | 23 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 23 | "github.com/bnema/vev/pkg/renderer" | 24 | "github.com/bnema/vev/pkg/renderer" | ||
| @@ -37,6 +38,7 @@ func TestPTYWriteErrorIsLogged(t *testing.T) { | |||||
| 37 | win := newTab(p, domain.Size{Cols: 80, Rows: 23}) | 38 | win := newTab(p, domain.Size{Cols: 80, Rows: 23}) | ||
| 38 | sess := &session{id: "manual", name: "work", tabs: []*tab{win}} | 39 | sess := &session{id: "manual", name: "work", tabs: []*tab{win}} | ||
| 39 | ac := &attachedClient{} | 40 | ac := &attachedClient{} | ||
| 41 | + | ac.initOverlays() | |||
| 40 | ac.setSession(sess) | 42 | ac.setSession(sess) | ||
| 41 | 43 | ||||
| 42 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("input")) | 44 | daemonKeyHandler{d: d, ac: ac}.Forward([]byte("input")) | ||
| @@ -215,6 +217,7 @@ func TestSchedulerDebounceCoalesces(t *testing.T) { | |||||
| 215 | sctx, cancel := context.WithCancel(context.Background()) | 217 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 216 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 20, Rows: 5}) | 218 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 20, Rows: 5}) | ||
| 217 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 219 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 220 | + | ac.initOverlays() | |||
| 218 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 221 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 219 | ac.setSession(sess) | 222 | ac.setSession(sess) | ||
| 220 | 223 | ||||
| @@ -278,6 +281,7 @@ func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) { | |||||
| 278 | 281 | ||||
| 279 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | 282 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | ||
| 280 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 283 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 284 | + | ac.initOverlays() | |||
| 281 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | 285 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | ||
| 282 | ac.setSession(sess) | 286 | ac.setSession(sess) | ||
| 283 | 287 | ||||
| @@ -388,6 +392,7 @@ func TestRenderWithNilTransportDoesNotAdvanceStateCounter(t *testing.T) { | |||||
| 388 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | 392 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | ||
| 389 | sess := &session{id: "s", name: "work", tabs: []*tab{tb}, active: 0} | 393 | sess := &session{id: "s", name: "work", tabs: []*tab{tb}, active: 0} | ||
| 390 | ac := &attachedClient{rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 10, Rows: 4}} | 394 | ac := &attachedClient{rend: renderer.New(renderer.Capabilities{}), size: domain.Size{Cols: 10, Rows: 4}} | ||
| 395 | + | ac.initOverlays() | |||
| 391 | sess.client = ac | 396 | sess.client = ac | ||
| 392 | ac.setSession(sess) | 397 | ac.setSession(sess) | ||
| 393 | d := newTestDaemon(t, nil, stubClock{}) | 398 | d := newTestDaemon(t, nil, stubClock{}) | ||
| @@ -449,6 +454,115 @@ func TestComposeTabFrameStackDrawsTitleBarsAndDimsCollapsed(t *testing.T) { | |||||
| 449 | require.True(t, frame.At(0, 1).Style.Inverse || frame.At(0, 1).Style.HasBackgroundRGB, "focused title bar should use accent chrome") | 454 | require.True(t, frame.At(0, 1).Style.Inverse || frame.At(0, 1).Style.HasBackgroundRGB, "focused title bar should use accent chrome") | ||
| 450 | } | 455 | } | ||
| 451 | 456 | ||||
| 457 | + | func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) { | |||
| 458 | + | tests := []struct { | |||
| 459 | + | name string | |||
| 460 | + | open func(*Daemon, *session, *attachedClient) | |||
| 461 | + | close func(*Daemon, *session, *attachedClient) | |||
| 462 | + | visible string | |||
| 463 | + | notVisible string | |||
| 464 | + | wantRestored string | |||
| 465 | + | prepareScreen func(*session) | |||
| 466 | + | }{ | |||
| 467 | + | { | |||
| 468 | + | name: "prompt", | |||
| 469 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |||
| 470 | + | d.enterPrompt(sess, ac, " Rename session ", "work", func(string) error { return nil }) | |||
| 471 | + | }, | |||
| 472 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handlePromptInput(ac, []byte("\x1b")) }, | |||
| 473 | + | visible: "Rename session", | |||
| 474 | + | notVisible: "Rename session", | |||
| 475 | + | }, | |||
| 476 | + | { | |||
| 477 | + | name: "palette", | |||
| 478 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterPalette(sess, ac) }, | |||
| 479 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handlePaletteInput(ac, []byte("\x1b")) }, | |||
| 480 | + | visible: "Commands", | |||
| 481 | + | notVisible: "Commands", | |||
| 482 | + | }, | |||
| 483 | + | { | |||
| 484 | + | name: "picker", | |||
| 485 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterPicker(sess, ac) }, | |||
| 486 | + | close: func(d *Daemon, sess *session, ac *attachedClient) { d.closePicker(ac); d.paint(sess, ac, true) }, | |||
| 487 | + | visible: "Sessions", | |||
| 488 | + | notVisible: "Sessions", | |||
| 489 | + | }, | |||
| 490 | + | { | |||
| 491 | + | name: "copy mode", | |||
| 492 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) }, | |||
| 493 | + | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) }, | |||
| 494 | + | prepareScreen: func(sess *session) { | |||
| 495 | + | sess.tabs[0].focusedPane().scrollback = scopy.NewScrollback(4) | |||
| 496 | + | }, | |||
| 497 | + | visible: "[VISUAL]", | |||
| 498 | + | notVisible: "[VISUAL]", | |||
| 499 | + | wantRestored: "live", | |||
| 500 | + | }, | |||
| 501 | + | } | |||
| 502 | + | for _, tt := range tests { | |||
| 503 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 504 | + | p, release := newBlockingPTY(t) | |||
| 505 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |||
| 506 | + | defer release() | |||
| 507 | + | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |||
| 508 | + | if tt.prepareScreen != nil { | |||
| 509 | + | tt.prepareScreen(sess) | |||
| 510 | + | } | |||
| 511 | + | if tt.wantRestored == "" { | |||
| 512 | + | tt.wantRestored = "live" | |||
| 513 | + | } | |||
| 514 | + | ||||
| 515 | + | tt.open(d, sess, ac) | |||
| 516 | + | shown := string(mustOutputData(t, sends)) | |||
| 517 | + | require.Contains(t, shown, tt.visible) | |||
| 518 | + | ||||
| 519 | + | tt.close(d, sess, ac) | |||
| 520 | + | restored := string(mustOutputData(t, sends)) | |||
| 521 | + | require.Contains(t, restored, tt.wantRestored) | |||
| 522 | + | require.NotContains(t, restored, tt.notVisible) | |||
| 523 | + | }) | |||
| 524 | + | } | |||
| 525 | + | } | |||
| 526 | + | ||||
| 527 | + | func TestPaintEarlyReturnReleasesOverlayRenderLocks(t *testing.T) { | |||
| 528 | + | tests := []struct { | |||
| 529 | + | name string | |||
| 530 | + | open func(*Daemon, *session, *attachedClient) | |||
| 531 | + | try func(*overlayRuntime) bool | |||
| 532 | + | give func(*overlayRuntime) | |||
| 533 | + | }{ | |||
| 534 | + | { | |||
| 535 | + | name: "prompt", | |||
| 536 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |||
| 537 | + | d.enterPrompt(sess, ac, " Rename session ", "work", func(string) error { return nil }) | |||
| 538 | + | }, | |||
| 539 | + | try: func(rt *overlayRuntime) bool { return rt.promptMu.TryLock() }, | |||
| 540 | + | give: func(rt *overlayRuntime) { rt.promptMu.Unlock() }, | |||
| 541 | + | }, | |||
| 542 | + | { | |||
| 543 | + | name: "palette", | |||
| 544 | + | open: func(d *Daemon, sess *session, ac *attachedClient) { | |||
| 545 | + | d.enterPalette(sess, ac) | |||
| 546 | + | }, | |||
| 547 | + | try: func(rt *overlayRuntime) bool { return rt.paletteMu.TryLock() }, | |||
| 548 | + | give: func(rt *overlayRuntime) { rt.paletteMu.Unlock() }, | |||
| 549 | + | }, | |||
| 550 | + | } | |||
| 551 | + | for _, tt := range tests { | |||
| 552 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 553 | + | p, release := newBlockingPTY(t) | |||
| 554 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | |||
| 555 | + | defer release() | |||
| 556 | + | sess.tabs[0].tree.Focus = layout.PaneID("missing") | |||
| 557 | + | ||||
| 558 | + | tt.open(d, sess, ac) | |||
| 559 | + | ||||
| 560 | + | require.True(t, tt.try(ac.overlays), "overlay render lock should be released after paint returns early") | |||
| 561 | + | tt.give(ac.overlays) | |||
| 562 | + | }) | |||
| 563 | + | } | |||
| 564 | + | } | |||
| 565 | + | ||||
| 452 | func TestComposeClientFrameBarCacheSkipsUnchangedBars(t *testing.T) { | 566 | func TestComposeClientFrameBarCacheSkipsUnchangedBars(t *testing.T) { | ||
| 453 | sess, win := newBarCacheTestSession() | 567 | sess, win := newBarCacheTestSession() | ||
| 454 | var cache barCache | 568 | var cache barCache | ||
| @@ -524,6 +638,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) { | |||||
| 524 | 638 | ||||
| 525 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | 639 | d := New(nil, stubClock{}, slog.New(slog.NewTextHandler(io.Discard, nil))) | ||
| 526 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 640 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 641 | + | ac.initOverlays() | |||
| 527 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | 642 | sess := &session{id: "s", name: "s", tabs: []*tab{win}, client: ac} | ||
| 528 | ac.setSession(sess) | 643 | ac.setSession(sess) | ||
| 529 | 644 | ||||
| @@ -549,6 +664,7 @@ func TestSendErrorKillsEphemeral(t *testing.T) { | |||||
| 549 | win := newTab(p, domain.Size{Cols: 20, Rows: 5}) | 664 | win := newTab(p, domain.Size{Cols: 20, Rows: 5}) | ||
| 550 | sctx, cancel := context.WithCancel(context.Background()) | 665 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 551 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 666 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 667 | + | ac.initOverlays() | |||
| 552 | sess := &session{id: "e", name: "0", ephemeral: true, tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 668 | sess := &session{id: "e", name: "0", ephemeral: true, tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 553 | ac.setSession(sess) | 669 | ac.setSession(sess) | ||
| 554 | d.sessions[sess.id] = sess | 670 | d.sessions[sess.id] = sess | ||
| @@ -594,6 +710,7 @@ func TestSchedulerDefersPendingDirtyTimerDuringSynchronizedUpdate(t *testing.T) | |||||
| 594 | p := win.focusedPane() | 710 | p := win.focusedPane() | ||
| 595 | p.ctx, p.cancel = sctx, cancel | 711 | p.ctx, p.cancel = sctx, cancel | ||
| 596 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 712 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 713 | + | ac.initOverlays() | |||
| 597 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 714 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 598 | ac.setSession(sess) | 715 | ac.setSession(sess) | ||
| 599 | 716 | ||||
| @@ -654,6 +771,7 @@ func TestPTYQueryGetsResponseWrittenBackToPTY(t *testing.T) { | |||||
| 654 | sctx, cancel := context.WithCancel(context.Background()) | 771 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 655 | win := newTestTabWithContext(p, sctx, cancel) | 772 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 656 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 773 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 774 | + | ac.initOverlays() | |||
| 657 | sess := &session{id: "query", name: "query", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 775 | sess := &session{id: "query", name: "query", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 658 | ac.setSession(sess) | 776 | ac.setSession(sess) | ||
| 659 | d.sessions[sess.id] = sess | 777 | d.sessions[sess.id] = sess | ||
| @@ -759,6 +877,7 @@ func TestSyncUpdateWatchdogAbandonedSyncRenders(t *testing.T) { | |||||
| 759 | sctx, cancel := context.WithCancel(context.Background()) | 877 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 760 | win := newTestTabWithContext(p, sctx, cancel) | 878 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 761 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 879 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 880 | + | ac.initOverlays() | |||
| 762 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 881 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 763 | ac.setSession(sess) | 882 | ac.setSession(sess) | ||
| 764 | d.sessions[sess.id] = sess | 883 | d.sessions[sess.id] = sess | ||
| @@ -803,6 +922,7 @@ func TestSyncUpdateWatchdogStaleGenerationNoopAfterEnd(t *testing.T) { | |||||
| 803 | sctx, cancel := context.WithCancel(context.Background()) | 922 | sctx, cancel := context.WithCancel(context.Background()) | ||
| 804 | win := newTestTabWithContext(p, sctx, cancel) | 923 | win := newTestTabWithContext(p, sctx, cancel) | ||
| 805 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | 924 | ac := &attachedClient{tr: tr, rend: renderer.New(renderer.Capabilities{})} | ||
| 925 | + | ac.initOverlays() | |||
| 806 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | 926 | sess := &session{id: "sync", name: "sync", tabs: []*tab{win}, ctx: sctx, cancel: cancel, client: ac} | ||
| 807 | ac.setSession(sess) | 927 | ac.setSession(sess) | ||
| 808 | d.sessions[sess.id] = sess | 928 | d.sessions[sess.id] = sess | ||
M internal/usecase/daemon/resume_test.go +1 -0
| @@ -278,6 +278,7 @@ func TestOutputStateNumberingIsSharedAndMonotone(t *testing.T) { | |||
| 278 | 278 | d := newTestDaemon(t, nil, stubClock{}) | |
| 279 | 279 | tr := &closeTrackingTransport{} | |
| 280 | 280 | ac := &attachedClient{tr: tr} | |
| 281 | + | ac.initOverlays() | |
| 281 | 282 | ||
| 282 | 283 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("copy"))) | |
| 283 | 284 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("more"))) | |
| @@ -278,6 +278,7 @@ func TestOutputStateNumberingIsSharedAndMonotone(t *testing.T) { | |||||
| 278 | d := newTestDaemon(t, nil, stubClock{}) | 278 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 279 | tr := &closeTrackingTransport{} | 279 | tr := &closeTrackingTransport{} | ||
| 280 | ac := &attachedClient{tr: tr} | 280 | ac := &attachedClient{tr: tr} | ||
| 281 | + | ac.initOverlays() | |||
| 281 | 282 | ||||
| 282 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("copy"))) | 283 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("copy"))) | ||
| 283 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("more"))) | 284 | require.NoError(t, d.boundedSendOutputErr(ac, []byte("more"))) | ||
M internal/usecase/palette/model.go +1 -1
| @@ -132,7 +132,7 @@ func (m *Model) Render(inner domain.Size, selectedStyle ...renderer.Style) rende | |||
| 132 | 132 | codeWidth = len([]rune(match.Command.Code)) | |
| 133 | 133 | } | |
| 134 | 134 | } | |
| 135 | - | for y := 0; y < visible; y++ { | |
| 135 | + | for y := range visible { | |
| 136 | 136 | idx := m.scroll + y | |
| 137 | 137 | if idx >= len(m.matches) { | |
| 138 | 138 | break | |
| @@ -132,7 +132,7 @@ func (m *Model) Render(inner domain.Size, selectedStyle ...renderer.Style) rende | |||||
| 132 | codeWidth = len([]rune(match.Command.Code)) | 132 | codeWidth = len([]rune(match.Command.Code)) | ||
| 133 | } | 133 | } | ||
| 134 | } | 134 | } | ||
| 135 | - | for y := 0; y < visible; y++ { | 135 | + | for y := range visible { |
| 136 | idx := m.scroll + y | 136 | idx := m.scroll + y | ||
| 137 | if idx >= len(m.matches) { | 137 | if idx >= len(m.matches) { | ||
| 138 | break | 138 | break | ||