v0.4.0 2de4e1d

input.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"fmt"
7	"os"
8	"time"
9
10	"github.com/bnema/nefergui/internal/keyboard"
11	"github.com/bnema/wlturbo/protocol/core"
12	"github.com/bnema/wlturbo/wl"
13)
14
15// InputReset tells the runtime to synthesize pointer leave, release all buttons,
16// keyboard focus-out and repeat cancellation before processing later inputs.
17const InputReset = "reset"
18
19// Input is the platform-to-UI port. wl_surface local fixed coordinates are
20// already logical pixels and must not be divided by buffer scale.
21type Input struct {
22	Kind                             string // motion, press, release, axis, leave, key, focus-in, focus-out, reset
23	X, Y, DX, DY                     float64
24	Button, Serial, Axis, AxisSource uint32
25	AxisStop                         bool
26	Enter                            bool   // motion from wl_pointer.enter; Serial is the enter serial
27	PointerGen                       uint64 // wl_pointer generation that sent Enter
28	Value120                         int32
29	Key                              keyboard.Key
30}
31
32// InputAdapter is owned by the session loop. Never call ApplyInput on the reader.
33// The session loop also owns the repeat timer (Tick and NextRepeat).
34type InputAdapter struct {
35	Keyboard *keyboard.Keyboard
36	serial   uint32
37	focused  bool
38}
39
40// resetHeld drops held keys, repeat and compose state after InputReset while
41// keeping keyboard focus, so later key events still apply without a new enter.
42func (a *InputAdapter) resetHeld() {
43	if a != nil && a.Keyboard != nil {
44		a.Keyboard.Focus(a.focused)
45	}
46}
47
48func NewInputAdapter(locale string) (*InputAdapter, error) {
49	k, err := keyboard.New(locale)
50	if err != nil {
51		return nil, err
52	}
53	k.Focus(false)
54	return &InputAdapter{Keyboard: k}, nil
55}
56func (a *InputAdapter) Close() {
57	if a != nil && a.Keyboard != nil {
58		a.Keyboard.Close()
59	}
60}
61func (a *InputAdapter) SelectionSerial() uint32 { return a.serial }
62func (a *InputAdapter) Tick(now time.Time) (Input, bool) {
63	key, ok := a.Keyboard.Tick(now)
64	return Input{Kind: "key", Key: key}, ok
65}
66func (a *InputAdapter) NextRepeat() (time.Time, bool) { return a.Keyboard.NextRepeat() }
67
68// InputAdapter returns the loop-owned interpreter; the reader never calls it.
69func (w *Window) InputAdapter() *InputAdapter { return w.inputAdapter }
70
71// ApplyInput consumes the raw event including its keymap FD, on every path.
72func (a *InputAdapter) ApplyInput(ev Event, now time.Time) ([]Input, error) {
73	switch ev.Kind {
74	case InputFrame:
75		return ev.Inputs, nil
76	case InputKeymap:
77		if ev.FD < 0 {
78			return nil, fmt.Errorf("missing keymap fd")
79		}
80		if ev.Format != 1 || ev.Size == 0 || ev.Size > 16<<20 {
81			CloseEventFD(ev)
82			return nil, fmt.Errorf("unsupported keymap format or size")
83		}
84		return nil, a.Keyboard.ReplaceFD(ev.FD, int(ev.Size))
85	case InputFocusIn:
86		a.Keyboard.Focus(true)
87		a.focused = true
88		a.serial = ev.Serial
89		return []Input{{Kind: "focus-in", Serial: ev.Serial}}, nil
90	case InputFocusOut:
91		a.Keyboard.Focus(false)
92		a.focused = false
93		a.serial = 0
94		return []Input{{Kind: "focus-out"}}, nil
95	case InputKey:
96		if ev.Pressed && a.Keyboard != nil {
97			a.serial = ev.Serial
98		}
99		key, err := a.Keyboard.Event(ev.Code, ev.Pressed, now)
100		if err != nil || key.Physical == 0 {
101			return nil, err
102		}
103		return []Input{{Kind: "key", Key: key, Serial: ev.Serial}}, nil
104	case InputModifiers:
105		return nil, a.Keyboard.Mask(ev.Mods[0], ev.Mods[1], ev.Mods[2], 0, 0, ev.Mods[3])
106	case InputRepeatInfo:
107		a.Keyboard.RepeatInfo(int(ev.Rate), time.Duration(ev.Delay)*time.Millisecond)
108		return nil, nil
109	}
110	return nil, fmt.Errorf("not an input event: %d", ev.Kind)
111}
112
113// CloseEventFD releases a keymap event not delivered to the loop, including FD 0.
114func CloseEventFD(ev Event) {
115	if ev.Kind == InputKeymap && ev.FD >= 0 {
116		_ = os.NewFile(uintptr(ev.FD), "keymap").Close()
117	}
118}
119
120// BindSeat binds the optional initial seat. The reader posts capability changes
121// and global removal; ApplySeat runs exclusively on the session loop.
122func (w *Window) BindSeat() error {
123	seat := core.NewSeat(w.Display.Context())
124	g, ok := w.Display.Registry().FindGlobal(core.SeatInterface)
125	if !ok {
126		w.Display.Context().Unregister(seat)
127		return nil
128	}
129	version, err := w.Display.Registry().BindNegotiated(core.SeatInterface, 9, seat)
130	if err != nil {
131		w.Display.Context().Unregister(seat)
132		return err
133	}
134	w.Seat, w.seatName, w.seatVersion = seat, g.Name, version
135	seat.OnCapabilities(func(c uint32) { w.post(Event{Kind: SeatCapabilities, Capabilities: c}) })
136	w.Display.Registry().AddGlobalRemoveHandler(seatRemoved{w: w, name: g.Name})
137	return nil
138}
139
140type seatRemoved struct {
141	w    *Window
142	name uint32
143}
144
145func (r seatRemoved) HandleRegistryGlobalRemove(e wl.RegistryGlobalRemoveEvent) {
146	if e.Name == r.name {
147		r.w.post(Event{Kind: SeatRemoved})
148	}
149}
150func (w *Window) ApplySeat(ev Event) error {
151	switch ev.Kind {
152	case SeatRemoved:
153		if w.WLKeyboard != nil && w.connection == nil {
154			w.ownerFocusOut()
155		}
156		return w.releaseSeat()
157	case SeatCapabilities:
158		if w.Seat == nil {
159			return nil
160		}
161		if ev.Capabilities&1 == 0 && w.Pointer != nil {
162			w.dropCursorDevice()
163			w.releaseDevice(w.Pointer)
164			w.Pointer = nil
165		}
166		if ev.Capabilities&2 == 0 && w.WLKeyboard != nil {
167			w.releaseDevice(w.WLKeyboard)
168			w.WLKeyboard = nil
169			if w.keymapCache != nil {
170				w.keymapCache.close()
171				w.keymapCache = nil
172			}
173			if w.connection == nil {
174				w.ownerFocusOut()
175			}
176		}
177		if ev.Capabilities&1 != 0 && w.Pointer == nil {
178			p, err := w.Seat.GetPointer()
179			if err != nil {
180				return err
181			}
182			w.Pointer = p
183			w.watchPointer(p)
184		}
185		if ev.Capabilities&2 != 0 && w.WLKeyboard == nil {
186			k, err := w.Seat.GetKeyboard()
187			if err != nil {
188				return err
189			}
190			w.WLKeyboard = k
191			w.watchKeyboard(k)
192		}
193		return nil
194	}
195	return fmt.Errorf("not a seat event: %d", ev.Kind)
196}
197
198// releaseDevice destroys a wl_pointer or wl_keyboard. Their release request
199// exists since seat version 3; older seats only forget the proxy locally.
200func (w *Window) releaseDevice(p interface {
201	wl.Proxy
202	Release() error
203}) {
204	if w.seatVersion >= 3 {
205		_ = p.Release()
206	} else if c := p.Context(); c != nil {
207		c.Unregister(p)
208	}
209}
210
211// TakeInputEvents transfers lock-only raw input ownership, including keymap
212// FDs. The secret owner must close each FD after consuming it. No Text/Key
213// translation occurs here; ordinary windows continue to use Inputs().
214func (w *Window) TakeInputEvents() []Event {
215	events := w.rawInput
216	w.rawInput = nil
217	return events
218}
219
220func (w *Window) ownerFocusOut() {
221	if w.LockSurface != nil {
222		for _, ev := range w.rawInput {
223			CloseEventFD(ev)
224		}
225		clear(w.rawInput)
226		w.rawInput = append(w.rawInput[:0], Event{Kind: InputFocusOut, Window: w})
227		return
228	}
229	if w.inputAdapter != nil {
230		w.inputAdapter.Keyboard.Focus(false)
231		w.inputAdapter.focused = false
232		w.inputAdapter.serial = 0
233	}
234	w.queueInput([]Input{{Kind: "focus-out"}})
235}
236
237func (w *Window) releaseSeat() error {
238	if w.keymapCache != nil {
239		w.keymapCache.close()
240		w.keymapCache = nil
241	}
242	if w.Pointer != nil {
243		w.dropCursorDevice()
244		w.releaseDevice(w.Pointer)
245		w.Pointer = nil
246	}
247	if w.WLKeyboard != nil {
248		w.releaseDevice(w.WLKeyboard)
249		w.WLKeyboard = nil
250	}
251	if w.Seat != nil {
252		if w.seatVersion >= 5 {
253			_ = w.Seat.Release()
254		} else {
255			w.Display.Context().Unregister(w.Seat)
256		}
257		w.Seat = nil
258	}
259	return nil
260}
261func (w *Window) watchPointer(p *core.Pointer) {
262	if w.connection != nil {
263		w.watchSharedPointer(p)
264		return
265	}
266	var frame []Input // only accessed by the reader
267	var x, y float64
268	var inside bool
269	var source uint32
270	// The generation tells a late enter from a released pointer apart from
271	// the current one; object IDs can be reused.
272	w.pointerGen++
273	surfaceID, version, gen := w.Surface.ID(), w.seatVersion, w.pointerGen
274	flush := func() {
275		if len(frame) > 0 {
276			w.post(Event{Kind: InputFrame, Inputs: frame})
277			frame = nil
278		}
279		source = 0
280	}
281	add := func(i Input) {
282		i.AxisSource = source
283		frame = append(frame, i)
284		if version < 5 {
285			flush()
286		}
287	}
288	p.OnEnter(func(serial uint32, id uint32, px, py wl.Fixed) {
289		if id != surfaceID {
290			return
291		}
292		inside = true
293		x, y = px.Float64(), py.Float64()
294		add(Input{Kind: "motion", X: x, Y: y, Enter: true, Serial: serial, PointerGen: gen})
295	})
296	p.OnLeave(func(_ uint32, id uint32) {
297		if id != surfaceID {
298			return
299		}
300		inside = false
301		add(Input{Kind: "leave"})
302	})
303	p.OnMotion(func(_ uint32, px, py wl.Fixed) {
304		if inside {
305			x, y = px.Float64(), py.Float64()
306			add(Input{Kind: "motion", X: x, Y: y})
307		}
308	})
309	p.OnButton(func(serial, _, button, state uint32) {
310		if !inside && state == 1 {
311			return
312		}
313		kind := "release"
314		if state == 1 {
315			kind = "press"
316		}
317		add(Input{Kind: kind, X: x, Y: y, Button: button, Serial: serial})
318	})
319	p.OnAxisSource(func(s uint32) { source = s })
320	p.OnAxis(func(_, axis uint32, v wl.Fixed) {
321		if inside {
322			i := Input{Kind: "axis", X: x, Y: y, Axis: axis}
323			if axis == 0 {
324				i.DY = v.Float64()
325			} else {
326				i.DX = v.Float64()
327			}
328			add(i)
329		}
330	})
331	p.OnAxisValue120(func(axis uint32, v int32) {
332		if inside {
333			add(Input{Kind: "axis", X: x, Y: y, Axis: axis, Value120: v})
334		}
335	})
336	p.OnAxisStop(func(_, axis uint32) {
337		if inside {
338			add(Input{Kind: "axis", X: x, Y: y, Axis: axis, AxisStop: true})
339		}
340	})
341	p.OnFrame(flush)
342}
343func (w *Window) watchKeyboard(k *core.Keyboard) {
344	if w.connection != nil {
345		w.watchSharedKeyboard(k)
346		return
347	}
348	surfaceID := w.Surface.ID()
349	k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
350		n, err := fd.Take()
351		if err != nil {
352			w.post(Event{Kind: TransportError, Err: err})
353			return
354		}
355		w.postFD(Event{Kind: InputKeymap, FD: n, Format: format, Size: size})
356	})
357	k.OnEnter(func(serial, id uint32, _ []byte) {
358		if id == surfaceID {
359			w.post(Event{Kind: InputFocusIn, Serial: serial})
360		}
361	})
362	k.OnLeave(func(_, id uint32) {
363		if id == surfaceID {
364			w.post(Event{Kind: InputFocusOut})
365		}
366	})
367	k.OnKey(func(serial, _, key, state uint32) {
368		w.post(Event{Kind: InputKey, Serial: serial, Code: key, Pressed: state == 1})
369	})
370	k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
371		w.post(Event{Kind: InputModifiers, Mods: [4]uint32{depressed, latched, locked, group}})
372	})
373	k.OnRepeatInfo(func(rate, delay int32) { w.post(Event{Kind: InputRepeatInfo, Rate: rate, Delay: delay}) })
374}
375
376// InputEvent is a loop-owned input value. Runtime integration drains these later.
377type InputEvent = Input
378
379// DrainInput appends pending input in order to dst and clears the queue.
380// The queue holds at most 4096 events. Motion is evicted first, compatible
381// adjacent axes are merged; if neither is possible it emits InputReset and
382// starts a new queue. InputOverflow counts discarded events. Call on the Apply loop.
383func (w *Window) DrainInput(dst []InputEvent) []InputEvent {
384	dst = append(dst, w.inputQueue...)
385	clear(w.inputQueue)
386	w.inputQueue = w.inputQueue[:0]
387	return dst
388}
389
390func mergeAxis(dst *Input, src Input) bool {
391	if dst.Kind != "axis" || src.Kind != "axis" || dst.Axis != src.Axis ||
392		dst.AxisSource != src.AxisSource || dst.AxisStop || src.AxisStop {
393		return false
394	}
395	dst.DX += src.DX
396	dst.DY += src.DY
397	dst.Value120 += src.Value120
398	dst.X, dst.Y = src.X, src.Y
399	return true
400}
401
402func (w *Window) queueInput(inputs []Input) {
403	for _, input := range inputs {
404		if len(w.inputQueue) == 4096 {
405			index := -1
406			for i, v := range w.inputQueue {
407				// An enter motion carries the serial cursor shapes need.
408				if v.Kind == "motion" && !v.Enter {
409					index = i
410					break
411				}
412			}
413			if index < 0 {
414				// Adjacent axes on the same axis/source can be combined without
415				// crossing a button or key event or losing scroll distance.
416				for i := 0; i+1 < len(w.inputQueue); i++ {
417					if mergeAxis(&w.inputQueue[i], w.inputQueue[i+1]) {
418						index = i + 1
419						break
420					}
421				}
422			}
423			if index >= 0 {
424				if w.inputQueue[index].Kind == "motion" {
425					w.InputOverflow++
426				}
427				copy(w.inputQueue[index:], w.inputQueue[index+1:])
428				w.inputQueue = w.inputQueue[:len(w.inputQueue)-1]
429			} else {
430				// A previous reset is still pending if the consumer has not
431				// drained yet; keep one marker across successive overflows.
432				dropped := len(w.inputQueue)
433				if w.inputQueue[0].Kind == InputReset {
434					dropped--
435				}
436				w.InputOverflow += uint64(dropped)
437				// Keep the latest discarded enter or leave: it still decides
438				// whether the pointer is inside, and an enter carries the serial
439				// cursor shapes need.
440				var focus *Input
441				for i := range w.inputQueue {
442					if v := w.inputQueue[i]; v.Enter || v.Kind == "leave" {
443						focus = &v
444					}
445				}
446				clear(w.inputQueue)
447				w.inputQueue = w.inputQueue[:0]
448				w.inputQueue = append(w.inputQueue, Input{Kind: InputReset})
449				if focus != nil {
450					w.inputQueue = append(w.inputQueue, *focus)
451				}
452				// Discarded presses must not keep repeating on the loop.
453				w.inputAdapter.resetHeld()
454			}
455		}
456		w.inputQueue = append(w.inputQueue, input)
457	}
458}

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 1

Tags 12

↑↓ moveenter openesc close