detached c76c9b5

refactor(core): split handleInput into one handler per input event

commit
c76c9b5fe6d859295279f60de9e77877733e1015
author
brice · · 2026-10-08 05:51 UTC
parent
834a1c2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +226 -200
M internal/core/input.go +226 -200
@@ -7,6 +7,8 @@ import (
77 "github.com/bnema/neferwl/internal/ports"
88 )
99
10+// handleInput dispatches one input event to its handler. A protected
11+// session routes every event to protectedInput instead.
1012 func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
1113 c.inputActive = true
1214 defer func() { c.inputActive = false }()
@@ -18,173 +20,187 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
1820 }
1921 switch v := ev.(type) {
2022 case ports.PointerMotion:
21- c.motionTime = v.Time
22- // A locked pointer stays still; relative motion still flows.
23- if c.constraint.Mode != ports.ConstraintLock {
24- c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
25- }
26- // The focused screen follows the pointer, so new windows
27- // and launchers open where the user is.
28- // Only a pointer entering another output switches: keyboard
29- // moves to another screen stick until then. Not mid-drag of a
30- // client; a window drag (drag.go) follows the pointer.
31- if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
32- name := c.screens[i].name()
33- c.pointerOutput = name
34- if name != c.cur().name() {
35- c.focusScreen = i
36- if err := c.publish(ctx); err != nil {
37- return err
38- }
39- }
23+ return c.pointerMotion(ctx, v)
24+ case ports.PointerButton:
25+ return c.pointerButton(ctx, v)
26+ case ports.PointerAxis:
27+ return c.pointerAxis(ctx, v)
28+ case ports.SwipeBegin:
29+ if c.swipeBegin(v) {
30+ return c.slid(ctx, false)
4031 }
41- if c.drag != nil {
42- // The dragged window and the others get no pointer events.
43- return c.dragMotion(ctx, v.Time)
32+ return nil
33+ case ports.SwipeUpdate:
34+ before := c.cur().mon.Current()
35+ if c.swipeUpdate(v) {
36+ return c.slid(ctx, c.cur().mon.Current() != before)
4437 }
45- id, x, y := c.hit(c.cursorX, c.cursorY)
46- // Layout changes are intentionally re-hit-tested only on motion.
47- if id != c.pointer {
48- c.pointer = id
49- if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
38+ return nil
39+ case ports.SwipeEnd:
40+ return c.slid(ctx, c.swipeEnd(v))
41+ case ports.KeyEvent:
42+ return c.keyEvent(ctx, v)
43+ }
44+ return nil
45+}
46+
47+func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
48+ c.motionTime = v.Time
49+ // A locked pointer stays still; relative motion still flows.
50+ if c.constraint.Mode != ports.ConstraintLock {
51+ c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
52+ }
53+ // The focused screen follows the pointer, so new windows
54+ // and launchers open where the user is.
55+ // Only a pointer entering another output switches: keyboard
56+ // moves to another screen stick until then. Not mid-drag of a
57+ // client; a window drag (drag.go) follows the pointer.
58+ if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
59+ name := c.screens[i].name()
60+ c.pointerOutput = name
61+ if name != c.cur().name() {
62+ c.focusScreen = i
63+ if err := c.publish(ctx); err != nil {
5064 return err
5165 }
5266 }
53- c.pointerAt = [2]float64{x, y}
54- if id != 0 {
55- if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
56- return err
57- }
67+ }
68+ if c.drag != nil {
69+ // The dragged window and the others get no pointer events.
70+ return c.dragMotion(ctx, v.Time)
71+ }
72+ id, x, y := c.hit(c.cursorX, c.cursorY)
73+ // Layout changes are intentionally re-hit-tested only on motion.
74+ if id != c.pointer {
75+ c.pointer = id
76+ if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
77+ return err
5878 }
79+ }
80+ c.pointerAt = [2]float64{x, y}
81+ if id != 0 {
82+ if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
83+ return err
84+ }
85+ }
86+ return nil
87+}
88+
89+func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
90+ // Buttons a drag took stay in c.buttons until released, so the
91+ // security gate admits their release; no client sees it.
92+ if !v.Pressed && c.swallow[v.Button] {
93+ delete(c.swallow, v.Button)
94+ delete(c.buttons, v.Button)
5995 return nil
60- case ports.PointerButton:
61- // Buttons a drag took stay in c.buttons until released, so the
62- // security gate admits their release; no client sees it.
63- if !v.Pressed && c.swallow[v.Button] {
64- delete(c.swallow, v.Button)
96+ }
97+ if d := c.drag; d != nil {
98+ if !v.Pressed && v.Button == d.button {
6599 delete(c.buttons, v.Button)
66- return nil
100+ return c.endDrag(ctx)
67101 }
68- if d := c.drag; d != nil {
69- if !v.Pressed && v.Button == d.button {
70- delete(c.buttons, v.Button)
71- return c.endDrag(ctx)
72- }
73- // Other buttons do nothing mid-drag; their releases neither.
74- if v.Pressed {
75- c.buttons[v.Button] = true
76- c.swallow[v.Button] = true
77- }
102+ // Other buttons do nothing mid-drag; their releases neither.
103+ if v.Pressed {
104+ c.buttons[v.Button] = true
105+ c.swallow[v.Button] = true
106+ }
107+ return nil
108+ }
109+ if started, err := c.startDrag(ctx, v); err != nil || started {
110+ return err
111+ }
112+ if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
113+ if picked, err := c.overviewClick(ctx); err != nil {
114+ return err
115+ } else if picked {
78116 return nil
79117 }
80- if started, err := c.startDrag(ctx, v); err != nil || started {
118+ }
119+ id := c.pointer
120+ if c.grab != 0 {
121+ id = c.grab
122+ }
123+ if v.Pressed && c.held() == 0 {
124+ // A click outside an open menu closes it.
125+ if err := c.dismissGrabs(ctx, id); err != nil {
81126 return err
82127 }
83- if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
84- if picked, err := c.overviewClick(ctx); err != nil {
85- return err
86- } else if picked {
87- return nil
88- }
128+ }
129+ if v.Pressed {
130+ if c.held() == 0 {
131+ c.grab = id
89132 }
90- id := c.pointer
91- if c.grab != 0 {
92- id = c.grab
133+ c.buttons[v.Button] = true
134+ c.lastButton = v.Button
135+ } else {
136+ delete(c.buttons, v.Button)
137+ }
138+ if id != 0 {
139+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
140+ return err
93141 }
94- if v.Pressed && c.held() == 0 {
95- // A click outside an open menu closes it.
96- if err := c.dismissGrabs(ctx, id); err != nil {
142+ // A click on an on-demand layer gives it the keyboard.
143+ if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
144+ if err := c.publish(ctx); err != nil {
97145 return err
98146 }
99- }
100- if v.Pressed {
101- if c.held() == 0 {
102- c.grab = id
103- }
104- c.buttons[v.Button] = true
105- c.lastButton = v.Button
106- } else {
107- delete(c.buttons, v.Button)
108- }
109- if id != 0 {
110- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
147+ } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
148+ // A click anywhere else takes the keyboard back.
149+ c.keyboard.takeBack()
150+ if err := c.publish(ctx); err != nil {
111151 return err
112152 }
113- // A click on an on-demand layer gives it the keyboard.
114- if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
115- if err := c.publish(ctx); err != nil {
116- return err
117- }
118- } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
119- // A click anywhere else takes the keyboard back.
120- c.keyboard.takeBack()
121- if err := c.publish(ctx); err != nil {
122- return err
123- }
124- }
125- // A click focuses the window and its output.
126- s, w := c.screenOf(id)
127- if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
128- now := c.now()
129- before := c.snapshot(now)
130- w.Click(id)
131- c.focusScreen = c.screenIndex(s.name())
132- c.transition(before, now)
133- if err := c.publish(ctx); err != nil {
134- return err
135- }
136- }
137- }
138- if c.held() == 0 {
139- c.grab = 0
140153 }
141- return nil
142- case ports.PointerAxis:
143- c.scrollStop(v)
144- // Only wheel/continuous scrolling navigates the overview.
145- // Two-finger scrolling belongs to clients, never to navigation.
146- if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
147- // overviewScroll owns which scrolls step; the shot costs no
148- // allocation once warm.
154+ // A click focuses the window and its output.
155+ s, w := c.screenOf(id)
156+ if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
149157 now := c.now()
150- shots := c.snapshot(now)
151- if c.cur().mon.overviewScroll(v) {
152- c.transition(shots, now)
153- if err := c.workspaceVisible(ctx, true); err != nil {
154- return err
155- }
156- if err := c.publish(ctx); err != nil {
157- return err
158- }
158+ before := c.snapshot(now)
159+ w.Click(id)
160+ c.focusScreen = c.screenIndex(s.name())
161+ c.transition(before, now)
162+ if err := c.publish(ctx); err != nil {
163+ return err
159164 }
160- return nil
161165 }
162- // Scroll goes to the window under the pointer, which has the
163- // pointer focus even mid-drag. None during a window drag.
164- if c.pointer != 0 && c.drag == nil {
165- if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
166+ }
167+ if c.held() == 0 {
168+ c.grab = 0
169+ }
170+ return nil
171+}
172+
173+func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
174+ c.scrollStop(v)
175+ // Only wheel/continuous scrolling navigates the overview.
176+ // Two-finger scrolling belongs to clients, never to navigation.
177+ if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
178+ // overviewScroll owns which scrolls step; the shot costs no
179+ // allocation once warm.
180+ now := c.now()
181+ shots := c.snapshot(now)
182+ if c.cur().mon.overviewScroll(v) {
183+ c.transition(shots, now)
184+ if err := c.workspaceVisible(ctx, true); err != nil {
185+ return err
186+ }
187+ if err := c.publish(ctx); err != nil {
166188 return err
167189 }
168190 }
169191 return nil
170- case ports.SwipeBegin:
171- if c.swipeBegin(v) {
172- return c.slid(ctx, false)
173- }
174- return nil
175- case ports.SwipeUpdate:
176- before := c.cur().mon.Current()
177- if c.swipeUpdate(v) {
178- return c.slid(ctx, c.cur().mon.Current() != before)
179- }
180- return nil
181- case ports.SwipeEnd:
182- return c.slid(ctx, c.swipeEnd(v))
183192 }
184- key, ok := ev.(ports.KeyEvent)
185- if !ok {
186- return nil
193+ // Scroll goes to the window under the pointer, which has the
194+ // pointer focus even mid-drag. None during a window drag.
195+ if c.pointer != 0 && c.drag == nil {
196+ if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
197+ return err
198+ }
187199 }
200+ return nil
201+}
202+
203+func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
188204 c.mods = key.Mods
189205 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
190206 // Escape cancels the drag; its release goes nowhere either.
@@ -253,68 +269,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
253269 if key.Pressed {
254270 if bound {
255271 c.pressed[held] = true
256- if action == ActionScaleUp || action == ActionScaleDown {
257- dir := 1
258- if action == ActionScaleDown {
259- dir = -1
260- }
261- sc := c.cur()
262- prev := sc.scale
263- sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
264- c.order()
265- if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
266- // Never wait on persistence: a stalled save drops the change.
267- select {
268- case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
269- default:
270- }
271- }
272- if err := c.publish(ctx); err != nil {
273- return err
274- }
275- return nil
276- }
277- before := c.cur().mon.Current()
278- swiped := c.swipedWorkspace()
279- now := c.now()
280- shots := c.snapshot(now)
281- c.keyboard.takeBack() // a bind acts on the windows
282- effect := c.applyAction(action)
283- if effect.Quit {
284- return ErrQuit
285- }
286- if effect.Spawn {
287- commands := effect.Commands
288- if commands == nil {
289- commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
290- }
291- // Explicit actions are suppressed on a full launcher queue;
292- // never keep the input owner waiting across an epoch change.
293- for _, argv := range commands {
294- if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
295- break
296- }
297- }
298- if c.inputEpochChanged() {
299- return errSecurityChanged
300- }
301- }
302- if c.swipedWorkspace() != swiped {
303- c.dropSwipe()
304- }
305- c.transition(shots, now)
306- if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
307- return err
308- }
309- if effect.Close != 0 {
310- if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
311- return err
312- }
313- }
314- if err := c.publish(ctx); err != nil {
315- return err
316- }
317- return nil
272+ return c.runBind(ctx, action)
318273 }
319274 c.pressed[held] = false
320275 }
@@ -325,3 +280,74 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
325280 }
326281 return nil
327282 }
283+
284+// runBind runs the action of a bound key press.
285+func (c *Core) runBind(ctx context.Context, action Action) error {
286+ if action == ActionScaleUp || action == ActionScaleDown {
287+ return c.scaleBind(ctx, action)
288+ }
289+ before := c.cur().mon.Current()
290+ swiped := c.swipedWorkspace()
291+ now := c.now()
292+ shots := c.snapshot(now)
293+ c.keyboard.takeBack() // a bind acts on the windows
294+ effect := c.applyAction(action)
295+ if effect.Quit {
296+ return ErrQuit
297+ }
298+ if effect.Spawn {
299+ commands := effect.Commands
300+ if commands == nil {
301+ commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
302+ }
303+ // Explicit actions are suppressed on a full launcher queue;
304+ // never keep the input owner waiting across an epoch change.
305+ for _, argv := range commands {
306+ if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
307+ break
308+ }
309+ }
310+ if c.inputEpochChanged() {
311+ return errSecurityChanged
312+ }
313+ }
314+ if c.swipedWorkspace() != swiped {
315+ c.dropSwipe()
316+ }
317+ c.transition(shots, now)
318+ if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
319+ return err
320+ }
321+ if effect.Close != 0 {
322+ if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
323+ return err
324+ }
325+ }
326+ if err := c.publish(ctx); err != nil {
327+ return err
328+ }
329+ return nil
330+}
331+
332+// scaleBind steps the focused output's scale and asks for it to be saved.
333+func (c *Core) scaleBind(ctx context.Context, action Action) error {
334+ dir := 1
335+ if action == ActionScaleDown {
336+ dir = -1
337+ }
338+ sc := c.cur()
339+ prev := sc.scale
340+ sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
341+ c.order()
342+ if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
343+ // Never wait on persistence: a stalled save drops the change.
344+ select {
345+ case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
346+ default:
347+ }
348+ }
349+ if err := c.publish(ctx); err != nil {
350+ return err
351+ }
352+ return nil
353+}
@@ -7,6 +7,8 @@ import (
7 "github.com/bnema/neferwl/internal/ports"7 "github.com/bnema/neferwl/internal/ports"
8 )8 )
9 9
10+// handleInput dispatches one input event to its handler. A protected
11+// session routes every event to protectedInput instead.
10 func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {12 func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
11 c.inputActive = true13 c.inputActive = true
12 defer func() { c.inputActive = false }()14 defer func() { c.inputActive = false }()
@@ -18,173 +20,187 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
18 }20 }
19 switch v := ev.(type) {21 switch v := ev.(type) {
20 case ports.PointerMotion:22 case ports.PointerMotion:
21- c.motionTime = v.Time23+ return c.pointerMotion(ctx, v)
22- // A locked pointer stays still; relative motion still flows.24+ case ports.PointerButton:
23- if c.constraint.Mode != ports.ConstraintLock {25+ return c.pointerButton(ctx, v)
24- c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))26+ case ports.PointerAxis:
25- }27+ return c.pointerAxis(ctx, v)
26- // The focused screen follows the pointer, so new windows28+ case ports.SwipeBegin:
27- // and launchers open where the user is.29+ if c.swipeBegin(v) {
28- // Only a pointer entering another output switches: keyboard30+ return c.slid(ctx, false)
29- // moves to another screen stick until then. Not mid-drag of a
30- // client; a window drag (drag.go) follows the pointer.
31- if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
32- name := c.screens[i].name()
33- c.pointerOutput = name
34- if name != c.cur().name() {
35- c.focusScreen = i
36- if err := c.publish(ctx); err != nil {
37- return err
38- }
39- }
40 }31 }
41- if c.drag != nil {32+ return nil
42- // The dragged window and the others get no pointer events.33+ case ports.SwipeUpdate:
43- return c.dragMotion(ctx, v.Time)34+ before := c.cur().mon.Current()
35+ if c.swipeUpdate(v) {
36+ return c.slid(ctx, c.cur().mon.Current() != before)
44 }37 }
45- id, x, y := c.hit(c.cursorX, c.cursorY)38+ return nil
46- // Layout changes are intentionally re-hit-tested only on motion.39+ case ports.SwipeEnd:
47- if id != c.pointer {40+ return c.slid(ctx, c.swipeEnd(v))
48- c.pointer = id41+ case ports.KeyEvent:
49- if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {42+ return c.keyEvent(ctx, v)
43+ }
44+ return nil
45+}
46+
47+func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
48+ c.motionTime = v.Time
49+ // A locked pointer stays still; relative motion still flows.
50+ if c.constraint.Mode != ports.ConstraintLock {
51+ c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
52+ }
53+ // The focused screen follows the pointer, so new windows
54+ // and launchers open where the user is.
55+ // Only a pointer entering another output switches: keyboard
56+ // moves to another screen stick until then. Not mid-drag of a
57+ // client; a window drag (drag.go) follows the pointer.
58+ if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
59+ name := c.screens[i].name()
60+ c.pointerOutput = name
61+ if name != c.cur().name() {
62+ c.focusScreen = i
63+ if err := c.publish(ctx); err != nil {
50 return err64 return err
51 }65 }
52 }66 }
53- c.pointerAt = [2]float64{x, y}67+ }
54- if id != 0 {68+ if c.drag != nil {
55- if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {69+ // The dragged window and the others get no pointer events.
56- return err70+ return c.dragMotion(ctx, v.Time)
57- }71+ }
72+ id, x, y := c.hit(c.cursorX, c.cursorY)
73+ // Layout changes are intentionally re-hit-tested only on motion.
74+ if id != c.pointer {
75+ c.pointer = id
76+ if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
77+ return err
58 }78 }
79+ }
80+ c.pointerAt = [2]float64{x, y}
81+ if id != 0 {
82+ if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
83+ return err
84+ }
85+ }
86+ return nil
87+}
88+
89+func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
90+ // Buttons a drag took stay in c.buttons until released, so the
91+ // security gate admits their release; no client sees it.
92+ if !v.Pressed && c.swallow[v.Button] {
93+ delete(c.swallow, v.Button)
94+ delete(c.buttons, v.Button)
59 return nil95 return nil
60- case ports.PointerButton:96+ }
61- // Buttons a drag took stay in c.buttons until released, so the97+ if d := c.drag; d != nil {
62- // security gate admits their release; no client sees it.98+ if !v.Pressed && v.Button == d.button {
63- if !v.Pressed && c.swallow[v.Button] {
64- delete(c.swallow, v.Button)
65 delete(c.buttons, v.Button)99 delete(c.buttons, v.Button)
66- return nil100+ return c.endDrag(ctx)
67 }101 }
68- if d := c.drag; d != nil {102+ // Other buttons do nothing mid-drag; their releases neither.
69- if !v.Pressed && v.Button == d.button {103+ if v.Pressed {
70- delete(c.buttons, v.Button)104+ c.buttons[v.Button] = true
71- return c.endDrag(ctx)105+ c.swallow[v.Button] = true
72- }106+ }
73- // Other buttons do nothing mid-drag; their releases neither.107+ return nil
74- if v.Pressed {108+ }
75- c.buttons[v.Button] = true109+ if started, err := c.startDrag(ctx, v); err != nil || started {
76- c.swallow[v.Button] = true110+ return err
77- }111+ }
112+ if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
113+ if picked, err := c.overviewClick(ctx); err != nil {
114+ return err
115+ } else if picked {
78 return nil116 return nil
79 }117 }
80- if started, err := c.startDrag(ctx, v); err != nil || started {118+ }
119+ id := c.pointer
120+ if c.grab != 0 {
121+ id = c.grab
122+ }
123+ if v.Pressed && c.held() == 0 {
124+ // A click outside an open menu closes it.
125+ if err := c.dismissGrabs(ctx, id); err != nil {
81 return err126 return err
82 }127 }
83- if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {128+ }
84- if picked, err := c.overviewClick(ctx); err != nil {129+ if v.Pressed {
85- return err130+ if c.held() == 0 {
86- } else if picked {131+ c.grab = id
87- return nil
88- }
89 }132 }
90- id := c.pointer133+ c.buttons[v.Button] = true
91- if c.grab != 0 {134+ c.lastButton = v.Button
92- id = c.grab135+ } else {
136+ delete(c.buttons, v.Button)
137+ }
138+ if id != 0 {
139+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
140+ return err
93 }141 }
94- if v.Pressed && c.held() == 0 {142+ // A click on an on-demand layer gives it the keyboard.
95- // A click outside an open menu closes it.143+ if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
96- if err := c.dismissGrabs(ctx, id); err != nil {144+ if err := c.publish(ctx); err != nil {
97 return err145 return err
98 }146 }
99- }147+ } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
100- if v.Pressed {148+ // A click anywhere else takes the keyboard back.
101- if c.held() == 0 {149+ c.keyboard.takeBack()
102- c.grab = id150+ if err := c.publish(ctx); err != nil {
103- }
104- c.buttons[v.Button] = true
105- c.lastButton = v.Button
106- } else {
107- delete(c.buttons, v.Button)
108- }
109- if id != 0 {
110- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
111 return err151 return err
112 }152 }
113- // A click on an on-demand layer gives it the keyboard.
114- if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
115- if err := c.publish(ctx); err != nil {
116- return err
117- }
118- } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
119- // A click anywhere else takes the keyboard back.
120- c.keyboard.takeBack()
121- if err := c.publish(ctx); err != nil {
122- return err
123- }
124- }
125- // A click focuses the window and its output.
126- s, w := c.screenOf(id)
127- if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
128- now := c.now()
129- before := c.snapshot(now)
130- w.Click(id)
131- c.focusScreen = c.screenIndex(s.name())
132- c.transition(before, now)
133- if err := c.publish(ctx); err != nil {
134- return err
135- }
136- }
137- }
138- if c.held() == 0 {
139- c.grab = 0
140 }153 }
141- return nil154+ // A click focuses the window and its output.
142- case ports.PointerAxis:155+ s, w := c.screenOf(id)
143- c.scrollStop(v)156+ if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
144- // Only wheel/continuous scrolling navigates the overview.
145- // Two-finger scrolling belongs to clients, never to navigation.
146- if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
147- // overviewScroll owns which scrolls step; the shot costs no
148- // allocation once warm.
149 now := c.now()157 now := c.now()
150- shots := c.snapshot(now)158+ before := c.snapshot(now)
151- if c.cur().mon.overviewScroll(v) {159+ w.Click(id)
152- c.transition(shots, now)160+ c.focusScreen = c.screenIndex(s.name())
153- if err := c.workspaceVisible(ctx, true); err != nil {161+ c.transition(before, now)
154- return err162+ if err := c.publish(ctx); err != nil {
155- }163+ return err
156- if err := c.publish(ctx); err != nil {
157- return err
158- }
159 }164 }
160- return nil
161 }165 }
162- // Scroll goes to the window under the pointer, which has the166+ }
163- // pointer focus even mid-drag. None during a window drag.167+ if c.held() == 0 {
164- if c.pointer != 0 && c.drag == nil {168+ c.grab = 0
165- if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {169+ }
170+ return nil
171+}
172+
173+func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
174+ c.scrollStop(v)
175+ // Only wheel/continuous scrolling navigates the overview.
176+ // Two-finger scrolling belongs to clients, never to navigation.
177+ if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
178+ // overviewScroll owns which scrolls step; the shot costs no
179+ // allocation once warm.
180+ now := c.now()
181+ shots := c.snapshot(now)
182+ if c.cur().mon.overviewScroll(v) {
183+ c.transition(shots, now)
184+ if err := c.workspaceVisible(ctx, true); err != nil {
185+ return err
186+ }
187+ if err := c.publish(ctx); err != nil {
166 return err188 return err
167 }189 }
168 }190 }
169 return nil191 return nil
170- case ports.SwipeBegin:
171- if c.swipeBegin(v) {
172- return c.slid(ctx, false)
173- }
174- return nil
175- case ports.SwipeUpdate:
176- before := c.cur().mon.Current()
177- if c.swipeUpdate(v) {
178- return c.slid(ctx, c.cur().mon.Current() != before)
179- }
180- return nil
181- case ports.SwipeEnd:
182- return c.slid(ctx, c.swipeEnd(v))
183 }192 }
184- key, ok := ev.(ports.KeyEvent)193+ // Scroll goes to the window under the pointer, which has the
185- if !ok {194+ // pointer focus even mid-drag. None during a window drag.
186- return nil195+ if c.pointer != 0 && c.drag == nil {
196+ if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
197+ return err
198+ }
187 }199 }
200+ return nil
201+}
202+
203+func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
188 c.mods = key.Mods204 c.mods = key.Mods
189 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {205 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
190 // Escape cancels the drag; its release goes nowhere either.206 // Escape cancels the drag; its release goes nowhere either.
@@ -253,68 +269,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
253 if key.Pressed {269 if key.Pressed {
254 if bound {270 if bound {
255 c.pressed[held] = true271 c.pressed[held] = true
256- if action == ActionScaleUp || action == ActionScaleDown {272+ return c.runBind(ctx, action)
257- dir := 1
258- if action == ActionScaleDown {
259- dir = -1
260- }
261- sc := c.cur()
262- prev := sc.scale
263- sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
264- c.order()
265- if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
266- // Never wait on persistence: a stalled save drops the change.
267- select {
268- case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
269- default:
270- }
271- }
272- if err := c.publish(ctx); err != nil {
273- return err
274- }
275- return nil
276- }
277- before := c.cur().mon.Current()
278- swiped := c.swipedWorkspace()
279- now := c.now()
280- shots := c.snapshot(now)
281- c.keyboard.takeBack() // a bind acts on the windows
282- effect := c.applyAction(action)
283- if effect.Quit {
284- return ErrQuit
285- }
286- if effect.Spawn {
287- commands := effect.Commands
288- if commands == nil {
289- commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
290- }
291- // Explicit actions are suppressed on a full launcher queue;
292- // never keep the input owner waiting across an epoch change.
293- for _, argv := range commands {
294- if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
295- break
296- }
297- }
298- if c.inputEpochChanged() {
299- return errSecurityChanged
300- }
301- }
302- if c.swipedWorkspace() != swiped {
303- c.dropSwipe()
304- }
305- c.transition(shots, now)
306- if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
307- return err
308- }
309- if effect.Close != 0 {
310- if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
311- return err
312- }
313- }
314- if err := c.publish(ctx); err != nil {
315- return err
316- }
317- return nil
318 }273 }
319 c.pressed[held] = false274 c.pressed[held] = false
320 }275 }
@@ -325,3 +280,74 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
325 }280 }
326 return nil281 return nil
327 }282 }
283+
284+// runBind runs the action of a bound key press.
285+func (c *Core) runBind(ctx context.Context, action Action) error {
286+ if action == ActionScaleUp || action == ActionScaleDown {
287+ return c.scaleBind(ctx, action)
288+ }
289+ before := c.cur().mon.Current()
290+ swiped := c.swipedWorkspace()
291+ now := c.now()
292+ shots := c.snapshot(now)
293+ c.keyboard.takeBack() // a bind acts on the windows
294+ effect := c.applyAction(action)
295+ if effect.Quit {
296+ return ErrQuit
297+ }
298+ if effect.Spawn {
299+ commands := effect.Commands
300+ if commands == nil {
301+ commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
302+ }
303+ // Explicit actions are suppressed on a full launcher queue;
304+ // never keep the input owner waiting across an epoch change.
305+ for _, argv := range commands {
306+ if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
307+ break
308+ }
309+ }
310+ if c.inputEpochChanged() {
311+ return errSecurityChanged
312+ }
313+ }
314+ if c.swipedWorkspace() != swiped {
315+ c.dropSwipe()
316+ }
317+ c.transition(shots, now)
318+ if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
319+ return err
320+ }
321+ if effect.Close != 0 {
322+ if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
323+ return err
324+ }
325+ }
326+ if err := c.publish(ctx); err != nil {
327+ return err
328+ }
329+ return nil
330+}
331+
332+// scaleBind steps the focused output's scale and asks for it to be saved.
333+func (c *Core) scaleBind(ctx context.Context, action Action) error {
334+ dir := 1
335+ if action == ActionScaleDown {
336+ dir = -1
337+ }
338+ sc := c.cur()
339+ prev := sc.scale
340+ sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
341+ c.order()
342+ if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
343+ // Never wait on persistence: a stalled save drops the change.
344+ select {
345+ case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
346+ default:
347+ }
348+ }
349+ if err := c.publish(ctx); err != nil {
350+ return err
351+ }
352+ return nil
353+}

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 25

Tags 10

↑↓ moveenter openesc close