Skip to content

Commit afca7ca

Browse files
committed
feat: text input for drawing motions
1 parent 2d6703a commit afca7ca

17 files changed

Lines changed: 1359 additions & 124 deletions

File tree

bin/pointerdriver.js

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
#!/usr/bin/env node
22

33
import { createServer } from 'node:http'
4+
import { execFile } from 'node:child_process'
45
import { readFile } from 'node:fs/promises'
56
import { resolve } from 'node:path'
7+
import { setTimeout as wait } from 'node:timers/promises'
68
import { fileURLToPath } from 'node:url'
9+
import { formatWithOptions, promisify } from 'node:util'
710

811
const root = fileURLToPath(new URL('..', import.meta.url))
912

13+
const release = (port, {
14+
timeout = 3000,
15+
poll: { interval = 50 } = {}
16+
} = {}) =>
17+
Promise.resolve(port)
18+
.then(port => Number.isFinite(port) && port >= 1 && port <= 65535
19+
? port
20+
: Promise.reject(new RangeError(`port: ${port}, must be 1 - 65535`)))
21+
.then(port => promisify(execFile)('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN']))
22+
.then(({ stdout }) => stdout.trim().split('\n').map(Number).filter(Boolean))
23+
.then(pids => pids.map(pid => (process.kill(pid, 'SIGTERM'), pid)))
24+
.then(function poll(pids, start = Date.now()) {
25+
const live = pids.filter(pid => {
26+
try { return process.kill(pid, 0), true }
27+
catch (err) {
28+
if (err.code === 'ESRCH') return false
29+
throw err
30+
}
31+
})
32+
33+
return !live.length ? pids :
34+
Date.now() - start >= timeout
35+
? (live.forEach(pid => process.kill(pid, 'SIGKILL')), pids)
36+
: wait(Math.max(10, interval)).then(() => poll(pids, start))
37+
})
38+
.catch(err => err.code === 1 ? [] : Promise.reject(err))
39+
40+
const log = (...args) =>
41+
console.log(formatWithOptions({ colors: true }, ...args))
42+
1043
const aliases = {
1144
'-p': '--port',
1245
'-H': '--host',
@@ -115,11 +148,15 @@ if ('help' in args) {
115148
const pathname = url.pathname
116149

117150
const request =
118-
pathname === '/pointerdriver.js'
119-
? { file: 'index.js' }
120-
: pathname.match(/^\/src\/[\w-]+\/index\.js$/)
121-
? { file: pathname.slice(1) }
122-
: null
151+
pathname === '/'
152+
? { file: 'bin/skill.md', type: 'text/markdown; charset=utf-8' }
153+
: pathname === '/pointerdriver.js'
154+
? { file: 'index.js', type: 'text/javascript; charset=utf-8' }
155+
: pathname.match(/^\/src\/[\w-]+\/index\.js$/)
156+
? { file: pathname.slice(1), type: 'text/javascript; charset=utf-8' }
157+
: pathname.match(/^\/fonts\/[\w-]+\.svg$/)
158+
? { file: pathname.slice(1), type: 'image/svg+xml' }
159+
: null
123160

124161
if (!request)
125162
return send(req, res, 404, {
@@ -128,11 +165,14 @@ if ('help' in args) {
128165
}, 'Not found')
129166

130167
const path = resolve(root, request.file)
131-
const body = await js(path)
168+
169+
const body = request.type.startsWith('text/javascript')
170+
? await js(path)
171+
: await readFile(path, 'utf8')
132172

133173
return send(req, res, 200, {
134174
...cors,
135-
'Content-Type': 'text/javascript; charset=utf-8',
175+
'Content-Type': request.type,
136176
'Cache-Control': 'no-store',
137177
'X-Content-Type-Options': 'nosniff',
138178
}, body)
@@ -157,10 +197,18 @@ if ('help' in args) {
157197
}
158198
})
159199

200+
log('starting server ...')
201+
log('checking port %d ...', port)
202+
203+
const released = await release(port)
204+
205+
if (released.length)
206+
log('killed: %s to free up port: %d', released.join(', '), port)
207+
160208
const logHost = host.includes(':') ? `[${host}]` : host
161209

162210
server.listen(port, host, () =>
163-
console.log(`http://${logHost}:${port}/pointerdriver.js`))
211+
log('listening: http://%s:%d/', logHost, port))
164212

165213
const shutdown = () => server.close()
166214

bin/skill.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Motions are the public API.
3535
const {
3636
DragMotion, GlideMotion, StrokeMotion,
3737
PinchMotion, TwistMotion, SwipeMotion,
38+
Font, Glass,
3839
} = await import('http://127.0.0.1:5619/pointerdriver.js')
3940
```
4041

@@ -49,6 +50,15 @@ Motions are the public API.
4950
]).perform()
5051
```
5152

53+
4. `Glass` is an optional overlay that visualizes dispatched events
54+
as colored dots.
55+
- Wrap a motion callback in `new Glass(fn)` to enable it.
56+
- Always include it by default. It only logs `isTrusted: false`
57+
events so it only captures synthetic input.
58+
- If the user asks to remove the glass/overlay/visualization,
59+
just run motions directly without wrapping them in `Glass`.
60+
- To tear down manually, call `glass.remove()` when done.
61+
5262
## Motions
5363

5464
Available motions exported from `pointerdriver.js`:
@@ -139,6 +149,35 @@ Constraints:
139149
- `separation` must be finite and `> 0`.
140150
- `steps` must be a positive integer.
141151

152+
## Writing text
153+
154+
Pass a string instead of a points array to write text
155+
as the target device.
156+
157+
```js
158+
await new StrokeMotion(el, 'hello', {
159+
x: 100, y: 200, size: 30
160+
}).perform()
161+
```
162+
163+
- `size` — text height in pixels (required).
164+
- `x`, `y` — starting position in viewport coordinates (required).
165+
- `font` — URL string to an SVG font, or a `Font` instance.
166+
Defaults to bundled Hershey Script.
167+
168+
Loading a custom font:
169+
170+
```js
171+
const font = await Font.load('https://example.com/font.svg')
172+
173+
await new StrokeMotion(el, 'hello', {
174+
font, x: 100, y: 200, size: 30
175+
}).perform()
176+
```
177+
178+
Multi-stroke glyphs (letters with pen lifts like "i", "t")
179+
dispatch separate down-move-up cycles per stroke.
180+
142181
## Coordinates, hit-testing, and common failures
143182

144183
All hit-testing is based on `document.elementFromPoint(x, y)`.
@@ -261,8 +300,10 @@ For HTTPS pages, prefer the Cloudflare Tunnel method instead of raw LAN HTTP.
261300
## What pointerdriver does not do
262301

263302
- It does not synthesize browser default actions.
264-
There is no `click` event synthesis, scrolling, text input, or focus behavior.
265-
- It does not bypass CSP, cross-origin iframes, or browser security policies.
303+
There is no `click` event synthesis, scrolling, text input,
304+
or focus behavior.
305+
- It does not bypass CSP, cross-origin iframes,
306+
or browser security policies.
266307

267308
## Reference
268309

0 commit comments

Comments
 (0)