1// Package headlessinput converts scripted text into keyboard events.
2package headlessinput
3
4import (
5 "context"
6 "math"
7 "strconv"
8 "strings"
9 "time"
10 "unicode"
11
12 "github.com/bnema/neferwl/internal/adapters/xkb"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/zerowrap"
15)
16
17// Run owns the keymap and converts script lines into input events. A keymap
18// received on keymaps replaces the current one, which is closed. Scripted
19// moves are global logical coordinates; moveCursor, when set, places the
20// software cursor of the output under them (physical position on it), using
21// the latest layout from layouts.
22func Run(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, script <-chan string, input chan<- ports.InputEvent, layouts <-chan ports.Layout, moveCursor func(output string, x, y float64), log zerowrap.Logger) error {
23 return RunSecure(ctx, km, keymaps, script, input, layouts, moveCursor, log, nil)
24}
25
26// RunSecure snapshots the protection epoch before each event enters the input
27// queue. Scripts are a headless test transport, never an unlock authority.
28func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, script <-chan string, input chan<- ports.InputEvent, layouts <-chan ports.Layout, moveCursor func(output string, x, y float64), log zerowrap.Logger, security ports.SessionSecurity) error {
29 snapshot := func() ports.SecurityState {
30 if security != nil {
31 return security.Snapshot()
32 }
33 return ports.SecurityState{}
34 }
35 stamp := func(ev ports.InputEvent, state ports.SecurityState) ports.InputEvent {
36 if security == nil {
37 return ev
38 }
39 return ports.SecurityInput{State: state, Event: ev}
40 }
41 // Script payloads can contain credentials even when parsing fails. Keep a
42 // useful generic warning while protected, never input-derived fields.
43 warnInput := func(field, value, message string) {
44 entry := log.Warn()
45 if snapshot().Protected {
46 entry.Msg("invalid script input")
47 return
48 }
49 entry.Str(field, value).Msg(message)
50 }
51 var layout ports.Layout
52 defer func() { km.Close() }()
53 emit := func(code uint32, down bool) error {
54 state := snapshot()
55 var key ports.KeyEvent
56 if security != nil {
57 var deliver bool
58 var err error
59 key, deliver, err = km.KeySecure(code, down, monotonic(), state)
60 if err != nil {
61 return err
62 }
63 if !deliver {
64 return nil
65 }
66 } else {
67 key = km.Key(code, down, monotonic())
68 }
69 ev := stamp(key, state)
70 select {
71 case <-ctx.Done():
72 return ctx.Err()
73 case input <- ev:
74 }
75 timer := time.NewTimer(2 * time.Millisecond)
76 defer timer.Stop()
77 select {
78 case <-ctx.Done():
79 return ctx.Err()
80 case <-timer.C:
81 return nil
82 }
83 }
84 // stroke presses mods then name, holds name for hold, and releases
85 // them in reverse order.
86 stroke := func(name string, mods []uint32, hold time.Duration) error {
87 code, shift, ok := km.KeycodeFor(name)
88 if !ok {
89 warnInput("key", name, "unknown key")
90 return nil
91 }
92 if shift {
93 found := false
94 for _, m := range mods {
95 if m == 42 || m == 54 {
96 found = true
97 }
98 }
99 if !found {
100 mods = append(mods, 42)
101 }
102 }
103 for _, m := range mods {
104 if err := emit(m, true); err != nil {
105 return err
106 }
107 }
108 if err := emit(code, true); err != nil {
109 return err
110 }
111 if hold > 0 {
112 timer := time.NewTimer(hold)
113 select {
114 case <-ctx.Done():
115 timer.Stop()
116 return ctx.Err()
117 case <-timer.C:
118 }
119 }
120 if err := emit(code, false); err != nil {
121 return err
122 }
123 for i := len(mods) - 1; i >= 0; i-- {
124 if err := emit(mods[i], false); err != nil {
125 return err
126 }
127 }
128 return nil
129 }
130 punctuation := map[rune]string{' ': "space", '\n': "Return", '.': "period", ',': "comma", '-': "minus", '_': "underscore", '/': "slash", ':': "colon", ';': "semicolon", '!': "exclam", '?': "question", '\'': "apostrophe", '"': "quotedbl", '(': "parenleft", ')': "parenright", '=': "equal", '+': "plus", '\\': "backslash", '@': "at", '#': "numbersign", '$': "dollar", '%': "percent", '&': "ampersand", '*': "asterisk", '[': "bracketleft", ']': "bracketright", '{': "braceleft", '}': "braceright", '<': "less", '>': "greater", '|': "bar", '`': "grave", '~': "asciitilde", '^': "asciicircum"}
131 for {
132 select {
133 case <-ctx.Done():
134 return ctx.Err()
135 case next := <-keymaps:
136 if security != nil {
137 next.QuarantineFrom(km)
138 }
139 km.Close()
140 km = next
141 log.Info().Msg("keymap replaced")
142 case layout = <-layouts:
143 case line, ok := <-script:
144 if !ok {
145 return nil
146 }
147 fields := strings.Fields(line)
148 sendPointer := func(ev ports.InputEvent) error {
149 ev = stamp(ev, snapshot())
150 select {
151 case <-ctx.Done():
152 return ctx.Err()
153 case input <- ev:
154 return nil
155 }
156 }
157 buttonCode := func(name string) uint32 {
158 switch name {
159 case "left":
160 return 0x110
161 case "right":
162 return 0x111
163 case "middle":
164 return 0x112
165 }
166 return 0
167 }
168 now := monotonic()
169 // Separate modifier edges allow mouse actions while a modifier is held.
170 if len(fields) > 0 && (fields[0] == "press" || fields[0] == "release") {
171 var code uint32
172 if len(fields) == 2 {
173 switch fields[1] {
174 case "Super":
175 code = 125
176 case "Shift":
177 code = 42
178 case "Ctrl":
179 code = 29
180 case "Alt":
181 code = 56
182 }
183 }
184 if code == 0 {
185 warnInput("line", line, "invalid modifier")
186 continue
187 }
188 if err := emit(code, fields[0] == "press"); err != nil {
189 return err
190 }
191 continue
192 }
193 if len(fields) > 0 && fields[0] == "move" {
194 if len(fields) == 3 {
195 x, ex := strconv.ParseFloat(fields[1], 64)
196 y, ey := strconv.ParseFloat(fields[2], 64)
197 if ex == nil && ey == nil && !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) {
198 if o, ok := layout.At(x, y); ok && moveCursor != nil {
199 tx, ty := o.ToTarget(x, y)
200 moveCursor(o.Info.Name, tx, ty)
201 }
202 if err := sendPointer(ports.PointerMotion{X: x, Y: y, Time: now}); err != nil {
203 return err
204 }
205 continue
206 }
207 }
208 warnInput("line", line, "invalid move")
209 continue
210 }
211 if len(fields) > 0 && (fields[0] == "swipe" || fields[0] == "swipe4") {
212 dirs := map[string][2]float64{"up": {0, -1}, "down": {0, 1}, "left": {-1, 0}, "right": {1, 0}}
213 d, ok := [2]float64{}, len(fields) == 2
214 if ok {
215 d, ok = dirs[fields[1]]
216 }
217 if !ok {
218 warnInput("line", line, "invalid swipe")
219 continue
220 }
221 fingers := 3
222 if fields[0] == "swipe4" {
223 fingers = 4
224 }
225 for _, ev := range swipeEvents(fingers, d[0], d[1], monotonic()) {
226 if err := sendPointer(ev); err != nil {
227 return err
228 }
229 }
230 continue
231 }
232 if len(fields) > 0 && (fields[0] == "click" || fields[0] == "down" || fields[0] == "up") {
233 name := "left"
234 if len(fields) == 2 {
235 name = fields[1]
236 }
237 code := buttonCode(name)
238 if len(fields) > 2 || (len(fields) != 2 && fields[0] != "click") || code == 0 {
239 warnInput("line", line, "invalid button")
240 continue
241 }
242 if fields[0] != "up" {
243 if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, Time: now}); err != nil {
244 return err
245 }
246 }
247 if fields[0] != "down" {
248 if err := sendPointer(ports.PointerButton{Button: code, Time: now}); err != nil {
249 return err
250 }
251 }
252 continue
253 }
254 switch {
255 case strings.HasPrefix(line, "type "):
256 for _, r := range strings.TrimPrefix(line, "type ") {
257 name := punctuation[r]
258 if name == "" && (unicode.IsLetter(r) || unicode.IsDigit(r)) {
259 name = string(r)
260 }
261 if name == "" {
262 warnInput("rune", string(r), "unknown rune")
263 continue
264 }
265 if err := stroke(name, nil, 0); err != nil {
266 return err
267 }
268 }
269 case strings.HasPrefix(line, "key "), strings.HasPrefix(line, "hold "):
270 combo, hold := strings.TrimPrefix(line, "key "), time.Duration(0)
271 if rest, ok := strings.CutPrefix(line, "hold "); ok {
272 f := strings.Fields(rest)
273 var err error
274 if len(f) == 2 {
275 hold, err = time.ParseDuration(f[1])
276 }
277 if len(f) != 2 || err != nil || hold <= 0 {
278 warnInput("line", line, "invalid hold")
279 continue
280 }
281 combo = f[0]
282 }
283 parts := strings.Split(combo, "+")
284 var mods []uint32
285 valid := true
286 for _, part := range parts[:len(parts)-1] {
287 switch part {
288 case "Super":
289 mods = append(mods, 125)
290 case "Shift":
291 mods = append(mods, 42)
292 case "Ctrl":
293 mods = append(mods, 29)
294 case "Alt":
295 mods = append(mods, 56)
296 default:
297 valid = false
298 }
299 }
300 if !valid {
301 warnInput("combo", line, "unknown modifier")
302 continue
303 }
304 if err := stroke(parts[len(parts)-1], mods, hold); err != nil {
305 return err
306 }
307 case strings.HasPrefix(line, "sleep "):
308 d, err := time.ParseDuration(strings.TrimPrefix(line, "sleep "))
309 if err != nil || d < 0 {
310 warnInput("line", line, "invalid sleep")
311 continue
312 }
313 timer := time.NewTimer(d)
314 select {
315 case <-ctx.Done():
316 timer.Stop()
317 return ctx.Err()
318 case <-timer.C:
319 }
320 default:
321 warnInput("line", line, "unknown script line")
322 }
323 }
324 }
325}