v0.4.0 b6be3a8

input.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"strconv"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
11	c.inputActive = true
12	defer func() { c.inputActive = false }()
13	if c.security.Protected {
14		if err := c.protectedInput(ctx, ev); err != nil {
15			return err
16		}
17		return nil
18	}
19	switch v := ev.(type) {
20	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			}
40		}
41		if c.drag != nil {
42			// The dragged window and the others get no pointer events.
43			return c.dragMotion(ctx, v.Time)
44		}
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 {
50				return err
51			}
52		}
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			}
58		}
59		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)
65			delete(c.buttons, v.Button)
66			return nil
67		}
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			}
78			return nil
79		}
80		if started, err := c.startDrag(ctx, v); err != nil || started {
81			return err
82		}
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			}
89		}
90		id := c.pointer
91		if c.grab != 0 {
92			id = c.grab
93		}
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 {
97				return err
98			}
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 {
111				return err
112			}
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		}
141		return nil
142	case ports.PointerAxis:
143		c.scrollStop(v)
144		// In the overview, scrolling moves the selection.
145		if c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
146			// overviewScroll owns which scrolls step; the shot costs no
147			// allocation once warm.
148			now := c.now()
149			shots := c.snapshot(now)
150			if c.cur().mon.overviewScroll(v) {
151				c.transition(shots, now)
152				if err := c.workspaceVisible(ctx, true); err != nil {
153					return err
154				}
155				if err := c.publish(ctx); err != nil {
156					return err
157				}
158			}
159			return nil
160		}
161		// Scroll goes to the window under the pointer, which has the
162		// pointer focus even mid-drag. None during a window drag.
163		if c.pointer != 0 && c.drag == nil {
164			if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
165				return err
166			}
167		}
168		return nil
169	case ports.SwipeBegin:
170		if c.swipeBegin(v) {
171			return c.slid(ctx, false)
172		}
173		return nil
174	case ports.SwipeUpdate:
175		if c.swipeUpdate(v) {
176			return c.slid(ctx, false)
177		}
178		return nil
179	case ports.SwipeEnd:
180		return c.slid(ctx, c.swipeEnd(v))
181	}
182	key, ok := ev.(ports.KeyEvent)
183	if !ok {
184		return nil
185	}
186	c.mods = key.Mods
187	if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
188		// Escape cancels the drag; its release goes nowhere either.
189		c.cancelDrag()
190		c.pressed[heldKey(key)] = true
191		if err := c.publish(ctx); err != nil {
192			return err
193		}
194		return c.rehit(ctx)
195	}
196	// The overview takes its keys before any window; others still
197	// run binds, and are not forwarded.
198	// A launcher or a menu holding the keyboard gets them first.
199	if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
200		now := c.now()
201		var shots []viewShot
202		if key.Pressed {
203			shots = c.snapshot(now)
204		}
205		if key.Pressed && mon.overviewKey(key) {
206			c.pressed[heldKey(key)] = true
207			c.transition(shots, now)
208			if err := c.workspaceVisible(ctx, true); err != nil {
209				return err
210			}
211			if err := c.publish(ctx); err != nil {
212				return err
213			}
214			return nil
215		}
216	}
217	// A focused window inhibiting shortcuts gets every key:
218	// no bind runs (emergency quit and VT switch are handled by
219	// input before core).
220	if c.keyboard.inhibited(c.keyboardFocus()) {
221		if held := heldKey(key); !key.Pressed && c.pressed[held] {
222			// Its press ran a bind before inhibiting began: the
223			// window never saw it.
224			delete(c.pressed, held)
225			return nil
226		}
227		if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
228			return err
229		}
230		return nil
231	}
232	name := keyName(key.Keysym)
233	// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
234	// prints exclam on US); then the physical key (code:N).
235	action, bound := c.binds[binding{key: name, mods: key.Mods}]
236	if !bound && key.Base != "" {
237		action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
238	}
239	if !bound && key.Keycode != 0 {
240		action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
241	}
242	// Track presses by physical key: Shift may be released before the key.
243	held := heldKey(key)
244	if !key.Pressed {
245		consumed := c.pressed[held]
246		delete(c.pressed, held)
247		if consumed {
248			return nil
249		}
250	}
251	if key.Pressed {
252		if bound {
253			c.pressed[held] = true
254			if action == ActionScaleUp || action == ActionScaleDown {
255				dir := 1
256				if action == ActionScaleDown {
257					dir = -1
258				}
259				sc := c.cur()
260				prev := sc.scale
261				sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
262				c.order()
263				if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
264					// Never wait on persistence: a stalled save drops the change.
265					select {
266					case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
267					default:
268					}
269				}
270				if err := c.publish(ctx); err != nil {
271					return err
272				}
273				return nil
274			}
275			before := c.cur().mon.Current()
276			swiped := c.swipedWorkspace()
277			now := c.now()
278			shots := c.snapshot(now)
279			c.keyboard.takeBack() // a bind acts on the windows
280			effect := c.applyAction(action)
281			if effect.Quit {
282				return ErrQuit
283			}
284			if effect.Spawn {
285				argv := effect.Argv
286				if argv == nil {
287					argv = append([]string(nil), c.cfg.Terminal.Command...)
288				}
289				// Explicit actions are suppressed on a full launcher queue;
290				// never keep the input owner waiting across an epoch change.
291				c.trySpawn(ctx, ports.SpawnRequest{Argv: argv})
292				if c.inputEpochChanged() {
293					return errSecurityChanged
294				}
295			}
296			if c.swipedWorkspace() != swiped {
297				c.dropSwipe()
298			}
299			c.transition(shots, now)
300			if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
301				return err
302			}
303			if effect.Close != 0 {
304				if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
305					return err
306				}
307			}
308			if err := c.publish(ctx); err != nil {
309				return err
310			}
311			return nil
312		}
313		c.pressed[held] = false
314	}
315	if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
316		if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
317			return err
318		}
319	}
320	return nil
321}

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