detached 533cc8f

perf(daemon): reuse attachment render capture scratch

commit
533cc8ff3bfe53f70bc7c2a4cea40585e4202f05
author
brice · · 2026-07-12 06:11 UTC
parent
9e9e07f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +93 -20
M internal/usecase/daemon/client.go +1 -0
@@ -37,6 +37,7 @@ type attachedClient struct {
3737 coordinatorReadyEpoch atomic.Uint64
3838 echoAck atomic.Uint64
3939 pipelineCache composeCacheInput // only touched while sendMu is held
40+ renderScratch renderCaptureScratch // only touched while sendMu is held
4041 captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held
4142 resizePaint pendingByteTimer // guarded by sendMu
4243 resizePaintGeneration uint64 // guarded by sendMu
@@ -37,6 +37,7 @@ type attachedClient struct {
37 coordinatorReadyEpoch atomic.Uint6437 coordinatorReadyEpoch atomic.Uint64
38 echoAck atomic.Uint6438 echoAck atomic.Uint64
39 pipelineCache composeCacheInput // only touched while sendMu is held39 pipelineCache composeCacheInput // only touched while sendMu is held
40+ renderScratch renderCaptureScratch // only touched while sendMu is held
40 captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held41 captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held
41 resizePaint pendingByteTimer // guarded by sendMu42 resizePaint pendingByteTimer // guarded by sendMu
42 resizePaintGeneration uint64 // guarded by sendMu43 resizePaintGeneration uint64 // guarded by sendMu
M internal/usecase/daemon/render.go +5 -2
@@ -309,8 +309,11 @@ func (d *Daemon) paintForResizeGeneration(sess *session, ac *attachedClient, res
309309 // Title refresh may inspect process state, so it remains before the capture
310310 // boundary and outside tab/pane ownership locks.
311311 tb.mu.Lock()
312- layoutSnap := solveTabLayoutLocked(tb)
313- titleIDs := titleBarPaneIDs(layoutSnap.placements, layoutSnap.ok)
312+ titleIDs := ac.renderScratch.titleIDs[:0]
313+ if tb.tree != nil {
314+ titleIDs = appendStackPaneIDs(titleIDs, tb.tree.Root)
315+ }
316+ ac.renderScratch.titleIDs = titleIDs
314317 floating := tb.floating.pane
315318 hasFloating := tb.floating.state == floatingVisible && floating != nil
316319 tb.mu.Unlock()
@@ -309,8 +309,11 @@ func (d *Daemon) paintForResizeGeneration(sess *session, ac *attachedClient, res
309 // Title refresh may inspect process state, so it remains before the capture309 // Title refresh may inspect process state, so it remains before the capture
310 // boundary and outside tab/pane ownership locks.310 // boundary and outside tab/pane ownership locks.
311 tb.mu.Lock()311 tb.mu.Lock()
312- layoutSnap := solveTabLayoutLocked(tb)312+ titleIDs := ac.renderScratch.titleIDs[:0]
313- titleIDs := titleBarPaneIDs(layoutSnap.placements, layoutSnap.ok)313+ if tb.tree != nil {
314+ titleIDs = appendStackPaneIDs(titleIDs, tb.tree.Root)
315+ }
316+ ac.renderScratch.titleIDs = titleIDs
314 floating := tb.floating.pane317 floating := tb.floating.pane
315 hasFloating := tb.floating.state == floatingVisible && floating != nil318 hasFloating := tb.floating.state == floatingVisible && floating != nil
316 tb.mu.Unlock()319 tb.mu.Unlock()
M internal/usecase/daemon/render_capture.go +80 -15
@@ -23,6 +23,21 @@ const (
2323 // capturedRenderState is the immutable hand-off from authoritative daemon
2424 // state to composition. Every field is a value or an owned bounded copy; the
2525 // composer must not retain or consult session, tab, pane, or overlay owners.
26+// renderCaptureScratch owns bounded snapshots for one attachment. The complete
27+// capture/compose/emit transaction holds that attachment's sendMu, so these
28+// buffers cannot be observed or reused before emission completes.
29+type renderCaptureScratch struct {
30+ state capturedRenderState
31+ panes []capturedPaneRenderState
32+ placements []layout.Placement
33+ statusTabs []statusTab
34+ mru []recentSession
35+ ranked []rankedRecent
36+ titleIDs []layout.PaneID
37+ treeNodes []layout.Node
38+ treeUsed int
39+}
40+
2641 type capturedRenderState struct {
2742 attachment *attachedClient // identity only; never dereferenced by composition
2843 attachmentEpoch uint64
@@ -170,25 +185,36 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
170185 epoch := ac.coordinatorEpoch.Load()
171186 sess.mu.Unlock()
172187
173- bars.status.tabs = append([]statusTab(nil), bars.status.tabs...)
174- bars.mru = append([]recentSession(nil), bars.mru...)
175- bars.rankedRecent = append([]rankedRecent(nil), bars.rankedRecent...)
176- preview = clonePickerPreview(preview)
188+ scratch := &ac.renderScratch
189+ scratch.statusTabs = append(scratch.statusTabs[:0], bars.status.tabs...)
190+ bars.status.tabs = scratch.statusTabs
191+ scratch.mru = append(scratch.mru[:0], bars.mru...)
192+ bars.mru = scratch.mru
193+ scratch.ranked = append(scratch.ranked[:0], bars.rankedRecent...)
194+ bars.rankedRecent = scratch.ranked
195+ if len(preview.Rows) > 0 {
196+ preview = clonePickerPreview(preview)
197+ } else {
198+ preview = picker.Preview{Width: preview.Width, Height: preview.Height}
199+ }
177200
178201 tb.mu.Lock()
179202 defer tb.mu.Unlock()
180203 layoutSnap := solveTabLayoutLocked(tb)
181- state := &capturedRenderState{
204+ scratch.placements = append(scratch.placements[:0], layoutSnap.placements...)
205+ scratch.treeUsed = 0
206+ var root *layout.Node
207+ if tb.tree != nil {
208+ root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch)
209+ }
210+ state := &scratch.state
211+ *state = capturedRenderState{
182212 attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme,
183213 overlays: overlays, preview: preview,
184- layout: capturedTabLayout{root: func() *layout.Node {
185- if tb.tree == nil {
186- return nil
187- }
188- return tb.tree.Clone().Root
189- }(), area: layoutSnap.area, focus: layoutSnap.focus, placements: append([]layout.Placement(nil), layoutSnap.placements...), fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok},
214+ layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok},
190215 floatingGeneration: tb.floating.generation,
191216 }
217+ state.panes = scratch.panes[:0]
192218 state.tabGeneration = uint64(len(layoutSnap.fingerprint))
193219 for _, placement := range layoutSnap.placements {
194220 p := tb.panes[placement.ID]
@@ -204,20 +230,20 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
204230 ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState)
205231 }
206232 captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id])
207- captured.placement = placement
208- captured.focused = placement.ID == layoutSnap.focus
233+ captured.placement, captured.focused = placement, placement.ID == layoutSnap.focus
209234 if captured.focused {
210235 state.cursor = captureCursorInputsLocked(p, placement.Content, overlays)
211236 }
212237 p.mu.Unlock()
213- translated := make([]renderer.Damage, 0, len(captured.damage))
238+ translated := captured.rawDamage[:0]
214239 for _, damage := range captured.damage {
215240 translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...)
216241 }
217- captured.damage = translated
242+ captured.rawDamage, captured.damage = captured.damage, translated
218243 ac.captureFrames[captured.id] = captured
219244 state.panes = append(state.panes, captured)
220245 }
246+ scratch.panes = state.panes
221247 if tb.floating.state == floatingVisible && tb.floating.pane != nil {
222248 p := tb.floating.pane
223249 p.mu.Lock()
@@ -234,6 +260,25 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
234260 return state, true
235261 }
236262
263+func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node {
264+ if src == nil {
265+ return nil
266+ }
267+ if scratch.treeUsed == len(scratch.treeNodes) {
268+ scratch.treeNodes = append(scratch.treeNodes, layout.Node{})
269+ }
270+ index := scratch.treeUsed
271+ scratch.treeUsed++
272+ dst := &scratch.treeNodes[index]
273+ children := dst.Children[:0]
274+ *dst = *src
275+ dst.Children = children
276+ for _, child := range src.Children {
277+ dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch))
278+ }
279+ return dst
280+}
281+
237282 func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs {
238283 style, hasStyle := p.screen.CursorStyle()
239284 hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive
@@ -247,3 +292,23 @@ func clonePickerPreview(in picker.Preview) picker.Preview {
247292 }
248293 return out
249294 }
295+
296+// appendStackPaneIDs returns exactly the panes whose solved placement can own a
297+// stack title bar. The caller holds tab.mu, and dst is attachment scratch.
298+func appendStackPaneIDs(dst []layout.PaneID, node *layout.Node) []layout.PaneID {
299+ if node == nil {
300+ return dst
301+ }
302+ if node.Kind == layout.Stack {
303+ for _, child := range node.Children {
304+ if child != nil && child.Kind == layout.Leaf {
305+ dst = append(dst, child.Leaf)
306+ }
307+ }
308+ return dst
309+ }
310+ for _, child := range node.Children {
311+ dst = appendStackPaneIDs(dst, child)
312+ }
313+ return dst
314+}
@@ -23,6 +23,21 @@ const (
23 // capturedRenderState is the immutable hand-off from authoritative daemon23 // capturedRenderState is the immutable hand-off from authoritative daemon
24 // state to composition. Every field is a value or an owned bounded copy; the24 // state to composition. Every field is a value or an owned bounded copy; the
25 // composer must not retain or consult session, tab, pane, or overlay owners.25 // composer must not retain or consult session, tab, pane, or overlay owners.
26+// renderCaptureScratch owns bounded snapshots for one attachment. The complete
27+// capture/compose/emit transaction holds that attachment's sendMu, so these
28+// buffers cannot be observed or reused before emission completes.
29+type renderCaptureScratch struct {
30+ state capturedRenderState
31+ panes []capturedPaneRenderState
32+ placements []layout.Placement
33+ statusTabs []statusTab
34+ mru []recentSession
35+ ranked []rankedRecent
36+ titleIDs []layout.PaneID
37+ treeNodes []layout.Node
38+ treeUsed int
39+}
40+
26 type capturedRenderState struct {41 type capturedRenderState struct {
27 attachment *attachedClient // identity only; never dereferenced by composition42 attachment *attachedClient // identity only; never dereferenced by composition
28 attachmentEpoch uint6443 attachmentEpoch uint64
@@ -170,25 +185,36 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
170 epoch := ac.coordinatorEpoch.Load()185 epoch := ac.coordinatorEpoch.Load()
171 sess.mu.Unlock()186 sess.mu.Unlock()
172 187
173- bars.status.tabs = append([]statusTab(nil), bars.status.tabs...)188+ scratch := &ac.renderScratch
174- bars.mru = append([]recentSession(nil), bars.mru...)189+ scratch.statusTabs = append(scratch.statusTabs[:0], bars.status.tabs...)
175- bars.rankedRecent = append([]rankedRecent(nil), bars.rankedRecent...)190+ bars.status.tabs = scratch.statusTabs
176- preview = clonePickerPreview(preview)191+ scratch.mru = append(scratch.mru[:0], bars.mru...)
192+ bars.mru = scratch.mru
193+ scratch.ranked = append(scratch.ranked[:0], bars.rankedRecent...)
194+ bars.rankedRecent = scratch.ranked
195+ if len(preview.Rows) > 0 {
196+ preview = clonePickerPreview(preview)
197+ } else {
198+ preview = picker.Preview{Width: preview.Width, Height: preview.Height}
199+ }
177 200
178 tb.mu.Lock()201 tb.mu.Lock()
179 defer tb.mu.Unlock()202 defer tb.mu.Unlock()
180 layoutSnap := solveTabLayoutLocked(tb)203 layoutSnap := solveTabLayoutLocked(tb)
181- state := &capturedRenderState{204+ scratch.placements = append(scratch.placements[:0], layoutSnap.placements...)
205+ scratch.treeUsed = 0
206+ var root *layout.Node
207+ if tb.tree != nil {
208+ root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch)
209+ }
210+ state := &scratch.state
211+ *state = capturedRenderState{
182 attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme,212 attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme,
183 overlays: overlays, preview: preview,213 overlays: overlays, preview: preview,
184- layout: capturedTabLayout{root: func() *layout.Node {214+ layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok},
185- if tb.tree == nil {
186- return nil
187- }
188- return tb.tree.Clone().Root
189- }(), area: layoutSnap.area, focus: layoutSnap.focus, placements: append([]layout.Placement(nil), layoutSnap.placements...), fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok},
190 floatingGeneration: tb.floating.generation,215 floatingGeneration: tb.floating.generation,
191 }216 }
217+ state.panes = scratch.panes[:0]
192 state.tabGeneration = uint64(len(layoutSnap.fingerprint))218 state.tabGeneration = uint64(len(layoutSnap.fingerprint))
193 for _, placement := range layoutSnap.placements {219 for _, placement := range layoutSnap.placements {
194 p := tb.panes[placement.ID]220 p := tb.panes[placement.ID]
@@ -204,20 +230,20 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
204 ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState)230 ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState)
205 }231 }
206 captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id])232 captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id])
207- captured.placement = placement233+ captured.placement, captured.focused = placement, placement.ID == layoutSnap.focus
208- captured.focused = placement.ID == layoutSnap.focus
209 if captured.focused {234 if captured.focused {
210 state.cursor = captureCursorInputsLocked(p, placement.Content, overlays)235 state.cursor = captureCursorInputsLocked(p, placement.Content, overlays)
211 }236 }
212 p.mu.Unlock()237 p.mu.Unlock()
213- translated := make([]renderer.Damage, 0, len(captured.damage))238+ translated := captured.rawDamage[:0]
214 for _, damage := range captured.damage {239 for _, damage := range captured.damage {
215 translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...)240 translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...)
216 }241 }
217- captured.damage = translated242+ captured.rawDamage, captured.damage = captured.damage, translated
218 ac.captureFrames[captured.id] = captured243 ac.captureFrames[captured.id] = captured
219 state.panes = append(state.panes, captured)244 state.panes = append(state.panes, captured)
220 }245 }
246+ scratch.panes = state.panes
221 if tb.floating.state == floatingVisible && tb.floating.pane != nil {247 if tb.floating.state == floatingVisible && tb.floating.pane != nil {
222 p := tb.floating.pane248 p := tb.floating.pane
223 p.mu.Lock()249 p.mu.Lock()
@@ -234,6 +260,25 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla
234 return state, true260 return state, true
235 }261 }
236 262
263+func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node {
264+ if src == nil {
265+ return nil
266+ }
267+ if scratch.treeUsed == len(scratch.treeNodes) {
268+ scratch.treeNodes = append(scratch.treeNodes, layout.Node{})
269+ }
270+ index := scratch.treeUsed
271+ scratch.treeUsed++
272+ dst := &scratch.treeNodes[index]
273+ children := dst.Children[:0]
274+ *dst = *src
275+ dst.Children = children
276+ for _, child := range src.Children {
277+ dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch))
278+ }
279+ return dst
280+}
281+
237 func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs {282 func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs {
238 style, hasStyle := p.screen.CursorStyle()283 style, hasStyle := p.screen.CursorStyle()
239 hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive284 hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive
@@ -247,3 +292,23 @@ func clonePickerPreview(in picker.Preview) picker.Preview {
247 }292 }
248 return out293 return out
249 }294 }
295+
296+// appendStackPaneIDs returns exactly the panes whose solved placement can own a
297+// stack title bar. The caller holds tab.mu, and dst is attachment scratch.
298+func appendStackPaneIDs(dst []layout.PaneID, node *layout.Node) []layout.PaneID {
299+ if node == nil {
300+ return dst
301+ }
302+ if node.Kind == layout.Stack {
303+ for _, child := range node.Children {
304+ if child != nil && child.Kind == layout.Leaf {
305+ dst = append(dst, child.Leaf)
306+ }
307+ }
308+ return dst
309+ }
310+ for _, child := range node.Children {
311+ dst = appendStackPaneIDs(dst, child)
312+ }
313+ return dst
314+}
M internal/usecase/daemon/render_pipeline.go +7 -3
@@ -21,6 +21,7 @@ type composeCacheInput struct {
2121 frame renderer.Frame
2222 layoutFingerprint string
2323 titleGenerations map[layout.PaneID]uint64
24+ damage []renderer.Damage
2425 floatingGeneration uint64
2526 floatingGeometry floatingGeometry
2627 floatingTitleGeneration uint64
@@ -60,8 +61,11 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende
6061 }
6162
6263 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme
63- titles := make(map[layout.PaneID]uint64, len(state.panes))
64- var damage []renderer.Damage
64+ titles := in.titleGenerations
65+ if titles == nil {
66+ titles = make(map[layout.PaneID]uint64, len(state.panes))
67+ }
68+ damage := in.damage[:0]
6569 for _, pane := range state.panes {
6670 pl := offsetPlacement(pane.placement, 0, 1)
6771 if pl.TitleBar.Height > 0 {
@@ -110,7 +114,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende
110114 cursorInputs := state.cursor
111115 cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active()
112116 cursor := desiredCapturedCursor(cursorInputs)
113- outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars}
117+ outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, damage: damage, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars}
114118 outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1))
115119 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}
116120 }
@@ -21,6 +21,7 @@ type composeCacheInput struct {
21 frame renderer.Frame21 frame renderer.Frame
22 layoutFingerprint string22 layoutFingerprint string
23 titleGenerations map[layout.PaneID]uint6423 titleGenerations map[layout.PaneID]uint64
24+ damage []renderer.Damage
24 floatingGeneration uint6425 floatingGeneration uint64
25 floatingGeometry floatingGeometry26 floatingGeometry floatingGeometry
26 floatingTitleGeneration uint6427 floatingTitleGeneration uint64
@@ -60,8 +61,11 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende
60 }61 }
61 62
62 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme63 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme
63- titles := make(map[layout.PaneID]uint64, len(state.panes))64+ titles := in.titleGenerations
64- var damage []renderer.Damage65+ if titles == nil {
66+ titles = make(map[layout.PaneID]uint64, len(state.panes))
67+ }
68+ damage := in.damage[:0]
65 for _, pane := range state.panes {69 for _, pane := range state.panes {
66 pl := offsetPlacement(pane.placement, 0, 1)70 pl := offsetPlacement(pane.placement, 0, 1)
67 if pl.TitleBar.Height > 0 {71 if pl.TitleBar.Height > 0 {
@@ -110,7 +114,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende
110 cursorInputs := state.cursor114 cursorInputs := state.cursor
111 cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active()115 cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active()
112 cursor := desiredCapturedCursor(cursorInputs)116 cursor := desiredCapturedCursor(cursorInputs)
113- outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars}117+ outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, damage: damage, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars}
114 outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1))118 outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1))
115 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}119 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}
116 }120 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close