detached 640f800

refactor(core): group Core's lock state and sent caches into sub-structs

commit
640f8005b6d59f19b0a3a9edf79f91e645274a52
author
brice · · 2026-10-08 05:54 UTC
parent
794cca9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +45 -33
M internal/core/core.go +16 -12
@@ -76,9 +76,7 @@ type Core struct {
7676 // workspace while overviewReal is filled) and by state(); never retained.
7777 realBuf []Placement
7878 security ports.SecurityState
79- lockSurfaces []ports.LockSurfacePlacement
80- lockFocus WindowID
81- lockPinned bool // lockFocus was clicked or typed into: it keeps focus
79+ lock lockState
8280 inputKeys map[string]bool
8381 inputActive bool
8482 startup [][]string
@@ -115,18 +113,15 @@ type Core struct {
115113 seq uint64
116114 // layerChanged is set once layer state arrives from wayland.
117115 layerChanged bool
118- sentOutputs ports.SetOutputs
116+ sent sentCache
119117 specs []NamedWorkspace
120118 presets []Width
121119 slots map[slotKey]*slotState
122120 placement spawnPlacement
123121 // toSpawn holds slots to start; Run sends them (apply has no context).
124122 toSpawn []slotKey
125- sentPending bool
126123 firstTerminalResolved bool
127124 windows windowRegistry
128- sentState ports.State
129- sentWorkspaces ports.Workspaces
130125 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
131126 popups map[WindowID]*popupState
132127 popupOrder []WindowID
@@ -154,6 +149,15 @@ type Core struct {
154149 pulse focusPulse
155150 }
156151
152+// sentCache is what core last sent on its latest-only channels; an
153+// unchanged value is not sent again.
154+type sentCache struct {
155+ outputs ports.SetOutputs
156+ state ports.State
157+ workspaces ports.Workspaces
158+ pending bool // spawn placement pending
159+}
160+
157161 func keyName(s string) string {
158162 if len(s) == 1 && unicode.IsLetter(rune(s[0])) {
159163 return strings.ToLower(s)
@@ -314,11 +318,11 @@ func (c *Core) apply(cfg ports.Config) error {
314318 // publishPending tells wayland at once when slots start waiting, before
315319 // their windows can map.
316320 func (c *Core) publishPending(ctx context.Context) error {
317- if p := c.placement.anyPending(); p != c.sentPending {
321+ if p := c.placement.anyPending(); p != c.sent.pending {
318322 if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
319323 return err
320324 }
321- c.sentPending = p
325+ c.sent.pending = p
322326 }
323327 return nil
324328 }
@@ -835,16 +839,16 @@ func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
835839 // geometry only when the outputs themselves moved.
836840 func (c *Core) syncOutputs(ctx context.Context) error {
837841 v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
838- if sameOutputs(v, c.sentOutputs) {
842+ if sameOutputs(v, c.sent.outputs) {
839843 return nil
840844 }
841845 if err := c.command(ctx, v); err != nil {
842846 return err
843847 }
844- if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {
848+ if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sent.outputs.Outputs) {
845849 latest(c.ch.Layouts, v.Outputs)
846850 }
847- c.sentOutputs = v
851+ c.sent.outputs = v
848852 return nil
849853 }
850854
@@ -76,9 +76,7 @@ type Core struct {
76 // workspace while overviewReal is filled) and by state(); never retained.76 // workspace while overviewReal is filled) and by state(); never retained.
77 realBuf []Placement77 realBuf []Placement
78 security ports.SecurityState78 security ports.SecurityState
79- lockSurfaces []ports.LockSurfacePlacement79+ lock lockState
80- lockFocus WindowID
81- lockPinned bool // lockFocus was clicked or typed into: it keeps focus
82 inputKeys map[string]bool80 inputKeys map[string]bool
83 inputActive bool81 inputActive bool
84 startup [][]string82 startup [][]string
@@ -115,18 +113,15 @@ type Core struct {
115 seq uint64113 seq uint64
116 // layerChanged is set once layer state arrives from wayland.114 // layerChanged is set once layer state arrives from wayland.
117 layerChanged bool115 layerChanged bool
118- sentOutputs ports.SetOutputs116+ sent sentCache
119 specs []NamedWorkspace117 specs []NamedWorkspace
120 presets []Width118 presets []Width
121 slots map[slotKey]*slotState119 slots map[slotKey]*slotState
122 placement spawnPlacement120 placement spawnPlacement
123 // toSpawn holds slots to start; Run sends them (apply has no context).121 // toSpawn holds slots to start; Run sends them (apply has no context).
124 toSpawn []slotKey122 toSpawn []slotKey
125- sentPending bool
126 firstTerminalResolved bool123 firstTerminalResolved bool
127 windows windowRegistry124 windows windowRegistry
128- sentState ports.State
129- sentWorkspaces ports.Workspaces
130 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.125 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
131 popups map[WindowID]*popupState126 popups map[WindowID]*popupState
132 popupOrder []WindowID127 popupOrder []WindowID
@@ -154,6 +149,15 @@ type Core struct {
154 pulse focusPulse149 pulse focusPulse
155 }150 }
156 151
152+// sentCache is what core last sent on its latest-only channels; an
153+// unchanged value is not sent again.
154+type sentCache struct {
155+ outputs ports.SetOutputs
156+ state ports.State
157+ workspaces ports.Workspaces
158+ pending bool // spawn placement pending
159+}
160+
157 func keyName(s string) string {161 func keyName(s string) string {
158 if len(s) == 1 && unicode.IsLetter(rune(s[0])) {162 if len(s) == 1 && unicode.IsLetter(rune(s[0])) {
159 return strings.ToLower(s)163 return strings.ToLower(s)
@@ -314,11 +318,11 @@ func (c *Core) apply(cfg ports.Config) error {
314 // publishPending tells wayland at once when slots start waiting, before318 // publishPending tells wayland at once when slots start waiting, before
315 // their windows can map.319 // their windows can map.
316 func (c *Core) publishPending(ctx context.Context) error {320 func (c *Core) publishPending(ctx context.Context) error {
317- if p := c.placement.anyPending(); p != c.sentPending {321+ if p := c.placement.anyPending(); p != c.sent.pending {
318 if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {322 if err := c.command(ctx, ports.SlotsPending{Pending: p}); err != nil {
319 return err323 return err
320 }324 }
321- c.sentPending = p325+ c.sent.pending = p
322 }326 }
323 return nil327 return nil
324 }328 }
@@ -835,16 +839,16 @@ func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
835 // geometry only when the outputs themselves moved.839 // geometry only when the outputs themselves moved.
836 func (c *Core) syncOutputs(ctx context.Context) error {840 func (c *Core) syncOutputs(ctx context.Context) error {
837 v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}841 v := ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
838- if sameOutputs(v, c.sentOutputs) {842+ if sameOutputs(v, c.sent.outputs) {
839 return nil843 return nil
840 }844 }
841 if err := c.command(ctx, v); err != nil {845 if err := c.command(ctx, v); err != nil {
842 return err846 return err
843 }847 }
844- if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sentOutputs.Outputs) {848+ if c.ch.Layouts != nil && !slices.Equal(v.Outputs, c.sent.outputs.Outputs) {
845 latest(c.ch.Layouts, v.Outputs)849 latest(c.ch.Layouts, v.Outputs)
846 }850 }
847- c.sentOutputs = v851+ c.sent.outputs = v
848 return nil852 return nil
849 }853 }
850 854
M internal/core/security.go +23 -15
@@ -7,6 +7,14 @@ import (
77 "github.com/bnema/neferwl/internal/ports"
88 )
99
10+// lockState is the session lock's surfaces and keyboard focus. A new
11+// security epoch resets it whole.
12+type lockState struct {
13+ surfaces []ports.LockSurfacePlacement
14+ focus WindowID
15+ pinned bool // focus was clicked or typed into: it keeps focus
16+}
17+
1018 // syncSecurity runs only on the owner. The gate, not event selection order,
1119 // establishes the epoch; surface events can only fill in that exact epoch.
1220 func (c *Core) syncSecurity() bool {
@@ -21,7 +29,7 @@ func (c *Core) syncSecurity() bool {
2129 return false
2230 }
2331 c.security = state
24- c.lockSurfaces, c.lockFocus, c.lockPinned = nil, 0, false
32+ c.lock = lockState{}
2533 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
2634 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
2735 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
@@ -95,7 +103,7 @@ func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool {
95103 if !c.security.Protected {
96104 return true
97105 }
98- c.lockSurfaces = slices.Clone(v.Surfaces)
106+ c.lock.surfaces = slices.Clone(v.Surfaces)
99107 c.lockKeyboardFocus()
100108 // A destroyed or resized role must not retain a pointer grab.
101109 if !c.validLockID(c.pointer) {
@@ -110,7 +118,7 @@ func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool {
110118
111119 func (c *Core) lockSurface(sc *screen) (ports.LockSurfacePlacement, bool) {
112120 o := sc.mon.Output()
113- for _, s := range c.lockSurfaces {
121+ for _, s := range c.lock.surfaces {
114122 if s.ID != 0 && sc.name() != "" && s.Output == sc.name() && s.Width == o.W && s.Height == o.H && o.W > 0 && o.H > 0 {
115123 return s, true
116124 }
@@ -136,27 +144,27 @@ func (c *Core) validLockID(id WindowID) bool {
136144 // surface once it maps: lock clients map their surfaces one by one, in any
137145 // order.
138146 func (c *Core) lockKeyboardFocus() WindowID {
139- valid := c.validLockID(c.lockFocus)
140- if valid && c.lockPinned {
141- return c.lockFocus
147+ valid := c.validLockID(c.lock.focus)
148+ if valid && c.lock.pinned {
149+ return c.lock.focus
142150 }
143151 if c.focusScreen >= 0 && c.focusScreen < len(c.screens) {
144152 if s, ok := c.lockSurface(c.screens[c.focusScreen]); ok {
145- c.lockFocus, c.lockPinned = s.ID, false
146- return c.lockFocus
153+ c.lock.focus, c.lock.pinned = s.ID, false
154+ return c.lock.focus
147155 }
148156 }
149157 if valid {
150- return c.lockFocus
158+ return c.lock.focus
151159 }
152- c.lockFocus, c.lockPinned = 0, false
160+ c.lock.focus, c.lock.pinned = 0, false
153161 for _, sc := range c.screens {
154162 if s, ok := c.lockSurface(sc); ok {
155- c.lockFocus = s.ID
163+ c.lock.focus = s.ID
156164 break
157165 }
158166 }
159- return c.lockFocus
167+ return c.lock.focus
160168 }
161169
162170 func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) {
@@ -242,7 +250,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
242250 switch v := ev.(type) {
243251 case ports.KeyEvent:
244252 if id := c.lockKeyboardFocus(); id != 0 {
245- c.lockPinned = true // typing must not move focus mid-secret
253+ c.lock.pinned = true // typing must not move focus mid-secret
246254 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})
247255 }
248256 case ports.PointerMotion:
@@ -277,8 +285,8 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
277285 }
278286 if c.validLockID(id) {
279287 if v.Pressed {
280- changed := c.lockFocus != id
281- c.lockFocus, c.lockPinned = id, true
288+ changed := c.lock.focus != id
289+ c.lock.focus, c.lock.pinned = id, true
282290 if changed {
283291 if err := c.publishProtected(ctx); err != nil {
284292 return err
@@ -7,6 +7,14 @@ import (
7 "github.com/bnema/neferwl/internal/ports"7 "github.com/bnema/neferwl/internal/ports"
8 )8 )
9 9
10+// lockState is the session lock's surfaces and keyboard focus. A new
11+// security epoch resets it whole.
12+type lockState struct {
13+ surfaces []ports.LockSurfacePlacement
14+ focus WindowID
15+ pinned bool // focus was clicked or typed into: it keeps focus
16+}
17+
10 // syncSecurity runs only on the owner. The gate, not event selection order,18 // syncSecurity runs only on the owner. The gate, not event selection order,
11 // establishes the epoch; surface events can only fill in that exact epoch.19 // establishes the epoch; surface events can only fill in that exact epoch.
12 func (c *Core) syncSecurity() bool {20 func (c *Core) syncSecurity() bool {
@@ -21,7 +29,7 @@ func (c *Core) syncSecurity() bool {
21 return false29 return false
22 }30 }
23 c.security = state31 c.security = state
24- c.lockSurfaces, c.lockFocus, c.lockPinned = nil, 0, false32+ c.lock = lockState{}
25 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}33 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
26 c.pointer, c.grab, c.pointerOutput = 0, 0, ""34 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
27 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 035 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
@@ -95,7 +103,7 @@ func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool {
95 if !c.security.Protected {103 if !c.security.Protected {
96 return true104 return true
97 }105 }
98- c.lockSurfaces = slices.Clone(v.Surfaces)106+ c.lock.surfaces = slices.Clone(v.Surfaces)
99 c.lockKeyboardFocus()107 c.lockKeyboardFocus()
100 // A destroyed or resized role must not retain a pointer grab.108 // A destroyed or resized role must not retain a pointer grab.
101 if !c.validLockID(c.pointer) {109 if !c.validLockID(c.pointer) {
@@ -110,7 +118,7 @@ func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool {
110 118
111 func (c *Core) lockSurface(sc *screen) (ports.LockSurfacePlacement, bool) {119 func (c *Core) lockSurface(sc *screen) (ports.LockSurfacePlacement, bool) {
112 o := sc.mon.Output()120 o := sc.mon.Output()
113- for _, s := range c.lockSurfaces {121+ for _, s := range c.lock.surfaces {
114 if s.ID != 0 && sc.name() != "" && s.Output == sc.name() && s.Width == o.W && s.Height == o.H && o.W > 0 && o.H > 0 {122 if s.ID != 0 && sc.name() != "" && s.Output == sc.name() && s.Width == o.W && s.Height == o.H && o.W > 0 && o.H > 0 {
115 return s, true123 return s, true
116 }124 }
@@ -136,27 +144,27 @@ func (c *Core) validLockID(id WindowID) bool {
136 // surface once it maps: lock clients map their surfaces one by one, in any144 // surface once it maps: lock clients map their surfaces one by one, in any
137 // order.145 // order.
138 func (c *Core) lockKeyboardFocus() WindowID {146 func (c *Core) lockKeyboardFocus() WindowID {
139- valid := c.validLockID(c.lockFocus)147+ valid := c.validLockID(c.lock.focus)
140- if valid && c.lockPinned {148+ if valid && c.lock.pinned {
141- return c.lockFocus149+ return c.lock.focus
142 }150 }
143 if c.focusScreen >= 0 && c.focusScreen < len(c.screens) {151 if c.focusScreen >= 0 && c.focusScreen < len(c.screens) {
144 if s, ok := c.lockSurface(c.screens[c.focusScreen]); ok {152 if s, ok := c.lockSurface(c.screens[c.focusScreen]); ok {
145- c.lockFocus, c.lockPinned = s.ID, false153+ c.lock.focus, c.lock.pinned = s.ID, false
146- return c.lockFocus154+ return c.lock.focus
147 }155 }
148 }156 }
149 if valid {157 if valid {
150- return c.lockFocus158+ return c.lock.focus
151 }159 }
152- c.lockFocus, c.lockPinned = 0, false160+ c.lock.focus, c.lock.pinned = 0, false
153 for _, sc := range c.screens {161 for _, sc := range c.screens {
154 if s, ok := c.lockSurface(sc); ok {162 if s, ok := c.lockSurface(sc); ok {
155- c.lockFocus = s.ID163+ c.lock.focus = s.ID
156 break164 break
157 }165 }
158 }166 }
159- return c.lockFocus167+ return c.lock.focus
160 }168 }
161 169
162 func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) {170 func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) {
@@ -242,7 +250,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
242 switch v := ev.(type) {250 switch v := ev.(type) {
243 case ports.KeyEvent:251 case ports.KeyEvent:
244 if id := c.lockKeyboardFocus(); id != 0 {252 if id := c.lockKeyboardFocus(); id != 0 {
245- c.lockPinned = true // typing must not move focus mid-secret253+ c.lock.pinned = true // typing must not move focus mid-secret
246 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})254 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})
247 }255 }
248 case ports.PointerMotion:256 case ports.PointerMotion:
@@ -277,8 +285,8 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
277 }285 }
278 if c.validLockID(id) {286 if c.validLockID(id) {
279 if v.Pressed {287 if v.Pressed {
280- changed := c.lockFocus != id288+ changed := c.lock.focus != id
281- c.lockFocus, c.lockPinned = id, true289+ c.lock.focus, c.lock.pinned = id, true
282 if changed {290 if changed {
283 if err := c.publishProtected(ctx); err != nil {291 if err := c.publishProtected(ctx); err != nil {
284 return err292 return err
M internal/core/security_internal_test.go +1 -1
@@ -62,7 +62,7 @@ func TestProtectedDropsPrivateOffscreenCaptureAndDesktopEffects(t *testing.T) {
6262 c.pointer, c.grab = 10, 10
6363 c.keyboard = keyboard{sent: 10}
6464 state = ports.SecurityState{Generation: 2}
65- if !c.syncSecurity() || len(c.inputKeys) != 0 || c.grab != 0 || c.keyboard.sent != 0 || len(c.lockSurfaces) != 0 {
65+ if !c.syncSecurity() || len(c.inputKeys) != 0 || c.grab != 0 || c.keyboard.sent != 0 || len(c.lock.surfaces) != 0 {
6666 t.Fatal("release did not reset input ownership")
6767 }
6868 for _, ev := range []ports.InputEvent{ports.KeyEvent{Keycode: 30}, ports.PointerButton{Button: 0x110}} {
@@ -62,7 +62,7 @@ func TestProtectedDropsPrivateOffscreenCaptureAndDesktopEffects(t *testing.T) {
62 c.pointer, c.grab = 10, 1062 c.pointer, c.grab = 10, 10
63 c.keyboard = keyboard{sent: 10}63 c.keyboard = keyboard{sent: 10}
64 state = ports.SecurityState{Generation: 2}64 state = ports.SecurityState{Generation: 2}
65- if !c.syncSecurity() || len(c.inputKeys) != 0 || c.grab != 0 || c.keyboard.sent != 0 || len(c.lockSurfaces) != 0 {65+ if !c.syncSecurity() || len(c.inputKeys) != 0 || c.grab != 0 || c.keyboard.sent != 0 || len(c.lock.surfaces) != 0 {
66 t.Fatal("release did not reset input ownership")66 t.Fatal("release did not reset input ownership")
67 }67 }
68 for _, ev := range []ports.InputEvent{ports.KeyEvent{Keycode: 30}, ports.PointerButton{Button: 0x110}} {68 for _, ev := range []ports.InputEvent{ports.KeyEvent{Keycode: 30}, ports.PointerButton{Button: 0x110}} {
M internal/core/security_review_test.go +1 -1
@@ -39,7 +39,7 @@ func TestInputOutputSwitchBackpressureKeepsAdmittedEpoch(t *testing.T) {
3939 return v
4040 })
4141 c.opts.Security = gate
42- c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
42+ c.sent.outputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
4343 commands := make(chan ports.ClientCommand)
4444 c.ch.Commands = commands
4545 done := make(chan error, 1)
@@ -39,7 +39,7 @@ func TestInputOutputSwitchBackpressureKeepsAdmittedEpoch(t *testing.T) {
39 return v39 return v
40 })40 })
41 c.opts.Security = gate41 c.opts.Security = gate
42- c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}42+ c.sent.outputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()}
43 commands := make(chan ports.ClientCommand)43 commands := make(chan ports.ClientCommand)
44 c.ch.Commands = commands44 c.ch.Commands = commands
45 done := make(chan error, 1)45 done := make(chan error, 1)
M internal/core/state.go +2 -2
@@ -14,10 +14,10 @@ func (c *Core) publishState() {
1414 return
1515 }
1616 st := c.state()
17- if reflect.DeepEqual(st, c.sentState) {
17+ if reflect.DeepEqual(st, c.sent.state) {
1818 return
1919 }
20- c.sentState = st
20+ c.sent.state = st
2121 latest(c.ch.State, st)
2222 }
2323
@@ -14,10 +14,10 @@ func (c *Core) publishState() {
14 return14 return
15 }15 }
16 st := c.state()16 st := c.state()
17- if reflect.DeepEqual(st, c.sentState) {17+ if reflect.DeepEqual(st, c.sent.state) {
18 return18 return
19 }19 }
20- c.sentState = st20+ c.sent.state = st
21 latest(c.ch.State, st)21 latest(c.ch.State, st)
22 }22 }
23 23
M internal/core/workspaces.go +2 -2
@@ -32,9 +32,9 @@ func (c *Core) publishWorkspaces() {
3232 }
3333 snapshot.Outputs = append(snapshot.Outputs, out)
3434 }
35- if reflect.DeepEqual(snapshot, c.sentWorkspaces) {
35+ if reflect.DeepEqual(snapshot, c.sent.workspaces) {
3636 return
3737 }
38- c.sentWorkspaces = snapshot
38+ c.sent.workspaces = snapshot
3939 latest(c.ch.Workspaces, snapshot)
4040 }
@@ -32,9 +32,9 @@ func (c *Core) publishWorkspaces() {
32 }32 }
33 snapshot.Outputs = append(snapshot.Outputs, out)33 snapshot.Outputs = append(snapshot.Outputs, out)
34 }34 }
35- if reflect.DeepEqual(snapshot, c.sentWorkspaces) {35+ if reflect.DeepEqual(snapshot, c.sent.workspaces) {
36 return36 return
37 }37 }
38- c.sentWorkspaces = snapshot38+ c.sent.workspaces = snapshot
39 latest(c.ch.Workspaces, snapshot)39 latest(c.ch.Workspaces, snapshot)
40 }40 }

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 23

Tags 10

↑↓ moveenter openesc close