textinput.go RawHistoryFolder

1package wayland
2
3import (
4	"github.com/bnema/go-wayland-bindings/server/inputmethod"
5	"github.com/bnema/go-wayland-bindings/server/textinput"
6	"github.com/bnema/go-wayland-bindings/server/wayland"
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/bnema/purego-libwayland/server"
9)
10
11// Text input (zwp_text_input_v3) lets clients take IME text; one input
12// method (zwp_input_method_v2, e.g. fcitx5 or IBus) composes it. Text
13// input focus follows keyboard focus. The first focused text input that
14// commits enable is the active one: the input method serves only it.
15
16// maxIMEText is the spec limit for strings carried by both protocols.
17const maxIMEText = 4000
18
19// textState is the double-buffered state of a text input.
20type textState struct {
21	enabled        bool
22	surrounding    string
23	cursor, anchor uint32
24	hasSurrounding bool
25	cause          uint32
26	hint, purpose  uint32
27	rect           ports.Rect
28	hasRect        bool
29}
30
31type textInput struct {
32	server *Server
33	res    *textinput.ZwpTextInputV3
34	client server.Client
35	// focus is the entered surface, nil after leave.
36	focus            *wayland.Surface
37	pending, current textState
38	// toggled: an enable or disable request is pending.
39	toggled bool
40	// serial counts commit requests, the serial of done events.
41	serial uint32
42}
43
44func registerTextInput(d *server.Display, s *Server) error {
45	return textinput.NewZwpTextInputManagerV3Global(d, 1, func(c server.Client, v, id uint32) {
46		_, _ = textinput.NewZwpTextInputManagerV3(c, int32(v), id, textInputManager{s})
47	})
48}
49
50type textInputManager struct{ server *Server }
51
52func (textInputManager) Destroy(*textinput.ZwpTextInputManagerV3) {}
53
54func (m textInputManager) GetTextInput(r *textinput.ZwpTextInputManagerV3, id uint32, _ *wayland.Seat) {
55	s := m.server
56	t := &textInput{server: s, client: r.Client()}
57	res, err := textinput.NewZwpTextInputV3(r.Client(), r.Version(), id, t)
58	if err != nil {
59		return
60	}
61	t.res = res
62	s.textInputs = append(s.textInputs, t)
63	res.OnDestroy = func() {
64		s.textInputs = removeItem(s.textInputs, t)
65		if s.activeText == t {
66			s.deactivateText()
67		}
68		t.focus = nil
69	}
70	if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == t.client {
71		t.enter(surf)
72	}
73}
74
75func (*textInput) Destroy(*textinput.ZwpTextInputV3) {}
76
77// Enable resets the pending state, as the spec asks.
78func (t *textInput) Enable(*textinput.ZwpTextInputV3) {
79	t.pending, t.toggled = textState{enabled: true}, true
80}
81
82// Disable invalidates the pending state.
83func (t *textInput) Disable(*textinput.ZwpTextInputV3) {
84	t.pending, t.toggled = textState{}, true
85}
86
87// SetSurroundingText ignores text past the spec limit or a cursor or
88// anchor outside it.
89func (t *textInput) SetSurroundingText(_ *textinput.ZwpTextInputV3, text string, cursor, anchor int32) {
90	if t.server.protected() {
91		return
92	}
93	if len(text) > maxIMEText || cursor < 0 || anchor < 0 || int(cursor) > len(text) || int(anchor) > len(text) {
94		t.server.log.Debug().Int("bytes", len(text)).Msg("surrounding text ignored")
95		return
96	}
97	p := &t.pending
98	p.surrounding, p.cursor, p.anchor, p.hasSurrounding = text, uint32(cursor), uint32(anchor), true
99}
100
101func (t *textInput) SetTextChangeCause(_ *textinput.ZwpTextInputV3, cause uint32) {
102	t.pending.cause = cause
103}
104
105func (t *textInput) SetContentType(_ *textinput.ZwpTextInputV3, hint, purpose uint32) {
106	t.pending.hint, t.pending.purpose = hint, purpose
107}
108
109func (t *textInput) SetCursorRectangle(_ *textinput.ZwpTextInputV3, x, y, w, h int32) {
110	t.pending.rect, t.pending.hasRect = ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}, true
111}
112
113// Commit applies the pending state. Every commit counts for the done
114// serial, but after leave the state is ignored until the next enter.
115func (t *textInput) Commit(*textinput.ZwpTextInputV3) {
116	t.serial++
117	if t.server.protected() {
118		t.pending, t.current, t.toggled = textState{}, textState{}, false
119		return
120	}
121	if t.focus == nil {
122		return
123	}
124	reenabled := t.toggled && t.pending.enabled
125	t.current = t.pending
126	t.pending.cause = uint32(textinput.ZwpTextInputV3ChangeCauseInputMethod)
127	t.toggled = false
128	t.server.textCommitted(t, reenabled)
129}
130
131// Version 2 requests: the global is version 1.
132func (*textInput) SetAvailableActions(*textinput.ZwpTextInputV3, []byte) {}
133func (*textInput) ShowInputPanel(*textinput.ZwpTextInputV3)              {}
134func (*textInput) HideInputPanel(*textinput.ZwpTextInputV3)              {}
135
136// enter focuses the text input on surf; its state starts over.
137func (t *textInput) enter(surf *wayland.Surface) {
138	if t.server.protected() {
139		return
140	}
141	t.focus = surf
142	t.pending, t.current, t.toggled = textState{}, textState{}, false
143	t.res.SendEnter(surf)
144}
145
146// leave clears the focus; a destroyed surface gets no leave event.
147func (t *textInput) leave() {
148	s := t.server
149	if s.activeText == t {
150		s.deactivateText()
151	}
152	if t.focus.Resource.Alive() {
153		t.res.SendLeave(t.focus)
154	}
155	t.focus = nil
156	t.pending, t.current, t.toggled = textState{}, textState{}, false
157}
158
159// textCommitted activates, updates or deactivates the input method for a
160// committed text input. Enabling a second text input is ignored.
161func (s *Server) textCommitted(t *textInput, reenabled bool) {
162	if s.protected() {
163		return
164	}
165	switch {
166	case t.current.enabled && s.activeText == nil:
167		s.activeText = t
168		s.activateIM()
169	case t.current.enabled && s.activeText == t:
170		if reenabled {
171			s.activateIM()
172		} else {
173			s.sendIMState()
174		}
175	case !t.current.enabled && s.activeText == t:
176		s.deactivateText()
177	}
178}
179
180// textInputFocus moves text input focus to the keyboard focus: leave for
181// text inputs on another surface, then enter for those of the focused
182// client, as the spec orders them.
183func (s *Server) textInputFocus() {
184	surf, _ := s.focusTarget(s.seat.focused)
185	for _, t := range s.textInputs {
186		if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) {
187			t.leave()
188		}
189	}
190	if surf == nil {
191		return
192	}
193	for _, t := range s.textInputs {
194		if t.focus == nil && t.client == surf.Client() {
195			t.enter(surf)
196		}
197	}
198}
199
200// deactivateText drops the active text input and tells the input method.
201func (s *Server) deactivateText() {
202	s.activeText = nil
203	im := s.inputMethod
204	if im == nil {
205		return
206	}
207	im.res.SendDeactivate()
208	im.done()
209}
210
211// activateIM sends activate and the active text input's state.
212func (s *Server) activateIM() {
213	im := s.inputMethod
214	if s.protected() || im == nil || s.activeText == nil {
215		return
216	}
217	im.pending = imState{}
218	im.activated = im.dones
219	im.res.SendActivate()
220	s.sendIMState()
221}
222
223// sendIMState sends the active text input's state, then done.
224func (s *Server) sendIMState() {
225	im, t := s.inputMethod, s.activeText
226	if s.protected() || im == nil || t == nil {
227		return
228	}
229	c := t.current
230	if c.hasSurrounding {
231		im.res.SendSurroundingText(c.surrounding, c.cursor, c.anchor)
232	}
233	im.res.SendTextChangeCause(c.cause)
234	im.res.SendContentType(c.hint, c.purpose)
235	for _, p := range im.popups {
236		p.sendRect()
237	}
238	im.done()
239}
240
241// imState is the double-buffered state of the input method.
242type imState struct {
243	preedit       string
244	begin, end    int32
245	hasPreedit    bool
246	commit        string
247	hasCommit     bool
248	before, after uint32
249}
250
251type inputMethod struct {
252	server *Server
253	res    *inputmethod.ZwpInputMethodV2
254	// inert: another input method was bound first; requests are ignored.
255	inert bool
256	dones uint32
257	// activated is dones when the last activate was sent: a commit with
258	// a serial up to it predates the activation.
259	activated uint32
260	pending   imState
261	grab      *keyboardGrab
262	popups    []*inputPopup
263}
264
265func registerInputMethod(d *server.Display, s *Server) error {
266	return inputmethod.NewZwpInputMethodManagerV2Global(d, 1, func(c server.Client, v, id uint32) {
267		_, _ = inputmethod.NewZwpInputMethodManagerV2(c, int32(v), id, inputMethodManager{s})
268	})
269}
270
271type inputMethodManager struct{ server *Server }
272
273func (inputMethodManager) Destroy(*inputmethod.ZwpInputMethodManagerV2) {}
274
275// GetInputMethod makes the first input method the seat's; later ones get
276// unavailable until it is gone.
277func (m inputMethodManager) GetInputMethod(r *inputmethod.ZwpInputMethodManagerV2, _ *wayland.Seat, id uint32) {
278	s := m.server
279	im := &inputMethod{server: s, inert: s.inputMethod != nil}
280	res, err := inputmethod.NewZwpInputMethodV2(r.Client(), r.Version(), id, im)
281	if err != nil {
282		return
283	}
284	im.res = res
285	if im.inert {
286		res.SendUnavailable()
287		s.log.Debug().Int("pid", r.Client().PID()).Msg("input method unavailable")
288		return
289	}
290	s.inputMethod = im
291	res.OnDestroy = func() { s.inputMethodGone(im) }
292	s.log.Info().Int("pid", r.Client().PID()).Msg("input method bound")
293	s.activateIM()
294}
295
296// inputMethodGone frees the seat slot. A grab ends; the active text
297// input's preedit is cleared by an empty done.
298func (s *Server) inputMethodGone(im *inputMethod) {
299	if s.inputMethod != im {
300		return
301	}
302	s.inputMethod = nil
303	if im.grab != nil {
304		im.grab = nil
305		s.grabEnded()
306	}
307	if t := s.activeText; t != nil {
308		t.res.SendDone(t.serial)
309	}
310	s.log.Info().Msg("input method gone")
311}
312
313// done sends done and counts it for commit serials.
314func (im *inputMethod) done() {
315	im.res.SendDone()
316	im.dones++
317}
318
319func (im *inputMethod) CommitString(_ *inputmethod.ZwpInputMethodV2, text string) {
320	if im.server.protected() || im.inert || !im.fits(text) {
321		return
322	}
323	im.pending.commit, im.pending.hasCommit = text, true
324}
325
326func (im *inputMethod) SetPreeditString(_ *inputmethod.ZwpInputMethodV2, text string, begin, end int32) {
327	if im.server.protected() || im.inert || !im.fits(text) {
328		return
329	}
330	p := &im.pending
331	p.preedit, p.begin, p.end, p.hasPreedit = text, begin, end, true
332}
333
334func (im *inputMethod) fits(text string) bool {
335	if len(text) > maxIMEText {
336		im.server.log.Debug().Int("bytes", len(text)).Msg("input method text too long")
337		return false
338	}
339	return true
340}
341
342func (im *inputMethod) DeleteSurroundingText(_ *inputmethod.ZwpInputMethodV2, before, after uint32) {
343	if im.server.protected() || im.inert {
344		return
345	}
346	im.pending.before, im.pending.after = before, after
347}
348
349// Commit sends the pending state to the active text input, in the
350// spec's order, with done carrying the text input's commit count.
351// A commit from before the last activate is dropped: its text belongs to
352// the previous text input. Within one activation, like wlroots, a stale
353// serial is only logged: dropping the commit would lose typed text.
354func (im *inputMethod) Commit(_ *inputmethod.ZwpInputMethodV2, serial uint32) {
355	if im.inert {
356		return
357	}
358	st := im.pending
359	im.pending = imState{}
360	s := im.server
361	if s.protected() {
362		return
363	}
364	t := s.activeText
365	if t == nil {
366		return
367	}
368	if serial <= im.activated {
369		s.log.Debug().Uint32("serial", serial).Uint32("activated", im.activated).Msg("input method commit before activation dropped")
370		return
371	}
372	if serial != im.dones {
373		s.log.Debug().Uint32("serial", serial).Uint32("done", im.dones).Msg("input method commit with stale serial")
374	}
375	if st.hasPreedit {
376		t.res.SendPreeditString(st.preedit, st.begin, st.end)
377	}
378	if st.hasCommit {
379		t.res.SendCommitString(st.commit)
380	}
381	if st.before != 0 || st.after != 0 {
382		t.res.SendDeleteSurroundingText(st.before, st.after)
383	}
384	t.res.SendDone(t.serial)
385}
386
387// GetInputPopupSurface gives a surface the input_popup role. The popup
388// learns the text cursor rectangle; it is not drawn.
389func (im *inputMethod) GetInputPopupSurface(r *inputmethod.ZwpInputMethodV2, id uint32, surf *wayland.Surface) {
390	s := im.server
391	if im.inert {
392		// An unavailable input method's requests have no effect.
393		_, _ = inputmethod.NewZwpInputPopupSurfaceV2(r.Client(), r.Version(), id, &inputPopup{im: im})
394		return
395	}
396	if surf == nil {
397		return
398	}
399	state := s.surfaces[surf.Resource]
400	if state == nil {
401		return
402	}
403	if state.kind != roleNone {
404		r.PostError(uint32(inputmethod.ZwpInputMethodV2ErrorRole), "surface already has a role")
405		return
406	}
407	p := &inputPopup{im: im}
408	res, err := inputmethod.NewZwpInputPopupSurfaceV2(r.Client(), r.Version(), id, p)
409	if err != nil {
410		return
411	}
412	state.kind, p.res = roleInputPopup, res
413	im.popups = append(im.popups, p)
414	res.OnDestroy = func() { im.popups = removeItem(im.popups, p) }
415	if s.activeText != nil {
416		p.sendRect()
417	}
418}
419
420// GrabKeyboard routes seat keys to the input method. A second grab
421// while one is held stays inert.
422func (im *inputMethod) GrabKeyboard(r *inputmethod.ZwpInputMethodV2, id uint32) {
423	g := &keyboardGrab{}
424	res, err := inputmethod.NewZwpInputMethodKeyboardGrabV2(r.Client(), r.Version(), id, g)
425	if err != nil || im.server.protected() || im.inert || im.grab != nil {
426		return
427	}
428	g.res, im.grab = res, g
429	res.OnDestroy = func() {
430		if im.grab == g {
431			im.grab = nil
432			im.server.grabEnded()
433		}
434	}
435	s := im.server
436	if s.seat.keymapFD >= 0 {
437		res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
438	}
439	res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
440	s.serial++
441	m := s.seat.modState
442	res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
443	s.log.Debug().Msg("input method grabbed keyboard")
444}
445
446func (*inputMethod) Destroy(*inputmethod.ZwpInputMethodV2) {}
447
448// inputPopup is an input method's candidate surface.
449type inputPopup struct {
450	im  *inputMethod
451	res *inputmethod.ZwpInputPopupSurfaceV2
452}
453
454func (*inputPopup) Destroy(*inputmethod.ZwpInputPopupSurfaceV2) {}
455
456// sendRect tells the popup where the active text cursor is.
457func (p *inputPopup) sendRect() {
458	if p.im.server.protected() {
459		return
460	}
461	t := p.im.server.activeText
462	if t == nil || !t.current.hasRect {
463		return
464	}
465	r := t.current.rect
466	p.res.SendTextInputRectangle(int32(r.X), int32(r.Y), int32(r.W), int32(r.H))
467}
468
469type keyboardGrab struct {
470	res *inputmethod.ZwpInputMethodKeyboardGrabV2
471}
472
473func (*keyboardGrab) Release(*inputmethod.ZwpInputMethodKeyboardGrabV2) {}
474
475// grab is the live keyboard grab, nil without one.
476func (s *Server) grab() *keyboardGrab {
477	if s.protected() {
478		return nil
479	}
480	if im := s.inputMethod; im != nil {
481		return im.grab
482	}
483	return nil
484}
485
486// grabKey sends a focused key to the input method grab and reports
487// whether it consumed it. The release of a key the client got pressed
488// goes to the client; the release of a key the grab got pressed never
489// does, even after the grab ends.
490func (s *Server) grabKey(c ports.ForwardKey) bool {
491	code := c.Key.Keycode
492	if s.protected() {
493		if !c.Key.Pressed && s.seat.grabKeys[code] {
494			delete(s.seat.grabKeys, code)
495			return true
496		}
497		return false // protected seat routing belongs to the parent
498	}
499	g := s.grab()
500	owned := !c.Key.Pressed && s.seat.grabKeys[code]
501	if owned {
502		delete(s.seat.grabKeys, code)
503		if g == nil {
504			s.grabReleaseDropped(c.Key.State)
505			return true
506		}
507	} else if g == nil || c.ID != s.seat.focused || !c.Key.Pressed && s.seat.heldKeys[code] {
508		return false
509	}
510	state := uint32(0)
511	if c.Key.Pressed {
512		state = 1
513		if s.seat.grabKeys == nil {
514			s.seat.grabKeys = make(map[uint32]bool)
515		}
516		s.seat.grabKeys[code] = true
517	}
518	s.serial++
519	g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
520	if c.Key.State != s.seat.modState {
521		s.seat.modState = c.Key.State
522		m := s.seat.modState
523		s.serial++
524		g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
525	}
526	return true
527}
528
529// grabReleaseDropped keeps the focused client's modifiers right when the
530// release of a grab-owned key is dropped.
531func (s *Server) grabReleaseDropped(m ports.ModState) {
532	if s.protected() {
533		return
534	}
535	if m == s.seat.modState {
536		return
537	}
538	s.seat.modState = m
539	s.serial++
540	for _, k := range s.clientKeyboards(s.focusClient()) {
541		s.sendModifiers(k)
542	}
543}
544
545// grabEnded gives the focused client the seat modifiers it missed.
546func (s *Server) grabEnded() {
547	if s.protected() {
548		return
549	}
550	s.serial++
551	for _, k := range s.clientKeyboards(s.focusClient()) {
552		s.sendModifiers(k)
553	}
554	s.log.Debug().Msg("input method keyboard grab ended")
555}
556
557// sendGrabKeymap updates a grab after a seat keymap or repeat change.
558func (s *Server) sendGrabKeymap(keymap bool) {
559	g := s.grab()
560	if g == nil {
561		return
562	}
563	if keymap && s.seat.keymapFD >= 0 {
564		g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
565	}
566	g.res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
567}

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