usecomputer 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -0
- package/dist/bridge-contract.test.js +124 -63
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +241 -46
- package/dist/cli-parsing.test.js +34 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +323 -28
- package/dist/coord-map.d.ts +14 -0
- package/dist/coord-map.d.ts.map +1 -0
- package/dist/coord-map.js +75 -0
- package/dist/coord-map.test.d.ts +2 -0
- package/dist/coord-map.test.d.ts.map +1 -0
- package/dist/coord-map.test.js +157 -0
- package/dist/darwin-arm64/usecomputer.node +0 -0
- package/dist/darwin-x64/usecomputer.node +0 -0
- package/dist/debug-point-image.d.ts +8 -0
- package/dist/debug-point-image.d.ts.map +1 -0
- package/dist/debug-point-image.js +43 -0
- package/dist/debug-point-image.test.d.ts +2 -0
- package/dist/debug-point-image.test.d.ts.map +1 -0
- package/dist/debug-point-image.test.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib.d.ts +26 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +88 -0
- package/dist/native-click-smoke.test.js +69 -29
- package/dist/native-lib.d.ts +59 -1
- package/dist/native-lib.d.ts.map +1 -1
- package/dist/terminal-table.d.ts +10 -0
- package/dist/terminal-table.d.ts.map +1 -0
- package/dist/terminal-table.js +55 -0
- package/dist/terminal-table.test.d.ts +2 -0
- package/dist/terminal-table.test.d.ts.map +1 -0
- package/dist/terminal-table.test.js +41 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/src/bridge-contract.test.ts +140 -69
- package/src/bridge.ts +293 -53
- package/src/cli-parsing.test.ts +61 -0
- package/src/cli.ts +393 -32
- package/src/coord-map.test.ts +178 -0
- package/src/coord-map.ts +105 -0
- package/src/debug-point-image.test.ts +50 -0
- package/src/debug-point-image.ts +69 -0
- package/src/index.ts +3 -1
- package/src/lib.ts +125 -0
- package/src/native-click-smoke.test.ts +81 -63
- package/src/native-lib.ts +39 -1
- package/src/terminal-table.test.ts +44 -0
- package/src/terminal-table.ts +88 -0
- package/src/types.ts +50 -0
- package/zig/src/lib.zig +1258 -267
- package/zig/src/scroll.zig +213 -0
- package/zig/src/window.zig +123 -0
package/zig/src/lib.zig
CHANGED
|
@@ -1,121 +1,362 @@
|
|
|
1
1
|
// Native N-API module for usecomputer commands on macOS using Zig.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
10
|
+
const c_macos = if (builtin.target.os.tag == .macos) @cImport({
|
|
9
11
|
@cInclude("CoreGraphics/CoreGraphics.h");
|
|
10
12
|
@cInclude("CoreFoundation/CoreFoundation.h");
|
|
11
13
|
@cInclude("ImageIO/ImageIO.h");
|
|
12
14
|
}) else struct {};
|
|
13
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
|
+
|
|
14
110
|
pub const std_options: std.Options = .{
|
|
15
111
|
.log_level = .err,
|
|
16
112
|
};
|
|
17
113
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
};
|
|
125
|
+
|
|
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
|
+
};
|
|
21
148
|
|
|
22
|
-
fn
|
|
23
|
-
return
|
|
149
|
+
fn DataResult(comptime T: type) type {
|
|
150
|
+
return struct {
|
|
151
|
+
ok: bool,
|
|
152
|
+
data: ?T = null,
|
|
153
|
+
@"error": ?NativeErrorObject = null,
|
|
154
|
+
};
|
|
24
155
|
}
|
|
25
156
|
|
|
26
|
-
fn
|
|
27
|
-
|
|
157
|
+
fn okCommand() CommandResult {
|
|
158
|
+
return .{ .ok = true };
|
|
159
|
+
}
|
|
28
160
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
50
171
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (std.mem.eql(u8, command, "screenshot")) {
|
|
58
|
-
return executeScreenshotCommand(allocator, payload_json);
|
|
59
|
-
}
|
|
172
|
+
fn okData(comptime T: type, value: T) DataResult(T) {
|
|
173
|
+
return .{
|
|
174
|
+
.ok = true,
|
|
175
|
+
.data = value,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
60
178
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
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
|
+
}
|
|
70
189
|
|
|
71
|
-
|
|
190
|
+
fn todoNotImplemented(command: []const u8) CommandResult {
|
|
191
|
+
return failCommand(command, "TODO_NOT_IMPLEMENTED", "TODO not implemented");
|
|
72
192
|
}
|
|
73
193
|
|
|
74
|
-
const
|
|
194
|
+
const Point = struct {
|
|
75
195
|
x: f64,
|
|
76
196
|
y: f64,
|
|
77
197
|
};
|
|
78
198
|
|
|
79
|
-
const
|
|
80
|
-
|
|
199
|
+
const MouseButtonKind = enum {
|
|
200
|
+
left,
|
|
201
|
+
right,
|
|
202
|
+
middle,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const ClickInput = struct {
|
|
206
|
+
point: Point,
|
|
81
207
|
button: ?[]const u8 = null,
|
|
82
|
-
count: ?
|
|
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,
|
|
221
|
+
button: ?[]const u8 = null,
|
|
222
|
+
};
|
|
223
|
+
|
|
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,
|
|
83
286
|
};
|
|
84
287
|
|
|
85
|
-
|
|
288
|
+
const ClipboardSetInput = struct {
|
|
289
|
+
text: []const u8,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
pub fn screenshot(input: ScreenshotInput) DataResult(ScreenshotOutput) {
|
|
86
293
|
if (builtin.target.os.tag != .macos) {
|
|
87
|
-
return
|
|
294
|
+
return failData(ScreenshotOutput, "screenshot", "UNSUPPORTED_PLATFORM", "screenshot is only supported on macOS");
|
|
88
295
|
}
|
|
89
296
|
|
|
90
|
-
|
|
91
|
-
|
|
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,
|
|
92
317
|
}) catch {
|
|
93
|
-
return
|
|
318
|
+
return failData(ScreenshotOutput, "screenshot", "WRITE_FAILED", "failed to write screenshot file");
|
|
94
319
|
};
|
|
95
|
-
defer parsed.deinit();
|
|
96
320
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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) {
|
|
100
341
|
break :blk 1;
|
|
101
342
|
}
|
|
102
|
-
break :blk
|
|
343
|
+
break :blk @as(u32, @intCast(normalized));
|
|
103
344
|
} else 1;
|
|
104
345
|
|
|
105
|
-
const button_kind = resolveMouseButton(
|
|
106
|
-
return
|
|
346
|
+
const button_kind = resolveMouseButton(input.button orelse "left") catch {
|
|
347
|
+
return failCommand("click", "INVALID_INPUT", "invalid click button");
|
|
107
348
|
};
|
|
108
349
|
|
|
109
350
|
const point: c.CGPoint = .{
|
|
110
|
-
.x =
|
|
111
|
-
.y =
|
|
351
|
+
.x = input.point.x,
|
|
352
|
+
.y = input.point.y,
|
|
112
353
|
};
|
|
113
354
|
|
|
114
355
|
var index: u32 = 0;
|
|
115
356
|
while (index < click_count) : (index += 1) {
|
|
116
357
|
const click_state = @as(i64, @intCast(index + 1));
|
|
117
358
|
postClickPair(point, button_kind, click_state) catch {
|
|
118
|
-
return
|
|
359
|
+
return failCommand("click", "EVENT_POST_FAILED", "failed to post click event");
|
|
119
360
|
};
|
|
120
361
|
|
|
121
362
|
if (index + 1 < click_count) {
|
|
@@ -123,299 +364,1035 @@ fn executeClickCommand(allocator: std.mem.Allocator, payload_json: []const u8) !
|
|
|
123
364
|
}
|
|
124
365
|
}
|
|
125
366
|
|
|
126
|
-
return
|
|
367
|
+
return okCommand();
|
|
127
368
|
}
|
|
128
369
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
370
|
+
pub fn mouseMove(input: MouseMoveInput) CommandResult {
|
|
371
|
+
if (builtin.target.os.tag != .macos) {
|
|
372
|
+
return failCommand("mouse-move", "UNSUPPORTED_PLATFORM", "mouse-move is only supported on macOS");
|
|
373
|
+
}
|
|
133
374
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
375
|
+
const point: c.CGPoint = .{
|
|
376
|
+
.x = input.x,
|
|
377
|
+
.y = input.y,
|
|
378
|
+
};
|
|
379
|
+
moveCursorToPoint(point) catch {
|
|
380
|
+
return failCommand("mouse-move", "EVENT_POST_FAILED", "failed to move mouse cursor");
|
|
381
|
+
};
|
|
137
382
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
to: ClickPoint,
|
|
141
|
-
durationMs: ?u64 = null,
|
|
142
|
-
button: ?[]const u8 = null,
|
|
143
|
-
};
|
|
383
|
+
return okCommand();
|
|
384
|
+
}
|
|
144
385
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
width: f64,
|
|
149
|
-
height: f64,
|
|
150
|
-
};
|
|
386
|
+
pub fn mouseDown(input: MouseButtonInput) CommandResult {
|
|
387
|
+
return handleMouseButtonInput(.{ .input = input, .is_down = true });
|
|
388
|
+
}
|
|
151
389
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
region: ?ScreenshotRegion = null,
|
|
156
|
-
};
|
|
390
|
+
pub fn mouseUp(input: MouseButtonInput) CommandResult {
|
|
391
|
+
return handleMouseButtonInput(.{ .input = input, .is_down = false });
|
|
392
|
+
}
|
|
157
393
|
|
|
158
|
-
fn
|
|
394
|
+
fn handleMouseButtonInput(args: struct {
|
|
395
|
+
input: MouseButtonInput,
|
|
396
|
+
is_down: bool,
|
|
397
|
+
}) CommandResult {
|
|
159
398
|
if (builtin.target.os.tag != .macos) {
|
|
160
|
-
return
|
|
399
|
+
return failCommand("mouse-button", "UNSUPPORTED_PLATFORM", "mouse button events are only supported on macOS");
|
|
161
400
|
}
|
|
162
401
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}) catch {
|
|
166
|
-
return makeErrorJson(allocator, "invalid screenshot payload json");
|
|
402
|
+
const button_kind = resolveMouseButton(args.input.button orelse "left") catch {
|
|
403
|
+
return failCommand("mouse-button", "INVALID_INPUT", "invalid mouse button");
|
|
167
404
|
};
|
|
168
|
-
defer parsed.deinit();
|
|
169
405
|
|
|
170
|
-
const
|
|
171
|
-
|
|
406
|
+
const point = currentCursorPoint() catch {
|
|
407
|
+
return failCommand("mouse-button", "CURSOR_READ_FAILED", "failed to read cursor position");
|
|
408
|
+
};
|
|
172
409
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.region = screenshot_payload.region,
|
|
176
|
-
}) catch {
|
|
177
|
-
return makeErrorJson(allocator, "failed to capture screenshot image");
|
|
410
|
+
postMouseButtonEvent(point, button_kind, args.is_down, 1) catch {
|
|
411
|
+
return failCommand("mouse-button", "EVENT_POST_FAILED", "failed to post mouse button event");
|
|
178
412
|
};
|
|
179
|
-
defer c.CFRelease(image);
|
|
180
413
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
414
|
+
return okCommand();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
pub fn mousePosition() DataResult(Point) {
|
|
418
|
+
if (builtin.target.os.tag != .macos) {
|
|
419
|
+
return failData(Point, "mouse-position", "UNSUPPORTED_PLATFORM", "mouse-position is only supported on macOS");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const point = currentCursorPoint() catch {
|
|
423
|
+
return failData(Point, "mouse-position", "CURSOR_READ_FAILED", "failed to read cursor position");
|
|
186
424
|
};
|
|
187
425
|
|
|
188
|
-
|
|
189
|
-
const payload_json_response = try std.fmt.allocPrint(allocator, "{{\"path\":{s}}}", .{path_json});
|
|
190
|
-
return makeOkJson(allocator, payload_json_response);
|
|
426
|
+
return okData(Point, .{ .x = std.math.round(point.x), .y = std.math.round(point.y) });
|
|
191
427
|
}
|
|
192
428
|
|
|
193
|
-
fn
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
429
|
+
pub fn hover(input: Point) CommandResult {
|
|
430
|
+
return mouseMove(input);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
pub fn drag(input: DragInput) CommandResult {
|
|
434
|
+
if (builtin.target.os.tag != .macos) {
|
|
435
|
+
return failCommand("drag", "UNSUPPORTED_PLATFORM", "drag is only supported on macOS");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const button_kind = resolveMouseButton(input.button orelse "left") catch {
|
|
439
|
+
return failCommand("drag", "INVALID_INPUT", "invalid drag button");
|
|
199
440
|
};
|
|
200
441
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 };
|
|
444
|
+
|
|
445
|
+
moveCursorToPoint(from) catch {
|
|
446
|
+
return failCommand("drag", "EVENT_POST_FAILED", "failed to move cursor to drag origin");
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
postMouseButtonEvent(from, button_kind, true, 1) catch {
|
|
450
|
+
return failCommand("drag", "EVENT_POST_FAILED", "failed to post drag mouse-down");
|
|
451
|
+
};
|
|
452
|
+
|
|
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;
|
|
461
|
+
const step_count: u64 = 16;
|
|
462
|
+
const step_duration_ns = if (step_count == 0) 0 else total_duration_ns / step_count;
|
|
463
|
+
|
|
464
|
+
var index: u64 = 1;
|
|
465
|
+
while (index <= step_count) : (index += 1) {
|
|
466
|
+
const fraction = @as(f64, @floatFromInt(index)) / @as(f64, @floatFromInt(step_count));
|
|
467
|
+
const next_point: c.CGPoint = .{
|
|
468
|
+
.x = from.x + (to.x - from.x) * fraction,
|
|
469
|
+
.y = from.y + (to.y - from.y) * fraction,
|
|
205
470
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
471
|
+
|
|
472
|
+
moveCursorToPoint(next_point) catch {
|
|
473
|
+
return failCommand("drag", "EVENT_POST_FAILED", "failed during drag cursor movement");
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
if (step_duration_ns > 0 and index < step_count) {
|
|
477
|
+
std.Thread.sleep(step_duration_ns);
|
|
209
478
|
}
|
|
210
|
-
return region_image;
|
|
211
479
|
}
|
|
212
480
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return
|
|
481
|
+
postMouseButtonEvent(to, button_kind, false, 1) catch {
|
|
482
|
+
return failCommand("drag", "EVENT_POST_FAILED", "failed to post drag mouse-up");
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
return okCommand();
|
|
218
486
|
}
|
|
219
487
|
|
|
220
|
-
fn
|
|
221
|
-
|
|
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
|
+
|
|
222
493
|
var display_ids: [16]c.CGDirectDisplayID = undefined;
|
|
223
494
|
var display_count: u32 = 0;
|
|
224
495
|
const list_result = c.CGGetActiveDisplayList(display_ids.len, &display_ids, &display_count);
|
|
225
496
|
if (list_result != c.kCGErrorSuccess) {
|
|
226
|
-
return
|
|
497
|
+
return failData([]const u8, "display-list", "DISPLAY_QUERY_FAILED", "failed to query active displays");
|
|
227
498
|
}
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
};
|
|
230
535
|
}
|
|
231
|
-
|
|
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);
|
|
232
546
|
}
|
|
233
547
|
|
|
234
|
-
fn
|
|
235
|
-
|
|
236
|
-
|
|
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;
|
|
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");
|
|
247
551
|
}
|
|
248
|
-
defer c.CFRelease(file_url);
|
|
249
552
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
},
|
|
253
591
|
}
|
|
254
|
-
|
|
592
|
+
}
|
|
255
593
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
},
|
|
259
617
|
}
|
|
260
|
-
|
|
618
|
+
}
|
|
261
619
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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,
|
|
647
|
+
};
|
|
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;
|
|
266
685
|
}
|
|
686
|
+
|
|
687
|
+
if (!saw_key) {
|
|
688
|
+
return error.MissingMainKey;
|
|
689
|
+
}
|
|
690
|
+
return parsed;
|
|
267
691
|
}
|
|
268
692
|
|
|
269
|
-
fn
|
|
270
|
-
if (
|
|
271
|
-
|
|
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
|
+
}
|
|
272
699
|
}
|
|
700
|
+
return 1;
|
|
701
|
+
}
|
|
273
702
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
712
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
725
|
|
|
287
|
-
|
|
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
|
+
}
|
|
288
748
|
}
|
|
289
749
|
|
|
290
|
-
fn
|
|
291
|
-
|
|
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;
|
|
292
833
|
}
|
|
293
834
|
|
|
294
|
-
fn
|
|
295
|
-
|
|
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);
|
|
296
840
|
}
|
|
297
841
|
|
|
298
|
-
fn
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
}
|
|
301
902
|
}
|
|
903
|
+
}
|
|
302
904
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
}
|
|
307
926
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
956
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
}
|
|
315
965
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
319
971
|
|
|
320
|
-
|
|
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
|
+
}
|
|
321
991
|
}
|
|
322
992
|
|
|
323
|
-
fn
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|
|
326
1014
|
}
|
|
1015
|
+
}
|
|
327
1016
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
1024
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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;
|
|
336
1040
|
}
|
|
337
1041
|
|
|
338
|
-
fn
|
|
339
|
-
|
|
340
|
-
|
|
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;
|
|
341
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
|
+
}
|
|
342
1050
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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);
|
|
347
1056
|
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
.y = parsed.value.y,
|
|
351
|
-
};
|
|
1057
|
+
const display = c_x11.XOpenDisplay(null) orelse return error.XOpenDisplayFailed;
|
|
1058
|
+
defer _ = c_x11.XCloseDisplay(display);
|
|
352
1059
|
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
1117
|
};
|
|
356
1118
|
|
|
357
|
-
|
|
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
|
+
};
|
|
358
1153
|
}
|
|
359
1154
|
|
|
360
|
-
fn
|
|
361
|
-
|
|
362
|
-
|
|
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;
|
|
363
1159
|
}
|
|
1160
|
+
return @intCast(normalized);
|
|
1161
|
+
}
|
|
364
1162
|
|
|
365
|
-
|
|
366
|
-
|
|
1163
|
+
fn findWindowBoundsById(target_window_id: u32) !c.CGRect {
|
|
1164
|
+
const Context = struct {
|
|
1165
|
+
target_id: u32,
|
|
1166
|
+
bounds: ?c.CGRect = null,
|
|
367
1167
|
};
|
|
368
|
-
defer parsed.deinit();
|
|
369
1168
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
1185
|
};
|
|
374
1186
|
|
|
375
|
-
|
|
376
|
-
|
|
1187
|
+
if (context.bounds) |bounds| {
|
|
1188
|
+
return bounds;
|
|
1189
|
+
}
|
|
1190
|
+
return error.WindowNotFound;
|
|
1191
|
+
}
|
|
377
1192
|
|
|
378
|
-
|
|
379
|
-
|
|
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),
|
|
380
1218
|
};
|
|
1219
|
+
}
|
|
381
1220
|
|
|
382
|
-
|
|
383
|
-
|
|
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,
|
|
384
1236
|
};
|
|
385
1237
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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);
|
|
389
1272
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
};
|
|
1273
|
+
try stream.writer().writeByte(']');
|
|
1274
|
+
return std.heap.c_allocator.dupe(u8, stream.getWritten());
|
|
1275
|
+
}
|
|
397
1276
|
|
|
398
|
-
|
|
399
|
-
|
|
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,
|
|
400
1287
|
};
|
|
1288
|
+
}
|
|
401
1289
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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;
|
|
405
1297
|
}
|
|
1298
|
+
defer c.CFRelease(color_space);
|
|
406
1299
|
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
},
|
|
409
1322
|
};
|
|
1323
|
+
c.CGContextDrawImage(context, draw_rect, image);
|
|
410
1324
|
|
|
411
|
-
|
|
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
|
+
};
|
|
412
1334
|
}
|
|
413
1335
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
1396
|
|
|
420
1397
|
fn resolveMouseButton(button: []const u8) !MouseButtonKind {
|
|
421
1398
|
if (std.ascii.eqlIgnoreCase(button, "left")) {
|
|
@@ -482,7 +1459,21 @@ fn moveCursorToPoint(point: c.CGPoint) !void {
|
|
|
482
1459
|
}
|
|
483
1460
|
|
|
484
1461
|
fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) !napigen.napi_value {
|
|
485
|
-
try js.setNamedProperty(exports, "
|
|
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));
|
|
486
1477
|
return exports;
|
|
487
1478
|
}
|
|
488
1479
|
|