v0.1.0 47aaa0f

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