feat/data-dnd e05cb15

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

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