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.motionMsec = v.TimeMsec
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.layout().Clamp(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 o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 {
32			c.pointerOutput = o.Info.Name
33			if o.Info.Name != c.cur().name() {
34				c.focusScreen = c.screenIndex(o.Info.Name)
35				if err := c.publish(ctx); err != nil {
36					return err
37				}
38			}
39		}
40		if c.drag != nil {
41			// The dragged window and the others get no pointer events.
42			return c.dragMotion(ctx, v.TimeMsec)
43		}
44		id, x, y := c.hit(c.cursorX, c.cursorY)
45		// Layout changes are intentionally re-hit-tested only on motion.
46		if id != c.pointer {
47			c.pointer = id
48			if err := c.command(ctx, ports.PointerFocus{ID: id, X: x, Y: y}); err != nil {
49				return err
50			}
51		}
52		c.pointerAt = [2]float64{x, y}
53		if id != 0 {
54			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, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil {
55				return err
56			}
57		}
58		return nil
59	case ports.PointerButton:
60		// Buttons a drag took stay in c.buttons until released, so the
61		// security gate admits their release; no client sees it.
62		if !v.Pressed && c.swallow[v.Button] {
63			delete(c.swallow, v.Button)
64			delete(c.buttons, v.Button)
65			return nil
66		}
67		if d := c.drag; d != nil {
68			if !v.Pressed && v.Button == d.button {
69				delete(c.buttons, v.Button)
70				return c.endDrag(ctx)
71			}
72			// Other buttons do nothing mid-drag; their releases neither.
73			if v.Pressed {
74				c.buttons[v.Button] = true
75				c.swallow[v.Button] = true
76			}
77			return nil
78		}
79		if started, err := c.startDrag(ctx, v); err != nil || started {
80			return err
81		}
82		if v.Pressed && c.pointer == 0 && c.grab == 0 && c.held() == 0 {
83			if picked, err := c.overviewClick(ctx); err != nil {
84				return err
85			} else if picked {
86				return nil
87			}
88		}
89		id := c.pointer
90		if c.grab != 0 {
91			id = c.grab
92		}
93		if v.Pressed && c.held() == 0 {
94			// A click outside an open menu closes it.
95			if err := c.dismissGrabs(ctx, id); err != nil {
96				return err
97			}
98		}
99		if v.Pressed {
100			if c.held() == 0 {
101				c.grab = id
102			}
103			c.buttons[v.Button] = true
104			c.lastButton = v.Button
105		} else {
106			delete(c.buttons, v.Button)
107		}
108		if id != 0 {
109			if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
110				return err
111			}
112			// A click on an on-demand layer gives it the keyboard.
113			if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
114				if err := c.publish(ctx); err != nil {
115					return err
116				}
117			} else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
118				// A click anywhere else takes the keyboard back.
119				c.keyboard.takeBack()
120				if err := c.publish(ctx); err != nil {
121					return err
122				}
123			}
124			// A click focuses the window and its output.
125			s, w := c.screenOf(id)
126			if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
127				w.Click(id)
128				c.focusScreen = c.screenIndex(s.name())
129				if err := c.publish(ctx); err != nil {
130					return err
131				}
132			}
133		}
134		if c.held() == 0 {
135			c.grab = 0
136		}
137		return nil
138	case ports.PointerAxis:
139		c.scrollStop(v)
140		// In the overview, scrolling moves the selection.
141		if c.cur().mon.ov.open && !c.overviewKeyboardTaken() {
142			if c.cur().mon.overviewScroll(v) {
143				if err := c.workspaceVisible(ctx, true); err != nil {
144					return err
145				}
146				if err := c.publish(ctx); err != nil {
147					return err
148				}
149			}
150			return nil
151		}
152		// Scroll goes to the window under the pointer, which has the
153		// pointer focus even mid-drag. None during a window drag.
154		if c.pointer != 0 && c.drag == nil {
155			if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
156				return err
157			}
158		}
159		return nil
160	case ports.SwipeBegin:
161		if c.swipeBegin(v) {
162			return c.slid(ctx, false)
163		}
164		return nil
165	case ports.SwipeUpdate:
166		if c.swipeUpdate(v) {
167			return c.slid(ctx, false)
168		}
169		return nil
170	case ports.SwipeEnd:
171		return c.slid(ctx, c.swipeEnd(v))
172	}
173	key, ok := ev.(ports.KeyEvent)
174	if !ok {
175		return nil
176	}
177	c.mods = key.Mods
178	if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
179		// Escape cancels the drag; its release goes nowhere either.
180		c.cancelDrag()
181		c.pressed[heldKey(key)] = true
182		if err := c.publish(ctx); err != nil {
183			return err
184		}
185		return c.rehit(ctx)
186	}
187	// The overview takes its keys before any window; others still
188	// run binds, and are not forwarded.
189	// A launcher or a menu holding the keyboard gets them first.
190	if mon := c.cur().mon; mon.ov.open && !c.overviewKeyboardTaken() {
191		if key.Pressed && mon.overviewKey(key) {
192			c.pressed[heldKey(key)] = true
193			if err := c.workspaceVisible(ctx, true); err != nil {
194				return err
195			}
196			if err := c.publish(ctx); err != nil {
197				return err
198			}
199			return nil
200		}
201	}
202	// A focused window inhibiting shortcuts gets every key:
203	// no bind runs (emergency quit and VT switch are handled by
204	// input before core).
205	if c.keyboard.inhibited(c.keyboardFocus()) {
206		if held := heldKey(key); !key.Pressed && c.pressed[held] {
207			// Its press ran a bind before inhibiting began: the
208			// window never saw it.
209			delete(c.pressed, held)
210			return nil
211		}
212		if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
213			return err
214		}
215		return nil
216	}
217	name := keyName(key.Keysym)
218	// A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
219	// prints exclam on US); then the physical key (code:N).
220	action, bound := c.binds[binding{key: name, mods: key.Mods}]
221	if !bound && key.Base != "" {
222		action, bound = c.binds[binding{key: keyName(key.Base), mods: key.Mods}]
223	}
224	if !bound && key.Keycode != 0 {
225		action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
226	}
227	// Track presses by physical key: Shift may be released before the key.
228	held := heldKey(key)
229	if !key.Pressed {
230		consumed := c.pressed[held]
231		delete(c.pressed, held)
232		if consumed {
233			return nil
234		}
235	}
236	if key.Pressed {
237		if bound {
238			c.pressed[held] = true
239			if action == ActionScaleUp || action == ActionScaleDown {
240				dir := 1
241				if action == ActionScaleDown {
242					dir = -1
243				}
244				sc := c.cur()
245				prev := sc.scale
246				sc.setScale(StepScale(sc.info.Width, sc.info.Height, sc.scale, dir))
247				c.order()
248				if c.ch.Scales != nil && sc.scale != prev && sc.name() != "" {
249					// Never wait on persistence: a stalled save drops the change.
250					select {
251					case c.ch.Scales <- ports.ScaleChanged{Output: sc.name(), Scale: sc.scale}:
252					default:
253					}
254				}
255				if err := c.publish(ctx); err != nil {
256					return err
257				}
258				return nil
259			}
260			before := c.cur().mon.Current()
261			swiped := c.swipedWorkspace()
262			c.keyboard.takeBack() // a bind acts on the windows
263			effect := c.applyAction(action)
264			if effect.Quit {
265				return ErrQuit
266			}
267			if effect.Spawn {
268				argv := effect.Argv
269				if argv == nil {
270					argv = append([]string(nil), c.cfg.Terminal.Command...)
271				}
272				// Explicit actions are suppressed on a full launcher queue;
273				// never keep the input owner waiting across an epoch change.
274				c.trySpawn(ctx, ports.SpawnRequest{Argv: argv})
275				if c.inputEpochChanged() {
276					return errSecurityChanged
277				}
278			}
279			if c.swipedWorkspace() != swiped {
280				c.dropSwipe()
281			}
282			if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
283				return err
284			}
285			if effect.Close != 0 {
286				if err := c.command(ctx, ports.CloseWindow{ID: effect.Close}); err != nil {
287					return err
288				}
289			}
290			if err := c.publish(ctx); err != nil {
291				return err
292			}
293			return nil
294		}
295		c.pressed[held] = false
296	}
297	if id := c.keyboardFocus(); id != 0 && (!c.cur().mon.ov.open || c.overviewKeyboardTaken()) {
298		if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil {
299			return err
300		}
301	}
302	return nil
303}

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