usecomputer 0.0.1 → 0.0.2

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +37 -0
  3. package/bin.js +3 -1
  4. package/build.zig +52 -0
  5. package/build.zig.zon +21 -0
  6. package/dist/bridge-contract.test.d.ts +2 -0
  7. package/dist/bridge-contract.test.d.ts.map +1 -0
  8. package/dist/bridge-contract.test.js +74 -0
  9. package/dist/bridge.d.ts +7 -0
  10. package/dist/bridge.d.ts.map +1 -0
  11. package/dist/bridge.js +130 -0
  12. package/dist/cli-parsing.test.d.ts +2 -0
  13. package/dist/cli-parsing.test.d.ts.map +1 -0
  14. package/dist/cli-parsing.test.js +30 -0
  15. package/dist/cli.d.ts +5 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +286 -335
  18. package/dist/command-parsers.d.ts +6 -0
  19. package/dist/command-parsers.d.ts.map +1 -0
  20. package/dist/command-parsers.js +54 -0
  21. package/dist/command-parsers.test.d.ts +2 -0
  22. package/dist/command-parsers.test.d.ts.map +1 -0
  23. package/dist/command-parsers.test.js +44 -0
  24. package/dist/darwin-arm64/usecomputer.node +0 -0
  25. package/dist/darwin-x64/usecomputer.node +0 -0
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -4
  29. package/dist/native-click-smoke.test.d.ts +2 -0
  30. package/dist/native-click-smoke.test.d.ts.map +1 -0
  31. package/dist/native-click-smoke.test.js +93 -0
  32. package/dist/native-lib.cjs +33 -0
  33. package/dist/native-lib.d.cts +7 -0
  34. package/dist/native-lib.d.ts +5 -0
  35. package/dist/native-lib.d.ts.map +1 -0
  36. package/dist/native-lib.js +27 -0
  37. package/dist/types.d.ts +80 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +2 -0
  40. package/package.json +23 -12
  41. package/src/bridge-contract.test.ts +85 -0
  42. package/src/bridge.ts +159 -0
  43. package/src/cli.ts +329 -473
  44. package/src/command-parsers.test.ts +50 -0
  45. package/src/command-parsers.ts +60 -0
  46. package/src/index.ts +5 -4
  47. package/src/native-click-smoke.test.ts +131 -0
  48. package/src/native-lib.ts +38 -0
  49. package/src/types.ts +87 -0
  50. package/zig/src/lib.zig +367 -0
@@ -0,0 +1,367 @@
1
+ // Native N-API module for usecomputer commands on macOS using Zig.
2
+ // First implementation step translates CUA macOS click semantics to Quartz
3
+ // events: post mouse down/up pairs at absolute coordinates with click state.
4
+
5
+ const std = @import("std");
6
+ const builtin = @import("builtin");
7
+ const napigen = if (builtin.is_test) undefined else @import("napigen");
8
+ const c = if (builtin.target.os.tag == .macos) @cImport({
9
+ @cInclude("CoreGraphics/CoreGraphics.h");
10
+ @cInclude("CoreFoundation/CoreFoundation.h");
11
+ }) else struct {};
12
+
13
+ pub const std_options: std.Options = .{
14
+ .log_level = .err,
15
+ };
16
+
17
+ fn makeOkJson(allocator: std.mem.Allocator, data_json: []const u8) ![]const u8 {
18
+ return std.fmt.allocPrint(allocator, "{{\"ok\":true,\"data\":{s}}}", .{data_json});
19
+ }
20
+
21
+ fn makeErrorJson(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
22
+ return std.fmt.allocPrint(allocator, "{{\"ok\":false,\"error\":\"{s}\"}}", .{message});
23
+ }
24
+
25
+ fn execute(command: []const u8, payload_json: []const u8) ![]const u8 {
26
+ const allocator = std.heap.c_allocator;
27
+
28
+ if (std.mem.eql(u8, command, "click")) {
29
+ return executeClickCommand(allocator, payload_json);
30
+ }
31
+ if (std.mem.eql(u8, command, "mouse-move")) {
32
+ return executeMouseMoveCommand(allocator, payload_json);
33
+ }
34
+ if (std.mem.eql(u8, command, "mouse-down")) {
35
+ return executeMouseDownCommand(allocator, payload_json);
36
+ }
37
+ if (std.mem.eql(u8, command, "mouse-up")) {
38
+ return executeMouseUpCommand(allocator, payload_json);
39
+ }
40
+ if (std.mem.eql(u8, command, "mouse-position")) {
41
+ return executeMousePositionCommand(allocator);
42
+ }
43
+ if (std.mem.eql(u8, command, "hover")) {
44
+ return executeHoverCommand(allocator, payload_json);
45
+ }
46
+ if (std.mem.eql(u8, command, "drag")) {
47
+ return executeDragCommand(allocator, payload_json);
48
+ }
49
+
50
+ if (std.mem.eql(u8, command, "display-list")) {
51
+ return makeOkJson(allocator, "[]");
52
+ }
53
+ if (std.mem.eql(u8, command, "clipboard-get")) {
54
+ return makeOkJson(allocator, "{\"text\":\"\"}");
55
+ }
56
+ if (std.mem.eql(u8, command, "screenshot")) {
57
+ return makeOkJson(allocator, "{\"path\":\"./screenshot.png\"}");
58
+ }
59
+
60
+ if (
61
+ std.mem.eql(u8, command, "type-text") or
62
+ std.mem.eql(u8, command, "press") or
63
+ std.mem.eql(u8, command, "scroll") or
64
+ std.mem.eql(u8, command, "clipboard-set")
65
+ ) {
66
+ return makeOkJson(allocator, "null");
67
+ }
68
+
69
+ return makeErrorJson(allocator, "unknown command");
70
+ }
71
+
72
+ const ClickPoint = struct {
73
+ x: f64,
74
+ y: f64,
75
+ };
76
+
77
+ const ClickPayload = struct {
78
+ point: ClickPoint,
79
+ button: ?[]const u8 = null,
80
+ count: ?u32 = null,
81
+ };
82
+
83
+ fn executeClickCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
84
+ if (builtin.target.os.tag != .macos) {
85
+ return makeErrorJson(allocator, "click is only supported on macOS");
86
+ }
87
+
88
+ var parsed = std.json.parseFromSlice(ClickPayload, allocator, payload_json, .{
89
+ .ignore_unknown_fields = true,
90
+ }) catch {
91
+ return makeErrorJson(allocator, "invalid click payload json");
92
+ };
93
+ defer parsed.deinit();
94
+
95
+ const click_payload = parsed.value;
96
+ const click_count: u32 = if (click_payload.count) |count| blk: {
97
+ if (count == 0) {
98
+ break :blk 1;
99
+ }
100
+ break :blk count;
101
+ } else 1;
102
+
103
+ const button_kind = resolveMouseButton(click_payload.button orelse "left") catch {
104
+ return makeErrorJson(allocator, "invalid click button");
105
+ };
106
+
107
+ const point: c.CGPoint = .{
108
+ .x = click_payload.point.x,
109
+ .y = click_payload.point.y,
110
+ };
111
+
112
+ var index: u32 = 0;
113
+ while (index < click_count) : (index += 1) {
114
+ const click_state = @as(i64, @intCast(index + 1));
115
+ postClickPair(point, button_kind, click_state) catch {
116
+ return makeErrorJson(allocator, "failed to post click event");
117
+ };
118
+
119
+ if (index + 1 < click_count) {
120
+ std.Thread.sleep(80 * std.time.ns_per_ms);
121
+ }
122
+ }
123
+
124
+ return makeOkJson(allocator, "null");
125
+ }
126
+
127
+ const MouseMovePayload = struct {
128
+ x: f64,
129
+ y: f64,
130
+ };
131
+
132
+ const MouseButtonPayload = struct {
133
+ button: ?[]const u8 = null,
134
+ };
135
+
136
+ const DragPayload = struct {
137
+ from: ClickPoint,
138
+ to: ClickPoint,
139
+ durationMs: ?u64 = null,
140
+ button: ?[]const u8 = null,
141
+ };
142
+
143
+ fn executeMouseMoveCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
144
+ if (builtin.target.os.tag != .macos) {
145
+ return makeErrorJson(allocator, "mouse-move is only supported on macOS");
146
+ }
147
+
148
+ var parsed = std.json.parseFromSlice(MouseMovePayload, allocator, payload_json, .{}) catch {
149
+ return makeErrorJson(allocator, "invalid mouse-move payload json");
150
+ };
151
+ defer parsed.deinit();
152
+
153
+ const point: c.CGPoint = .{
154
+ .x = parsed.value.x,
155
+ .y = parsed.value.y,
156
+ };
157
+ moveCursorToPoint(point) catch {
158
+ return makeErrorJson(allocator, "failed to move mouse cursor");
159
+ };
160
+
161
+ return makeOkJson(allocator, "null");
162
+ }
163
+
164
+ fn executeMouseDownCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
165
+ return executeMouseButtonCommand(allocator, payload_json, true);
166
+ }
167
+
168
+ fn executeMouseUpCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
169
+ return executeMouseButtonCommand(allocator, payload_json, false);
170
+ }
171
+
172
+ fn executeMouseButtonCommand(allocator: std.mem.Allocator, payload_json: []const u8, is_down: bool) ![]const u8 {
173
+ if (builtin.target.os.tag != .macos) {
174
+ return makeErrorJson(allocator, "mouse button events are only supported on macOS");
175
+ }
176
+
177
+ var parsed = std.json.parseFromSlice(MouseButtonPayload, allocator, payload_json, .{}) catch {
178
+ return makeErrorJson(allocator, "invalid mouse button payload json");
179
+ };
180
+ defer parsed.deinit();
181
+
182
+ const button_kind = resolveMouseButton(parsed.value.button orelse "left") catch {
183
+ return makeErrorJson(allocator, "invalid mouse button");
184
+ };
185
+
186
+ const point = currentCursorPoint() catch {
187
+ return makeErrorJson(allocator, "failed to read cursor position");
188
+ };
189
+
190
+ postMouseButtonEvent(point, button_kind, is_down, 1) catch {
191
+ return makeErrorJson(allocator, "failed to post mouse button event");
192
+ };
193
+
194
+ return makeOkJson(allocator, "null");
195
+ }
196
+
197
+ fn executeMousePositionCommand(allocator: std.mem.Allocator) ![]const u8 {
198
+ if (builtin.target.os.tag != .macos) {
199
+ return makeErrorJson(allocator, "mouse-position is only supported on macOS");
200
+ }
201
+
202
+ const point = currentCursorPoint() catch {
203
+ return makeErrorJson(allocator, "failed to read cursor position");
204
+ };
205
+
206
+ const x = @as(i64, @intFromFloat(std.math.round(point.x)));
207
+ const y = @as(i64, @intFromFloat(std.math.round(point.y)));
208
+ const point_json = try std.fmt.allocPrint(allocator, "{{\"x\":{d},\"y\":{d}}}", .{ x, y });
209
+ return makeOkJson(allocator, point_json);
210
+ }
211
+
212
+ fn executeHoverCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
213
+ if (builtin.target.os.tag != .macos) {
214
+ return makeErrorJson(allocator, "hover is only supported on macOS");
215
+ }
216
+
217
+ var parsed = std.json.parseFromSlice(MouseMovePayload, allocator, payload_json, .{}) catch {
218
+ return makeErrorJson(allocator, "invalid hover payload json");
219
+ };
220
+ defer parsed.deinit();
221
+
222
+ const point: c.CGPoint = .{
223
+ .x = parsed.value.x,
224
+ .y = parsed.value.y,
225
+ };
226
+
227
+ moveCursorToPoint(point) catch {
228
+ return makeErrorJson(allocator, "failed to move cursor for hover");
229
+ };
230
+
231
+ return makeOkJson(allocator, "null");
232
+ }
233
+
234
+ fn executeDragCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
235
+ if (builtin.target.os.tag != .macos) {
236
+ return makeErrorJson(allocator, "drag is only supported on macOS");
237
+ }
238
+
239
+ var parsed = std.json.parseFromSlice(DragPayload, allocator, payload_json, .{}) catch {
240
+ return makeErrorJson(allocator, "invalid drag payload json");
241
+ };
242
+ defer parsed.deinit();
243
+
244
+ const drag_payload = parsed.value;
245
+ const button_kind = resolveMouseButton(drag_payload.button orelse "left") catch {
246
+ return makeErrorJson(allocator, "invalid drag button");
247
+ };
248
+
249
+ const from: c.CGPoint = .{ .x = drag_payload.from.x, .y = drag_payload.from.y };
250
+ const to: c.CGPoint = .{ .x = drag_payload.to.x, .y = drag_payload.to.y };
251
+
252
+ moveCursorToPoint(from) catch {
253
+ return makeErrorJson(allocator, "failed to move cursor to drag origin");
254
+ };
255
+
256
+ postMouseButtonEvent(from, button_kind, true, 1) catch {
257
+ return makeErrorJson(allocator, "failed to post drag mouse-down");
258
+ };
259
+
260
+ const total_duration_ns = (drag_payload.durationMs orelse 400) * std.time.ns_per_ms;
261
+ const step_count: u64 = 16;
262
+ const step_duration_ns = if (step_count == 0) 0 else total_duration_ns / step_count;
263
+
264
+ var index: u64 = 1;
265
+ while (index <= step_count) : (index += 1) {
266
+ const fraction = @as(f64, @floatFromInt(index)) / @as(f64, @floatFromInt(step_count));
267
+ const next_point: c.CGPoint = .{
268
+ .x = from.x + (to.x - from.x) * fraction,
269
+ .y = from.y + (to.y - from.y) * fraction,
270
+ };
271
+
272
+ moveCursorToPoint(next_point) catch {
273
+ return makeErrorJson(allocator, "failed during drag cursor movement");
274
+ };
275
+
276
+ if (step_duration_ns > 0 and index < step_count) {
277
+ std.Thread.sleep(step_duration_ns);
278
+ }
279
+ }
280
+
281
+ postMouseButtonEvent(to, button_kind, false, 1) catch {
282
+ return makeErrorJson(allocator, "failed to post drag mouse-up");
283
+ };
284
+
285
+ return makeOkJson(allocator, "null");
286
+ }
287
+
288
+ const MouseButtonKind = enum {
289
+ left,
290
+ right,
291
+ middle,
292
+ };
293
+
294
+ fn resolveMouseButton(button: []const u8) !MouseButtonKind {
295
+ if (std.ascii.eqlIgnoreCase(button, "left")) {
296
+ return .left;
297
+ }
298
+ if (std.ascii.eqlIgnoreCase(button, "right")) {
299
+ return .right;
300
+ }
301
+ if (std.ascii.eqlIgnoreCase(button, "middle")) {
302
+ return .middle;
303
+ }
304
+ return error.InvalidMouseButton;
305
+ }
306
+
307
+ fn postClickPair(point: c.CGPoint, button: MouseButtonKind, click_state: i64) !void {
308
+ try postMouseButtonEvent(point, button, true, click_state);
309
+ try postMouseButtonEvent(point, button, false, click_state);
310
+ }
311
+
312
+ fn postMouseButtonEvent(point: c.CGPoint, button: MouseButtonKind, is_down: bool, click_state: i64) !void {
313
+ const button_code: c.CGMouseButton = switch (button) {
314
+ .left => c.kCGMouseButtonLeft,
315
+ .right => c.kCGMouseButtonRight,
316
+ .middle => c.kCGMouseButtonCenter,
317
+ };
318
+
319
+ const event_type: c.CGEventType = switch (button) {
320
+ .left => if (is_down) c.kCGEventLeftMouseDown else c.kCGEventLeftMouseUp,
321
+ .right => if (is_down) c.kCGEventRightMouseDown else c.kCGEventRightMouseUp,
322
+ .middle => if (is_down) c.kCGEventOtherMouseDown else c.kCGEventOtherMouseUp,
323
+ };
324
+
325
+ const event = c.CGEventCreateMouseEvent(null, event_type, point, button_code);
326
+ if (event == null) {
327
+ return error.CGEventCreateFailed;
328
+ }
329
+ defer c.CFRelease(event);
330
+
331
+ c.CGEventSetIntegerValueField(event, c.kCGMouseEventClickState, click_state);
332
+ c.CGEventPost(c.kCGHIDEventTap, event);
333
+ }
334
+
335
+ fn currentCursorPoint() !c.CGPoint {
336
+ const event = c.CGEventCreate(null);
337
+ if (event == null) {
338
+ return error.CGEventCreateFailed;
339
+ }
340
+ defer c.CFRelease(event);
341
+ return c.CGEventGetLocation(event);
342
+ }
343
+
344
+ fn moveCursorToPoint(point: c.CGPoint) !void {
345
+ const warp_result = c.CGWarpMouseCursorPosition(point);
346
+ if (warp_result != c.kCGErrorSuccess) {
347
+ return error.CGWarpMouseFailed;
348
+ }
349
+
350
+ const move_event = c.CGEventCreateMouseEvent(null, c.kCGEventMouseMoved, point, c.kCGMouseButtonLeft);
351
+ if (move_event == null) {
352
+ return error.CGEventCreateFailed;
353
+ }
354
+ defer c.CFRelease(move_event);
355
+ c.CGEventPost(c.kCGHIDEventTap, move_event);
356
+ }
357
+
358
+ fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) !napigen.napi_value {
359
+ try js.setNamedProperty(exports, "execute", try js.createFunction(execute));
360
+ return exports;
361
+ }
362
+
363
+ comptime {
364
+ if (!builtin.is_test) {
365
+ napigen.defineModule(initModule);
366
+ }
367
+ }