fix(render): scope captured frames to pane identity
- commit
- 588d04f3da3e1c82ad9c320344804740ef689d9d
- author
- brice · · 2026-07-13 06:18 UTC
- committer
- brice · · 2026-07-13 06:18 UTC
- parent
- a4dfd3e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +62 -23
M internal/usecase/daemon/client.go +16 -15
| @@ -14,7 +14,6 @@ import ( | |||
| 14 | 14 | "github.com/bnema/vev/internal/domain" | |
| 15 | 15 | "github.com/bnema/vev/internal/ports" | |
| 16 | 16 | "github.com/bnema/vev/internal/usecase/keys" | |
| 17 | - | "github.com/bnema/vev/internal/usecase/layout" | |
| 18 | 17 | "github.com/bnema/vev/internal/usecase/mouse" | |
| 19 | 18 | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 20 | 19 | ) | |
| @@ -41,17 +40,19 @@ type attachedClient struct { | |||
| 41 | 40 | // sendMu and must never share mutable backing storage. | |
| 42 | 41 | pipelineCache composeCacheInput | |
| 43 | 42 | pipelineScratch composeCacheInput | |
| 44 | - | renderScratch renderCaptureScratch // only touched while sendMu is held | |
| 45 | - | captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held | |
| 46 | - | size domain.Size | |
| 47 | - | keys *keys.Router | |
| 48 | - | sess Guarded[*session] | |
| 49 | - | mouseScan mouse.Scanner | |
| 50 | - | themeMu sync.Mutex | |
| 51 | - | theme themeui.Theme | |
| 52 | - | clientTheme themeui.Theme | |
| 53 | - | lastCursor cursorOut | |
| 54 | - | renderStages renderStageHooks // invoked at real pipeline boundaries while sendMu is held | |
| 43 | + | renderScratch renderCaptureScratch // only touched while sendMu is held | |
| 44 | + | // captureFrames is keyed by pane ownership, not the tab-local PaneID, so | |
| 45 | + | // snapshots cannot leak when an attachment switches tabs or sessions. | |
| 46 | + | captureFrames map[*pane]capturedPaneRenderState // only touched while sendMu is held | |
| 47 | + | size domain.Size | |
| 48 | + | keys *keys.Router | |
| 49 | + | sess Guarded[*session] | |
| 50 | + | mouseScan mouse.Scanner | |
| 51 | + | themeMu sync.Mutex | |
| 52 | + | theme themeui.Theme | |
| 53 | + | clientTheme themeui.Theme | |
| 54 | + | lastCursor cursorOut | |
| 55 | + | renderStages renderStageHooks // invoked at real pipeline boundaries while sendMu is held | |
| 55 | 56 | // previousSession is guarded independently. It is retained through temporary | |
| 56 | 57 | // setSession(nil) hand-offs and cleared only on terminal teardown. | |
| 57 | 58 | previousSession Guarded[*session] | |
| @@ -383,9 +384,9 @@ func (d *Daemon) handoffCoordinator(from, target *session, old, current *attache | |||
| 383 | 384 | } | |
| 384 | 385 | current.sendMu.Lock() | |
| 385 | 386 | current.output.rebase() | |
| 386 | - | // Capture snapshots are attachment-owned but pane IDs are only session- | |
| 387 | - | // local. Discard them while the attachment is exclusively held so a pane | |
| 388 | - | // in target cannot reuse pixels from an identically named source pane. | |
| 387 | + | // Capture snapshots are attachment-owned. Discard them while the attachment | |
| 388 | + | // is exclusively held so the transfer releases panes no longer reachable | |
| 389 | + | // from the target session. | |
| 389 | 390 | current.captureFrames = nil | |
| 390 | 391 | current.sendMu.Unlock() | |
| 391 | 392 | d.attachCoordinator(target, old, current, true) | |
| @@ -14,7 +14,6 @@ import ( | |||||
| 14 | "github.com/bnema/vev/internal/domain" | 14 | "github.com/bnema/vev/internal/domain" | ||
| 15 | "github.com/bnema/vev/internal/ports" | 15 | "github.com/bnema/vev/internal/ports" | ||
| 16 | "github.com/bnema/vev/internal/usecase/keys" | 16 | "github.com/bnema/vev/internal/usecase/keys" | ||
| 17 | - | "github.com/bnema/vev/internal/usecase/layout" | |||
| 18 | "github.com/bnema/vev/internal/usecase/mouse" | 17 | "github.com/bnema/vev/internal/usecase/mouse" | ||
| 19 | themeui "github.com/bnema/vev/internal/usecase/theme" | 18 | themeui "github.com/bnema/vev/internal/usecase/theme" | ||
| 20 | ) | 19 | ) | ||
| @@ -41,17 +40,19 @@ type attachedClient struct { | |||||
| 41 | // sendMu and must never share mutable backing storage. | 40 | // sendMu and must never share mutable backing storage. | ||
| 42 | pipelineCache composeCacheInput | 41 | pipelineCache composeCacheInput | ||
| 43 | pipelineScratch composeCacheInput | 42 | pipelineScratch composeCacheInput | ||
| 44 | - | renderScratch renderCaptureScratch // only touched while sendMu is held | 43 | + | renderScratch renderCaptureScratch // only touched while sendMu is held |
| 45 | - | captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held | 44 | + | // captureFrames is keyed by pane ownership, not the tab-local PaneID, so |
| 46 | - | size domain.Size | 45 | + | // snapshots cannot leak when an attachment switches tabs or sessions. |
| 47 | - | keys *keys.Router | 46 | + | captureFrames map[*pane]capturedPaneRenderState // only touched while sendMu is held |
| 48 | - | sess Guarded[*session] | 47 | + | size domain.Size |
| 49 | - | mouseScan mouse.Scanner | 48 | + | keys *keys.Router |
| 50 | - | themeMu sync.Mutex | 49 | + | sess Guarded[*session] |
| 51 | - | theme themeui.Theme | 50 | + | mouseScan mouse.Scanner |
| 52 | - | clientTheme themeui.Theme | 51 | + | themeMu sync.Mutex |
| 53 | - | lastCursor cursorOut | 52 | + | theme themeui.Theme |
| 54 | - | renderStages renderStageHooks // invoked at real pipeline boundaries while sendMu is held | 53 | + | clientTheme themeui.Theme |
| 54 | + | lastCursor cursorOut | |||
| 55 | + | renderStages renderStageHooks // invoked at real pipeline boundaries while sendMu is held | |||
| 55 | // previousSession is guarded independently. It is retained through temporary | 56 | // previousSession is guarded independently. It is retained through temporary | ||
| 56 | // setSession(nil) hand-offs and cleared only on terminal teardown. | 57 | // setSession(nil) hand-offs and cleared only on terminal teardown. | ||
| 57 | previousSession Guarded[*session] | 58 | previousSession Guarded[*session] | ||
| @@ -383,9 +384,9 @@ func (d *Daemon) handoffCoordinator(from, target *session, old, current *attache | |||||
| 383 | } | 384 | } | ||
| 384 | current.sendMu.Lock() | 385 | current.sendMu.Lock() | ||
| 385 | current.output.rebase() | 386 | current.output.rebase() | ||
| 386 | - | // Capture snapshots are attachment-owned but pane IDs are only session- | 387 | + | // Capture snapshots are attachment-owned. Discard them while the attachment |
| 387 | - | // local. Discard them while the attachment is exclusively held so a pane | 388 | + | // is exclusively held so the transfer releases panes no longer reachable |
| 388 | - | // in target cannot reuse pixels from an identically named source pane. | 389 | + | // from the target session. |
| 389 | current.captureFrames = nil | 390 | current.captureFrames = nil | ||
| 390 | current.sendMu.Unlock() | 391 | current.sendMu.Unlock() | ||
| 391 | d.attachCoordinator(target, old, current, true) | 392 | d.attachCoordinator(target, old, current, true) | ||
M internal/usecase/daemon/input_test.go +34 -0
| @@ -87,6 +87,40 @@ func TestAltDigitSwitchesBetweenThreeTabs(t *testing.T) { | |||
| 87 | 87 | d.sessWg.Wait() | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) { | |
| 91 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, nil, nil) | |
| 92 | + | source := sess.tabs[0].focusedPane() | |
| 93 | + | target := sess.tabs[1].focusedPane() | |
| 94 | + | require.Equal(t, source.id, target.id, "tabs deliberately reuse pane-1") | |
| 95 | + | ||
| 96 | + | source.mu.Lock() | |
| 97 | + | source.screen.Write([]byte("source")) | |
| 98 | + | source.mu.Unlock() | |
| 99 | + | d.paint(sess, ac, true) | |
| 100 | + | first := <-sends | |
| 101 | + | firstOutput, err := ports.UnmarshalOutput(first.Payload) | |
| 102 | + | require.NoError(t, err) | |
| 103 | + | terminal := vt.NewScreen(ac.size.Cols, ac.size.Rows) | |
| 104 | + | terminal.Write(firstOutput.Data) | |
| 105 | + | ||
| 106 | + | // A clean pane relies on the attachment capture cache for its retained | |
| 107 | + | // snapshot. Both tabs deliberately use the tab-local default pane ID. | |
| 108 | + | target.mu.Lock() | |
| 109 | + | target.screen.ClearDamage() | |
| 110 | + | target.mu.Unlock() | |
| 111 | + | // Use the real key action: it requests the mandatory complete repaint for | |
| 112 | + | // the first target-tab frame. | |
| 113 | + | daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2) | |
| 114 | + | require.Equal(t, 1, activeTabIndex(sess)) | |
| 115 | + | ||
| 116 | + | second := <-sends | |
| 117 | + | secondOutput, err := ports.UnmarshalOutput(second.Payload) | |
| 118 | + | require.NoError(t, err) | |
| 119 | + | require.Zero(t, secondOutput.BaseStateNum, "tab switch must emit the complete target frame first") | |
| 120 | + | terminal.Write(secondOutput.Data) | |
| 121 | + | require.NotContains(t, strings.Join(frameRows(terminal.Frame), "\n"), "source", "the first target-tab frame must not retain source pane cells") | |
| 122 | + | } | |
| 123 | + | ||
| 90 | 124 | func TestAltCForwardsToPTY(t *testing.T) { | |
| 91 | 125 | writes := make(chan []byte, 2) | |
| 92 | 126 | p, releasePTY := newBlockingPTYWithWrites(t, writes) | |
| @@ -87,6 +87,40 @@ func TestAltDigitSwitchesBetweenThreeTabs(t *testing.T) { | |||||
| 87 | d.sessWg.Wait() | 87 | d.sessWg.Wait() | ||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | + | func TestSwitchTabFirstFrameDoesNotReuseSamePaneIDCapture(t *testing.T) { | |||
| 91 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, nil, nil) | |||
| 92 | + | source := sess.tabs[0].focusedPane() | |||
| 93 | + | target := sess.tabs[1].focusedPane() | |||
| 94 | + | require.Equal(t, source.id, target.id, "tabs deliberately reuse pane-1") | |||
| 95 | + | ||||
| 96 | + | source.mu.Lock() | |||
| 97 | + | source.screen.Write([]byte("source")) | |||
| 98 | + | source.mu.Unlock() | |||
| 99 | + | d.paint(sess, ac, true) | |||
| 100 | + | first := <-sends | |||
| 101 | + | firstOutput, err := ports.UnmarshalOutput(first.Payload) | |||
| 102 | + | require.NoError(t, err) | |||
| 103 | + | terminal := vt.NewScreen(ac.size.Cols, ac.size.Rows) | |||
| 104 | + | terminal.Write(firstOutput.Data) | |||
| 105 | + | ||||
| 106 | + | // A clean pane relies on the attachment capture cache for its retained | |||
| 107 | + | // snapshot. Both tabs deliberately use the tab-local default pane ID. | |||
| 108 | + | target.mu.Lock() | |||
| 109 | + | target.screen.ClearDamage() | |||
| 110 | + | target.mu.Unlock() | |||
| 111 | + | // Use the real key action: it requests the mandatory complete repaint for | |||
| 112 | + | // the first target-tab frame. | |||
| 113 | + | daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2) | |||
| 114 | + | require.Equal(t, 1, activeTabIndex(sess)) | |||
| 115 | + | ||||
| 116 | + | second := <-sends | |||
| 117 | + | secondOutput, err := ports.UnmarshalOutput(second.Payload) | |||
| 118 | + | require.NoError(t, err) | |||
| 119 | + | require.Zero(t, secondOutput.BaseStateNum, "tab switch must emit the complete target frame first") | |||
| 120 | + | terminal.Write(secondOutput.Data) | |||
| 121 | + | require.NotContains(t, strings.Join(frameRows(terminal.Frame), "\n"), "source", "the first target-tab frame must not retain source pane cells") | |||
| 122 | + | } | |||
| 123 | + | ||||
| 90 | func TestAltCForwardsToPTY(t *testing.T) { | 124 | func TestAltCForwardsToPTY(t *testing.T) { | ||
| 91 | writes := make(chan []byte, 2) | 125 | writes := make(chan []byte, 2) | ||
| 92 | p, releasePTY := newBlockingPTYWithWrites(t, writes) | 126 | p, releasePTY := newBlockingPTYWithWrites(t, writes) | ||
M internal/usecase/daemon/render_capture.go +12 -8
| @@ -132,7 +132,8 @@ func capturePaneRenderStateLockedInto(p *pane, visible domain.Rect, _ damageCapt | |||
| 132 | 132 | width := min(max(visible.Width, 0), p.screen.Frame.Width) | |
| 133 | 133 | height := min(max(visible.Height, 0), p.screen.Frame.Height) | |
| 134 | 134 | needsFrame := len(damage.Damage) > 0 | |
| 135 | - | if out.frame.Width != width || out.frame.Height != height { | |
| 135 | + | frameChanged := out.frame.Width != width || out.frame.Height != height | |
| 136 | + | if frameChanged { | |
| 136 | 137 | out.frame = renderer.NewFrame(width, height) | |
| 137 | 138 | needsFrame = true | |
| 138 | 139 | } | |
| @@ -144,7 +145,10 @@ func capturePaneRenderStateLockedInto(p *pane, visible domain.Rect, _ damageCapt | |||
| 144 | 145 | } | |
| 145 | 146 | } | |
| 146 | 147 | ||
| 147 | - | if uncertainDamage(damage.Damage, p.screen.Frame.Width, p.screen.Frame.Height) { | |
| 148 | + | if frameChanged || uncertainDamage(damage.Damage, p.screen.Frame.Width, p.screen.Frame.Height) { | |
| 149 | + | // A cache entry with a new frame has no terminal-shadow equivalent. | |
| 150 | + | // Redraw it even when the pane has no VT damage, such as an untouched | |
| 151 | + | // pane first made visible after a tab or session switch. | |
| 148 | 152 | out.damage = []renderer.Damage{renderer.FullRedraw()} | |
| 149 | 153 | } else { | |
| 150 | 154 | out.damage = append(out.damage[:0], damage.Damage...) | |
| @@ -232,9 +236,9 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 232 | 236 | } | |
| 233 | 237 | p.mu.Lock() | |
| 234 | 238 | if ac.captureFrames == nil { | |
| 235 | - | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | |
| 239 | + | ac.captureFrames = make(map[*pane]capturedPaneRenderState) | |
| 236 | 240 | } | |
| 237 | - | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id]) | |
| 241 | + | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p]) | |
| 238 | 242 | if mode == damageCaptureConsume { | |
| 239 | 243 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | |
| 240 | 244 | } | |
| @@ -248,7 +252,7 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 248 | 252 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | |
| 249 | 253 | } | |
| 250 | 254 | captured.rawDamage, captured.damage = captured.damage, translated | |
| 251 | - | ac.captureFrames[captured.id] = captured | |
| 255 | + | ac.captureFrames[p] = captured | |
| 252 | 256 | state.panes = append(state.panes, captured) | |
| 253 | 257 | } | |
| 254 | 258 | scratch.panes = state.panes | |
| @@ -257,9 +261,9 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 257 | 261 | p.mu.Lock() | |
| 258 | 262 | geometry := p.committedFloatingGeometryLocked(calculateContentFloatingGeometry(domain.Size{Cols: layoutSnap.area.Width, Rows: layoutSnap.area.Height}, floatingCfg)) | |
| 259 | 263 | if ac.captureFrames == nil { | |
| 260 | - | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | |
| 264 | + | ac.captureFrames = make(map[*pane]capturedPaneRenderState) | |
| 261 | 265 | } | |
| 262 | - | captured := capturePaneRenderStateLockedInto(p, geometry.Inner, mode, ac.captureFrames[p.id]) | |
| 266 | + | captured := capturePaneRenderStateLockedInto(p, geometry.Inner, mode, ac.captureFrames[p]) | |
| 263 | 267 | if mode == damageCaptureConsume { | |
| 264 | 268 | seen := false | |
| 265 | 269 | for _, receipt := range state.receipts { | |
| @@ -272,7 +276,7 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 272 | 276 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | |
| 273 | 277 | } | |
| 274 | 278 | } | |
| 275 | - | ac.captureFrames[captured.id] = captured | |
| 279 | + | ac.captureFrames[p] = captured | |
| 276 | 280 | state.floating = capturedFloatingRenderState{visible: true, pane: captured, geometry: geometry, title: captured.title, generation: tb.floating.generation, titleGeneration: captured.titleGeneration} | |
| 277 | 281 | state.cursor = captureCursorInputsLocked(p, geometry.Inner, overlays) | |
| 278 | 282 | p.mu.Unlock() | |
| @@ -132,7 +132,8 @@ func capturePaneRenderStateLockedInto(p *pane, visible domain.Rect, _ damageCapt | |||||
| 132 | width := min(max(visible.Width, 0), p.screen.Frame.Width) | 132 | width := min(max(visible.Width, 0), p.screen.Frame.Width) | ||
| 133 | height := min(max(visible.Height, 0), p.screen.Frame.Height) | 133 | height := min(max(visible.Height, 0), p.screen.Frame.Height) | ||
| 134 | needsFrame := len(damage.Damage) > 0 | 134 | needsFrame := len(damage.Damage) > 0 | ||
| 135 | - | if out.frame.Width != width || out.frame.Height != height { | 135 | + | frameChanged := out.frame.Width != width || out.frame.Height != height |
| 136 | + | if frameChanged { | |||
| 136 | out.frame = renderer.NewFrame(width, height) | 137 | out.frame = renderer.NewFrame(width, height) | ||
| 137 | needsFrame = true | 138 | needsFrame = true | ||
| 138 | } | 139 | } | ||
| @@ -144,7 +145,10 @@ func capturePaneRenderStateLockedInto(p *pane, visible domain.Rect, _ damageCapt | |||||
| 144 | } | 145 | } | ||
| 145 | } | 146 | } | ||
| 146 | 147 | ||||
| 147 | - | if uncertainDamage(damage.Damage, p.screen.Frame.Width, p.screen.Frame.Height) { | 148 | + | if frameChanged || uncertainDamage(damage.Damage, p.screen.Frame.Width, p.screen.Frame.Height) { |
| 149 | + | // A cache entry with a new frame has no terminal-shadow equivalent. | |||
| 150 | + | // Redraw it even when the pane has no VT damage, such as an untouched | |||
| 151 | + | // pane first made visible after a tab or session switch. | |||
| 148 | out.damage = []renderer.Damage{renderer.FullRedraw()} | 152 | out.damage = []renderer.Damage{renderer.FullRedraw()} | ||
| 149 | } else { | 153 | } else { | ||
| 150 | out.damage = append(out.damage[:0], damage.Damage...) | 154 | out.damage = append(out.damage[:0], damage.Damage...) | ||
| @@ -232,9 +236,9 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 232 | } | 236 | } | ||
| 233 | p.mu.Lock() | 237 | p.mu.Lock() | ||
| 234 | if ac.captureFrames == nil { | 238 | if ac.captureFrames == nil { | ||
| 235 | - | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | 239 | + | ac.captureFrames = make(map[*pane]capturedPaneRenderState) |
| 236 | } | 240 | } | ||
| 237 | - | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id]) | 241 | + | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p]) |
| 238 | if mode == damageCaptureConsume { | 242 | if mode == damageCaptureConsume { | ||
| 239 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | 243 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | ||
| 240 | } | 244 | } | ||
| @@ -248,7 +252,7 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 248 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | 252 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | ||
| 249 | } | 253 | } | ||
| 250 | captured.rawDamage, captured.damage = captured.damage, translated | 254 | captured.rawDamage, captured.damage = captured.damage, translated | ||
| 251 | - | ac.captureFrames[captured.id] = captured | 255 | + | ac.captureFrames[p] = captured |
| 252 | state.panes = append(state.panes, captured) | 256 | state.panes = append(state.panes, captured) | ||
| 253 | } | 257 | } | ||
| 254 | scratch.panes = state.panes | 258 | scratch.panes = state.panes | ||
| @@ -257,9 +261,9 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 257 | p.mu.Lock() | 261 | p.mu.Lock() | ||
| 258 | geometry := p.committedFloatingGeometryLocked(calculateContentFloatingGeometry(domain.Size{Cols: layoutSnap.area.Width, Rows: layoutSnap.area.Height}, floatingCfg)) | 262 | geometry := p.committedFloatingGeometryLocked(calculateContentFloatingGeometry(domain.Size{Cols: layoutSnap.area.Width, Rows: layoutSnap.area.Height}, floatingCfg)) | ||
| 259 | if ac.captureFrames == nil { | 263 | if ac.captureFrames == nil { | ||
| 260 | - | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | 264 | + | ac.captureFrames = make(map[*pane]capturedPaneRenderState) |
| 261 | } | 265 | } | ||
| 262 | - | captured := capturePaneRenderStateLockedInto(p, geometry.Inner, mode, ac.captureFrames[p.id]) | 266 | + | captured := capturePaneRenderStateLockedInto(p, geometry.Inner, mode, ac.captureFrames[p]) |
| 263 | if mode == damageCaptureConsume { | 267 | if mode == damageCaptureConsume { | ||
| 264 | seen := false | 268 | seen := false | ||
| 265 | for _, receipt := range state.receipts { | 269 | for _, receipt := range state.receipts { | ||
| @@ -272,7 +276,7 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 272 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | 276 | state.receipts = append(state.receipts, damageReceipt{pane: p, generation: captured.damageGeneration}) | ||
| 273 | } | 277 | } | ||
| 274 | } | 278 | } | ||
| 275 | - | ac.captureFrames[captured.id] = captured | 279 | + | ac.captureFrames[p] = captured |
| 276 | state.floating = capturedFloatingRenderState{visible: true, pane: captured, geometry: geometry, title: captured.title, generation: tb.floating.generation, titleGeneration: captured.titleGeneration} | 280 | state.floating = capturedFloatingRenderState{visible: true, pane: captured, geometry: geometry, title: captured.title, generation: tb.floating.generation, titleGeneration: captured.titleGeneration} | ||
| 277 | state.cursor = captureCursorInputsLocked(p, geometry.Inner, overlays) | 281 | state.cursor = captureCursorInputsLocked(p, geometry.Inner, overlays) | ||
| 278 | p.mu.Unlock() | 282 | p.mu.Unlock() | ||