main f2d166b

input.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"strconv"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// handleInput dispatches one input event to its handler. A protected
11// session routes every event to protectedInput instead.
12func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
13	c.inputActive = true
14	defer func() { c.inputActive = false }()
15	if c.security.Protected {
16		if err := c.protectedInput(ctx, ev); err != nil {
17			return err
18		}
19		return nil
20	}
21	switch v := ev.(type) {
22	case ports.PointerMotion:
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.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30			// The drag owns the pointer: no navigation under it.
31			return nil
32		}
33		if forwardsSwipe(v.Fingers) {
34			return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
35		}
36		if err := c.gestureAbort(ctx, v.Time); err != nil {
37			return err
38		}
39		if c.swipeBegin(v) {
40			return c.slid(ctx, false)
41		}
42		return nil
43	case ports.SwipeUpdate:
44		if c.gesture.on {
45			return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time})
46		}
47		before := c.cur().mon.Current()
48		if c.swipeUpdate(v) {
49			return c.slid(ctx, c.cur().mon.Current() != before)
50		}
51		return nil
52	case ports.SwipeEnd:
53		if c.gesture.on && c.gesture.kind == ports.GestureSwipe {
54			return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time)
55		}
56		shown, spawn := c.swipeEnd(v)
57		if spawn != nil {
58			// As a bind: the terminal starts before the new state is published.
59			if err := c.runSpawn(ctx, spawn, Effect{Spawn: true, Stash: true}); err != nil {
60				return err
61			}
62		}
63		return c.slid(ctx, shown)
64	case ports.PinchBegin:
65		return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time)
66	case ports.PinchUpdate:
67		return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time})
68	case ports.PinchEnd:
69		return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time)
70	case ports.HoldBegin:
71		return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time)
72	case ports.HoldEnd:
73		return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time)
74	case ports.KeyEvent:
75		return c.keyEvent(ctx, v)
76	}
77	return nil
78}
79
80func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
81	c.motionTime = v.Time
82	// A locked pointer stays still; relative motion still flows.
83	if c.constraint.Mode != ports.ConstraintLock {
84		c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
85	}
86	// The focused screen follows the pointer, so new windows
87	// and launchers open where the user is.
88	// Only a pointer entering another output switches: keyboard
89	// moves to another screen stick until then. Not mid-drag of a
90	// client; a window drag (drag.go) follows the pointer.
91	if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
92		name := c.screens[i].name()
93		c.pointerOutput = name
94		if name != c.cur().name() {
95			c.focusScreen = i
96			if err := c.publish(ctx); err != nil {
97				return err
98			}
99		}
100	}
101	if c.pgrab != nil {
102		return c.pgrab.motion(ctx, v.Time)
103	}
104	if c.drag != nil {
105		// The dragged window and the others get no pointer events.
106		return c.dragMotion(ctx, v.Time)
107	}
108	if c.dataDrag != nil {
109		// A data drag goes to the surface under the pointer, by its own
110		// commands.
111		return c.dataDragMotion(ctx, v.Time)
112	}
113	id, x, y := c.hit(c.cursorX, c.cursorY)
114	// Layout changes are intentionally re-hit-tested only on motion.
115	if id != c.pointer {
116		c.pointer = id
117		if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
118			return err
119		}
120	}
121	c.pointerAt = [2]float64{x, y}
122	if id != 0 {
123		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 {
124			return err
125		}
126	}
127	return nil
128}
129
130// clickFocus focuses the window id and its output the way a click on it
131// does, unless it already has the keyboard.
132func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
133	s, w := c.screenOf(id)
134	if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
135		return nil
136	}
137	now := c.now()
138	before := c.snapshot(now)
139	c.focusClick(s, w, id)
140	c.transition(before, now)
141	return c.publish(ctx)
142}
143
144// focusClick raises the clicked window and focuses its output. Modal dialogs
145// can take the parent's focus, so flash the resolved focus, not the click target.
146func (c *Core) focusClick(s *screen, w *Workspace, id WindowID) {
147	before, _ := c.cur().mon.Focused()
148	w.Click(id)
149	c.focusScreen = c.screenIndex(s.name())
150	after, _ := c.cur().mon.Focused()
151	if after != 0 && after != before {
152		c.requestFlash(flashWindow, after)
153	}
154}
155
156func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
157	// Buttons a drag took stay in c.buttons until released, so the
158	// security gate admits their release; no client sees it.
159	if !v.Pressed && c.swallow[v.Button] {
160		delete(c.swallow, v.Button)
161		delete(c.buttons, v.Button)
162		return nil
163	}
164	if c.dataDrag != nil {
165		return c.dataDragButton(ctx, v)
166	}
167	if g := c.pgrab; g != nil {
168		if !v.Pressed && v.Button == g.button() {
169			delete(c.buttons, v.Button)
170			return c.endPointerGrab(ctx)
171		}
172		if v.Pressed {
173			c.buttons[v.Button], c.swallow[v.Button] = true, true
174		}
175		return nil
176	}
177	if d := c.drag; d != nil {
178		if !v.Pressed && v.Button == d.button {
179			delete(c.buttons, v.Button)
180			return c.endDrag(ctx)
181		}
182		// Other buttons do nothing mid-drag; their releases neither.
183		if v.Pressed {
184			c.buttons[v.Button] = true
185			c.swallow[v.Button] = true
186		}
187		return nil
188	}
189	if v.Pressed && c.switching() {
190		if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
191			return err
192		}
193	}
194	if started, err := c.startPointerGrab(ctx, v); err != nil || started {
195		return err
196	}
197	if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
198		if picked, err := c.overviewClick(ctx); err != nil {
199			return err
200		} else if picked {
201			return nil
202		}
203	}
204	id := c.pointer
205	if c.grab != 0 {
206		id = c.grab
207	}
208	if v.Pressed && c.held() == 0 {
209		// A click outside an open menu closes it.
210		if err := c.dismissGrabs(ctx, id); err != nil {
211			return err
212		}
213	}
214	grab := v.Pressed && c.held() == 0
215	if v.Pressed {
216		if grab {
217			c.grab = id
218		}
219		c.buttons[v.Button] = true
220		c.lastButton = v.Button
221	} else {
222		delete(c.buttons, v.Button)
223	}
224	if id != 0 {
225		if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
226			return err
227		}
228		// A click on an on-demand layer gives it the keyboard.
229		if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
230			if err := c.publish(ctx); err != nil {
231				return err
232			}
233		} else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
234			// A click anywhere else takes the keyboard back.
235			c.keyboard.takeBack()
236			if err := c.publish(ctx); err != nil {
237				return err
238			}
239		}
240		// A click focuses the window and its output.
241		if v.Pressed {
242			if err := c.clickFocus(ctx, id); err != nil {
243				return err
244			}
245		}
246	}
247	if c.held() == 0 {
248		c.grab = 0
249	}
250	return nil
251}
252
253func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
254	c.scrollStop(v)
255	// Only wheel/continuous scrolling navigates the overview.
256	// Two-finger scrolling belongs to clients, never to navigation.
257	if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
258		// overviewScroll owns which scrolls step; the shot costs no
259		// allocation once warm.
260		now := c.now()
261		shots := c.snapshot(now)
262		if c.cur().mon.overviewScroll(v) {
263			c.transition(shots, now)
264			if err := c.workspaceVisible(ctx, true); err != nil {
265				return err
266			}
267			if err := c.publish(ctx); err != nil {
268				return err
269			}
270		}
271		return nil
272	}
273	// Scroll goes to the window under the pointer, which has the
274	// pointer focus even mid-drag. None during a window drag.
275	if c.pointer != 0 && !c.pointerBusy() && c.dataDrag == nil {
276		if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
277			return err
278		}
279	}
280	return nil
281}
282
283func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
284	c.mods = key.Mods
285	c.keyState = key.State
286	c.repeatKey(key)
287	// Releasing the command key commits the switcher; the release still
288	// goes on to the bind lookup and the clients.
289	if c.switching() && c.mods&c.cmdMod == 0 {
290		if err := c.releaseSwitcher(ctx); err != nil {
291			return err
292		}
293	}
294	if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
295		// Escape cancels the data drag first, even over the switcher;
296		// its release goes nowhere either.
297		c.pressed[heldKey(key)] = true
298		return c.cancelDataDrag(ctx)
299	}
300	if c.switching() && key.Pressed && key.Keysym == "Escape" {
301		// Escape cancels the switcher; its release goes nowhere either.
302		c.pressed[heldKey(key)] = true
303		return c.abortSwitcher(ctx)
304	}
305	if c.pointerBusy() && key.Pressed && key.Keysym == "Escape" {
306		// Escape cancels the grab; its release goes nowhere either.
307		if c.pgrab != nil {
308			c.pgrab.cancel()
309			if err := c.abortPointerGrab(ctx); err != nil {
310				return err
311			}
312		} else {
313			c.cancelDrag()
314		}
315		c.pressed[heldKey(key)] = true
316		if err := c.publish(ctx); err != nil {
317			return err
318		}
319		return c.rehit(ctx)
320	}
321	// Hint mode takes every key press before binds and windows.
322	if handled, err := c.hintKey(ctx, key); handled || err != nil {
323		return err
324	}
325	// The overview takes its keys before any window; others still
326	// run binds, and are not forwarded.
327	// A launcher or a menu holding the keyboard gets them first.
328	if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
329		now := c.now()
330		var shots []viewShot
331		if key.Pressed {
332			shots = c.snapshot(now)
333		}
334		if key.Pressed && mon.overviewKey(key) {
335			if !mon.ov.open && keyName(key.Keysym) != "Escape" {
336				id, _ := mon.Focused()
337				if id != 0 {
338					c.requestFlash(flashWindow, id)
339				}
340			}
341			c.pressed[heldKey(key)] = true
342			if mon.ov.open && overviewRepeats(key) {
343				c.startRepeat(key, "")
344			}
345			c.transition(shots, now)
346			if err := c.workspaceVisible(ctx, true); err != nil {
347				return err
348			}
349			if err := c.publish(ctx); err != nil {
350				return err
351			}
352			return nil
353		}
354	}
355	// A focused window inhibiting shortcuts gets every key:
356	// no bind runs (emergency quit and VT switch are handled by
357	// input before core).
358	if c.keyboard.inhibited(c.keyboardFocus()) {
359		if held := heldKey(key); !key.Pressed && c.pressed[held] {
360			// Its press ran a bind before inhibiting began: the
361			// window never saw it.
362			delete(c.pressed, held)
363			return nil
364		}
365		if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
366			return err
367		}
368		return nil
369	}
370	name := keyName(key.Keysym)
371	// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
372	// prints exclam on US); then the physical key (code:N).
373	action, bound := c.binds[binding{key: name, mods: key.Mods}]
374	if !bound && key.Base != "" {
375		action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
376	}
377	if !bound && key.Keycode != 0 {
378		action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
379	}
380	// Track presses by physical key: Shift may be released before the key.
381	held := heldKey(key)
382	if !key.Pressed {
383		consumed := c.pressed[held]
384		delete(c.pressed, held)
385		if consumed {
386			return nil
387		}
388	}
389	if key.Pressed {
390		if bound {
391			c.pressed[held] = true
392			if repeats(action) {
393				c.startRepeat(key, action)
394			}
395			return c.runBind(ctx, action)
396		}
397		c.pressed[held] = false
398	}
399	if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
400		if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
401			return err
402		}
403	}
404	return nil
405}
406
407// runBind runs the action of a bound key press.
408func (c *Core) runBind(ctx context.Context, action Action) error {
409	if action == ActionScaleUp || action == ActionScaleDown {
410		return c.scaleBind(ctx, action)
411	}
412	if dir := switchDir(action); dir != 0 {
413		return c.switchBind(ctx, dir)
414	}
415	before := c.cur().mon.Current()
416	swiped := c.swipedWorkspace()
417	now := c.now()
418	// Snapshot before the switcher closes: its cards fly back to the layout.
419	shots := c.snapshot(now)
420	if c.switching() {
421		c.cancelSwitcher()
422	}
423	c.keyboard.takeBack() // a bind acts on the windows
424	effect := c.applyAction(action)
425	if effect.Quit {
426		return ErrQuit
427	}
428	if effect.Spawn {
429		if err := c.runSpawn(ctx, before, effect); err != nil {
430			return err
431		}
432	}
433	if c.swipedWorkspace() != swiped {
434		c.dropSwipe()
435	}
436	c.transition(shots, now)
437	if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
438		return err
439	}
440	if effect.Close != 0 {
441		if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
442			return err
443		}
444	}
445	if err := c.publish(ctx); err != nil {
446		return err
447	}
448	if c.dataDrag != nil {
449		// A data drag follows the layout under the still cursor.
450		return c.rehit(ctx)
451	}
452	return nil
453}
454
455// runSpawn starts the programs of a spawn effect of an action applied to w:
456// the given commands, else the configured terminal. A stash effect opens the
457// windows in the stash of w. It reports errSecurityChanged when the input
458// epoch changed meanwhile.
459func (c *Core) runSpawn(ctx context.Context, w *Workspace, effect Effect) error {
460	commands := effect.Commands
461	if commands == nil {
462		commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
463	}
464	if effect.Stash {
465		if err := c.spawnStash(ctx, w, commands, effect.Commands == nil); err != nil {
466			return err
467		}
468	} else {
469		// Explicit actions are suppressed on a full launcher queue;
470		// never keep the input owner waiting across an epoch change.
471		for _, argv := range commands {
472			if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
473				break
474			}
475		}
476	}
477	if c.inputEpochChanged() {
478		return errSecurityChanged
479	}
480	return nil
481}
482
483// spawnStash runs commands whose windows open in the stash of w, the
484// workspace the bind acted on. Wayland reads the claim tokens only while it
485// knows slots are pending, so that is published before the spawn, as
486// spawnEmpty does. A send the gate suppresses leaves no claim behind. With
487// terminal set (the terminal of an empty stash), nothing starts while the
488// previous one is still awaited.
489func (c *Core) spawnStash(ctx context.Context, w *Workspace, commands [][]string, terminal bool) error {
490	if c.protectionRequested() {
491		return nil
492	}
493	now := c.now()
494	c.placement.expireWorkspaceClaims(now)
495	if terminal && c.placement.workspacePending(w, spawnStash) {
496		return nil
497	}
498	reqs := make([]ports.SpawnRequest, len(commands))
499	for i, argv := range commands {
500		reqs[i] = c.placement.request(argv, spawnTarget{kind: spawnStash, ws: w, at: now})
501	}
502	_, err := c.dispatch(ctx, reqs)
503	return err
504}
505
506// scaleBind steps the focused output's scale and asks for it to be saved.
507func (c *Core) scaleBind(ctx context.Context, action Action) error {
508	dir := 1
509	if action == ActionScaleDown {
510		dir = -1
511	}
512	sc := c.cur()
513	prev := sc.scale
514	sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
515	c.order()
516	if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
517		// Never wait on persistence: a stalled save drops the change.
518		select {
519		case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
520		default:
521		}
522	}
523	return c.publish(ctx)
524}

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