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 var layout ports.Layout
24 defer func() { km.Close() }()
25 emit := func(code uint32, down bool) error {
26 ev := km.Key(code, down, uint32(time.Now().UnixMilli()))
27 select {
28 case <-ctx.Done():
29 return ctx.Err()
30 case input <- ev:
31 }
32 timer := time.NewTimer(2 * time.Millisecond)
33 defer timer.Stop()
34 select {
35 case <-ctx.Done():
36 return ctx.Err()
37 case <-timer.C:
38 return nil
39 }
40 }
41 stroke := func(name string, mods []uint32) error {
42 code, shift, ok := km.KeycodeFor(name)
43 if !ok {
44 log.Warn().Str("key", name).Msg("unknown key")
45 return nil
46 }
47 if shift {
48 found := false
49 for _, m := range mods {
50 if m == 42 || m == 54 {
51 found = true
52 }
53 }
54 if !found {
55 mods = append(mods, 42)
56 }
57 }
58 for _, m := range mods {
59 if err := emit(m, true); err != nil {
60 return err
61 }
62 }
63 if err := emit(code, true); err != nil {
64 return err
65 }
66 if err := emit(code, false); err != nil {
67 return err
68 }
69 for i := len(mods) - 1; i >= 0; i-- {
70 if err := emit(mods[i], false); err != nil {
71 return err
72 }
73 }
74 return nil
75 }
76 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"}
77 for {
78 select {
79 case <-ctx.Done():
80 return ctx.Err()
81 case next := <-keymaps:
82 km.Close()
83 km = next
84 log.Info().Msg("keymap replaced")
85 case layout = <-layouts:
86 case line, ok := <-script:
87 if !ok {
88 return nil
89 }
90 fields := strings.Fields(line)
91 sendPointer := func(ev ports.InputEvent) error {
92 select {
93 case <-ctx.Done():
94 return ctx.Err()
95 case input <- ev:
96 return nil
97 }
98 }
99 buttonCode := func(name string) uint32 {
100 switch name {
101 case "left":
102 return 0x110
103 case "right":
104 return 0x111
105 case "middle":
106 return 0x112
107 }
108 return 0
109 }
110 now := uint32(time.Now().UnixMilli())
111 if len(fields) > 0 && fields[0] == "move" {
112 if len(fields) == 3 {
113 x, ex := strconv.ParseFloat(fields[1], 64)
114 y, ey := strconv.ParseFloat(fields[2], 64)
115 if ex == nil && ey == nil && !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) {
116 if o, ok := layout.At(x, y); ok && moveCursor != nil {
117 moveCursor(o.Info.Name, (x-float64(o.X))*o.Scale, (y-float64(o.Y))*o.Scale)
118 }
119 if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil {
120 return err
121 }
122 continue
123 }
124 }
125 log.Warn().Str("line", line).Msg("invalid move")
126 continue
127 }
128 if len(fields) > 0 && (fields[0] == "click" || fields[0] == "down" || fields[0] == "up") {
129 name := "left"
130 if len(fields) == 2 {
131 name = fields[1]
132 }
133 code := buttonCode(name)
134 if len(fields) > 2 || (len(fields) != 2 && fields[0] != "click") || code == 0 {
135 log.Warn().Str("line", line).Msg("invalid button")
136 continue
137 }
138 if fields[0] != "up" {
139 if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, TimeMsec: now}); err != nil {
140 return err
141 }
142 }
143 if fields[0] != "down" {
144 if err := sendPointer(ports.PointerButton{Button: code, TimeMsec: now}); err != nil {
145 return err
146 }
147 }
148 continue
149 }
150 switch {
151 case strings.HasPrefix(line, "type "):
152 for _, r := range strings.TrimPrefix(line, "type ") {
153 name := punctuation[r]
154 if name == "" && (unicode.IsLetter(r) || unicode.IsDigit(r)) {
155 name = string(r)
156 }
157 if name == "" {
158 log.Warn().Str("rune", string(r)).Msg("unknown rune")
159 continue
160 }
161 if err := stroke(name, nil); err != nil {
162 return err
163 }
164 }
165 case strings.HasPrefix(line, "key "):
166 parts := strings.Split(strings.TrimPrefix(line, "key "), "+")
167 var mods []uint32
168 valid := true
169 for _, part := range parts[:len(parts)-1] {
170 switch part {
171 case "Super":
172 mods = append(mods, 125)
173 case "Shift":
174 mods = append(mods, 42)
175 case "Ctrl":
176 mods = append(mods, 29)
177 case "Alt":
178 mods = append(mods, 56)
179 default:
180 valid = false
181 }
182 }
183 if !valid {
184 log.Warn().Str("combo", line).Msg("unknown modifier")
185 continue
186 }
187 if err := stroke(parts[len(parts)-1], mods); err != nil {
188 return err
189 }
190 case strings.HasPrefix(line, "sleep "):
191 d, err := time.ParseDuration(strings.TrimPrefix(line, "sleep "))
192 if err != nil || d < 0 {
193 log.Warn().Str("line", line).Msg("invalid sleep")
194 continue
195 }
196 timer := time.NewTimer(d)
197 select {
198 case <-ctx.Done():
199 timer.Stop()
200 return ctx.Err()
201 case <-timer.C:
202 }
203 default:
204 log.Warn().Str("line", line).Msg("unknown script line")
205 }
206 }
207 }
208}