usecomputer 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -0
- package/dist/bridge-contract.test.js +124 -63
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +241 -46
- package/dist/cli-parsing.test.js +34 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +323 -28
- package/dist/coord-map.d.ts +14 -0
- package/dist/coord-map.d.ts.map +1 -0
- package/dist/coord-map.js +75 -0
- package/dist/coord-map.test.d.ts +2 -0
- package/dist/coord-map.test.d.ts.map +1 -0
- package/dist/coord-map.test.js +157 -0
- package/dist/darwin-arm64/usecomputer.node +0 -0
- package/dist/darwin-x64/usecomputer.node +0 -0
- package/dist/debug-point-image.d.ts +8 -0
- package/dist/debug-point-image.d.ts.map +1 -0
- package/dist/debug-point-image.js +43 -0
- package/dist/debug-point-image.test.d.ts +2 -0
- package/dist/debug-point-image.test.d.ts.map +1 -0
- package/dist/debug-point-image.test.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib.d.ts +26 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +88 -0
- package/dist/native-click-smoke.test.js +69 -29
- package/dist/native-lib.d.ts +59 -1
- package/dist/native-lib.d.ts.map +1 -1
- package/dist/terminal-table.d.ts +10 -0
- package/dist/terminal-table.d.ts.map +1 -0
- package/dist/terminal-table.js +55 -0
- package/dist/terminal-table.test.d.ts +2 -0
- package/dist/terminal-table.test.d.ts.map +1 -0
- package/dist/terminal-table.test.js +41 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/src/bridge-contract.test.ts +140 -69
- package/src/bridge.ts +293 -53
- package/src/cli-parsing.test.ts +61 -0
- package/src/cli.ts +393 -32
- package/src/coord-map.test.ts +178 -0
- package/src/coord-map.ts +105 -0
- package/src/debug-point-image.test.ts +50 -0
- package/src/debug-point-image.ts +69 -0
- package/src/index.ts +3 -1
- package/src/lib.ts +125 -0
- package/src/native-click-smoke.test.ts +81 -63
- package/src/native-lib.ts +39 -1
- package/src/terminal-table.test.ts +44 -0
- package/src/terminal-table.ts +88 -0
- package/src/types.ts +50 -0
- package/zig/src/lib.zig +1258 -267
- package/zig/src/scroll.zig +213 -0
- package/zig/src/window.zig +123 -0
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
// Optional host smoke test for
|
|
1
|
+
// Optional host smoke test for direct native mouse methods.
|
|
2
2
|
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { z } from 'zod'
|
|
4
5
|
import { native } from './native-lib.js'
|
|
5
6
|
|
|
6
7
|
const runNativeSmoke = process.env.USECOMPUTER_NATIVE_SMOKE === '1'
|
|
7
8
|
|
|
9
|
+
const displayListSchema = z.array(
|
|
10
|
+
z.object({
|
|
11
|
+
id: z.number(),
|
|
12
|
+
index: z.number(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
x: z.number(),
|
|
15
|
+
y: z.number(),
|
|
16
|
+
width: z.number(),
|
|
17
|
+
height: z.number(),
|
|
18
|
+
scale: z.number(),
|
|
19
|
+
isPrimary: z.boolean(),
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
|
|
8
23
|
describe('native click smoke', () => {
|
|
9
24
|
const smokeTest = runNativeSmoke ? test : test.skip
|
|
10
25
|
|
|
@@ -14,23 +29,19 @@ describe('native click smoke', () => {
|
|
|
14
29
|
return
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
const response = native.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
count: 1,
|
|
23
|
-
}),
|
|
24
|
-
)
|
|
32
|
+
const response = native.click({
|
|
33
|
+
point: { x: 10, y: 10 },
|
|
34
|
+
button: 'left',
|
|
35
|
+
count: 1,
|
|
36
|
+
})
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
expect(parsed).toMatchInlineSnapshot(`
|
|
38
|
+
expect(response).toMatchInlineSnapshot(`
|
|
28
39
|
{
|
|
29
|
-
"
|
|
40
|
+
"error": null,
|
|
30
41
|
"ok": true,
|
|
31
42
|
}
|
|
32
43
|
`)
|
|
33
|
-
expect(
|
|
44
|
+
expect(response.ok).toBe(true)
|
|
34
45
|
})
|
|
35
46
|
|
|
36
47
|
smokeTest('executes mouse-move/down/up/position/hover/drag without crashing', () => {
|
|
@@ -39,49 +50,19 @@ describe('native click smoke', () => {
|
|
|
39
50
|
return
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
const moveResponse =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const upResponse = JSON.parse(
|
|
57
|
-
native.execute('mouse-up', JSON.stringify({ button: 'left' })),
|
|
58
|
-
) as { ok: boolean; data?: null; error?: string }
|
|
59
|
-
|
|
60
|
-
const positionResponse = JSON.parse(
|
|
61
|
-
native.execute('mouse-position', JSON.stringify({})),
|
|
62
|
-
) as { ok: boolean; data?: { x: number; y: number }; error?: string }
|
|
63
|
-
|
|
64
|
-
const hoverResponse = JSON.parse(
|
|
65
|
-
native.execute(
|
|
66
|
-
'hover',
|
|
67
|
-
JSON.stringify({
|
|
68
|
-
x: 24,
|
|
69
|
-
y: 24,
|
|
70
|
-
}),
|
|
71
|
-
),
|
|
72
|
-
) as { ok: boolean; data?: null; error?: string }
|
|
73
|
-
|
|
74
|
-
const dragResponse = JSON.parse(
|
|
75
|
-
native.execute(
|
|
76
|
-
'drag',
|
|
77
|
-
JSON.stringify({
|
|
78
|
-
from: { x: 24, y: 24 },
|
|
79
|
-
to: { x: 30, y: 30 },
|
|
80
|
-
button: 'left',
|
|
81
|
-
durationMs: 10,
|
|
82
|
-
}),
|
|
83
|
-
),
|
|
84
|
-
) as { ok: boolean; data?: null; error?: string }
|
|
53
|
+
const moveResponse = native.mouseMove({ x: 0, y: 0 })
|
|
54
|
+
const downResponse = native.mouseDown({ button: 'left' })
|
|
55
|
+
const upResponse = native.mouseUp({ button: 'left' })
|
|
56
|
+
const positionResponse = native.mousePosition()
|
|
57
|
+
const hoverResponse = native.hover({ x: 0, y: 0 })
|
|
58
|
+
const dragResponse = native.drag({
|
|
59
|
+
from: { x: 0, y: 0 },
|
|
60
|
+
to: { x: 0, y: 0 },
|
|
61
|
+
button: 'left',
|
|
62
|
+
durationMs: 10,
|
|
63
|
+
})
|
|
64
|
+
const typeResponse = native.typeText({ text: 'h', delayMs: 1 })
|
|
65
|
+
const pressResponse = native.press({ key: 'backspace', count: 1, delayMs: 1 })
|
|
85
66
|
|
|
86
67
|
expect({
|
|
87
68
|
moveResponse,
|
|
@@ -90,33 +71,44 @@ describe('native click smoke', () => {
|
|
|
90
71
|
positionResponse,
|
|
91
72
|
hoverResponse,
|
|
92
73
|
dragResponse,
|
|
74
|
+
typeResponse,
|
|
75
|
+
pressResponse,
|
|
93
76
|
}).toMatchInlineSnapshot(`
|
|
94
77
|
{
|
|
95
78
|
"downResponse": {
|
|
96
|
-
"
|
|
79
|
+
"error": null,
|
|
97
80
|
"ok": true,
|
|
98
81
|
},
|
|
99
82
|
"dragResponse": {
|
|
100
|
-
"
|
|
83
|
+
"error": null,
|
|
101
84
|
"ok": true,
|
|
102
85
|
},
|
|
103
86
|
"hoverResponse": {
|
|
104
|
-
"
|
|
87
|
+
"error": null,
|
|
105
88
|
"ok": true,
|
|
106
89
|
},
|
|
107
90
|
"moveResponse": {
|
|
108
|
-
"
|
|
91
|
+
"error": null,
|
|
109
92
|
"ok": true,
|
|
110
93
|
},
|
|
111
94
|
"positionResponse": {
|
|
112
95
|
"data": {
|
|
113
|
-
"x":
|
|
114
|
-
"y":
|
|
96
|
+
"x": 0,
|
|
97
|
+
"y": 0,
|
|
115
98
|
},
|
|
99
|
+
"error": null,
|
|
100
|
+
"ok": true,
|
|
101
|
+
},
|
|
102
|
+
"pressResponse": {
|
|
103
|
+
"error": null,
|
|
104
|
+
"ok": true,
|
|
105
|
+
},
|
|
106
|
+
"typeResponse": {
|
|
107
|
+
"error": null,
|
|
116
108
|
"ok": true,
|
|
117
109
|
},
|
|
118
110
|
"upResponse": {
|
|
119
|
-
"
|
|
111
|
+
"error": null,
|
|
120
112
|
"ok": true,
|
|
121
113
|
},
|
|
122
114
|
}
|
|
@@ -127,5 +119,31 @@ describe('native click smoke', () => {
|
|
|
127
119
|
expect(positionResponse.ok).toBe(true)
|
|
128
120
|
expect(hoverResponse.ok).toBe(true)
|
|
129
121
|
expect(dragResponse.ok).toBe(true)
|
|
122
|
+
expect(typeResponse.ok).toBe(true)
|
|
123
|
+
expect(pressResponse.ok).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
smokeTest('returns display payload for desktop list command', () => {
|
|
127
|
+
expect(native).toBeTruthy()
|
|
128
|
+
if (!native) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = native.displayList()
|
|
133
|
+
expect(result.ok).toBe(true)
|
|
134
|
+
if (!result.ok || !result.data) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parsedJson: unknown = JSON.parse(result.data)
|
|
139
|
+
const parsed = displayListSchema.safeParse(parsedJson)
|
|
140
|
+
expect(parsed.success).toBe(true)
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
expect(parsed.data.length).toBeGreaterThan(0)
|
|
146
|
+
expect(parsed.data[0]?.width).toBeGreaterThan(0)
|
|
147
|
+
expect(parsed.data[0]?.height).toBeGreaterThan(0)
|
|
130
148
|
})
|
|
131
149
|
})
|
package/src/native-lib.ts
CHANGED
|
@@ -2,11 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
|
+
import type {
|
|
6
|
+
MouseButton,
|
|
7
|
+
NativeCommandResult,
|
|
8
|
+
NativeDataResult,
|
|
9
|
+
Point,
|
|
10
|
+
Region,
|
|
11
|
+
} from './types.js'
|
|
12
|
+
|
|
13
|
+
type NativeScreenshotOutput = {
|
|
14
|
+
path: string
|
|
15
|
+
desktopIndex: number
|
|
16
|
+
captureX: number
|
|
17
|
+
captureY: number
|
|
18
|
+
captureWidth: number
|
|
19
|
+
captureHeight: number
|
|
20
|
+
imageWidth: number
|
|
21
|
+
imageHeight: number
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
const require = createRequire(import.meta.url)
|
|
7
25
|
|
|
8
26
|
export interface NativeModule {
|
|
9
|
-
|
|
27
|
+
screenshot(input: {
|
|
28
|
+
path: string | null
|
|
29
|
+
display: number | null
|
|
30
|
+
window: number | null
|
|
31
|
+
region: Region | null
|
|
32
|
+
annotate: boolean | null
|
|
33
|
+
}): NativeDataResult<NativeScreenshotOutput>
|
|
34
|
+
click(input: { point: Point; button: MouseButton | null; count: number | null }): NativeCommandResult
|
|
35
|
+
typeText(input: { text: string; delayMs: number | null }): NativeCommandResult
|
|
36
|
+
press(input: { key: string; count: number | null; delayMs: number | null }): NativeCommandResult
|
|
37
|
+
scroll(input: { direction: string; amount: number; at: Point | null }): NativeCommandResult
|
|
38
|
+
drag(input: { from: Point; to: Point; durationMs: number | null; button: MouseButton | null }): NativeCommandResult
|
|
39
|
+
hover(input: Point): NativeCommandResult
|
|
40
|
+
mouseMove(input: Point): NativeCommandResult
|
|
41
|
+
mouseDown(input: { button: MouseButton | null }): NativeCommandResult
|
|
42
|
+
mouseUp(input: { button: MouseButton | null }): NativeCommandResult
|
|
43
|
+
mousePosition(): NativeDataResult<Point>
|
|
44
|
+
displayList(): NativeDataResult<string>
|
|
45
|
+
windowList(): NativeDataResult<string>
|
|
46
|
+
clipboardGet(): NativeDataResult<string>
|
|
47
|
+
clipboardSet(input: { text: string }): NativeCommandResult
|
|
10
48
|
}
|
|
11
49
|
|
|
12
50
|
function loadCandidate(path: string): NativeModule | null {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Tests aligned terminal table formatting for deterministic CLI rendering.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { renderAlignedTable } from './terminal-table.js'
|
|
5
|
+
|
|
6
|
+
describe('terminal table', () => {
|
|
7
|
+
test('renders aligned columns for mixed widths', () => {
|
|
8
|
+
const lines = renderAlignedTable({
|
|
9
|
+
rows: [
|
|
10
|
+
{ id: 2, app: 'Zed', size: '1720x1440' },
|
|
11
|
+
{ id: 102, app: 'Google Chrome', size: '3440x1440' },
|
|
12
|
+
],
|
|
13
|
+
columns: [
|
|
14
|
+
{
|
|
15
|
+
header: 'id',
|
|
16
|
+
align: 'right',
|
|
17
|
+
value: (row) => {
|
|
18
|
+
return String(row.id)
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
header: 'app',
|
|
23
|
+
value: (row) => {
|
|
24
|
+
return row.app
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
header: 'size',
|
|
29
|
+
align: 'right',
|
|
30
|
+
value: (row) => {
|
|
31
|
+
return row.size
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(lines.join('\n')).toMatchInlineSnapshot(`
|
|
38
|
+
" id app size
|
|
39
|
+
--- ------------- ---------
|
|
40
|
+
2 Zed 1720x1440
|
|
41
|
+
102 Google Chrome 3440x1440"
|
|
42
|
+
`)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Generic aligned terminal table renderer for CLI command output.
|
|
2
|
+
|
|
3
|
+
export type TableColumn<Row> = {
|
|
4
|
+
header: string
|
|
5
|
+
align?: 'left' | 'right'
|
|
6
|
+
value: (row: Row) => string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderAlignedTable<Row>({
|
|
10
|
+
rows,
|
|
11
|
+
columns,
|
|
12
|
+
}: {
|
|
13
|
+
rows: Row[]
|
|
14
|
+
columns: TableColumn<Row>[]
|
|
15
|
+
}): string[] {
|
|
16
|
+
if (columns.length === 0) {
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const widthByColumn = columns.map((column) => {
|
|
21
|
+
const rowWidth = rows.reduce((maxWidth, row) => {
|
|
22
|
+
const width = printableWidth(column.value(row))
|
|
23
|
+
return Math.max(maxWidth, width)
|
|
24
|
+
}, 0)
|
|
25
|
+
return Math.max(printableWidth(column.header), rowWidth)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const formatCell = ({
|
|
29
|
+
value,
|
|
30
|
+
width,
|
|
31
|
+
align,
|
|
32
|
+
}: {
|
|
33
|
+
value: string
|
|
34
|
+
width: number
|
|
35
|
+
align: 'left' | 'right'
|
|
36
|
+
}): string => {
|
|
37
|
+
const currentWidth = printableWidth(value)
|
|
38
|
+
const padSize = Math.max(0, width - currentWidth)
|
|
39
|
+
const padding = ' '.repeat(padSize)
|
|
40
|
+
if (align === 'right') {
|
|
41
|
+
return `${padding}${value}`
|
|
42
|
+
}
|
|
43
|
+
return `${value}${padding}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const renderRow = ({
|
|
47
|
+
values,
|
|
48
|
+
}: {
|
|
49
|
+
values: string[]
|
|
50
|
+
}): string => {
|
|
51
|
+
return values.map((value, index) => {
|
|
52
|
+
const column = columns[index]
|
|
53
|
+
if (!column) {
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
return formatCell({
|
|
57
|
+
value,
|
|
58
|
+
width: widthByColumn[index] ?? value.length,
|
|
59
|
+
align: column.align ?? 'left',
|
|
60
|
+
})
|
|
61
|
+
}).join(' ')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const header = renderRow({
|
|
65
|
+
values: columns.map((column) => {
|
|
66
|
+
return column.header
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const divider = widthByColumn.map((width) => {
|
|
71
|
+
return '-'.repeat(width)
|
|
72
|
+
}).join(' ')
|
|
73
|
+
|
|
74
|
+
const lines = rows.map((row) => {
|
|
75
|
+
return renderRow({
|
|
76
|
+
values: columns.map((column) => {
|
|
77
|
+
return column.value(row)
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return [header, divider, ...lines]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printableWidth(value: string): number {
|
|
86
|
+
const ansiStripped = value.replace(/\u001b\[[0-9;]*m/g, '')
|
|
87
|
+
return ansiStripped.length
|
|
88
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -16,8 +16,18 @@ export type Region = {
|
|
|
16
16
|
height: number
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type CoordMap = {
|
|
20
|
+
captureX: number
|
|
21
|
+
captureY: number
|
|
22
|
+
captureWidth: number
|
|
23
|
+
captureHeight: number
|
|
24
|
+
imageWidth: number
|
|
25
|
+
imageHeight: number
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export type DisplayInfo = {
|
|
20
29
|
id: number
|
|
30
|
+
index: number
|
|
21
31
|
name: string
|
|
22
32
|
x: number
|
|
23
33
|
y: number
|
|
@@ -27,15 +37,37 @@ export type DisplayInfo = {
|
|
|
27
37
|
isPrimary: boolean
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
export type WindowInfo = {
|
|
41
|
+
id: number
|
|
42
|
+
ownerPid: number
|
|
43
|
+
ownerName: string
|
|
44
|
+
title: string
|
|
45
|
+
x: number
|
|
46
|
+
y: number
|
|
47
|
+
width: number
|
|
48
|
+
height: number
|
|
49
|
+
desktopIndex: number
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
export type ScreenshotInput = {
|
|
31
53
|
path?: string
|
|
32
54
|
display?: number
|
|
55
|
+
window?: number
|
|
33
56
|
region?: Region
|
|
34
57
|
annotate?: boolean
|
|
35
58
|
}
|
|
36
59
|
|
|
37
60
|
export type ScreenshotResult = {
|
|
38
61
|
path: string
|
|
62
|
+
desktopIndex: number
|
|
63
|
+
captureX: number
|
|
64
|
+
captureY: number
|
|
65
|
+
captureWidth: number
|
|
66
|
+
captureHeight: number
|
|
67
|
+
imageWidth: number
|
|
68
|
+
imageHeight: number
|
|
69
|
+
coordMap: string
|
|
70
|
+
hint: string
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
export type ClickInput = {
|
|
@@ -69,6 +101,23 @@ export type DragInput = {
|
|
|
69
101
|
button: MouseButton
|
|
70
102
|
}
|
|
71
103
|
|
|
104
|
+
export type NativeErrorObject = {
|
|
105
|
+
code: string
|
|
106
|
+
message: string
|
|
107
|
+
command: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type NativeCommandResult = {
|
|
111
|
+
ok: boolean
|
|
112
|
+
error?: NativeErrorObject
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type NativeDataResult<T> = {
|
|
116
|
+
ok: boolean
|
|
117
|
+
data?: T
|
|
118
|
+
error?: NativeErrorObject
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
export interface UseComputerBridge {
|
|
73
122
|
screenshot(input: ScreenshotInput): Promise<ScreenshotResult>
|
|
74
123
|
click(input: ClickInput): Promise<void>
|
|
@@ -82,6 +131,7 @@ export interface UseComputerBridge {
|
|
|
82
131
|
mouseUp(input: { button: MouseButton }): Promise<void>
|
|
83
132
|
mousePosition(): Promise<Point>
|
|
84
133
|
displayList(): Promise<DisplayInfo[]>
|
|
134
|
+
windowList(): Promise<WindowInfo[]>
|
|
85
135
|
clipboardGet(): Promise<string>
|
|
86
136
|
clipboardSet(input: { text: string }): Promise<void>
|
|
87
137
|
}
|