1// Command testime is a minimal zwp_input_method_v2 client for testing text
2// input. Each time a text input is activated it sends a preedit string,
3// then commits the final text:
4//
5// WAYLAND_DISPLAY=wayland-1 go run ./examples/testime -preedit nihon -commit 日本
6package main
7
8import (
9 "errors"
10 "flag"
11 "fmt"
12 "os"
13 "time"
14
15 "github.com/bnema/go-wayland-bindings/server/inputmethod"
16 "github.com/bnema/wlturbo"
17 clientcore "github.com/bnema/wlturbo/protocol/core"
18 clientime "github.com/bnema/wlturbo/protocol/inputmethod"
19)
20
21func main() {
22 preedit := flag.String("preedit", "nihon", "preedit string sent first")
23 commit := flag.String("commit", "日本", "text committed after the preedit")
24 delay := flag.Duration("delay", 100*time.Millisecond, "time between preedit and commit")
25 count := flag.Int("count", 0, "exit after this many commits; 0 runs until the compositor closes")
26 flag.Parse()
27 if err := run(*preedit, *commit, *delay, *count); err != nil {
28 fmt.Fprintln(os.Stderr, "testime:", err)
29 os.Exit(1)
30 }
31}
32
33// inputMethod follows the activate/done cycle of zwp_input_method_v2.
34type inputMethod struct {
35 wlturbo.BaseProxy
36 c *wlturbo.Display
37 dones uint32
38 active bool // state of the last done
39 activating bool // activate seen, not yet applied by done
40 unavail bool
41 activated bool // activate seen since the last done
42 serve bool // an activation applied by done, not yet served
43}
44
45// EventSignature delegates to the generated client schema, including events
46// this example intentionally ignores and their descriptor ownership.
47func (*inputMethod) EventSignature(op uint16) (string, bool) {
48 return (&clientime.InputMethod{}).EventSignature(op)
49}
50
51func (m *inputMethod) Dispatch(e *wlturbo.Event) {
52 switch uint32(e.Opcode) {
53 case inputmethod.ZwpInputMethodV2EventActivate:
54 // Every activate starts a new text input session, even while
55 // active: an app enabling its text input again gets one.
56 m.activating, m.activated = true, true
57 case inputmethod.ZwpInputMethodV2EventDeactivate:
58 m.activating, m.activated = false, false
59 case inputmethod.ZwpInputMethodV2EventDone:
60 // Requests only count for an activation once its done is in:
61 // the compositor drops commits with an older serial.
62 m.dones++
63 m.active = m.activating
64 m.serve = m.serve && m.active || m.activated
65 m.activated = false
66 case inputmethod.ZwpInputMethodV2EventUnavailable:
67 m.unavail = true
68 }
69}
70
71// ignored drops the events of an object testime does not use.
72type ignored struct{ wlturbo.BaseProxy }
73
74func (*ignored) Dispatch(*wlturbo.Event) {}
75
76func (*ignored) EventSignature(op uint16) (string, bool) {
77 return (&clientcore.Seat{}).EventSignature(op)
78}
79
80func (m *inputMethod) send(op uint32, args ...any) error {
81 return m.c.SendRequest(m.ID(), uint16(op), args...)
82}
83
84func run(preedit, commit string, delay time.Duration, count int) error {
85 c, err := wlturbo.Connect("")
86 if err != nil {
87 return err
88 }
89 defer c.Close()
90 if err := c.Roundtrip(); err != nil {
91 return err
92 }
93 bind := func(iface string) (uint32, error) {
94 g, ok := c.Registry().FindGlobal(iface)
95 if !ok {
96 return 0, fmt.Errorf("compositor has no %s", iface)
97 }
98 return c.Registry().BindID(g.Name, g.Interface, 1)
99 }
100 seat, err := bind("wl_seat")
101 if err != nil {
102 return err
103 }
104 // wlturbo fails on events for unregistered objects: the seat sends some.
105 seatProxy := &ignored{}
106 seatProxy.SetID(seat)
107 seatProxy.SetContext(c.Context())
108 seatProxy.SetVersion(1)
109 c.Context().Register(seatProxy)
110 manager, err := bind("zwp_input_method_manager_v2")
111 if err != nil {
112 return err
113 }
114 m := &inputMethod{c: c}
115 m.SetID(c.AllocateID())
116 m.SetContext(c.Context())
117 m.SetVersion(1)
118 c.Context().Register(m)
119 if err := c.SendRequest(manager, uint16(inputmethod.ZwpInputMethodManagerV2RequestGetInputMethod), seat, m.ID()); err != nil {
120 return err
121 }
122 fmt.Fprintln(os.Stderr, "testime: ready")
123 for commits := 0; ; {
124 if err := c.Dispatch(); err != nil {
125 return err
126 }
127 if m.unavail {
128 return errors.New("another input method is running")
129 }
130 if !m.serve {
131 continue
132 }
133 m.serve = false
134 if err := m.send(inputmethod.ZwpInputMethodV2RequestSetPreeditString, preedit, int32(len(preedit)), int32(len(preedit))); err != nil {
135 return err
136 }
137 if err := m.send(inputmethod.ZwpInputMethodV2RequestCommit, m.dones); err != nil {
138 return err
139 }
140 time.Sleep(delay)
141 // Events that arrived meanwhile update the done count first.
142 if err := c.Roundtrip(); err != nil {
143 return err
144 }
145 if !m.active {
146 continue
147 }
148 if err := m.send(inputmethod.ZwpInputMethodV2RequestCommitString, commit); err != nil {
149 return err
150 }
151 if err := m.send(inputmethod.ZwpInputMethodV2RequestCommit, m.dones); err != nil {
152 return err
153 }
154 if err := c.Roundtrip(); err != nil {
155 return err
156 }
157 fmt.Fprintf(os.Stderr, "testime: committed %q\n", commit)
158 if commits++; count > 0 && commits == count {
159 return nil
160 }
161 }
162}