v0.4.0 2de4e1d

keyboard.go RawHistoryFolder

1// Package keyboard interprets Wayland evdev key events on the UI loop.
2package keyboard
3
4import (
5	"errors"
6	"os"
7	"time"
8
9	xkb "github.com/bnema/purego-xkbcommon"
10	"github.com/bnema/purego-xkbcommon/raw"
11)
12
13type Key struct {
14	Physical        uint32
15	Logical         uint32
16	Name, Text      string
17	Pressed, Repeat bool
18	Shift, Ctrl     bool
19	Group           uint32
20}
21type Keyboard struct {
22	context        *xkb.Context
23	keymap         *xkb.Keymap
24	state          *xkb.State
25	compose        *xkb.ComposeState
26	held           map[uint32]Key
27	secretHeld     map[uint32]SecretKey
28	secretMode     bool
29	rate           int
30	delay          time.Duration
31	next           time.Time
32	repeating      uint32
33	focus          bool
34	composePending bool
35}
36
37func New(locale string) (*Keyboard, error) {
38	c, err := xkb.NewContext()
39	if err != nil {
40		return nil, err
41	}
42	k := &Keyboard{context: c, held: make(map[uint32]Key), focus: true}
43	if locale != "" {
44		t, e := c.NewComposeTable(locale)
45		if e != nil {
46			c.Close()
47			return nil, e
48		}
49		k.compose, e = t.NewState()
50		t.Close()
51		if e != nil {
52			c.Close()
53			return nil, e
54		}
55	}
56	return k, nil
57}
58func (k *Keyboard) Close() {
59	k.Focus(false)
60	if k.compose != nil {
61		k.compose.Close()
62	}
63	if k.state != nil {
64		k.state.Close()
65	}
66	if k.keymap != nil {
67		k.keymap.Close()
68	}
69	if k.context != nil {
70		k.context.Close()
71	}
72}
73
74// ReplaceFD consumes the received FD even on failure; the previous keymap remains valid on failure.
75func (k *Keyboard) ReplaceFD(fd, size int) error {
76	if fd < 0 {
77		return errors.New("keyboard: invalid fd")
78	}
79	file := os.NewFile(uintptr(fd), "keymap")
80	if file == nil {
81		return errors.New("keyboard: invalid fd")
82	}
83	defer file.Close()
84	km, err := k.context.NewKeymapFD(fd, size)
85	if err != nil {
86		return err
87	}
88	return k.replace(km)
89}
90func (k *Keyboard) Rules(layout, options string) error {
91	km, err := k.context.NewKeymapRules("evdev", "pc105", layout, "", options)
92	if err != nil {
93		return err
94	}
95	return k.replace(km)
96}
97func (k *Keyboard) replace(km *xkb.Keymap) error {
98	s, err := km.NewState()
99	if err != nil {
100		km.Close()
101		return err
102	}
103	k.Focus(false)
104	if k.state != nil {
105		k.state.Close()
106	}
107	if k.keymap != nil {
108		k.keymap.Close()
109	}
110	k.keymap = km
111	k.state = s
112	k.Focus(true)
113	return nil
114}
115func (k *Keyboard) Mask(depressed, latched, locked, groupDepressed, groupLatched, groupLocked uint32) error {
116	if k.state == nil {
117		return errors.New("keyboard: no keymap")
118	}
119	_, err := k.state.UpdateMask(depressed, latched, locked, groupDepressed, groupLatched, groupLocked)
120	return err
121}
122func (k *Keyboard) Focus(on bool) {
123	k.focus = on
124	k.held = make(map[uint32]Key)
125	clear(k.secretHeld)
126	k.secretMode = false
127	k.repeating = 0
128	k.next = time.Time{}
129	if k.compose != nil {
130		k.compose.Reset()
131	}
132	k.composePending = false
133}
134func (k *Keyboard) RepeatInfo(rate int, delay time.Duration) {
135	k.rate = rate
136	k.delay = delay
137	if rate <= 0 {
138		k.repeating = 0
139	}
140}
141func (k *Keyboard) Event(evdev uint32, pressed bool, now time.Time) (Key, error) {
142	if k.secretMode {
143		k.Focus(k.focus)
144	}
145	if !pressed {
146		key := k.held[evdev]
147		delete(k.held, evdev)
148		if k.repeating == evdev {
149			k.repeating = 0
150		}
151		key.Pressed = false
152		key.Text = ""
153		return key, nil
154	}
155	if !k.focus || k.state == nil {
156		return Key{}, nil
157	}
158	code := xkb.WaylandKeycode(evdev)
159	sym, err := k.state.KeySym(code)
160	if err != nil {
161		return Key{}, err
162	}
163	text, err := k.state.UTF8(code)
164	if err != nil {
165		return Key{}, err
166	}
167	name, _ := xkb.KeysymName(sym)
168	group, _ := k.state.Layout()
169	key := Key{Physical: evdev, Logical: sym, Name: name, Text: text, Pressed: true, Group: group}
170	if key.Shift, key.Ctrl, err = k.modifiers(); err != nil {
171		return Key{}, err
172	}
173	if k.compose != nil {
174		if _, err = k.compose.Feed(sym); err != nil {
175			return Key{}, err
176		}
177		status, _ := k.compose.Status()
178		switch status {
179		case raw.XKB_COMPOSE_COMPOSING, raw.XKB_COMPOSE_CANCELLED:
180			key.Text = ""
181			k.composePending = status == raw.XKB_COMPOSE_COMPOSING
182			if status == raw.XKB_COMPOSE_CANCELLED {
183				k.compose.Reset()
184			}
185		case raw.XKB_COMPOSE_COMPOSED:
186			key.Text, err = k.compose.UTF8()
187			k.compose.Reset()
188			k.composePending = false
189			if err != nil {
190				return Key{}, err
191			}
192		default:
193			k.composePending = false
194		}
195	}
196	k.held[evdev] = key
197	if k.rate > 0 && repeatable(sym) {
198		k.repeating = evdev
199		k.next = now.Add(k.delay)
200	}
201	return key, nil
202}
203
204// Modifier, dead and compose keys never repeat. The compositor provides rate/delay;
205// its keymap is authoritative for text and layout.
206func repeatable(sym uint32) bool {
207	switch sym {
208	case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
209		raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
210		raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock, raw.XKB_KEY_Multi_key:
211		return false
212	}
213	return sym < raw.XKB_KEY_dead_grave || sym > raw.XKB_KEY_dead_currency
214}
215
216// Tick returns at most one repeat for a due deadline, without accumulating missed ticks.
217// It resolves the held physical key against the current modifier/group mask.
218// Compose is fed only by physical presses: synthetic repeats neither advance a
219// compose sequence nor replay the press-time composed text.
220func (k *Keyboard) Tick(now time.Time) (Key, bool) {
221	if k.secretMode || !k.focus || k.rate <= 0 || k.repeating == 0 || now.Before(k.next) {
222		return Key{}, false
223	}
224	physical := k.repeating
225	if _, ok := k.held[physical]; !ok {
226		return Key{}, false
227	}
228	code := xkb.WaylandKeycode(physical)
229	sym, err := k.state.KeySym(code)
230	if err != nil || !repeatable(sym) {
231		k.repeating = 0
232		return Key{}, false
233	}
234	text, err := k.state.UTF8(code)
235	if err != nil {
236		k.repeating = 0
237		return Key{}, false
238	}
239	group, err := k.state.Layout()
240	if err != nil {
241		k.repeating = 0
242		return Key{}, false
243	}
244	name, _ := xkb.KeysymName(sym)
245	if k.composePending {
246		text = ""
247	}
248	key := Key{Physical: physical, Logical: sym, Name: name, Text: text, Pressed: true, Repeat: true, Group: group}
249	// Repeats carry the current modifiers so e.g. held Shift+arrow keeps extending.
250	if key.Shift, key.Ctrl, err = k.modifiers(); err != nil {
251		k.repeating = 0
252		return Key{}, false
253	}
254	k.next = now.Add(time.Second / time.Duration(k.rate))
255	return key, true
256}
257
258// modifiers reports whether Shift and Control are active in the current state.
259func (k *Keyboard) modifiers() (shift, ctrl bool, err error) {
260	mods, err := k.state.Mods()
261	if err != nil {
262		return false, false, err
263	}
264	if idx, e := k.keymap.ModIndex("Shift"); e == nil && idx < 32 {
265		shift = mods&(1<<idx) != 0
266	}
267	if idx, e := k.keymap.ModIndex("Control"); e == nil && idx < 32 {
268		ctrl = mods&(1<<idx) != 0
269	}
270	return shift, ctrl, nil
271}
272func (k *Keyboard) NextRepeat() (time.Time, bool) { return k.next, k.repeating != 0 }

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