detached d5d679d

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.drag != nil {
102		// The dragged window and the others get no pointer events.
103		return c.dragMotion(ctx, v.Time)
104	}
105	if c.dataDrag != nil {
106		// A data drag goes to the surface under the pointer, by its own
107		// commands.
108		return c.dataDragMotion(ctx, v.Time)
109	}
110	id, x, y := c.hit(c.cursorX, c.cursorY)
111	// Layout changes are intentionally re-hit-tested only on motion.
112	if id != c.pointer {
113		c.pointer = id
114		if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
115			return err
116		}
117	}
118	c.pointerAt = [2]float64{x, y}
119	if id != 0 {
120		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 {
121			return err
122		}
123	}
124	return nil
125}
126
127// clickFocus focuses the window id and its output the way a click on it
128// does, unless it already has the keyboard.
129func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
130	s, w := c.screenOf(id)
131	if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
132		return nil
133	}
134	now := c.now()
135	before := c.snapshot(now)
136	w.Click(id)
137	c.focusScreen = c.screenIndex(s.name())
138	c.transition(before, now)
139	return c.publish(ctx)
140}
141
142func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
143	// Buttons a drag took stay in c.buttons until released, so the
144	// security gate admits their release; no client sees it.
145	if !v.Pressed && c.swallow[v.Button] {
146		delete(c.swallow, v.Button)
147		delete(c.buttons, v.Button)
148		return nil
149	}
150	if c.dataDrag != nil {
151		return c.dataDragButton(ctx, v)
152	}
153	if d := c.drag; d != nil {
154		if !v.Pressed && v.Button == d.button {
155			delete(c.buttons, v.Button)
156			return c.endDrag(ctx)
157		}
158		// Other buttons do nothing mid-drag; their releases neither.
159		if v.Pressed {
160			c.buttons[v.Button] = true
161			c.swallow[v.Button] = true
162		}
163		return nil
164	}
165	if v.Pressed && c.switching() {
166		if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
167			return err
168		}
169	}
170	if started, err := c.startDrag(ctx, v); err != nil || started {
171		return err
172	}
173	if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
174		if picked, err := c.overviewClick(ctx); err != nil {
175			return err
176		} else if picked {
177			return nil
178		}
179	}
180	id := c.pointer
181	if c.grab != 0 {
182		id = c.grab
183	}
184	if v.Pressed && c.held() == 0 {
185		// A click outside an open menu closes it.
186		if err := c.dismissGrabs(ctx, id); err != nil {
187			return err
188		}
189	}
190	grab := v.Pressed && c.held() == 0
191	if v.Pressed {
192		if grab {
193			c.grab = id
194		}
195		c.buttons[v.Button] = true
196		c.lastButton = v.Button
197	} else {
198		delete(c.buttons, v.Button)
199	}
200	if id != 0 {
201		if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil {
202			return err
203		}
204		// A click on an on-demand layer gives it the keyboard.
205		if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
206			if err := c.publish(ctx); err != nil {
207				return err
208			}
209		} else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
210			// A click anywhere else takes the keyboard back.
211			c.keyboard.takeBack()
212			if err := c.publish(ctx); err != nil {
213				return err
214			}
215		}
216		// A click focuses the window and its output.
217		if v.Pressed {
218			if err := c.clickFocus(ctx, id); err != nil {
219				return err
220			}
221		}
222	}
223	if c.held() == 0 {
224		c.grab = 0
225	}
226	return nil
227}
228
229func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
230	c.scrollStop(v)
231	// Only wheel/continuous scrolling navigates the overview.
232	// Two-finger scrolling belongs to clients, never to navigation.
233	if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
234		// overviewScroll owns which scrolls step; the shot costs no
235		// allocation once warm.
236		now := c.now()
237		shots := c.snapshot(now)
238		if c.cur().mon.overviewScroll(v) {
239			c.transition(shots, now)
240			if err := c.workspaceVisible(ctx, true); err != nil {
241				return err
242			}
243			if err := c.publish(ctx); err != nil {
244				return err
245			}
246		}
247		return nil
248	}
249	// Scroll goes to the window under the pointer, which has the
250	// pointer focus even mid-drag. None during a window drag.
251	if c.pointer != 0 && c.drag == nil && c.dataDrag == nil {
252		if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
253			return err
254		}
255	}
256	return nil
257}
258
259func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
260	c.mods = key.Mods
261	c.keyState = key.State
262	c.repeatKey(key)
263	// Releasing the command key commits the switcher; the release still
264	// goes on to the bind lookup and the clients.
265	if c.switching() && c.mods&c.cmdMod == 0 {
266		if err := c.releaseSwitcher(ctx); err != nil {
267			return err
268		}
269	}
270	if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
271		// Escape cancels the data drag first, even over the switcher;
272		// its release goes nowhere either.
273		c.pressed[heldKey(key)] = true
274		return c.cancelDataDrag(ctx)
275	}
276	if c.switching() && key.Pressed && key.Keysym == "Escape" {
277		// Escape cancels the switcher; its release goes nowhere either.
278		c.pressed[heldKey(key)] = true
279		return c.abortSwitcher(ctx)
280	}
281	if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
282		// Escape cancels the drag; its release goes nowhere either.
283		c.cancelDrag()
284		c.pressed[heldKey(key)] = true
285		if err := c.publish(ctx); err != nil {
286			return err
287		}
288		return c.rehit(ctx)
289	}
290	// Hint mode takes every key press before binds and windows.
291	if handled, err := c.hintKey(ctx, key); handled || err != nil {
292		return err
293	}
294	// The overview takes its keys before any window; others still
295	// run binds, and are not forwarded.
296	// A launcher or a menu holding the keyboard gets them first.
297	if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
298		now := c.now()
299		var shots []viewShot
300		if key.Pressed {
301			shots = c.snapshot(now)
302		}
303		if key.Pressed && mon.overviewKey(key) {
304			if !mon.ov.open && keyName(key.Keysym) != "Escape" {
305				id, _ := mon.Focused()
306				if id != 0 {
307					c.requestFlash(flashWindow, id)
308				}
309			}
310			c.pressed[heldKey(key)] = true
311			if mon.ov.open && overviewRepeats(key) {
312				c.startRepeat(key, "")
313			}
314			c.transition(shots, now)
315			if err := c.workspaceVisible(ctx, true); err != nil {
316				return err
317			}
318			if err := c.publish(ctx); err != nil {
319				return err
320			}
321			return nil
322		}
323	}
324	// A focused window inhibiting shortcuts gets every key:
325	// no bind runs (emergency quit and VT switch are handled by
326	// input before core).
327	if c.keyboard.inhibited(c.keyboardFocus()) {
328		if held := heldKey(key); !key.Pressed && c.pressed[held] {
329			// Its press ran a bind before inhibiting began: the
330			// window never saw it.
331			delete(c.pressed, held)
332			return nil
333		}
334		if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
335			return err
336		}
337		return nil
338	}
339	name := keyName(key.Keysym)
340	// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
341	// prints exclam on US); then the physical key (code:N).
342	action, bound := c.binds[binding{key: name, mods: key.Mods}]
343	if !bound && key.Base != "" {
344		action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
345	}
346	if !bound && key.Keycode != 0 {
347		action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
348	}
349	// Track presses by physical key: Shift may be released before the key.
350	held := heldKey(key)
351	if !key.Pressed {
352		consumed := c.pressed[held]
353		delete(c.pressed, held)
354		if consumed {
355			return nil
356		}
357	}
358	if key.Pressed {
359		if bound {
360			c.pressed[held] = true
361			if repeats(action) {
362				c.startRepeat(key, action)
363			}
364			return c.runBind(ctx, action)
365		}
366		c.pressed[held] = false
367	}
368	if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
369		if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
370			return err
371		}
372	}
373	return nil
374}
375
376// runBind runs the action of a bound key press.
377func (c *Core) runBind(ctx context.Context, action Action) error {
378	if action == ActionScaleUp || action == ActionScaleDown {
379		return c.scaleBind(ctx, action)
380	}
381	if dir := switchDir(action); dir != 0 {
382		return c.switchBind(ctx, dir)
383	}
384	before := c.cur().mon.Current()
385	swiped := c.swipedWorkspace()
386	now := c.now()
387	// Snapshot before the switcher closes: its cards fly back to the layout.
388	shots := c.snapshot(now)
389	if c.switching() {
390		c.cancelSwitcher()
391	}
392	c.keyboard.takeBack() // a bind acts on the windows
393	effect := c.applyAction(action)
394	if effect.Quit {
395		return ErrQuit
396	}
397	if effect.Spawn {
398		if err := c.runSpawn(ctx, before, effect); err != nil {
399			return err
400		}
401	}
402	if c.swipedWorkspace() != swiped {
403		c.dropSwipe()
404	}
405	c.transition(shots, now)
406	if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
407		return err
408	}
409	if effect.Close != 0 {
410		if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
411			return err
412		}
413	}
414	if err := c.publish(ctx); err != nil {
415		return err
416	}
417	if c.dataDrag != nil {
418		// A data drag follows the layout under the still cursor.
419		return c.rehit(ctx)
420	}
421	return nil
422}
423
424// runSpawn starts the programs of a spawn effect of an action applied to w:
425// the given commands, else the configured terminal. A stash effect opens the
426// windows in the stash of w. It reports errSecurityChanged when the input
427// epoch changed meanwhile.
428func (c *Core) runSpawn(ctx context.Context, w *Workspace, effect Effect) error {
429	commands := effect.Commands
430	if commands == nil {
431		commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
432	}
433	if effect.Stash {
434		if err := c.spawnStash(ctx, w, commands, effect.Commands == nil); err != nil {
435			return err
436		}
437	} else {
438		// Explicit actions are suppressed on a full launcher queue;
439		// never keep the input owner waiting across an epoch change.
440		for _, argv := range commands {
441			if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
442				break
443			}
444		}
445	}
446	if c.inputEpochChanged() {
447		return errSecurityChanged
448	}
449	return nil
450}
451
452// spawnStash runs commands whose windows open in the stash of w, the
453// workspace the bind acted on. Wayland reads the claim tokens only while it
454// knows slots are pending, so that is published before the spawn, as
455// spawnEmpty does. A send the gate suppresses leaves no claim behind. With
456// terminal set (the terminal of an empty stash), nothing starts while the
457// previous one is still awaited.
458func (c *Core) spawnStash(ctx context.Context, w *Workspace, commands [][]string, terminal bool) error {
459	if c.protectionRequested() {
460		return nil
461	}
462	now := c.now()
463	c.placement.expireWorkspaceClaims(now)
464	if terminal && c.placement.workspacePending(w, spawnStash) {
465		return nil
466	}
467	reqs := make([]ports.SpawnRequest, len(commands))
468	for i, argv := range commands {
469		reqs[i] = c.placement.request(argv, spawnTarget{kind: spawnStash, ws: w, at: now})
470	}
471	_, err := c.dispatch(ctx, reqs)
472	return err
473}
474
475// scaleBind steps the focused output's scale and asks for it to be saved.
476func (c *Core) scaleBind(ctx context.Context, action Action) error {
477	dir := 1
478	if action == ActionScaleDown {
479		dir = -1
480	}
481	sc := c.cur()
482	prev := sc.scale
483	sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
484	c.order()
485	if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
486		// Never wait on persistence: a stalled save drops the change.
487		select {
488		case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
489		default:
490		}
491	}
492	return c.publish(ctx)
493}

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