usecomputer 0.0.2 → 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +338 -0
  3. package/build.zig +1 -0
  4. package/dist/bridge-contract.test.js +124 -63
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +241 -46
  7. package/dist/cli-parsing.test.js +34 -11
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +328 -22
  10. package/dist/coord-map.d.ts +14 -0
  11. package/dist/coord-map.d.ts.map +1 -0
  12. package/dist/coord-map.js +75 -0
  13. package/dist/coord-map.test.d.ts +2 -0
  14. package/dist/coord-map.test.d.ts.map +1 -0
  15. package/dist/coord-map.test.js +157 -0
  16. package/dist/darwin-arm64/usecomputer.node +0 -0
  17. package/dist/darwin-x64/usecomputer.node +0 -0
  18. package/dist/debug-point-image.d.ts +8 -0
  19. package/dist/debug-point-image.d.ts.map +1 -0
  20. package/dist/debug-point-image.js +43 -0
  21. package/dist/debug-point-image.test.d.ts +2 -0
  22. package/dist/debug-point-image.test.d.ts.map +1 -0
  23. package/dist/debug-point-image.test.js +44 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/lib.d.ts +26 -0
  28. package/dist/lib.d.ts.map +1 -0
  29. package/dist/lib.js +88 -0
  30. package/dist/native-click-smoke.test.js +69 -29
  31. package/dist/native-lib.d.ts +59 -1
  32. package/dist/native-lib.d.ts.map +1 -1
  33. package/dist/terminal-table.d.ts +10 -0
  34. package/dist/terminal-table.d.ts.map +1 -0
  35. package/dist/terminal-table.js +55 -0
  36. package/dist/terminal-table.test.d.ts +2 -0
  37. package/dist/terminal-table.test.d.ts.map +1 -0
  38. package/dist/terminal-table.test.js +41 -0
  39. package/dist/types.d.ts +45 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +16 -4
  42. package/src/bridge-contract.test.ts +140 -69
  43. package/src/bridge.ts +293 -53
  44. package/src/cli-parsing.test.ts +61 -0
  45. package/src/cli.ts +401 -25
  46. package/src/coord-map.test.ts +178 -0
  47. package/src/coord-map.ts +105 -0
  48. package/src/debug-point-image.test.ts +50 -0
  49. package/src/debug-point-image.ts +69 -0
  50. package/src/index.ts +3 -1
  51. package/src/lib.ts +125 -0
  52. package/src/native-click-smoke.test.ts +81 -63
  53. package/src/native-lib.ts +39 -1
  54. package/src/terminal-table.test.ts +44 -0
  55. package/src/terminal-table.ts +88 -0
  56. package/src/types.ts +50 -0
  57. package/zig/src/lib.zig +1280 -163
  58. package/zig/src/scroll.zig +213 -0
  59. package/zig/src/window.zig +123 -0
package/zig/src/lib.zig CHANGED
@@ -1,119 +1,362 @@
1
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.
2
+ // Exports direct typed methods (no string command dispatcher) so TS can call
3
+ // high-level native functions and receive structured error objects.
4
4
 
5
5
  const std = @import("std");
6
6
  const builtin = @import("builtin");
7
+ const scroll_impl = @import("scroll.zig");
8
+ const window = @import("window.zig");
7
9
  const napigen = if (builtin.is_test) undefined else @import("napigen");
8
- const c = if (builtin.target.os.tag == .macos) @cImport({
10
+ const c_macos = if (builtin.target.os.tag == .macos) @cImport({
9
11
  @cInclude("CoreGraphics/CoreGraphics.h");
10
12
  @cInclude("CoreFoundation/CoreFoundation.h");
13
+ @cInclude("ImageIO/ImageIO.h");
11
14
  }) else struct {};
12
15
 
16
+ const c_windows = if (builtin.target.os.tag == .windows) @cImport({
17
+ @cInclude("windows.h");
18
+ }) else struct {};
19
+
20
+ const c_x11 = if (builtin.target.os.tag == .linux) @cImport({
21
+ @cInclude("X11/Xlib.h");
22
+ @cInclude("X11/keysym.h");
23
+ @cInclude("X11/extensions/XTest.h");
24
+ }) else struct {};
25
+
26
+ const c = c_macos;
27
+ const screenshot_max_long_edge_px: f64 = 1568;
28
+
29
+ const mac_keycode = struct {
30
+ const a = 0x00;
31
+ const s = 0x01;
32
+ const d = 0x02;
33
+ const f = 0x03;
34
+ const h = 0x04;
35
+ const g = 0x05;
36
+ const z = 0x06;
37
+ const x = 0x07;
38
+ const c = 0x08;
39
+ const v = 0x09;
40
+ const b = 0x0B;
41
+ const q = 0x0C;
42
+ const w = 0x0D;
43
+ const e = 0x0E;
44
+ const r = 0x0F;
45
+ const y = 0x10;
46
+ const t = 0x11;
47
+ const one = 0x12;
48
+ const two = 0x13;
49
+ const three = 0x14;
50
+ const four = 0x15;
51
+ const six = 0x16;
52
+ const five = 0x17;
53
+ const equal = 0x18;
54
+ const nine = 0x19;
55
+ const seven = 0x1A;
56
+ const minus = 0x1B;
57
+ const eight = 0x1C;
58
+ const zero = 0x1D;
59
+ const right_bracket = 0x1E;
60
+ const o = 0x1F;
61
+ const u = 0x20;
62
+ const left_bracket = 0x21;
63
+ const i = 0x22;
64
+ const p = 0x23;
65
+ const l = 0x25;
66
+ const j = 0x26;
67
+ const quote = 0x27;
68
+ const k = 0x28;
69
+ const semicolon = 0x29;
70
+ const backslash = 0x2A;
71
+ const comma = 0x2B;
72
+ const slash = 0x2C;
73
+ const n = 0x2D;
74
+ const m = 0x2E;
75
+ const period = 0x2F;
76
+ const tab = 0x30;
77
+ const space = 0x31;
78
+ const grave = 0x32;
79
+ const delete = 0x33;
80
+ const enter = 0x24;
81
+ const escape = 0x35;
82
+ const command = 0x37;
83
+ const shift = 0x38;
84
+ const option = 0x3A;
85
+ const control = 0x3B;
86
+ const fn_key = 0x3F;
87
+ const f1 = 0x7A;
88
+ const f2 = 0x78;
89
+ const f3 = 0x63;
90
+ const f4 = 0x76;
91
+ const f5 = 0x60;
92
+ const f6 = 0x61;
93
+ const f7 = 0x62;
94
+ const f8 = 0x64;
95
+ const f9 = 0x65;
96
+ const f10 = 0x6D;
97
+ const f11 = 0x67;
98
+ const f12 = 0x6F;
99
+ const home = 0x73;
100
+ const page_up = 0x74;
101
+ const forward_delete = 0x75;
102
+ const end = 0x77;
103
+ const page_down = 0x79;
104
+ const left_arrow = 0x7B;
105
+ const right_arrow = 0x7C;
106
+ const down_arrow = 0x7D;
107
+ const up_arrow = 0x7E;
108
+ };
109
+
13
110
  pub const std_options: std.Options = .{
14
111
  .log_level = .err,
15
112
  };
16
113
 
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
- }
114
+ const DisplayInfoOutput = struct {
115
+ id: u32,
116
+ index: u32,
117
+ name: []const u8,
118
+ x: f64,
119
+ y: f64,
120
+ width: f64,
121
+ height: f64,
122
+ scale: f64,
123
+ isPrimary: bool,
124
+ };
20
125
 
21
- fn makeErrorJson(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
22
- return std.fmt.allocPrint(allocator, "{{\"ok\":false,\"error\":\"{s}\"}}", .{message});
126
+ const WindowInfoOutput = struct {
127
+ id: u32,
128
+ ownerPid: i32,
129
+ ownerName: []const u8,
130
+ title: []const u8,
131
+ x: f64,
132
+ y: f64,
133
+ width: f64,
134
+ height: f64,
135
+ desktopIndex: u32,
136
+ };
137
+
138
+ const NativeErrorObject = struct {
139
+ code: []const u8,
140
+ message: []const u8,
141
+ command: []const u8,
142
+ };
143
+
144
+ const CommandResult = struct {
145
+ ok: bool,
146
+ @"error": ?NativeErrorObject = null,
147
+ };
148
+
149
+ fn DataResult(comptime T: type) type {
150
+ return struct {
151
+ ok: bool,
152
+ data: ?T = null,
153
+ @"error": ?NativeErrorObject = null,
154
+ };
23
155
  }
24
156
 
25
- fn execute(command: []const u8, payload_json: []const u8) ![]const u8 {
26
- const allocator = std.heap.c_allocator;
157
+ fn okCommand() CommandResult {
158
+ return .{ .ok = true };
159
+ }
27
160
 
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
- }
161
+ fn failCommand(command: []const u8, code: []const u8, message: []const u8) CommandResult {
162
+ return .{
163
+ .ok = false,
164
+ .@"error" = .{
165
+ .code = code,
166
+ .message = message,
167
+ .command = command,
168
+ },
169
+ };
170
+ }
49
171
 
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
- }
172
+ fn okData(comptime T: type, value: T) DataResult(T) {
173
+ return .{
174
+ .ok = true,
175
+ .data = value,
176
+ };
177
+ }
59
178
 
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
- }
179
+ fn failData(comptime T: type, command: []const u8, code: []const u8, message: []const u8) DataResult(T) {
180
+ return .{
181
+ .ok = false,
182
+ .@"error" = .{
183
+ .code = code,
184
+ .message = message,
185
+ .command = command,
186
+ },
187
+ };
188
+ }
68
189
 
69
- return makeErrorJson(allocator, "unknown command");
190
+ fn todoNotImplemented(command: []const u8) CommandResult {
191
+ return failCommand(command, "TODO_NOT_IMPLEMENTED", "TODO not implemented");
70
192
  }
71
193
 
72
- const ClickPoint = struct {
194
+ const Point = struct {
73
195
  x: f64,
74
196
  y: f64,
75
197
  };
76
198
 
77
- const ClickPayload = struct {
78
- point: ClickPoint,
199
+ const MouseButtonKind = enum {
200
+ left,
201
+ right,
202
+ middle,
203
+ };
204
+
205
+ const ClickInput = struct {
206
+ point: Point,
207
+ button: ?[]const u8 = null,
208
+ count: ?f64 = null,
209
+ };
210
+
211
+ const MouseMoveInput = Point;
212
+
213
+ const MouseButtonInput = struct {
214
+ button: ?[]const u8 = null,
215
+ };
216
+
217
+ const DragInput = struct {
218
+ from: Point,
219
+ to: Point,
220
+ durationMs: ?f64 = null,
79
221
  button: ?[]const u8 = null,
80
- count: ?u32 = null,
81
222
  };
82
223
 
83
- fn executeClickCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
224
+ const ScreenshotRegion = struct {
225
+ x: f64,
226
+ y: f64,
227
+ width: f64,
228
+ height: f64,
229
+ };
230
+
231
+ const ScreenshotInput = struct {
232
+ path: ?[]const u8 = null,
233
+ display: ?f64 = null,
234
+ window: ?f64 = null,
235
+ region: ?ScreenshotRegion = null,
236
+ annotate: ?bool = null,
237
+ };
238
+
239
+ const ScreenshotOutput = struct {
240
+ path: []const u8,
241
+ desktopIndex: f64,
242
+ captureX: f64,
243
+ captureY: f64,
244
+ captureWidth: f64,
245
+ captureHeight: f64,
246
+ imageWidth: f64,
247
+ imageHeight: f64,
248
+ };
249
+
250
+ const SelectedDisplay = struct {
251
+ id: c.CGDirectDisplayID,
252
+ index: usize,
253
+ bounds: c.CGRect,
254
+ };
255
+
256
+ const ScreenshotCapture = struct {
257
+ image: c.CGImageRef,
258
+ capture_x: f64,
259
+ capture_y: f64,
260
+ capture_width: f64,
261
+ capture_height: f64,
262
+ desktop_index: usize,
263
+ };
264
+
265
+ const ScaledScreenshotImage = struct {
266
+ image: c.CGImageRef,
267
+ width: f64,
268
+ height: f64,
269
+ };
270
+
271
+ const TypeTextInput = struct {
272
+ text: []const u8,
273
+ delayMs: ?f64 = null,
274
+ };
275
+
276
+ const PressInput = struct {
277
+ key: []const u8,
278
+ count: ?f64 = null,
279
+ delayMs: ?f64 = null,
280
+ };
281
+
282
+ const ScrollInput = struct {
283
+ direction: []const u8,
284
+ amount: f64,
285
+ at: ?Point = null,
286
+ };
287
+
288
+ const ClipboardSetInput = struct {
289
+ text: []const u8,
290
+ };
291
+
292
+ pub fn screenshot(input: ScreenshotInput) DataResult(ScreenshotOutput) {
84
293
  if (builtin.target.os.tag != .macos) {
85
- return makeErrorJson(allocator, "click is only supported on macOS");
294
+ return failData(ScreenshotOutput, "screenshot", "UNSUPPORTED_PLATFORM", "screenshot is only supported on macOS");
86
295
  }
87
296
 
88
- var parsed = std.json.parseFromSlice(ClickPayload, allocator, payload_json, .{
89
- .ignore_unknown_fields = true,
297
+ _ = input.annotate;
298
+ const output_path = input.path orelse "./screenshot.png";
299
+
300
+ const capture = createScreenshotImage(.{
301
+ .display_index = input.display,
302
+ .window_id = input.window,
303
+ .region = input.region,
304
+ }) catch {
305
+ return failData(ScreenshotOutput, "screenshot", "CAPTURE_FAILED", "failed to capture screenshot image");
306
+ };
307
+ defer c.CFRelease(capture.image);
308
+
309
+ const scaled_image = scaleScreenshotImageIfNeeded(capture.image) catch {
310
+ return failData(ScreenshotOutput, "screenshot", "SCALE_FAILED", "failed to scale screenshot image");
311
+ };
312
+ defer c.CFRelease(scaled_image.image);
313
+
314
+ writeScreenshotPng(.{
315
+ .image = scaled_image.image,
316
+ .output_path = output_path,
90
317
  }) catch {
91
- return makeErrorJson(allocator, "invalid click payload json");
318
+ return failData(ScreenshotOutput, "screenshot", "WRITE_FAILED", "failed to write screenshot file");
92
319
  };
93
- defer parsed.deinit();
94
320
 
95
- const click_payload = parsed.value;
96
- const click_count: u32 = if (click_payload.count) |count| blk: {
97
- if (count == 0) {
321
+ return okData(ScreenshotOutput, .{
322
+ .path = output_path,
323
+ .desktopIndex = @as(f64, @floatFromInt(capture.desktop_index)),
324
+ .captureX = capture.capture_x,
325
+ .captureY = capture.capture_y,
326
+ .captureWidth = capture.capture_width,
327
+ .captureHeight = capture.capture_height,
328
+ .imageWidth = scaled_image.width,
329
+ .imageHeight = scaled_image.height,
330
+ });
331
+ }
332
+
333
+ pub fn click(input: ClickInput) CommandResult {
334
+ if (builtin.target.os.tag != .macos) {
335
+ return failCommand("click", "UNSUPPORTED_PLATFORM", "click is only supported on macOS");
336
+ }
337
+
338
+ const click_count: u32 = if (input.count) |count| blk: {
339
+ const normalized = @as(i64, @intFromFloat(std.math.round(count)));
340
+ if (normalized <= 0) {
98
341
  break :blk 1;
99
342
  }
100
- break :blk count;
343
+ break :blk @as(u32, @intCast(normalized));
101
344
  } else 1;
102
345
 
103
- const button_kind = resolveMouseButton(click_payload.button orelse "left") catch {
104
- return makeErrorJson(allocator, "invalid click button");
346
+ const button_kind = resolveMouseButton(input.button orelse "left") catch {
347
+ return failCommand("click", "INVALID_INPUT", "invalid click button");
105
348
  };
106
349
 
107
350
  const point: c.CGPoint = .{
108
- .x = click_payload.point.x,
109
- .y = click_payload.point.y,
351
+ .x = input.point.x,
352
+ .y = input.point.y,
110
353
  };
111
354
 
112
355
  var index: u32 = 0;
113
356
  while (index < click_count) : (index += 1) {
114
357
  const click_state = @as(i64, @intCast(index + 1));
115
358
  postClickPair(point, button_kind, click_state) catch {
116
- return makeErrorJson(allocator, "failed to post click event");
359
+ return failCommand("click", "EVENT_POST_FAILED", "failed to post click event");
117
360
  };
118
361
 
119
362
  if (index + 1 < click_count) {
@@ -121,143 +364,100 @@ fn executeClickCommand(allocator: std.mem.Allocator, payload_json: []const u8) !
121
364
  }
122
365
  }
123
366
 
124
- return makeOkJson(allocator, "null");
367
+ return okCommand();
125
368
  }
126
369
 
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 {
370
+ pub fn mouseMove(input: MouseMoveInput) CommandResult {
144
371
  if (builtin.target.os.tag != .macos) {
145
- return makeErrorJson(allocator, "mouse-move is only supported on macOS");
372
+ return failCommand("mouse-move", "UNSUPPORTED_PLATFORM", "mouse-move is only supported on macOS");
146
373
  }
147
374
 
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
375
  const point: c.CGPoint = .{
154
- .x = parsed.value.x,
155
- .y = parsed.value.y,
376
+ .x = input.x,
377
+ .y = input.y,
156
378
  };
157
379
  moveCursorToPoint(point) catch {
158
- return makeErrorJson(allocator, "failed to move mouse cursor");
380
+ return failCommand("mouse-move", "EVENT_POST_FAILED", "failed to move mouse cursor");
159
381
  };
160
382
 
161
- return makeOkJson(allocator, "null");
383
+ return okCommand();
162
384
  }
163
385
 
164
- fn executeMouseDownCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
165
- return executeMouseButtonCommand(allocator, payload_json, true);
386
+ pub fn mouseDown(input: MouseButtonInput) CommandResult {
387
+ return handleMouseButtonInput(.{ .input = input, .is_down = true });
166
388
  }
167
389
 
168
- fn executeMouseUpCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
169
- return executeMouseButtonCommand(allocator, payload_json, false);
390
+ pub fn mouseUp(input: MouseButtonInput) CommandResult {
391
+ return handleMouseButtonInput(.{ .input = input, .is_down = false });
170
392
  }
171
393
 
172
- fn executeMouseButtonCommand(allocator: std.mem.Allocator, payload_json: []const u8, is_down: bool) ![]const u8 {
394
+ fn handleMouseButtonInput(args: struct {
395
+ input: MouseButtonInput,
396
+ is_down: bool,
397
+ }) CommandResult {
173
398
  if (builtin.target.os.tag != .macos) {
174
- return makeErrorJson(allocator, "mouse button events are only supported on macOS");
399
+ return failCommand("mouse-button", "UNSUPPORTED_PLATFORM", "mouse button events are only supported on macOS");
175
400
  }
176
401
 
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");
402
+ const button_kind = resolveMouseButton(args.input.button orelse "left") catch {
403
+ return failCommand("mouse-button", "INVALID_INPUT", "invalid mouse button");
184
404
  };
185
405
 
186
406
  const point = currentCursorPoint() catch {
187
- return makeErrorJson(allocator, "failed to read cursor position");
407
+ return failCommand("mouse-button", "CURSOR_READ_FAILED", "failed to read cursor position");
188
408
  };
189
409
 
190
- postMouseButtonEvent(point, button_kind, is_down, 1) catch {
191
- return makeErrorJson(allocator, "failed to post mouse button event");
410
+ postMouseButtonEvent(point, button_kind, args.is_down, 1) catch {
411
+ return failCommand("mouse-button", "EVENT_POST_FAILED", "failed to post mouse button event");
192
412
  };
193
413
 
194
- return makeOkJson(allocator, "null");
414
+ return okCommand();
195
415
  }
196
416
 
197
- fn executeMousePositionCommand(allocator: std.mem.Allocator) ![]const u8 {
417
+ pub fn mousePosition() DataResult(Point) {
198
418
  if (builtin.target.os.tag != .macos) {
199
- return makeErrorJson(allocator, "mouse-position is only supported on macOS");
419
+ return failData(Point, "mouse-position", "UNSUPPORTED_PLATFORM", "mouse-position is only supported on macOS");
200
420
  }
201
421
 
202
422
  const point = currentCursorPoint() catch {
203
- return makeErrorJson(allocator, "failed to read cursor position");
423
+ return failData(Point, "mouse-position", "CURSOR_READ_FAILED", "failed to read cursor position");
204
424
  };
205
425
 
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);
426
+ return okData(Point, .{ .x = std.math.round(point.x), .y = std.math.round(point.y) });
210
427
  }
211
428
 
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");
429
+ pub fn hover(input: Point) CommandResult {
430
+ return mouseMove(input);
232
431
  }
233
432
 
234
- fn executeDragCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
433
+ pub fn drag(input: DragInput) CommandResult {
235
434
  if (builtin.target.os.tag != .macos) {
236
- return makeErrorJson(allocator, "drag is only supported on macOS");
435
+ return failCommand("drag", "UNSUPPORTED_PLATFORM", "drag is only supported on macOS");
237
436
  }
238
437
 
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");
438
+ const button_kind = resolveMouseButton(input.button orelse "left") catch {
439
+ return failCommand("drag", "INVALID_INPUT", "invalid drag button");
247
440
  };
248
441
 
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 };
442
+ const from: c.CGPoint = .{ .x = input.from.x, .y = input.from.y };
443
+ const to: c.CGPoint = .{ .x = input.to.x, .y = input.to.y };
251
444
 
252
445
  moveCursorToPoint(from) catch {
253
- return makeErrorJson(allocator, "failed to move cursor to drag origin");
446
+ return failCommand("drag", "EVENT_POST_FAILED", "failed to move cursor to drag origin");
254
447
  };
255
448
 
256
449
  postMouseButtonEvent(from, button_kind, true, 1) catch {
257
- return makeErrorJson(allocator, "failed to post drag mouse-down");
450
+ return failCommand("drag", "EVENT_POST_FAILED", "failed to post drag mouse-down");
258
451
  };
259
452
 
260
- const total_duration_ns = (drag_payload.durationMs orelse 400) * std.time.ns_per_ms;
453
+ const duration_ms = if (input.durationMs) |value| blk: {
454
+ const normalized = @as(i64, @intFromFloat(std.math.round(value)));
455
+ if (normalized <= 0) {
456
+ break :blk 400;
457
+ }
458
+ break :blk normalized;
459
+ } else 400;
460
+ const total_duration_ns = @as(u64, @intCast(duration_ms)) * std.time.ns_per_ms;
261
461
  const step_count: u64 = 16;
262
462
  const step_duration_ns = if (step_count == 0) 0 else total_duration_ns / step_count;
263
463
 
@@ -270,7 +470,7 @@ fn executeDragCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![
270
470
  };
271
471
 
272
472
  moveCursorToPoint(next_point) catch {
273
- return makeErrorJson(allocator, "failed during drag cursor movement");
473
+ return failCommand("drag", "EVENT_POST_FAILED", "failed during drag cursor movement");
274
474
  };
275
475
 
276
476
  if (step_duration_ns > 0 and index < step_count) {
@@ -279,18 +479,921 @@ fn executeDragCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![
279
479
  }
280
480
 
281
481
  postMouseButtonEvent(to, button_kind, false, 1) catch {
282
- return makeErrorJson(allocator, "failed to post drag mouse-up");
482
+ return failCommand("drag", "EVENT_POST_FAILED", "failed to post drag mouse-up");
283
483
  };
284
484
 
285
- return makeOkJson(allocator, "null");
485
+ return okCommand();
286
486
  }
287
487
 
288
- const MouseButtonKind = enum {
289
- left,
290
- right,
291
- middle,
488
+ pub fn displayList() DataResult([]const u8) {
489
+ if (builtin.target.os.tag != .macos) {
490
+ return failData([]const u8, "display-list", "UNSUPPORTED_PLATFORM", "display-list is only supported on macOS");
491
+ }
492
+
493
+ var display_ids: [16]c.CGDirectDisplayID = undefined;
494
+ var display_count: u32 = 0;
495
+ const list_result = c.CGGetActiveDisplayList(display_ids.len, &display_ids, &display_count);
496
+ if (list_result != c.kCGErrorSuccess) {
497
+ return failData([]const u8, "display-list", "DISPLAY_QUERY_FAILED", "failed to query active displays");
498
+ }
499
+
500
+ var write_buffer: [32 * 1024]u8 = undefined;
501
+ var stream = std.io.fixedBufferStream(&write_buffer);
502
+ const writer = stream.writer();
503
+
504
+ writer.writeByte('[') catch {
505
+ return failData([]const u8, "display-list", "SERIALIZE_FAILED", "failed to serialize display list");
506
+ };
507
+
508
+ var i: usize = 0;
509
+ while (i < display_count) : (i += 1) {
510
+ if (i > 0) {
511
+ writer.writeByte(',') catch {
512
+ return failData([]const u8, "display-list", "SERIALIZE_FAILED", "failed to serialize display list");
513
+ };
514
+ }
515
+
516
+ const display_id = display_ids[i];
517
+ const bounds = c.CGDisplayBounds(display_id);
518
+ var name_buffer: [64]u8 = undefined;
519
+ const fallback_name = std.fmt.bufPrint(&name_buffer, "Display {d}", .{display_id}) catch "Display";
520
+ const item = DisplayInfoOutput{
521
+ .id = display_id,
522
+ .index = @intCast(i),
523
+ .name = fallback_name,
524
+ .x = std.math.round(bounds.origin.x),
525
+ .y = std.math.round(bounds.origin.y),
526
+ .width = std.math.round(bounds.size.width),
527
+ .height = std.math.round(bounds.size.height),
528
+ .scale = 1,
529
+ .isPrimary = c.CGDisplayIsMain(display_id) != 0,
530
+ };
531
+
532
+ writer.print("{f}", .{std.json.fmt(item, .{})}) catch {
533
+ return failData([]const u8, "display-list", "SERIALIZE_FAILED", "failed to serialize display list");
534
+ };
535
+ }
536
+
537
+ writer.writeByte(']') catch {
538
+ return failData([]const u8, "display-list", "SERIALIZE_FAILED", "failed to serialize display list");
539
+ };
540
+
541
+ // TODO: Add Mission Control desktop/space enumeration via private SkyLight APIs.
542
+ const payload = std.heap.c_allocator.dupe(u8, stream.getWritten()) catch {
543
+ return failData([]const u8, "display-list", "ALLOC_FAILED", "failed to allocate display list response");
544
+ };
545
+ return okData([]const u8, payload);
546
+ }
547
+
548
+ pub fn windowList() DataResult([]const u8) {
549
+ if (builtin.target.os.tag != .macos) {
550
+ return failData([]const u8, "window-list", "UNSUPPORTED_PLATFORM", "window-list is only supported on macOS");
551
+ }
552
+
553
+ const payload = serializeWindowListJson() catch {
554
+ return failData([]const u8, "window-list", "WINDOW_QUERY_FAILED", "failed to query visible windows");
555
+ };
556
+ return okData([]const u8, payload);
557
+ }
558
+
559
+ pub fn clipboardGet() DataResult([]const u8) {
560
+ return failData([]const u8, "clipboard-get", "TODO_NOT_IMPLEMENTED", "TODO not implemented: clipboard-get");
561
+ }
562
+
563
+ pub fn clipboardSet(input: ClipboardSetInput) CommandResult {
564
+ _ = input;
565
+ return todoNotImplemented("clipboard-set");
566
+ }
567
+
568
+ pub fn typeText(input: TypeTextInput) CommandResult {
569
+ switch (builtin.target.os.tag) {
570
+ .macos => {
571
+ typeTextMacos(input) catch |err| {
572
+ return failCommand("type-text", "EVENT_POST_FAILED", @errorName(err));
573
+ };
574
+ return okCommand();
575
+ },
576
+ .windows => {
577
+ typeTextWindows(input) catch |err| {
578
+ return failCommand("type-text", "EVENT_POST_FAILED", @errorName(err));
579
+ };
580
+ return okCommand();
581
+ },
582
+ .linux => {
583
+ typeTextX11(input) catch |err| {
584
+ return failCommand("type-text", "EVENT_POST_FAILED", @errorName(err));
585
+ };
586
+ return okCommand();
587
+ },
588
+ else => {
589
+ return failCommand("type-text", "UNSUPPORTED_PLATFORM", "type-text is unsupported on this platform");
590
+ },
591
+ }
592
+ }
593
+
594
+ pub fn press(input: PressInput) CommandResult {
595
+ switch (builtin.target.os.tag) {
596
+ .macos => {
597
+ pressMacos(input) catch |err| {
598
+ return failCommand("press", "EVENT_POST_FAILED", @errorName(err));
599
+ };
600
+ return okCommand();
601
+ },
602
+ .windows => {
603
+ pressWindows(input) catch |err| {
604
+ return failCommand("press", "EVENT_POST_FAILED", @errorName(err));
605
+ };
606
+ return okCommand();
607
+ },
608
+ .linux => {
609
+ pressX11(input) catch |err| {
610
+ return failCommand("press", "EVENT_POST_FAILED", @errorName(err));
611
+ };
612
+ return okCommand();
613
+ },
614
+ else => {
615
+ return failCommand("press", "UNSUPPORTED_PLATFORM", "press is unsupported on this platform");
616
+ },
617
+ }
618
+ }
619
+
620
+ pub fn scroll(input: ScrollInput) CommandResult {
621
+ scroll_impl.scroll(.{
622
+ .direction = input.direction,
623
+ .amount = input.amount,
624
+ .at_x = if (input.at) |point| point.x else null,
625
+ .at_y = if (input.at) |point| point.y else null,
626
+ }) catch |err| {
627
+ const error_name = @errorName(err);
628
+ if (std.mem.eql(u8, error_name, "InvalidDirection") or
629
+ std.mem.eql(u8, error_name, "InvalidAmount") or
630
+ std.mem.eql(u8, error_name, "AmountTooLarge") or
631
+ std.mem.eql(u8, error_name, "InvalidPoint"))
632
+ {
633
+ return failCommand("scroll", "INVALID_INPUT", error_name);
634
+ }
635
+ return failCommand("scroll", "EVENT_POST_FAILED", error_name);
636
+ };
637
+ return okCommand();
638
+ }
639
+
640
+ const ParsedPress = struct {
641
+ key: []const u8,
642
+ cmd: bool = false,
643
+ alt: bool = false,
644
+ ctrl: bool = false,
645
+ shift: bool = false,
646
+ fn_key: bool = false,
292
647
  };
293
648
 
649
+ fn parsePressKey(key_input: []const u8) !ParsedPress {
650
+ var parsed: ParsedPress = .{ .key = "" };
651
+ var saw_key = false;
652
+ var parts = std.mem.splitScalar(u8, key_input, '+');
653
+ while (parts.next()) |part| {
654
+ const trimmed = std.mem.trim(u8, part, " \t\r\n");
655
+ if (trimmed.len == 0) {
656
+ continue;
657
+ }
658
+
659
+ if (std.ascii.eqlIgnoreCase(trimmed, "cmd") or std.ascii.eqlIgnoreCase(trimmed, "command") or std.ascii.eqlIgnoreCase(trimmed, "meta")) {
660
+ parsed.cmd = true;
661
+ continue;
662
+ }
663
+ if (std.ascii.eqlIgnoreCase(trimmed, "alt") or std.ascii.eqlIgnoreCase(trimmed, "option")) {
664
+ parsed.alt = true;
665
+ continue;
666
+ }
667
+ if (std.ascii.eqlIgnoreCase(trimmed, "ctrl") or std.ascii.eqlIgnoreCase(trimmed, "control")) {
668
+ parsed.ctrl = true;
669
+ continue;
670
+ }
671
+ if (std.ascii.eqlIgnoreCase(trimmed, "shift")) {
672
+ parsed.shift = true;
673
+ continue;
674
+ }
675
+ if (std.ascii.eqlIgnoreCase(trimmed, "fn")) {
676
+ parsed.fn_key = true;
677
+ continue;
678
+ }
679
+
680
+ if (saw_key) {
681
+ return error.MultipleMainKeys;
682
+ }
683
+ parsed.key = trimmed;
684
+ saw_key = true;
685
+ }
686
+
687
+ if (!saw_key) {
688
+ return error.MissingMainKey;
689
+ }
690
+ return parsed;
691
+ }
692
+
693
+ fn normalizedCount(value: ?f64) u32 {
694
+ if (value) |count| {
695
+ const rounded = @as(i64, @intFromFloat(std.math.round(count)));
696
+ if (rounded > 0) {
697
+ return @as(u32, @intCast(rounded));
698
+ }
699
+ }
700
+ return 1;
701
+ }
702
+
703
+ fn normalizedDelayNs(value: ?f64) u64 {
704
+ if (value) |delay_ms| {
705
+ const rounded = @as(i64, @intFromFloat(std.math.round(delay_ms)));
706
+ if (rounded > 0) {
707
+ return @as(u64, @intCast(rounded)) * std.time.ns_per_ms;
708
+ }
709
+ }
710
+ return 0;
711
+ }
712
+
713
+ fn codepointToUtf16(codepoint: u21) !struct { units: [2]u16, len: usize } {
714
+ if (codepoint <= 0xD7FF or (codepoint >= 0xE000 and codepoint <= 0xFFFF)) {
715
+ return .{ .units = .{ @as(u16, @intCast(codepoint)), 0 }, .len = 1 };
716
+ }
717
+ if (codepoint >= 0x10000 and codepoint <= 0x10FFFF) {
718
+ const value = codepoint - 0x10000;
719
+ const high = @as(u16, @intCast(0xD800 + (value >> 10)));
720
+ const low = @as(u16, @intCast(0xDC00 + (value & 0x3FF)));
721
+ return .{ .units = .{ high, low }, .len = 2 };
722
+ }
723
+ return error.InvalidCodepoint;
724
+ }
725
+
726
+ fn typeTextMacos(input: TypeTextInput) !void {
727
+ const delay_ns = normalizedDelayNs(input.delayMs);
728
+ var view = try std.unicode.Utf8View.init(input.text);
729
+ var iterator = view.iterator();
730
+ while (iterator.nextCodepoint()) |codepoint| {
731
+ const utf16 = try codepointToUtf16(codepoint);
732
+ const down = c_macos.CGEventCreateKeyboardEvent(null, 0, true) orelse return error.CGEventCreateFailed;
733
+ defer c_macos.CFRelease(down);
734
+ c_macos.CGEventSetFlags(down, 0);
735
+ c_macos.CGEventKeyboardSetUnicodeString(down, @as(c_macos.UniCharCount, @intCast(utf16.len)), @ptrCast(&utf16.units[0]));
736
+ c_macos.CGEventPost(c_macos.kCGHIDEventTap, down);
737
+
738
+ const up = c_macos.CGEventCreateKeyboardEvent(null, 0, false) orelse return error.CGEventCreateFailed;
739
+ defer c_macos.CFRelease(up);
740
+ c_macos.CGEventSetFlags(up, 0);
741
+ c_macos.CGEventKeyboardSetUnicodeString(up, @as(c_macos.UniCharCount, @intCast(utf16.len)), @ptrCast(&utf16.units[0]));
742
+ c_macos.CGEventPost(c_macos.kCGHIDEventTap, up);
743
+
744
+ if (delay_ns > 0) {
745
+ std.Thread.sleep(delay_ns);
746
+ }
747
+ }
748
+ }
749
+
750
+ fn keyCodeForMacosKey(key_name: []const u8) !c_macos.CGKeyCode {
751
+ if (key_name.len == 1) {
752
+ const ch = std.ascii.toLower(key_name[0]);
753
+ return switch (ch) {
754
+ 'a' => mac_keycode.a,
755
+ 'b' => mac_keycode.b,
756
+ 'c' => mac_keycode.c,
757
+ 'd' => mac_keycode.d,
758
+ 'e' => mac_keycode.e,
759
+ 'f' => mac_keycode.f,
760
+ 'g' => mac_keycode.g,
761
+ 'h' => mac_keycode.h,
762
+ 'i' => mac_keycode.i,
763
+ 'j' => mac_keycode.j,
764
+ 'k' => mac_keycode.k,
765
+ 'l' => mac_keycode.l,
766
+ 'm' => mac_keycode.m,
767
+ 'n' => mac_keycode.n,
768
+ 'o' => mac_keycode.o,
769
+ 'p' => mac_keycode.p,
770
+ 'q' => mac_keycode.q,
771
+ 'r' => mac_keycode.r,
772
+ 's' => mac_keycode.s,
773
+ 't' => mac_keycode.t,
774
+ 'u' => mac_keycode.u,
775
+ 'v' => mac_keycode.v,
776
+ 'w' => mac_keycode.w,
777
+ 'x' => mac_keycode.x,
778
+ 'y' => mac_keycode.y,
779
+ 'z' => mac_keycode.z,
780
+ '0' => mac_keycode.zero,
781
+ '1' => mac_keycode.one,
782
+ '2' => mac_keycode.two,
783
+ '3' => mac_keycode.three,
784
+ '4' => mac_keycode.four,
785
+ '5' => mac_keycode.five,
786
+ '6' => mac_keycode.six,
787
+ '7' => mac_keycode.seven,
788
+ '8' => mac_keycode.eight,
789
+ '9' => mac_keycode.nine,
790
+ '=' => mac_keycode.equal,
791
+ '-' => mac_keycode.minus,
792
+ '[' => mac_keycode.left_bracket,
793
+ ']' => mac_keycode.right_bracket,
794
+ ';' => mac_keycode.semicolon,
795
+ '\'' => mac_keycode.quote,
796
+ '\\' => mac_keycode.backslash,
797
+ ',' => mac_keycode.comma,
798
+ '.' => mac_keycode.period,
799
+ '/' => mac_keycode.slash,
800
+ '`' => mac_keycode.grave,
801
+ else => error.UnknownKey,
802
+ };
803
+ }
804
+
805
+ if (std.ascii.eqlIgnoreCase(key_name, "enter") or std.ascii.eqlIgnoreCase(key_name, "return")) return mac_keycode.enter;
806
+ if (std.ascii.eqlIgnoreCase(key_name, "tab")) return mac_keycode.tab;
807
+ if (std.ascii.eqlIgnoreCase(key_name, "space")) return mac_keycode.space;
808
+ if (std.ascii.eqlIgnoreCase(key_name, "escape") or std.ascii.eqlIgnoreCase(key_name, "esc")) return mac_keycode.escape;
809
+ if (std.ascii.eqlIgnoreCase(key_name, "backspace")) return mac_keycode.delete;
810
+ if (std.ascii.eqlIgnoreCase(key_name, "delete")) return mac_keycode.forward_delete;
811
+ if (std.ascii.eqlIgnoreCase(key_name, "left")) return mac_keycode.left_arrow;
812
+ if (std.ascii.eqlIgnoreCase(key_name, "right")) return mac_keycode.right_arrow;
813
+ if (std.ascii.eqlIgnoreCase(key_name, "up")) return mac_keycode.up_arrow;
814
+ if (std.ascii.eqlIgnoreCase(key_name, "down")) return mac_keycode.down_arrow;
815
+ if (std.ascii.eqlIgnoreCase(key_name, "home")) return mac_keycode.home;
816
+ if (std.ascii.eqlIgnoreCase(key_name, "end")) return mac_keycode.end;
817
+ if (std.ascii.eqlIgnoreCase(key_name, "pageup")) return mac_keycode.page_up;
818
+ if (std.ascii.eqlIgnoreCase(key_name, "pagedown")) return mac_keycode.page_down;
819
+ if (std.ascii.eqlIgnoreCase(key_name, "f1")) return mac_keycode.f1;
820
+ if (std.ascii.eqlIgnoreCase(key_name, "f2")) return mac_keycode.f2;
821
+ if (std.ascii.eqlIgnoreCase(key_name, "f3")) return mac_keycode.f3;
822
+ if (std.ascii.eqlIgnoreCase(key_name, "f4")) return mac_keycode.f4;
823
+ if (std.ascii.eqlIgnoreCase(key_name, "f5")) return mac_keycode.f5;
824
+ if (std.ascii.eqlIgnoreCase(key_name, "f6")) return mac_keycode.f6;
825
+ if (std.ascii.eqlIgnoreCase(key_name, "f7")) return mac_keycode.f7;
826
+ if (std.ascii.eqlIgnoreCase(key_name, "f8")) return mac_keycode.f8;
827
+ if (std.ascii.eqlIgnoreCase(key_name, "f9")) return mac_keycode.f9;
828
+ if (std.ascii.eqlIgnoreCase(key_name, "f10")) return mac_keycode.f10;
829
+ if (std.ascii.eqlIgnoreCase(key_name, "f11")) return mac_keycode.f11;
830
+ if (std.ascii.eqlIgnoreCase(key_name, "f12")) return mac_keycode.f12;
831
+
832
+ return error.UnknownKey;
833
+ }
834
+
835
+ fn postMacosKey(key_code: c_macos.CGKeyCode, is_down: bool, flags: c_macos.CGEventFlags) !void {
836
+ const event = c_macos.CGEventCreateKeyboardEvent(null, key_code, is_down) orelse return error.CGEventCreateFailed;
837
+ defer c_macos.CFRelease(event);
838
+ c_macos.CGEventSetFlags(event, flags);
839
+ c_macos.CGEventPost(c_macos.kCGHIDEventTap, event);
840
+ }
841
+
842
+ fn pressMacos(input: PressInput) !void {
843
+ const parsed = try parsePressKey(input.key);
844
+ const key_code = try keyCodeForMacosKey(parsed.key);
845
+ const repeat_count = normalizedCount(input.count);
846
+ const delay_ns = normalizedDelayNs(input.delayMs);
847
+
848
+ var flags: c_macos.CGEventFlags = 0;
849
+ if (parsed.cmd) flags |= c_macos.kCGEventFlagMaskCommand;
850
+ if (parsed.alt) flags |= c_macos.kCGEventFlagMaskAlternate;
851
+ if (parsed.ctrl) flags |= c_macos.kCGEventFlagMaskControl;
852
+ if (parsed.shift) flags |= c_macos.kCGEventFlagMaskShift;
853
+ if (parsed.fn_key) flags |= c_macos.kCGEventFlagMaskSecondaryFn;
854
+
855
+ var index: u32 = 0;
856
+ while (index < repeat_count) : (index += 1) {
857
+ if (parsed.cmd) try postMacosKey(mac_keycode.command, true, flags);
858
+ if (parsed.alt) try postMacosKey(mac_keycode.option, true, flags);
859
+ if (parsed.ctrl) try postMacosKey(mac_keycode.control, true, flags);
860
+ if (parsed.shift) try postMacosKey(mac_keycode.shift, true, flags);
861
+ if (parsed.fn_key) try postMacosKey(mac_keycode.fn_key, true, flags);
862
+
863
+ try postMacosKey(key_code, true, flags);
864
+ try postMacosKey(key_code, false, flags);
865
+
866
+ if (parsed.fn_key) try postMacosKey(mac_keycode.fn_key, false, flags);
867
+ if (parsed.shift) try postMacosKey(mac_keycode.shift, false, flags);
868
+ if (parsed.ctrl) try postMacosKey(mac_keycode.control, false, flags);
869
+ if (parsed.alt) try postMacosKey(mac_keycode.option, false, flags);
870
+ if (parsed.cmd) try postMacosKey(mac_keycode.command, false, flags);
871
+
872
+ if (delay_ns > 0 and index + 1 < repeat_count) {
873
+ std.Thread.sleep(delay_ns);
874
+ }
875
+ }
876
+ }
877
+
878
+ fn typeTextWindows(input: TypeTextInput) !void {
879
+ const delay_ns = normalizedDelayNs(input.delayMs);
880
+ var view = try std.unicode.Utf8View.init(input.text);
881
+ var iterator = view.iterator();
882
+ while (iterator.nextCodepoint()) |codepoint| {
883
+ const utf16 = try codepointToUtf16(codepoint);
884
+ var unit_index: usize = 0;
885
+ while (unit_index < utf16.len) : (unit_index += 1) {
886
+ const unit = utf16.units[unit_index];
887
+ var down = std.mem.zeroes(c_windows.INPUT);
888
+ down.type = c_windows.INPUT_KEYBOARD;
889
+ down.Anonymous.ki.wVk = 0;
890
+ down.Anonymous.ki.wScan = unit;
891
+ down.Anonymous.ki.dwFlags = c_windows.KEYEVENTF_UNICODE;
892
+ _ = c_windows.SendInput(1, &down, @sizeOf(c_windows.INPUT));
893
+
894
+ var up = down;
895
+ up.Anonymous.ki.dwFlags = c_windows.KEYEVENTF_UNICODE | c_windows.KEYEVENTF_KEYUP;
896
+ _ = c_windows.SendInput(1, &up, @sizeOf(c_windows.INPUT));
897
+ }
898
+
899
+ if (delay_ns > 0) {
900
+ std.Thread.sleep(delay_ns);
901
+ }
902
+ }
903
+ }
904
+
905
+ fn keyCodeForWindowsKey(key_name: []const u8) !u16 {
906
+ if (key_name.len == 1) {
907
+ const ch = std.ascii.toUpper(key_name[0]);
908
+ if ((ch >= 'A' and ch <= 'Z') or (ch >= '0' and ch <= '9')) {
909
+ return ch;
910
+ }
911
+ return switch (key_name[0]) {
912
+ '=' => c_windows.VK_OEM_PLUS,
913
+ '-' => c_windows.VK_OEM_MINUS,
914
+ '[' => c_windows.VK_OEM_4,
915
+ ']' => c_windows.VK_OEM_6,
916
+ ';' => c_windows.VK_OEM_1,
917
+ '\'' => c_windows.VK_OEM_7,
918
+ '\\' => c_windows.VK_OEM_5,
919
+ ',' => c_windows.VK_OEM_COMMA,
920
+ '.' => c_windows.VK_OEM_PERIOD,
921
+ '/' => c_windows.VK_OEM_2,
922
+ '`' => c_windows.VK_OEM_3,
923
+ else => error.UnknownKey,
924
+ };
925
+ }
926
+
927
+ if (std.ascii.eqlIgnoreCase(key_name, "enter") or std.ascii.eqlIgnoreCase(key_name, "return")) return c_windows.VK_RETURN;
928
+ if (std.ascii.eqlIgnoreCase(key_name, "tab")) return c_windows.VK_TAB;
929
+ if (std.ascii.eqlIgnoreCase(key_name, "space")) return c_windows.VK_SPACE;
930
+ if (std.ascii.eqlIgnoreCase(key_name, "escape") or std.ascii.eqlIgnoreCase(key_name, "esc")) return c_windows.VK_ESCAPE;
931
+ if (std.ascii.eqlIgnoreCase(key_name, "backspace")) return c_windows.VK_BACK;
932
+ if (std.ascii.eqlIgnoreCase(key_name, "delete")) return c_windows.VK_DELETE;
933
+ if (std.ascii.eqlIgnoreCase(key_name, "left")) return c_windows.VK_LEFT;
934
+ if (std.ascii.eqlIgnoreCase(key_name, "right")) return c_windows.VK_RIGHT;
935
+ if (std.ascii.eqlIgnoreCase(key_name, "up")) return c_windows.VK_UP;
936
+ if (std.ascii.eqlIgnoreCase(key_name, "down")) return c_windows.VK_DOWN;
937
+ if (std.ascii.eqlIgnoreCase(key_name, "home")) return c_windows.VK_HOME;
938
+ if (std.ascii.eqlIgnoreCase(key_name, "end")) return c_windows.VK_END;
939
+ if (std.ascii.eqlIgnoreCase(key_name, "pageup")) return c_windows.VK_PRIOR;
940
+ if (std.ascii.eqlIgnoreCase(key_name, "pagedown")) return c_windows.VK_NEXT;
941
+ if (std.ascii.eqlIgnoreCase(key_name, "f1")) return c_windows.VK_F1;
942
+ if (std.ascii.eqlIgnoreCase(key_name, "f2")) return c_windows.VK_F2;
943
+ if (std.ascii.eqlIgnoreCase(key_name, "f3")) return c_windows.VK_F3;
944
+ if (std.ascii.eqlIgnoreCase(key_name, "f4")) return c_windows.VK_F4;
945
+ if (std.ascii.eqlIgnoreCase(key_name, "f5")) return c_windows.VK_F5;
946
+ if (std.ascii.eqlIgnoreCase(key_name, "f6")) return c_windows.VK_F6;
947
+ if (std.ascii.eqlIgnoreCase(key_name, "f7")) return c_windows.VK_F7;
948
+ if (std.ascii.eqlIgnoreCase(key_name, "f8")) return c_windows.VK_F8;
949
+ if (std.ascii.eqlIgnoreCase(key_name, "f9")) return c_windows.VK_F9;
950
+ if (std.ascii.eqlIgnoreCase(key_name, "f10")) return c_windows.VK_F10;
951
+ if (std.ascii.eqlIgnoreCase(key_name, "f11")) return c_windows.VK_F11;
952
+ if (std.ascii.eqlIgnoreCase(key_name, "f12")) return c_windows.VK_F12;
953
+
954
+ return error.UnknownKey;
955
+ }
956
+
957
+ fn postWindowsVirtualKey(virtual_key: u16, is_down: bool) void {
958
+ var event = std.mem.zeroes(c_windows.INPUT);
959
+ event.type = c_windows.INPUT_KEYBOARD;
960
+ event.Anonymous.ki.wVk = virtual_key;
961
+ event.Anonymous.ki.wScan = 0;
962
+ event.Anonymous.ki.dwFlags = if (is_down) 0 else c_windows.KEYEVENTF_KEYUP;
963
+ _ = c_windows.SendInput(1, &event, @sizeOf(c_windows.INPUT));
964
+ }
965
+
966
+ fn pressWindows(input: PressInput) !void {
967
+ const parsed = try parsePressKey(input.key);
968
+ const key_code = try keyCodeForWindowsKey(parsed.key);
969
+ const repeat_count = normalizedCount(input.count);
970
+ const delay_ns = normalizedDelayNs(input.delayMs);
971
+
972
+ var index: u32 = 0;
973
+ while (index < repeat_count) : (index += 1) {
974
+ if (parsed.cmd) postWindowsVirtualKey(c_windows.VK_LWIN, true);
975
+ if (parsed.alt) postWindowsVirtualKey(c_windows.VK_MENU, true);
976
+ if (parsed.ctrl) postWindowsVirtualKey(c_windows.VK_CONTROL, true);
977
+ if (parsed.shift) postWindowsVirtualKey(c_windows.VK_SHIFT, true);
978
+
979
+ postWindowsVirtualKey(key_code, true);
980
+ postWindowsVirtualKey(key_code, false);
981
+
982
+ if (parsed.shift) postWindowsVirtualKey(c_windows.VK_SHIFT, false);
983
+ if (parsed.ctrl) postWindowsVirtualKey(c_windows.VK_CONTROL, false);
984
+ if (parsed.alt) postWindowsVirtualKey(c_windows.VK_MENU, false);
985
+ if (parsed.cmd) postWindowsVirtualKey(c_windows.VK_LWIN, false);
986
+
987
+ if (delay_ns > 0 and index + 1 < repeat_count) {
988
+ std.Thread.sleep(delay_ns);
989
+ }
990
+ }
991
+ }
992
+
993
+ fn typeTextX11(input: TypeTextInput) !void {
994
+ const delay_ns = normalizedDelayNs(input.delayMs);
995
+ const display = c_x11.XOpenDisplay(null) orelse return error.XOpenDisplayFailed;
996
+ defer _ = c_x11.XCloseDisplay(display);
997
+
998
+ for (input.text) |byte| {
999
+ if (byte >= 0x80) {
1000
+ return error.NonAsciiUnsupported;
1001
+ }
1002
+ var key_name = [_:0]u8{ byte, 0 };
1003
+ const key_sym = c_x11.XStringToKeysym(&key_name);
1004
+ if (key_sym == 0) {
1005
+ return error.UnknownKey;
1006
+ }
1007
+ const key_code = c_x11.XKeysymToKeycode(display, @intCast(key_sym));
1008
+ _ = c_x11.XTestFakeKeyEvent(display, key_code, c_x11.True, c_x11.CurrentTime);
1009
+ _ = c_x11.XTestFakeKeyEvent(display, key_code, c_x11.False, c_x11.CurrentTime);
1010
+ _ = c_x11.XFlush(display);
1011
+ if (delay_ns > 0) {
1012
+ std.Thread.sleep(delay_ns);
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ fn keySymForX11Key(key_name: []const u8) !c_ulong {
1018
+ if (key_name.len == 1) {
1019
+ var key_buffer = [_:0]u8{ key_name[0], 0 };
1020
+ const key_sym = c_x11.XStringToKeysym(&key_buffer);
1021
+ if (key_sym == 0) return error.UnknownKey;
1022
+ return @intCast(key_sym);
1023
+ }
1024
+
1025
+ if (std.ascii.eqlIgnoreCase(key_name, "enter") or std.ascii.eqlIgnoreCase(key_name, "return")) return c_x11.XK_Return;
1026
+ if (std.ascii.eqlIgnoreCase(key_name, "tab")) return c_x11.XK_Tab;
1027
+ if (std.ascii.eqlIgnoreCase(key_name, "space")) return c_x11.XK_space;
1028
+ if (std.ascii.eqlIgnoreCase(key_name, "escape") or std.ascii.eqlIgnoreCase(key_name, "esc")) return c_x11.XK_Escape;
1029
+ if (std.ascii.eqlIgnoreCase(key_name, "backspace")) return c_x11.XK_BackSpace;
1030
+ if (std.ascii.eqlIgnoreCase(key_name, "delete")) return c_x11.XK_Delete;
1031
+ if (std.ascii.eqlIgnoreCase(key_name, "left")) return c_x11.XK_Left;
1032
+ if (std.ascii.eqlIgnoreCase(key_name, "right")) return c_x11.XK_Right;
1033
+ if (std.ascii.eqlIgnoreCase(key_name, "up")) return c_x11.XK_Up;
1034
+ if (std.ascii.eqlIgnoreCase(key_name, "down")) return c_x11.XK_Down;
1035
+ if (std.ascii.eqlIgnoreCase(key_name, "home")) return c_x11.XK_Home;
1036
+ if (std.ascii.eqlIgnoreCase(key_name, "end")) return c_x11.XK_End;
1037
+ if (std.ascii.eqlIgnoreCase(key_name, "pageup")) return c_x11.XK_Page_Up;
1038
+ if (std.ascii.eqlIgnoreCase(key_name, "pagedown")) return c_x11.XK_Page_Down;
1039
+ return error.UnknownKey;
1040
+ }
1041
+
1042
+ fn postX11Key(display: *c_x11.Display, key_sym: c_ulong, is_down: bool) !void {
1043
+ const key_code = c_x11.XKeysymToKeycode(display, @intCast(key_sym));
1044
+ if (key_code == 0) {
1045
+ return error.UnknownKey;
1046
+ }
1047
+ _ = c_x11.XTestFakeKeyEvent(display, key_code, if (is_down) c_x11.True else c_x11.False, c_x11.CurrentTime);
1048
+ _ = c_x11.XFlush(display);
1049
+ }
1050
+
1051
+ fn pressX11(input: PressInput) !void {
1052
+ const parsed = try parsePressKey(input.key);
1053
+ const key_sym = try keySymForX11Key(parsed.key);
1054
+ const repeat_count = normalizedCount(input.count);
1055
+ const delay_ns = normalizedDelayNs(input.delayMs);
1056
+
1057
+ const display = c_x11.XOpenDisplay(null) orelse return error.XOpenDisplayFailed;
1058
+ defer _ = c_x11.XCloseDisplay(display);
1059
+
1060
+ var index: u32 = 0;
1061
+ while (index < repeat_count) : (index += 1) {
1062
+ if (parsed.cmd) try postX11Key(display, c_x11.XK_Super_L, true);
1063
+ if (parsed.alt) try postX11Key(display, c_x11.XK_Alt_L, true);
1064
+ if (parsed.ctrl) try postX11Key(display, c_x11.XK_Control_L, true);
1065
+ if (parsed.shift) try postX11Key(display, c_x11.XK_Shift_L, true);
1066
+
1067
+ try postX11Key(display, key_sym, true);
1068
+ try postX11Key(display, key_sym, false);
1069
+
1070
+ if (parsed.shift) try postX11Key(display, c_x11.XK_Shift_L, false);
1071
+ if (parsed.ctrl) try postX11Key(display, c_x11.XK_Control_L, false);
1072
+ if (parsed.alt) try postX11Key(display, c_x11.XK_Alt_L, false);
1073
+ if (parsed.cmd) try postX11Key(display, c_x11.XK_Super_L, false);
1074
+
1075
+ if (delay_ns > 0 and index + 1 < repeat_count) {
1076
+ std.Thread.sleep(delay_ns);
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ fn createScreenshotImage(input: struct {
1082
+ display_index: ?f64,
1083
+ window_id: ?f64,
1084
+ region: ?ScreenshotRegion,
1085
+ }) !ScreenshotCapture {
1086
+ if (input.window_id != null and input.region != null) {
1087
+ return error.InvalidScreenshotInput;
1088
+ }
1089
+
1090
+ if (input.window_id) |window_id| {
1091
+ const normalized_window_id = normalizeWindowId(window_id) catch {
1092
+ return error.InvalidWindowId;
1093
+ };
1094
+ const window_bounds = findWindowBoundsById(normalized_window_id) catch {
1095
+ return error.WindowNotFound;
1096
+ };
1097
+ const selected_display = resolveDisplayForRect(window_bounds) catch {
1098
+ return error.DisplayResolutionFailed;
1099
+ };
1100
+
1101
+ const window_image = c.CGDisplayCreateImageForRect(selected_display.id, window_bounds);
1102
+ if (window_image == null) {
1103
+ return error.CaptureFailed;
1104
+ }
1105
+ return .{
1106
+ .image = window_image,
1107
+ .capture_x = window_bounds.origin.x,
1108
+ .capture_y = window_bounds.origin.y,
1109
+ .capture_width = window_bounds.size.width,
1110
+ .capture_height = window_bounds.size.height,
1111
+ .desktop_index = selected_display.index,
1112
+ };
1113
+ }
1114
+
1115
+ const selected_display = resolveDisplayId(input.display_index) catch {
1116
+ return error.DisplayResolutionFailed;
1117
+ };
1118
+
1119
+ if (input.region) |region| {
1120
+ const rect: c.CGRect = .{
1121
+ .origin = .{
1122
+ .x = selected_display.bounds.origin.x + region.x,
1123
+ .y = selected_display.bounds.origin.y + region.y,
1124
+ },
1125
+ .size = .{ .width = region.width, .height = region.height },
1126
+ };
1127
+ const region_image = c.CGDisplayCreateImageForRect(selected_display.id, rect);
1128
+ if (region_image == null) {
1129
+ return error.CaptureFailed;
1130
+ }
1131
+ return .{
1132
+ .image = region_image,
1133
+ .capture_x = rect.origin.x,
1134
+ .capture_y = rect.origin.y,
1135
+ .capture_width = rect.size.width,
1136
+ .capture_height = rect.size.height,
1137
+ .desktop_index = selected_display.index,
1138
+ };
1139
+ }
1140
+
1141
+ const full_image = c.CGDisplayCreateImage(selected_display.id);
1142
+ if (full_image == null) {
1143
+ return error.CaptureFailed;
1144
+ }
1145
+ return .{
1146
+ .image = full_image,
1147
+ .capture_x = selected_display.bounds.origin.x,
1148
+ .capture_y = selected_display.bounds.origin.y,
1149
+ .capture_width = selected_display.bounds.size.width,
1150
+ .capture_height = selected_display.bounds.size.height,
1151
+ .desktop_index = selected_display.index,
1152
+ };
1153
+ }
1154
+
1155
+ fn normalizeWindowId(raw_id: f64) !u32 {
1156
+ const normalized = @as(i64, @intFromFloat(std.math.round(raw_id)));
1157
+ if (normalized <= 0) {
1158
+ return error.InvalidWindowId;
1159
+ }
1160
+ return @intCast(normalized);
1161
+ }
1162
+
1163
+ fn findWindowBoundsById(target_window_id: u32) !c.CGRect {
1164
+ const Context = struct {
1165
+ target_id: u32,
1166
+ bounds: ?c.CGRect = null,
1167
+ };
1168
+
1169
+ var context = Context{ .target_id = target_window_id };
1170
+ window.forEachVisibleWindow(Context, &context, struct {
1171
+ fn callback(ctx: *Context, info: window.WindowInfo) !void {
1172
+ if (info.id != ctx.target_id) {
1173
+ return;
1174
+ }
1175
+ ctx.bounds = .{
1176
+ .origin = .{ .x = info.bounds.x, .y = info.bounds.y },
1177
+ .size = .{ .width = info.bounds.width, .height = info.bounds.height },
1178
+ };
1179
+ return error.Found;
1180
+ }
1181
+ }.callback) catch |err| {
1182
+ if (err != error.Found) {
1183
+ return err;
1184
+ }
1185
+ };
1186
+
1187
+ if (context.bounds) |bounds| {
1188
+ return bounds;
1189
+ }
1190
+ return error.WindowNotFound;
1191
+ }
1192
+
1193
+ fn resolveDisplayForRect(rect: c.CGRect) !SelectedDisplay {
1194
+ var display_ids: [16]c.CGDirectDisplayID = undefined;
1195
+ var display_count: u32 = 0;
1196
+ const list_result = c.CGGetActiveDisplayList(display_ids.len, &display_ids, &display_count);
1197
+ if (list_result != c.kCGErrorSuccess or display_count == 0) {
1198
+ return error.DisplayQueryFailed;
1199
+ }
1200
+
1201
+ var best_index: usize = 0;
1202
+ var best_overlap: f64 = -1;
1203
+ var i: usize = 0;
1204
+ while (i < display_count) : (i += 1) {
1205
+ const bounds = c.CGDisplayBounds(display_ids[i]);
1206
+ const overlap = intersectionArea(rect, bounds);
1207
+ if (overlap > best_overlap) {
1208
+ best_overlap = overlap;
1209
+ best_index = i;
1210
+ }
1211
+ }
1212
+
1213
+ const id = display_ids[best_index];
1214
+ return .{
1215
+ .id = id,
1216
+ .index = best_index,
1217
+ .bounds = c.CGDisplayBounds(id),
1218
+ };
1219
+ }
1220
+
1221
+ fn intersectionArea(a: c.CGRect, b: c.CGRect) f64 {
1222
+ const left = @max(a.origin.x, b.origin.x);
1223
+ const top = @max(a.origin.y, b.origin.y);
1224
+ const right = @min(a.origin.x + a.size.width, b.origin.x + b.size.width);
1225
+ const bottom = @min(a.origin.y + a.size.height, b.origin.y + b.size.height);
1226
+ if (right <= left or bottom <= top) {
1227
+ return 0;
1228
+ }
1229
+ return (right - left) * (bottom - top);
1230
+ }
1231
+
1232
+ fn serializeWindowListJson() ![]u8 {
1233
+ const Context = struct {
1234
+ stream: *std.io.FixedBufferStream([]u8),
1235
+ first: bool,
1236
+ };
1237
+
1238
+ var write_buffer: [64 * 1024]u8 = undefined;
1239
+ var stream = std.io.fixedBufferStream(&write_buffer);
1240
+
1241
+ try stream.writer().writeByte('[');
1242
+ var context = Context{ .stream = &stream, .first = true };
1243
+
1244
+ try window.forEachVisibleWindow(Context, &context, struct {
1245
+ fn callback(ctx: *Context, info: window.WindowInfo) !void {
1246
+ const rect: c.CGRect = .{
1247
+ .origin = .{ .x = info.bounds.x, .y = info.bounds.y },
1248
+ .size = .{ .width = info.bounds.width, .height = info.bounds.height },
1249
+ };
1250
+ const selected_display = resolveDisplayForRect(rect) catch {
1251
+ return;
1252
+ };
1253
+ const item = WindowInfoOutput{
1254
+ .id = info.id,
1255
+ .ownerPid = info.owner_pid,
1256
+ .ownerName = info.owner_name,
1257
+ .title = info.title,
1258
+ .x = info.bounds.x,
1259
+ .y = info.bounds.y,
1260
+ .width = info.bounds.width,
1261
+ .height = info.bounds.height,
1262
+ .desktopIndex = @intCast(selected_display.index),
1263
+ };
1264
+
1265
+ if (!ctx.first) {
1266
+ try ctx.stream.writer().writeByte(',');
1267
+ }
1268
+ ctx.first = false;
1269
+ try ctx.stream.writer().print("{f}", .{std.json.fmt(item, .{})});
1270
+ }
1271
+ }.callback);
1272
+
1273
+ try stream.writer().writeByte(']');
1274
+ return std.heap.c_allocator.dupe(u8, stream.getWritten());
1275
+ }
1276
+
1277
+ fn scaleScreenshotImageIfNeeded(image: c.CGImageRef) !ScaledScreenshotImage {
1278
+ const image_width = @as(f64, @floatFromInt(c.CGImageGetWidth(image)));
1279
+ const image_height = @as(f64, @floatFromInt(c.CGImageGetHeight(image)));
1280
+ const long_edge = @max(image_width, image_height);
1281
+ if (long_edge <= screenshot_max_long_edge_px) {
1282
+ _ = c.CFRetain(image);
1283
+ return .{
1284
+ .image = image,
1285
+ .width = image_width,
1286
+ .height = image_height,
1287
+ };
1288
+ }
1289
+
1290
+ const scale = screenshot_max_long_edge_px / long_edge;
1291
+ const target_width = @max(1, @as(usize, @intFromFloat(std.math.round(image_width * scale))));
1292
+ const target_height = @max(1, @as(usize, @intFromFloat(std.math.round(image_height * scale))));
1293
+
1294
+ const color_space = c.CGColorSpaceCreateDeviceRGB();
1295
+ if (color_space == null) {
1296
+ return error.ScaleFailed;
1297
+ }
1298
+ defer c.CFRelease(color_space);
1299
+
1300
+ const bitmap_info: c.CGBitmapInfo = c.kCGImageAlphaPremultipliedLast;
1301
+ const context = c.CGBitmapContextCreate(
1302
+ null,
1303
+ target_width,
1304
+ target_height,
1305
+ 8,
1306
+ 0,
1307
+ color_space,
1308
+ bitmap_info,
1309
+ );
1310
+ if (context == null) {
1311
+ return error.ScaleFailed;
1312
+ }
1313
+ defer c.CFRelease(context);
1314
+
1315
+ c.CGContextSetInterpolationQuality(context, c.kCGInterpolationHigh);
1316
+ const draw_rect: c.CGRect = .{
1317
+ .origin = .{ .x = 0, .y = 0 },
1318
+ .size = .{
1319
+ .width = @as(c.CGFloat, @floatFromInt(target_width)),
1320
+ .height = @as(c.CGFloat, @floatFromInt(target_height)),
1321
+ },
1322
+ };
1323
+ c.CGContextDrawImage(context, draw_rect, image);
1324
+
1325
+ const scaled = c.CGBitmapContextCreateImage(context);
1326
+ if (scaled == null) {
1327
+ return error.ScaleFailed;
1328
+ }
1329
+ return .{
1330
+ .image = scaled,
1331
+ .width = @as(f64, @floatFromInt(target_width)),
1332
+ .height = @as(f64, @floatFromInt(target_height)),
1333
+ };
1334
+ }
1335
+
1336
+ fn resolveDisplayId(display_index: ?f64) !SelectedDisplay {
1337
+ const selected_index: usize = if (display_index) |value| blk: {
1338
+ const normalized = @as(i64, @intFromFloat(std.math.round(value)));
1339
+ if (normalized < 0) {
1340
+ return error.InvalidDisplayIndex;
1341
+ }
1342
+ break :blk @as(usize, @intCast(normalized));
1343
+ } else 0;
1344
+ var display_ids: [16]c.CGDirectDisplayID = undefined;
1345
+ var display_count: u32 = 0;
1346
+ const list_result = c.CGGetActiveDisplayList(display_ids.len, &display_ids, &display_count);
1347
+ if (list_result != c.kCGErrorSuccess) {
1348
+ return error.DisplayQueryFailed;
1349
+ }
1350
+ if (selected_index >= display_count) {
1351
+ return error.InvalidDisplayIndex;
1352
+ }
1353
+ const selected_id = display_ids[selected_index];
1354
+ const bounds = c.CGDisplayBounds(selected_id);
1355
+ return .{
1356
+ .id = selected_id,
1357
+ .index = selected_index,
1358
+ .bounds = bounds,
1359
+ };
1360
+ }
1361
+
1362
+ fn writeScreenshotPng(input: struct {
1363
+ image: c.CGImageRef,
1364
+ output_path: []const u8,
1365
+ }) !void {
1366
+ const path_as_u8: [*]const u8 = @ptrCast(input.output_path.ptr);
1367
+ const file_url = c.CFURLCreateFromFileSystemRepresentation(
1368
+ null,
1369
+ path_as_u8,
1370
+ @as(c_long, @intCast(input.output_path.len)),
1371
+ 0,
1372
+ );
1373
+ if (file_url == null) {
1374
+ return error.FileUrlCreateFailed;
1375
+ }
1376
+ defer c.CFRelease(file_url);
1377
+
1378
+ const png_type = c.CFStringCreateWithCString(null, "public.png", c.kCFStringEncodingUTF8);
1379
+ if (png_type == null) {
1380
+ return error.PngTypeCreateFailed;
1381
+ }
1382
+ defer c.CFRelease(png_type);
1383
+
1384
+ const destination = c.CGImageDestinationCreateWithURL(file_url, png_type, 1, null);
1385
+ if (destination == null) {
1386
+ return error.ImageDestinationCreateFailed;
1387
+ }
1388
+ defer c.CFRelease(destination);
1389
+
1390
+ c.CGImageDestinationAddImage(destination, input.image, null);
1391
+ const did_finalize = c.CGImageDestinationFinalize(destination);
1392
+ if (!did_finalize) {
1393
+ return error.ImageDestinationFinalizeFailed;
1394
+ }
1395
+ }
1396
+
294
1397
  fn resolveMouseButton(button: []const u8) !MouseButtonKind {
295
1398
  if (std.ascii.eqlIgnoreCase(button, "left")) {
296
1399
  return .left;
@@ -356,7 +1459,21 @@ fn moveCursorToPoint(point: c.CGPoint) !void {
356
1459
  }
357
1460
 
358
1461
  fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) !napigen.napi_value {
359
- try js.setNamedProperty(exports, "execute", try js.createFunction(execute));
1462
+ try js.setNamedProperty(exports, "screenshot", try js.createFunction(screenshot));
1463
+ try js.setNamedProperty(exports, "click", try js.createFunction(click));
1464
+ try js.setNamedProperty(exports, "typeText", try js.createFunction(typeText));
1465
+ try js.setNamedProperty(exports, "press", try js.createFunction(press));
1466
+ try js.setNamedProperty(exports, "scroll", try js.createFunction(scroll));
1467
+ try js.setNamedProperty(exports, "drag", try js.createFunction(drag));
1468
+ try js.setNamedProperty(exports, "hover", try js.createFunction(hover));
1469
+ try js.setNamedProperty(exports, "mouseMove", try js.createFunction(mouseMove));
1470
+ try js.setNamedProperty(exports, "mouseDown", try js.createFunction(mouseDown));
1471
+ try js.setNamedProperty(exports, "mouseUp", try js.createFunction(mouseUp));
1472
+ try js.setNamedProperty(exports, "mousePosition", try js.createFunction(mousePosition));
1473
+ try js.setNamedProperty(exports, "displayList", try js.createFunction(displayList));
1474
+ try js.setNamedProperty(exports, "windowList", try js.createFunction(windowList));
1475
+ try js.setNamedProperty(exports, "clipboardGet", try js.createFunction(clipboardGet));
1476
+ try js.setNamedProperty(exports, "clipboardSet", try js.createFunction(clipboardSet));
360
1477
  return exports;
361
1478
  }
362
1479