usecomputer 0.0.1 → 0.0.3
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/CHANGELOG.md +31 -0
- package/README.md +51 -0
- package/bin.js +3 -1
- package/build.zig +53 -0
- package/build.zig.zon +21 -0
- package/dist/bridge-contract.test.d.ts +2 -0
- package/dist/bridge-contract.test.d.ts.map +1 -0
- package/dist/bridge-contract.test.js +74 -0
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +130 -0
- package/dist/cli-parsing.test.d.ts +2 -0
- package/dist/cli-parsing.test.d.ts.map +1 -0
- package/dist/cli-parsing.test.js +30 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +297 -335
- package/dist/command-parsers.d.ts +6 -0
- package/dist/command-parsers.d.ts.map +1 -0
- package/dist/command-parsers.js +54 -0
- package/dist/command-parsers.test.d.ts +2 -0
- package/dist/command-parsers.test.d.ts.map +1 -0
- package/dist/command-parsers.test.js +44 -0
- package/dist/darwin-arm64/usecomputer.node +0 -0
- package/dist/darwin-x64/usecomputer.node +0 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/native-click-smoke.test.d.ts +2 -0
- package/dist/native-click-smoke.test.d.ts.map +1 -0
- package/dist/native-click-smoke.test.js +93 -0
- package/dist/native-lib.cjs +33 -0
- package/dist/native-lib.d.cts +7 -0
- package/dist/native-lib.d.ts +5 -0
- package/dist/native-lib.d.ts.map +1 -0
- package/dist/native-lib.js +27 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +23 -12
- package/src/bridge-contract.test.ts +85 -0
- package/src/bridge.ts +159 -0
- package/src/cli.ts +344 -473
- package/src/command-parsers.test.ts +50 -0
- package/src/command-parsers.ts +60 -0
- package/src/index.ts +5 -4
- package/src/native-click-smoke.test.ts +131 -0
- package/src/native-lib.ts +38 -0
- package/src/types.ts +87 -0
- package/zig/src/lib.zig +493 -0
package/zig/src/lib.zig
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
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
|
+
@cInclude("ImageIO/ImageIO.h");
|
|
12
|
+
}) else struct {};
|
|
13
|
+
|
|
14
|
+
pub const std_options: std.Options = .{
|
|
15
|
+
.log_level = .err,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
fn makeOkJson(allocator: std.mem.Allocator, data_json: []const u8) ![]const u8 {
|
|
19
|
+
return std.fmt.allocPrint(allocator, "{{\"ok\":true,\"data\":{s}}}", .{data_json});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn makeErrorJson(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
|
|
23
|
+
return std.fmt.allocPrint(allocator, "{{\"ok\":false,\"error\":\"{s}\"}}", .{message});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn execute(command: []const u8, payload_json: []const u8) ![]const u8 {
|
|
27
|
+
const allocator = std.heap.c_allocator;
|
|
28
|
+
|
|
29
|
+
if (std.mem.eql(u8, command, "click")) {
|
|
30
|
+
return executeClickCommand(allocator, payload_json);
|
|
31
|
+
}
|
|
32
|
+
if (std.mem.eql(u8, command, "mouse-move")) {
|
|
33
|
+
return executeMouseMoveCommand(allocator, payload_json);
|
|
34
|
+
}
|
|
35
|
+
if (std.mem.eql(u8, command, "mouse-down")) {
|
|
36
|
+
return executeMouseDownCommand(allocator, payload_json);
|
|
37
|
+
}
|
|
38
|
+
if (std.mem.eql(u8, command, "mouse-up")) {
|
|
39
|
+
return executeMouseUpCommand(allocator, payload_json);
|
|
40
|
+
}
|
|
41
|
+
if (std.mem.eql(u8, command, "mouse-position")) {
|
|
42
|
+
return executeMousePositionCommand(allocator);
|
|
43
|
+
}
|
|
44
|
+
if (std.mem.eql(u8, command, "hover")) {
|
|
45
|
+
return executeHoverCommand(allocator, payload_json);
|
|
46
|
+
}
|
|
47
|
+
if (std.mem.eql(u8, command, "drag")) {
|
|
48
|
+
return executeDragCommand(allocator, payload_json);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (std.mem.eql(u8, command, "display-list")) {
|
|
52
|
+
return makeOkJson(allocator, "[]");
|
|
53
|
+
}
|
|
54
|
+
if (std.mem.eql(u8, command, "clipboard-get")) {
|
|
55
|
+
return makeOkJson(allocator, "{\"text\":\"\"}");
|
|
56
|
+
}
|
|
57
|
+
if (std.mem.eql(u8, command, "screenshot")) {
|
|
58
|
+
return executeScreenshotCommand(allocator, payload_json);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
std.mem.eql(u8, command, "type-text") or
|
|
63
|
+
std.mem.eql(u8, command, "press") or
|
|
64
|
+
std.mem.eql(u8, command, "scroll") or
|
|
65
|
+
std.mem.eql(u8, command, "clipboard-set")
|
|
66
|
+
) {
|
|
67
|
+
const message = try std.fmt.allocPrint(allocator, "TODO not implemented: {s}", .{command});
|
|
68
|
+
return makeErrorJson(allocator, message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return makeErrorJson(allocator, "unknown command");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ClickPoint = struct {
|
|
75
|
+
x: f64,
|
|
76
|
+
y: f64,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ClickPayload = struct {
|
|
80
|
+
point: ClickPoint,
|
|
81
|
+
button: ?[]const u8 = null,
|
|
82
|
+
count: ?u32 = null,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
fn executeClickCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
86
|
+
if (builtin.target.os.tag != .macos) {
|
|
87
|
+
return makeErrorJson(allocator, "click is only supported on macOS");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var parsed = std.json.parseFromSlice(ClickPayload, allocator, payload_json, .{
|
|
91
|
+
.ignore_unknown_fields = true,
|
|
92
|
+
}) catch {
|
|
93
|
+
return makeErrorJson(allocator, "invalid click payload json");
|
|
94
|
+
};
|
|
95
|
+
defer parsed.deinit();
|
|
96
|
+
|
|
97
|
+
const click_payload = parsed.value;
|
|
98
|
+
const click_count: u32 = if (click_payload.count) |count| blk: {
|
|
99
|
+
if (count == 0) {
|
|
100
|
+
break :blk 1;
|
|
101
|
+
}
|
|
102
|
+
break :blk count;
|
|
103
|
+
} else 1;
|
|
104
|
+
|
|
105
|
+
const button_kind = resolveMouseButton(click_payload.button orelse "left") catch {
|
|
106
|
+
return makeErrorJson(allocator, "invalid click button");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const point: c.CGPoint = .{
|
|
110
|
+
.x = click_payload.point.x,
|
|
111
|
+
.y = click_payload.point.y,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
var index: u32 = 0;
|
|
115
|
+
while (index < click_count) : (index += 1) {
|
|
116
|
+
const click_state = @as(i64, @intCast(index + 1));
|
|
117
|
+
postClickPair(point, button_kind, click_state) catch {
|
|
118
|
+
return makeErrorJson(allocator, "failed to post click event");
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (index + 1 < click_count) {
|
|
122
|
+
std.Thread.sleep(80 * std.time.ns_per_ms);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return makeOkJson(allocator, "null");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const MouseMovePayload = struct {
|
|
130
|
+
x: f64,
|
|
131
|
+
y: f64,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const MouseButtonPayload = struct {
|
|
135
|
+
button: ?[]const u8 = null,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const DragPayload = struct {
|
|
139
|
+
from: ClickPoint,
|
|
140
|
+
to: ClickPoint,
|
|
141
|
+
durationMs: ?u64 = null,
|
|
142
|
+
button: ?[]const u8 = null,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ScreenshotRegion = struct {
|
|
146
|
+
x: f64,
|
|
147
|
+
y: f64,
|
|
148
|
+
width: f64,
|
|
149
|
+
height: f64,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const ScreenshotPayload = struct {
|
|
153
|
+
path: ?[]const u8 = null,
|
|
154
|
+
display: ?usize = null,
|
|
155
|
+
region: ?ScreenshotRegion = null,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
fn executeScreenshotCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
159
|
+
if (builtin.target.os.tag != .macos) {
|
|
160
|
+
return makeErrorJson(allocator, "screenshot is only supported on macOS");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var parsed = std.json.parseFromSlice(ScreenshotPayload, allocator, payload_json, .{
|
|
164
|
+
.ignore_unknown_fields = true,
|
|
165
|
+
}) catch {
|
|
166
|
+
return makeErrorJson(allocator, "invalid screenshot payload json");
|
|
167
|
+
};
|
|
168
|
+
defer parsed.deinit();
|
|
169
|
+
|
|
170
|
+
const screenshot_payload = parsed.value;
|
|
171
|
+
const output_path = screenshot_payload.path orelse "./screenshot.png";
|
|
172
|
+
|
|
173
|
+
const image = createScreenshotImage(.{
|
|
174
|
+
.display_index = screenshot_payload.display,
|
|
175
|
+
.region = screenshot_payload.region,
|
|
176
|
+
}) catch {
|
|
177
|
+
return makeErrorJson(allocator, "failed to capture screenshot image");
|
|
178
|
+
};
|
|
179
|
+
defer c.CFRelease(image);
|
|
180
|
+
|
|
181
|
+
writeScreenshotPng(.{
|
|
182
|
+
.image = image,
|
|
183
|
+
.output_path = output_path,
|
|
184
|
+
}) catch {
|
|
185
|
+
return makeErrorJson(allocator, "failed to write screenshot file");
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const path_json = try std.fmt.allocPrint(allocator, "\"{s}\"", .{output_path});
|
|
189
|
+
const payload_json_response = try std.fmt.allocPrint(allocator, "{{\"path\":{s}}}", .{path_json});
|
|
190
|
+
return makeOkJson(allocator, payload_json_response);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn createScreenshotImage(input: struct {
|
|
194
|
+
display_index: ?usize,
|
|
195
|
+
region: ?ScreenshotRegion,
|
|
196
|
+
}) !c.CGImageRef {
|
|
197
|
+
const display_id = resolveDisplayId(input.display_index) catch {
|
|
198
|
+
return error.DisplayResolutionFailed;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (input.region) |region| {
|
|
202
|
+
const rect: c.CGRect = .{
|
|
203
|
+
.origin = .{ .x = region.x, .y = region.y },
|
|
204
|
+
.size = .{ .width = region.width, .height = region.height },
|
|
205
|
+
};
|
|
206
|
+
const region_image = c.CGDisplayCreateImageForRect(display_id, rect);
|
|
207
|
+
if (region_image == null) {
|
|
208
|
+
return error.CaptureFailed;
|
|
209
|
+
}
|
|
210
|
+
return region_image;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const full_image = c.CGDisplayCreateImage(display_id);
|
|
214
|
+
if (full_image == null) {
|
|
215
|
+
return error.CaptureFailed;
|
|
216
|
+
}
|
|
217
|
+
return full_image;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn resolveDisplayId(display_index: ?usize) !c.CGDirectDisplayID {
|
|
221
|
+
const selected_index = display_index orelse 0;
|
|
222
|
+
var display_ids: [16]c.CGDirectDisplayID = undefined;
|
|
223
|
+
var display_count: u32 = 0;
|
|
224
|
+
const list_result = c.CGGetActiveDisplayList(display_ids.len, &display_ids, &display_count);
|
|
225
|
+
if (list_result != c.kCGErrorSuccess) {
|
|
226
|
+
return error.DisplayQueryFailed;
|
|
227
|
+
}
|
|
228
|
+
if (selected_index >= display_count) {
|
|
229
|
+
return error.InvalidDisplayIndex;
|
|
230
|
+
}
|
|
231
|
+
return display_ids[selected_index];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn writeScreenshotPng(input: struct {
|
|
235
|
+
image: c.CGImageRef,
|
|
236
|
+
output_path: []const u8,
|
|
237
|
+
}) !void {
|
|
238
|
+
const path_as_u8: [*]const u8 = @ptrCast(input.output_path.ptr);
|
|
239
|
+
const file_url = c.CFURLCreateFromFileSystemRepresentation(
|
|
240
|
+
null,
|
|
241
|
+
path_as_u8,
|
|
242
|
+
@as(c_long, @intCast(input.output_path.len)),
|
|
243
|
+
0,
|
|
244
|
+
);
|
|
245
|
+
if (file_url == null) {
|
|
246
|
+
return error.FileUrlCreateFailed;
|
|
247
|
+
}
|
|
248
|
+
defer c.CFRelease(file_url);
|
|
249
|
+
|
|
250
|
+
const png_type = c.CFStringCreateWithCString(null, "public.png", c.kCFStringEncodingUTF8);
|
|
251
|
+
if (png_type == null) {
|
|
252
|
+
return error.PngTypeCreateFailed;
|
|
253
|
+
}
|
|
254
|
+
defer c.CFRelease(png_type);
|
|
255
|
+
|
|
256
|
+
const destination = c.CGImageDestinationCreateWithURL(file_url, png_type, 1, null);
|
|
257
|
+
if (destination == null) {
|
|
258
|
+
return error.ImageDestinationCreateFailed;
|
|
259
|
+
}
|
|
260
|
+
defer c.CFRelease(destination);
|
|
261
|
+
|
|
262
|
+
c.CGImageDestinationAddImage(destination, input.image, null);
|
|
263
|
+
const did_finalize = c.CGImageDestinationFinalize(destination);
|
|
264
|
+
if (!did_finalize) {
|
|
265
|
+
return error.ImageDestinationFinalizeFailed;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn executeMouseMoveCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
270
|
+
if (builtin.target.os.tag != .macos) {
|
|
271
|
+
return makeErrorJson(allocator, "mouse-move is only supported on macOS");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var parsed = std.json.parseFromSlice(MouseMovePayload, allocator, payload_json, .{}) catch {
|
|
275
|
+
return makeErrorJson(allocator, "invalid mouse-move payload json");
|
|
276
|
+
};
|
|
277
|
+
defer parsed.deinit();
|
|
278
|
+
|
|
279
|
+
const point: c.CGPoint = .{
|
|
280
|
+
.x = parsed.value.x,
|
|
281
|
+
.y = parsed.value.y,
|
|
282
|
+
};
|
|
283
|
+
moveCursorToPoint(point) catch {
|
|
284
|
+
return makeErrorJson(allocator, "failed to move mouse cursor");
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return makeOkJson(allocator, "null");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn executeMouseDownCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
291
|
+
return executeMouseButtonCommand(allocator, payload_json, true);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn executeMouseUpCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
295
|
+
return executeMouseButtonCommand(allocator, payload_json, false);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn executeMouseButtonCommand(allocator: std.mem.Allocator, payload_json: []const u8, is_down: bool) ![]const u8 {
|
|
299
|
+
if (builtin.target.os.tag != .macos) {
|
|
300
|
+
return makeErrorJson(allocator, "mouse button events are only supported on macOS");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
var parsed = std.json.parseFromSlice(MouseButtonPayload, allocator, payload_json, .{}) catch {
|
|
304
|
+
return makeErrorJson(allocator, "invalid mouse button payload json");
|
|
305
|
+
};
|
|
306
|
+
defer parsed.deinit();
|
|
307
|
+
|
|
308
|
+
const button_kind = resolveMouseButton(parsed.value.button orelse "left") catch {
|
|
309
|
+
return makeErrorJson(allocator, "invalid mouse button");
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const point = currentCursorPoint() catch {
|
|
313
|
+
return makeErrorJson(allocator, "failed to read cursor position");
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
postMouseButtonEvent(point, button_kind, is_down, 1) catch {
|
|
317
|
+
return makeErrorJson(allocator, "failed to post mouse button event");
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return makeOkJson(allocator, "null");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn executeMousePositionCommand(allocator: std.mem.Allocator) ![]const u8 {
|
|
324
|
+
if (builtin.target.os.tag != .macos) {
|
|
325
|
+
return makeErrorJson(allocator, "mouse-position is only supported on macOS");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const point = currentCursorPoint() catch {
|
|
329
|
+
return makeErrorJson(allocator, "failed to read cursor position");
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const x = @as(i64, @intFromFloat(std.math.round(point.x)));
|
|
333
|
+
const y = @as(i64, @intFromFloat(std.math.round(point.y)));
|
|
334
|
+
const point_json = try std.fmt.allocPrint(allocator, "{{\"x\":{d},\"y\":{d}}}", .{ x, y });
|
|
335
|
+
return makeOkJson(allocator, point_json);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn executeHoverCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
339
|
+
if (builtin.target.os.tag != .macos) {
|
|
340
|
+
return makeErrorJson(allocator, "hover is only supported on macOS");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var parsed = std.json.parseFromSlice(MouseMovePayload, allocator, payload_json, .{}) catch {
|
|
344
|
+
return makeErrorJson(allocator, "invalid hover payload json");
|
|
345
|
+
};
|
|
346
|
+
defer parsed.deinit();
|
|
347
|
+
|
|
348
|
+
const point: c.CGPoint = .{
|
|
349
|
+
.x = parsed.value.x,
|
|
350
|
+
.y = parsed.value.y,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
moveCursorToPoint(point) catch {
|
|
354
|
+
return makeErrorJson(allocator, "failed to move cursor for hover");
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return makeOkJson(allocator, "null");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fn executeDragCommand(allocator: std.mem.Allocator, payload_json: []const u8) ![]const u8 {
|
|
361
|
+
if (builtin.target.os.tag != .macos) {
|
|
362
|
+
return makeErrorJson(allocator, "drag is only supported on macOS");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
var parsed = std.json.parseFromSlice(DragPayload, allocator, payload_json, .{}) catch {
|
|
366
|
+
return makeErrorJson(allocator, "invalid drag payload json");
|
|
367
|
+
};
|
|
368
|
+
defer parsed.deinit();
|
|
369
|
+
|
|
370
|
+
const drag_payload = parsed.value;
|
|
371
|
+
const button_kind = resolveMouseButton(drag_payload.button orelse "left") catch {
|
|
372
|
+
return makeErrorJson(allocator, "invalid drag button");
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const from: c.CGPoint = .{ .x = drag_payload.from.x, .y = drag_payload.from.y };
|
|
376
|
+
const to: c.CGPoint = .{ .x = drag_payload.to.x, .y = drag_payload.to.y };
|
|
377
|
+
|
|
378
|
+
moveCursorToPoint(from) catch {
|
|
379
|
+
return makeErrorJson(allocator, "failed to move cursor to drag origin");
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
postMouseButtonEvent(from, button_kind, true, 1) catch {
|
|
383
|
+
return makeErrorJson(allocator, "failed to post drag mouse-down");
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const total_duration_ns = (drag_payload.durationMs orelse 400) * std.time.ns_per_ms;
|
|
387
|
+
const step_count: u64 = 16;
|
|
388
|
+
const step_duration_ns = if (step_count == 0) 0 else total_duration_ns / step_count;
|
|
389
|
+
|
|
390
|
+
var index: u64 = 1;
|
|
391
|
+
while (index <= step_count) : (index += 1) {
|
|
392
|
+
const fraction = @as(f64, @floatFromInt(index)) / @as(f64, @floatFromInt(step_count));
|
|
393
|
+
const next_point: c.CGPoint = .{
|
|
394
|
+
.x = from.x + (to.x - from.x) * fraction,
|
|
395
|
+
.y = from.y + (to.y - from.y) * fraction,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
moveCursorToPoint(next_point) catch {
|
|
399
|
+
return makeErrorJson(allocator, "failed during drag cursor movement");
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (step_duration_ns > 0 and index < step_count) {
|
|
403
|
+
std.Thread.sleep(step_duration_ns);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
postMouseButtonEvent(to, button_kind, false, 1) catch {
|
|
408
|
+
return makeErrorJson(allocator, "failed to post drag mouse-up");
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
return makeOkJson(allocator, "null");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const MouseButtonKind = enum {
|
|
415
|
+
left,
|
|
416
|
+
right,
|
|
417
|
+
middle,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
fn resolveMouseButton(button: []const u8) !MouseButtonKind {
|
|
421
|
+
if (std.ascii.eqlIgnoreCase(button, "left")) {
|
|
422
|
+
return .left;
|
|
423
|
+
}
|
|
424
|
+
if (std.ascii.eqlIgnoreCase(button, "right")) {
|
|
425
|
+
return .right;
|
|
426
|
+
}
|
|
427
|
+
if (std.ascii.eqlIgnoreCase(button, "middle")) {
|
|
428
|
+
return .middle;
|
|
429
|
+
}
|
|
430
|
+
return error.InvalidMouseButton;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fn postClickPair(point: c.CGPoint, button: MouseButtonKind, click_state: i64) !void {
|
|
434
|
+
try postMouseButtonEvent(point, button, true, click_state);
|
|
435
|
+
try postMouseButtonEvent(point, button, false, click_state);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fn postMouseButtonEvent(point: c.CGPoint, button: MouseButtonKind, is_down: bool, click_state: i64) !void {
|
|
439
|
+
const button_code: c.CGMouseButton = switch (button) {
|
|
440
|
+
.left => c.kCGMouseButtonLeft,
|
|
441
|
+
.right => c.kCGMouseButtonRight,
|
|
442
|
+
.middle => c.kCGMouseButtonCenter,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const event_type: c.CGEventType = switch (button) {
|
|
446
|
+
.left => if (is_down) c.kCGEventLeftMouseDown else c.kCGEventLeftMouseUp,
|
|
447
|
+
.right => if (is_down) c.kCGEventRightMouseDown else c.kCGEventRightMouseUp,
|
|
448
|
+
.middle => if (is_down) c.kCGEventOtherMouseDown else c.kCGEventOtherMouseUp,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const event = c.CGEventCreateMouseEvent(null, event_type, point, button_code);
|
|
452
|
+
if (event == null) {
|
|
453
|
+
return error.CGEventCreateFailed;
|
|
454
|
+
}
|
|
455
|
+
defer c.CFRelease(event);
|
|
456
|
+
|
|
457
|
+
c.CGEventSetIntegerValueField(event, c.kCGMouseEventClickState, click_state);
|
|
458
|
+
c.CGEventPost(c.kCGHIDEventTap, event);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fn currentCursorPoint() !c.CGPoint {
|
|
462
|
+
const event = c.CGEventCreate(null);
|
|
463
|
+
if (event == null) {
|
|
464
|
+
return error.CGEventCreateFailed;
|
|
465
|
+
}
|
|
466
|
+
defer c.CFRelease(event);
|
|
467
|
+
return c.CGEventGetLocation(event);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fn moveCursorToPoint(point: c.CGPoint) !void {
|
|
471
|
+
const warp_result = c.CGWarpMouseCursorPosition(point);
|
|
472
|
+
if (warp_result != c.kCGErrorSuccess) {
|
|
473
|
+
return error.CGWarpMouseFailed;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const move_event = c.CGEventCreateMouseEvent(null, c.kCGEventMouseMoved, point, c.kCGMouseButtonLeft);
|
|
477
|
+
if (move_event == null) {
|
|
478
|
+
return error.CGEventCreateFailed;
|
|
479
|
+
}
|
|
480
|
+
defer c.CFRelease(move_event);
|
|
481
|
+
c.CGEventPost(c.kCGHIDEventTap, move_event);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) !napigen.napi_value {
|
|
485
|
+
try js.setNamedProperty(exports, "execute", try js.createFunction(execute));
|
|
486
|
+
return exports;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
comptime {
|
|
490
|
+
if (!builtin.is_test) {
|
|
491
|
+
napigen.defineModule(initModule);
|
|
492
|
+
}
|
|
493
|
+
}
|