v0.4.0 2de4e1d

connection_input.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"sync"
7	"syscall"
8
9	"github.com/bnema/wlturbo/protocol/core"
10	"github.com/bnema/wlturbo/wl"
11	"golang.org/x/sys/unix"
12)
13
14// A cache belongs to exactly one keyboard generation. Dup/replace/close share
15// its FD mutex, not a window-state mutex, so capability loss cannot close an FD
16// while a reader callback duplicates it. Retired callbacks are inert.
17type keyboardMapCache struct {
18	mu     sync.Mutex
19	fd     int
20	closed bool
21}
22
23func (m *keyboardMapCache) replace(fd int) {
24	m.mu.Lock()
25	defer m.mu.Unlock()
26	if m.closed {
27		_ = syscall.Close(fd)
28		return
29	}
30	if m.fd >= 0 {
31		_ = syscall.Close(m.fd)
32	}
33	m.fd = fd
34}
35func (m *keyboardMapCache) duplicate() (int, error) {
36	m.mu.Lock()
37	defer m.mu.Unlock()
38	if m.closed || m.fd < 0 {
39		return -1, nil
40	}
41	// Close-on-exec: the map must not leak into any child process.
42	return unix.FcntlInt(uintptr(m.fd), unix.F_DUPFD_CLOEXEC, 0)
43}
44func (m *keyboardMapCache) close() {
45	m.mu.Lock()
46	defer m.mu.Unlock()
47	if !m.closed && m.fd >= 0 {
48		_ = syscall.Close(m.fd)
49	}
50	m.fd = -1
51	m.closed = true
52}
53
54func (c *Connection) route(id uint32, ev Event) {
55	if value, ok := c.routes.Load(id); ok {
56		ev.Window = value.(*Window)
57		c.post(ev)
58	} else {
59		CloseEventFD(ev)
60	}
61}
62
63// Focus and frame accumulators below are exclusively reader-owned. They carry
64// surface IDs, never pointers to mutable UI/window state.
65func (w *Window) watchSharedPointer(p *core.Pointer) {
66	c := w.connection
67	w.pointerGen++
68	generation, version := w.pointerGen, w.seatVersion
69	var focus uint32
70	var x, y float64
71	var frame []Input
72	var source uint32
73	flush := func() {
74		if len(frame) > 0 {
75			c.route(focus, Event{Kind: InputFrame, Inputs: frame})
76			frame = nil
77		}
78		source = 0
79	}
80	add := func(in Input) {
81		in.AxisSource = source
82		frame = append(frame, in)
83		if version < 5 {
84			flush()
85		}
86	}
87	p.OnEnter(func(serial, id uint32, px, py wl.Fixed) {
88		flush()
89		focus = id
90		x, y = px.Float64(), py.Float64()
91		add(Input{Kind: "motion", X: x, Y: y, Enter: true, Serial: serial, PointerGen: generation})
92	})
93	p.OnLeave(func(_ uint32, id uint32) {
94		if focus == id {
95			add(Input{Kind: "leave"})
96			flush()
97			focus = 0
98		}
99	})
100	p.OnMotion(func(_ uint32, px, py wl.Fixed) {
101		if focus != 0 {
102			x, y = px.Float64(), py.Float64()
103			add(Input{Kind: "motion", X: x, Y: y})
104		}
105	})
106	p.OnButton(func(serial, _, button, state uint32) {
107		if focus != 0 {
108			kind := "release"
109			if state == 1 {
110				kind = "press"
111			}
112			add(Input{Kind: kind, X: x, Y: y, Button: button, Serial: serial})
113		}
114	})
115	p.OnAxisSource(func(value uint32) { source = value })
116	p.OnAxis(func(_, axis uint32, value wl.Fixed) {
117		if focus != 0 {
118			in := Input{Kind: "axis", X: x, Y: y, Axis: axis}
119			if axis == 0 {
120				in.DY = value.Float64()
121			} else {
122				in.DX = value.Float64()
123			}
124			add(in)
125		}
126	})
127	p.OnAxisValue120(func(axis uint32, value int32) {
128		if focus != 0 {
129			add(Input{Kind: "axis", X: x, Y: y, Axis: axis, Value120: value})
130		}
131	})
132	p.OnAxisStop(func(_, axis uint32) {
133		if focus != 0 {
134			add(Input{Kind: "axis", X: x, Y: y, Axis: axis, AxisStop: true})
135		}
136	})
137	p.OnFrame(flush)
138}
139
140func (w *Window) watchSharedKeyboard(k *core.Keyboard) {
141	c := w.connection
142	var focus uint32
143	cache := &keyboardMapCache{fd: -1}
144	if w.keymapCache != nil {
145		w.keymapCache.close() // one live cache per keyboard generation
146	}
147	w.keymapCache = cache
148	var format, size uint32
149	var mods [4]uint32
150	var rate, delay int32
151	// Keep one reader-owned duplicate to initialize windows created on hotplug.
152	// Its lifetime ends when this keyboard proxy or the connection is released.
153	sendMap := func(id uint32) {
154		fd, err := cache.duplicate()
155		if err != nil {
156			c.post(Event{Kind: TransportError, Err: err})
157			return
158		}
159		if fd >= 0 {
160			c.route(id, Event{Kind: InputKeymap, FD: fd, Format: format, Size: size})
161		}
162	}
163	k.OnKeymap(func(f uint32, fd *wl.OwnedFD, n uint32) {
164		next, err := fd.Take()
165		if err != nil {
166			c.post(Event{Kind: TransportError, Err: err})
167			return
168		}
169		cache.replace(next)
170		format, size = f, n
171		if focus != 0 {
172			sendMap(focus)
173		}
174	})
175	k.OnEnter(func(serial, id uint32, _ []byte) {
176		focus = id
177		sendMap(id)
178		c.route(id, Event{Kind: InputRepeatInfo, Rate: rate, Delay: delay})
179		c.route(id, Event{Kind: InputModifiers, Mods: mods})
180		c.route(id, Event{Kind: InputFocusIn, Serial: serial})
181	})
182	k.OnLeave(func(_, id uint32) {
183		c.route(id, Event{Kind: InputFocusOut})
184		if focus == id {
185			focus = 0
186		}
187	})
188	k.OnKey(func(serial, _, key, state uint32) {
189		if focus != 0 {
190			c.route(focus, Event{Kind: InputKey, Serial: serial, Code: key, Pressed: state == 1})
191		}
192	})
193	k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
194		mods = [4]uint32{depressed, latched, locked, group}
195		if focus != 0 {
196			c.route(focus, Event{Kind: InputModifiers, Mods: mods})
197		}
198	})
199	k.OnRepeatInfo(func(r, d int32) {
200		rate, delay = r, d
201		if focus != 0 {
202			c.route(focus, Event{Kind: InputRepeatInfo, Rate: r, Delay: d})
203		}
204	})
205}

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