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 forwardsSwipe(v.Fingers) {
30			return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
31		}
32		if err := c.gestureAbort(ctx, v.Time); err != nil {
33			return err
34		}
35		if c.swipeBegin(v) {
36			return c.slid(ctx, false)
37		}
38		return nil
39	case ports.SwipeUpdate:
40		if c.gesture.on {
41			return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GestureSwipe, DX: v.AccelDX, DY: v.AccelDY, Time: v.Time})
42		}
43		before := c.cur().mon.Current()
44		if c.swipeUpdate(v) {
45			return c.slid(ctx, c.cur().mon.Current() != before)
46		}
47		return nil
48	case ports.SwipeEnd:
49		if c.gesture.on && c.gesture.kind == ports.GestureSwipe {
50			return c.gestureEnd(ctx, ports.GestureSwipe, v.Cancelled, v.Time)
51		}
52		return c.slid(ctx, c.swipeEnd(v))
53	case ports.PinchBegin:
54		return c.gestureBegin(ctx, ports.GesturePinch, v.Fingers, v.Time)
55	case ports.PinchUpdate:
56		return c.gestureUpdate(ctx, ports.GestureUpdateTo{Kind: ports.GesturePinch, DX: v.DX, DY: v.DY, Scale: v.Scale, Rotation: v.Rotation, Time: v.Time})
57	case ports.PinchEnd:
58		return c.gestureEnd(ctx, ports.GesturePinch, v.Cancelled, v.Time)
59	case ports.HoldBegin:
60		return c.gestureBegin(ctx, ports.GestureHold, v.Fingers, v.Time)
61	case ports.HoldEnd:
62		return c.gestureEnd(ctx, ports.GestureHold, v.Cancelled, v.Time)
63	case ports.KeyEvent:
64		return c.keyEvent(ctx, v)
65	}
66	return nil
67}
68
69func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error {
70	c.motionTime = v.Time
71	// A locked pointer stays still; relative motion still flows.
72	if c.constraint.Mode != ports.ConstraintLock {
73		c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
74	}
75	// The focused screen follows the pointer, so new windows
76	// and launchers open where the user is.
77	// Only a pointer entering another output switches: keyboard
78	// moves to another screen stick until then. Not mid-drag of a
79	// client; a window drag (drag.go) follows the pointer.
80	if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 {
81		name := c.screens[i].name()
82		c.pointerOutput = name
83		if name != c.cur().name() {
84			c.focusScreen = i
85			if err := c.publish(ctx); err != nil {
86				return err
87			}
88		}
89	}
90	if c.drag != nil {
91		// The dragged window and the others get no pointer events.
92		return c.dragMotion(ctx, v.Time)
93	}
94	id, x, y := c.hit(c.cursorX, c.cursorY)
95	// Layout changes are intentionally re-hit-tested only on motion.
96	if id != c.pointer {
97		c.pointer = id
98		if err := c.pointerFocus(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
99			return err
100		}
101	}
102	c.pointerAt = [2]float64{x, y}
103	if id != 0 {
104		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 {
105			return err
106		}
107	}
108	return nil
109}
110
111func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error {
112	// Buttons a drag took stay in c.buttons until released, so the
113	// security gate admits their release; no client sees it.
114	if !v.Pressed && c.swallow[v.Button] {
115		delete(c.swallow, v.Button)
116		delete(c.buttons, v.Button)
117		return nil
118	}
119	if d := c.drag; d != nil {
120		if !v.Pressed && v.Button == d.button {
121			delete(c.buttons, v.Button)
122			return c.endDrag(ctx)
123		}
124		// Other buttons do nothing mid-drag; their releases neither.
125		if v.Pressed {
126			c.buttons[v.Button] = true
127			c.swallow[v.Button] = true
128		}
129		return nil
130	}
131	if v.Pressed && c.switching() {
132		if handled, err := c.switcherClick(ctx, v.Button); err != nil || handled {
133			return err
134		}
135	}
136	if started, err := c.startDrag(ctx, v); err != nil || started {
137		return err
138	}
139	if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
140		if picked, err := c.overviewClick(ctx); err != nil {
141			return err
142		} else if picked {
143			return nil
144		}
145	}
146	id := c.pointer
147	if c.grab != 0 {
148		id = c.grab
149	}
150	if v.Pressed && c.held() == 0 {
151		// A click outside an open menu closes it.
152		if err := c.dismissGrabs(ctx, id); err != nil {
153			return err
154		}
155	}
156	if v.Pressed {
157		if c.held() == 0 {
158			c.grab = id
159		}
160		c.buttons[v.Button] = true
161		c.lastButton = v.Button
162	} else {
163		delete(c.buttons, v.Button)
164	}
165	if id != 0 {
166		if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
167			return err
168		}
169		// A click on an on-demand layer gives it the keyboard.
170		if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
171			if err := c.publish(ctx); err != nil {
172				return err
173			}
174		} else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
175			// A click anywhere else takes the keyboard back.
176			c.keyboard.takeBack()
177			if err := c.publish(ctx); err != nil {
178				return err
179			}
180		}
181		// A click focuses the window and its output.
182		s, w := c.screenOf(id)
183		if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
184			now := c.now()
185			before := c.snapshot(now)
186			w.Click(id)
187			c.focusScreen = c.screenIndex(s.name())
188			c.transition(before, now)
189			if err := c.publish(ctx); err != nil {
190				return err
191			}
192		}
193	}
194	if c.held() == 0 {
195		c.grab = 0
196	}
197	return nil
198}
199
200func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error {
201	c.scrollStop(v)
202	// Only wheel/continuous scrolling navigates the overview.
203	// Two-finger scrolling belongs to clients, never to navigation.
204	if v.Source != ports.AxisFinger && c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
205		// overviewScroll owns which scrolls step; the shot costs no
206		// allocation once warm.
207		now := c.now()
208		shots := c.snapshot(now)
209		if c.cur().mon.overviewScroll(v) {
210			c.transition(shots, now)
211			if err := c.workspaceVisible(ctx, true); err != nil {
212				return err
213			}
214			if err := c.publish(ctx); err != nil {
215				return err
216			}
217		}
218		return nil
219	}
220	// Scroll goes to the window under the pointer, which has the
221	// pointer focus even mid-drag. None during a window drag.
222	if c.pointer != 0 && c.drag == nil {
223		if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
224			return err
225		}
226	}
227	return nil
228}
229
230func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
231	c.mods = key.Mods
232	// Releasing the command key commits the switcher; the release still
233	// goes on to the bind lookup and the clients.
234	if c.switching() && c.mods&c.cmdMod == 0 {
235		if err := c.releaseSwitcher(ctx); err != nil {
236			return err
237		}
238	}
239	if c.switching() && key.Pressed && key.Keysym == "Escape" {
240		// Escape cancels the switcher; its release goes nowhere either.
241		c.pressed[heldKey(key)] = true
242		return c.abortSwitcher(ctx)
243	}
244	if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
245		// Escape cancels the drag; its release goes nowhere either.
246		c.cancelDrag()
247		c.pressed[heldKey(key)] = true
248		if err := c.publish(ctx); err != nil {
249			return err
250		}
251		return c.rehit(ctx)
252	}
253	// The overview takes its keys before any window; others still
254	// run binds, and are not forwarded.
255	// A launcher or a menu holding the keyboard gets them first.
256	if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
257		now := c.now()
258		var shots []viewShot
259		if key.Pressed {
260			shots = c.snapshot(now)
261		}
262		if key.Pressed && mon.overviewKey(key) {
263			c.pressed[heldKey(key)] = true
264			c.transition(shots, now)
265			if err := c.workspaceVisible(ctx, true); err != nil {
266				return err
267			}
268			if err := c.publish(ctx); err != nil {
269				return err
270			}
271			return nil
272		}
273	}
274	// A focused window inhibiting shortcuts gets every key:
275	// no bind runs (emergency quit and VT switch are handled by
276	// input before core).
277	if c.keyboard.inhibited(c.keyboardFocus()) {
278		if held := heldKey(key); !key.Pressed && c.pressed[held] {
279			// Its press ran a bind before inhibiting began: the
280			// window never saw it.
281			delete(c.pressed, held)
282			return nil
283		}
284		if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
285			return err
286		}
287		return nil
288	}
289	name := keyName(key.Keysym)
290	// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
291	// prints exclam on US); then the physical key (code:N).
292	action, bound := c.binds[binding{key: name, mods: key.Mods}]
293	if !bound && key.Base != "" {
294		action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
295	}
296	if !bound && key.Keycode != 0 {
297		action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
298	}
299	// Track presses by physical key: Shift may be released before the key.
300	held := heldKey(key)
301	if !key.Pressed {
302		consumed := c.pressed[held]
303		delete(c.pressed, held)
304		if consumed {
305			return nil
306		}
307	}
308	if key.Pressed {
309		if bound {
310			c.pressed[held] = true
311			return c.runBind(ctx, action)
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}
322
323// runBind runs the action of a bound key press.
324func (c *Core) runBind(ctx context.Context, action Action) error {
325	if action == ActionScaleUp || action == ActionScaleDown {
326		return c.scaleBind(ctx, action)
327	}
328	if dir := switchDir(action); dir != 0 {
329		return c.switchBind(ctx, dir)
330	}
331	before := c.cur().mon.Current()
332	swiped := c.swipedWorkspace()
333	now := c.now()
334	// Snapshot before the switcher closes: its cards fly back to the layout.
335	shots := c.snapshot(now)
336	if c.switching() {
337		c.cancelSwitcher()
338	}
339	c.keyboard.takeBack() // a bind acts on the windows
340	effect := c.applyAction(action)
341	if effect.Quit {
342		return ErrQuit
343	}
344	if effect.Spawn {
345		commands := effect.Commands
346		if commands == nil {
347			commands = [][]string{append([]string(nil), c.cfg.Terminal.Command...)}
348		}
349		// Explicit actions are suppressed on a full launcher queue;
350		// never keep the input owner waiting across an epoch change.
351		for _, argv := range commands {
352			if !c.trySpawn(ctx, ports.SpawnRequest{Argv: argv}) {
353				break
354			}
355		}
356		if c.inputEpochChanged() {
357			return errSecurityChanged
358		}
359	}
360	if c.swipedWorkspace() != swiped {
361		c.dropSwipe()
362	}
363	c.transition(shots, now)
364	if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
365		return err
366	}
367	if effect.Close != 0 {
368		if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
369			return err
370		}
371	}
372	return c.publish(ctx)
373}
374
375// scaleBind steps the focused output's scale and asks for it to be saved.
376func (c *Core) scaleBind(ctx context.Context, action Action) error {
377	dir := 1
378	if action == ActionScaleDown {
379		dir = -1
380	}
381	sc := c.cur()
382	prev := sc.scale
383	sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
384	c.order()
385	if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
386		// Never wait on persistence: a stalled save drops the change.
387		select {
388		case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
389		default:
390		}
391	}
392	return c.publish(ctx)
393}

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