usecomputer 0.0.3 → 0.1.0
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 +67 -0
- package/README.md +324 -0
- package/build.zig +95 -11
- package/build.zig.zon +5 -0
- package/dist/bridge-contract.test.js +61 -67
- 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 +19 -5
- package/src/bridge-contract.test.ts +68 -73
- 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 +1966 -270
- package/zig/src/main.zig +382 -0
- package/zig/src/scroll.zig +213 -0
- package/zig/src/window.zig +123 -0
package/zig/src/main.zig
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/// Standalone CLI for usecomputer — no Node.js required.
|
|
2
|
+
/// Calls the same native functions as the N-API module via lib.zig.
|
|
3
|
+
const std = @import("std");
|
|
4
|
+
const zeke = @import("zeke");
|
|
5
|
+
const lib = @import("usecomputer_lib");
|
|
6
|
+
|
|
7
|
+
const File = std.fs.File;
|
|
8
|
+
const Writer = File.DeprecatedWriter;
|
|
9
|
+
|
|
10
|
+
fn getStdout() Writer {
|
|
11
|
+
return File.stdout().deprecatedWriter();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn getStderr() Writer {
|
|
15
|
+
return File.stderr().deprecatedWriter();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ───
|
|
19
|
+
|
|
20
|
+
fn parseF64(s: []const u8) ?f64 {
|
|
21
|
+
return std.fmt.parseFloat(f64, s) catch null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn parseRegion(s: []const u8) ?lib.ScreenshotRegion {
|
|
25
|
+
// Parse "x,y,w,h" format
|
|
26
|
+
var iter = std.mem.splitScalar(u8, s, ',');
|
|
27
|
+
const x_str = iter.next() orelse return null;
|
|
28
|
+
const y_str = iter.next() orelse return null;
|
|
29
|
+
const w_str = iter.next() orelse return null;
|
|
30
|
+
const h_str = iter.next() orelse return null;
|
|
31
|
+
return .{
|
|
32
|
+
.x = std.fmt.parseFloat(f64, x_str) catch return null,
|
|
33
|
+
.y = std.fmt.parseFloat(f64, y_str) catch return null,
|
|
34
|
+
.width = std.fmt.parseFloat(f64, w_str) catch return null,
|
|
35
|
+
.height = std.fmt.parseFloat(f64, h_str) catch return null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn printError(result: anytype) void {
|
|
40
|
+
const stderr = getStderr();
|
|
41
|
+
if (result.@"error") |err| {
|
|
42
|
+
stderr.print("error: {s} ({s})\n", .{ err.message, err.code }) catch {};
|
|
43
|
+
} else {
|
|
44
|
+
stderr.print("error: command failed\n", .{}) catch {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn printScreenshotJson(data: lib.ScreenshotOutput) void {
|
|
49
|
+
const stdout = getStdout();
|
|
50
|
+
stdout.print(
|
|
51
|
+
"{{\"path\":\"{s}\",\"desktopIndex\":{d:.0},\"captureX\":{d:.0},\"captureY\":{d:.0},\"captureWidth\":{d:.0},\"captureHeight\":{d:.0},\"imageWidth\":{d:.0},\"imageHeight\":{d:.0}}}\n",
|
|
52
|
+
.{ data.path, data.desktopIndex, data.captureX, data.captureY, data.captureWidth, data.captureHeight, data.imageWidth, data.imageHeight },
|
|
53
|
+
) catch {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Command definitions ───
|
|
57
|
+
|
|
58
|
+
const Screenshot = zeke.cmd("screenshot [path]", "Take a screenshot")
|
|
59
|
+
.option("--region [region]", "Capture specific region (x,y,w,h)")
|
|
60
|
+
.option("--display [id]", "Target display")
|
|
61
|
+
.option("--window [id]", "Target window")
|
|
62
|
+
.option("--annotate", "Annotate with grid overlay")
|
|
63
|
+
.option("--json", "Output as JSON");
|
|
64
|
+
|
|
65
|
+
const Click = zeke.cmd("click [target]", "Click at coordinates or target")
|
|
66
|
+
.option("-x <x>", "X coordinate")
|
|
67
|
+
.option("-y <y>", "Y coordinate")
|
|
68
|
+
.option("--button [button]", "Mouse button: left, right, middle")
|
|
69
|
+
.option("--count [count]", "Click count");
|
|
70
|
+
|
|
71
|
+
const DebugPoint = zeke.cmd("debug-point [target]", "Validate click coordinates visually")
|
|
72
|
+
.option("-x [x]", "X coordinate")
|
|
73
|
+
.option("-y [y]", "Y coordinate")
|
|
74
|
+
.option("--output [path]", "Save annotated screenshot")
|
|
75
|
+
.option("--json", "Output as JSON");
|
|
76
|
+
|
|
77
|
+
const TypeText = zeke.cmd("type [text]", "Type text using keyboard")
|
|
78
|
+
.option("--delay [ms]", "Delay between keystrokes in ms");
|
|
79
|
+
|
|
80
|
+
const Press = zeke.cmd("press <key>", "Press a key or key combination")
|
|
81
|
+
.option("--count [n]", "Number of times to press")
|
|
82
|
+
.option("--delay [ms]", "Delay between presses in ms");
|
|
83
|
+
|
|
84
|
+
const Scroll = zeke.cmd("scroll <direction> [amount]", "Scroll in a direction")
|
|
85
|
+
.option("--at [coords]", "Scroll at specific coordinates (x,y)");
|
|
86
|
+
|
|
87
|
+
const Drag = zeke.cmd("drag <from> <to>", "Drag from one point to another")
|
|
88
|
+
.option("--duration [ms]", "Drag duration in ms")
|
|
89
|
+
.option("--button [button]", "Mouse button");
|
|
90
|
+
|
|
91
|
+
const Hover = zeke.cmd("hover", "Move mouse without clicking")
|
|
92
|
+
.option("-x <x>", "X coordinate")
|
|
93
|
+
.option("-y <y>", "Y coordinate");
|
|
94
|
+
|
|
95
|
+
const MouseMove = zeke.cmd("mouse move", "Move to absolute coordinates")
|
|
96
|
+
.option("-x <x>", "X coordinate")
|
|
97
|
+
.option("-y <y>", "Y coordinate");
|
|
98
|
+
|
|
99
|
+
const MouseDown = zeke.cmd("mouse down", "Press and hold mouse button")
|
|
100
|
+
.option("--button [button]", "Mouse button");
|
|
101
|
+
|
|
102
|
+
const MouseUp = zeke.cmd("mouse up", "Release mouse button")
|
|
103
|
+
.option("--button [button]", "Mouse button");
|
|
104
|
+
|
|
105
|
+
const MousePosition = zeke.cmd("mouse position", "Print current mouse position")
|
|
106
|
+
.option("--json", "Output as JSON");
|
|
107
|
+
|
|
108
|
+
const DisplayList = zeke.cmd("display list", "List connected displays")
|
|
109
|
+
.option("--json", "Output as JSON");
|
|
110
|
+
|
|
111
|
+
const WindowList = zeke.cmd("window list", "List open windows")
|
|
112
|
+
.option("--json", "Output as JSON");
|
|
113
|
+
|
|
114
|
+
const ClipboardGet = zeke.cmd("clipboard get", "Print clipboard text");
|
|
115
|
+
|
|
116
|
+
const ClipboardSet = zeke.cmd("clipboard set <text>", "Set clipboard text");
|
|
117
|
+
|
|
118
|
+
// ─── Action functions ───
|
|
119
|
+
|
|
120
|
+
fn screenshotAction(args: Screenshot.Args, opts: Screenshot.Options) !void {
|
|
121
|
+
const result = lib.screenshot(.{
|
|
122
|
+
.path = args.path,
|
|
123
|
+
.display = if (opts.display) |d| parseF64(d) else null,
|
|
124
|
+
.window = if (opts.window) |w| parseF64(w) else null,
|
|
125
|
+
.region = if (opts.region) |r| parseRegion(r) else null,
|
|
126
|
+
.annotate = opts.annotate,
|
|
127
|
+
});
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
printError(result);
|
|
130
|
+
return error.CommandFailed;
|
|
131
|
+
}
|
|
132
|
+
if (opts.json) {
|
|
133
|
+
if (result.data) |data| {
|
|
134
|
+
printScreenshotJson(data);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const stdout = getStdout();
|
|
138
|
+
if (result.data) |data| {
|
|
139
|
+
try stdout.print("Screenshot saved to {s} ({d:.0}x{d:.0})\n", .{
|
|
140
|
+
data.path, data.imageWidth, data.imageHeight,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn clickAction(_: Click.Args, opts: Click.Options) !void {
|
|
147
|
+
const x = parseF64(opts.x) orelse return error.InvalidCoordinate;
|
|
148
|
+
const y = parseF64(opts.y) orelse return error.InvalidCoordinate;
|
|
149
|
+
const result = lib.click(.{
|
|
150
|
+
.point = .{ .x = x, .y = y },
|
|
151
|
+
.button = opts.button,
|
|
152
|
+
.count = if (opts.count) |c| parseF64(c) else null,
|
|
153
|
+
});
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
printError(result);
|
|
156
|
+
return error.CommandFailed;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn debugPointAction(_: DebugPoint.Args, _: DebugPoint.Options) !void {
|
|
161
|
+
const stderr = getStderr();
|
|
162
|
+
try stderr.print("debug-point: TODO\n", .{});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn typeTextAction(args: TypeText.Args, opts: TypeText.Options) !void {
|
|
166
|
+
const text = args.text orelse {
|
|
167
|
+
const stderr = getStderr();
|
|
168
|
+
try stderr.print("error: text argument required\n", .{});
|
|
169
|
+
return error.MissingArgument;
|
|
170
|
+
};
|
|
171
|
+
const result = lib.typeText(.{
|
|
172
|
+
.text = text,
|
|
173
|
+
.delayMs = if (opts.delay) |d| parseF64(d) else null,
|
|
174
|
+
});
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
printError(result);
|
|
177
|
+
return error.CommandFailed;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn pressAction(args: Press.Args, opts: Press.Options) !void {
|
|
182
|
+
const result = lib.press(.{
|
|
183
|
+
.key = args.key,
|
|
184
|
+
.count = if (opts.count) |c| parseF64(c) else null,
|
|
185
|
+
.delayMs = if (opts.delay) |d| parseF64(d) else null,
|
|
186
|
+
});
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
printError(result);
|
|
189
|
+
return error.CommandFailed;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn scrollAction(args: Scroll.Args, opts: Scroll.Options) !void {
|
|
194
|
+
const amount: f64 = if (args.amount) |a| (parseF64(a) orelse 3.0) else 3.0;
|
|
195
|
+
var at: ?lib.Point = null;
|
|
196
|
+
if (opts.at) |at_str| {
|
|
197
|
+
var iter = std.mem.splitScalar(u8, at_str, ',');
|
|
198
|
+
const x_str = iter.next() orelse return error.InvalidCoordinate;
|
|
199
|
+
const y_str = iter.next() orelse return error.InvalidCoordinate;
|
|
200
|
+
at = .{
|
|
201
|
+
.x = std.fmt.parseFloat(f64, x_str) catch return error.InvalidCoordinate,
|
|
202
|
+
.y = std.fmt.parseFloat(f64, y_str) catch return error.InvalidCoordinate,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const result = lib.scroll(.{
|
|
206
|
+
.direction = args.direction,
|
|
207
|
+
.amount = amount,
|
|
208
|
+
.at = at,
|
|
209
|
+
});
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
printError(result);
|
|
212
|
+
return error.CommandFailed;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn dragAction(args: Drag.Args, opts: Drag.Options) !void {
|
|
217
|
+
// Parse "x,y" format for from and to
|
|
218
|
+
const from = parsePointArg(args.from) orelse return error.InvalidCoordinate;
|
|
219
|
+
const to = parsePointArg(args.to) orelse return error.InvalidCoordinate;
|
|
220
|
+
const result = lib.drag(.{
|
|
221
|
+
.from = from,
|
|
222
|
+
.to = to,
|
|
223
|
+
.durationMs = if (opts.duration) |d| parseF64(d) else null,
|
|
224
|
+
.button = opts.button,
|
|
225
|
+
});
|
|
226
|
+
if (!result.ok) {
|
|
227
|
+
printError(result);
|
|
228
|
+
return error.CommandFailed;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn parsePointArg(s: []const u8) ?lib.Point {
|
|
233
|
+
var iter = std.mem.splitScalar(u8, s, ',');
|
|
234
|
+
const x_str = iter.next() orelse return null;
|
|
235
|
+
const y_str = iter.next() orelse return null;
|
|
236
|
+
return .{
|
|
237
|
+
.x = std.fmt.parseFloat(f64, x_str) catch return null,
|
|
238
|
+
.y = std.fmt.parseFloat(f64, y_str) catch return null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn hoverAction(_: Hover.Args, opts: Hover.Options) !void {
|
|
243
|
+
const x = parseF64(opts.x) orelse return error.InvalidCoordinate;
|
|
244
|
+
const y = parseF64(opts.y) orelse return error.InvalidCoordinate;
|
|
245
|
+
const result = lib.hover(.{ .x = x, .y = y });
|
|
246
|
+
if (!result.ok) {
|
|
247
|
+
printError(result);
|
|
248
|
+
return error.CommandFailed;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn mouseMoveAction(_: MouseMove.Args, opts: MouseMove.Options) !void {
|
|
253
|
+
const x = parseF64(opts.x) orelse return error.InvalidCoordinate;
|
|
254
|
+
const y = parseF64(opts.y) orelse return error.InvalidCoordinate;
|
|
255
|
+
const result = lib.mouseMove(.{ .x = x, .y = y });
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
printError(result);
|
|
258
|
+
return error.CommandFailed;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn mouseDownAction(_: MouseDown.Args, opts: MouseDown.Options) !void {
|
|
263
|
+
const result = lib.mouseDown(.{ .button = opts.button });
|
|
264
|
+
if (!result.ok) {
|
|
265
|
+
printError(result);
|
|
266
|
+
return error.CommandFailed;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn mouseUpAction(_: MouseUp.Args, opts: MouseUp.Options) !void {
|
|
271
|
+
const result = lib.mouseUp(.{ .button = opts.button });
|
|
272
|
+
if (!result.ok) {
|
|
273
|
+
printError(result);
|
|
274
|
+
return error.CommandFailed;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn mousePositionAction(_: MousePosition.Args, opts: MousePosition.Options) !void {
|
|
279
|
+
const result = lib.mousePosition();
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
printError(result);
|
|
282
|
+
return error.CommandFailed;
|
|
283
|
+
}
|
|
284
|
+
if (result.data) |pos| {
|
|
285
|
+
const stdout = getStdout();
|
|
286
|
+
if (opts.json) {
|
|
287
|
+
try stdout.print("{{\"x\":{d:.0},\"y\":{d:.0}}}\n", .{ pos.x, pos.y });
|
|
288
|
+
} else {
|
|
289
|
+
try stdout.print("{d:.0}, {d:.0}\n", .{ pos.x, pos.y });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn displayListAction(_: DisplayList.Args, opts: DisplayList.Options) !void {
|
|
295
|
+
const result = lib.displayList();
|
|
296
|
+
if (!result.ok) {
|
|
297
|
+
printError(result);
|
|
298
|
+
return error.CommandFailed;
|
|
299
|
+
}
|
|
300
|
+
if (result.data) |data| {
|
|
301
|
+
const stdout = getStdout();
|
|
302
|
+
if (opts.json) {
|
|
303
|
+
try stdout.print("{s}\n", .{data});
|
|
304
|
+
} else {
|
|
305
|
+
try stdout.print("{s}\n", .{data});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn windowListAction(_: WindowList.Args, opts: WindowList.Options) !void {
|
|
311
|
+
const result = lib.windowList();
|
|
312
|
+
if (!result.ok) {
|
|
313
|
+
printError(result);
|
|
314
|
+
return error.CommandFailed;
|
|
315
|
+
}
|
|
316
|
+
if (result.data) |data| {
|
|
317
|
+
const stdout = getStdout();
|
|
318
|
+
if (opts.json) {
|
|
319
|
+
try stdout.print("{s}\n", .{data});
|
|
320
|
+
} else {
|
|
321
|
+
try stdout.print("{s}\n", .{data});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn clipboardGetAction(_: ClipboardGet.Args, _: ClipboardGet.Options) !void {
|
|
327
|
+
const result = lib.clipboardGet();
|
|
328
|
+
if (!result.ok) {
|
|
329
|
+
printError(result);
|
|
330
|
+
return error.CommandFailed;
|
|
331
|
+
}
|
|
332
|
+
if (result.data) |data| {
|
|
333
|
+
const stdout = getStdout();
|
|
334
|
+
try stdout.print("{s}\n", .{data});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn clipboardSetAction(args: ClipboardSet.Args, _: ClipboardSet.Options) !void {
|
|
339
|
+
const result = lib.clipboardSet(.{ .text = args.text });
|
|
340
|
+
if (!result.ok) {
|
|
341
|
+
printError(result);
|
|
342
|
+
return error.CommandFailed;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Main ───
|
|
347
|
+
|
|
348
|
+
pub fn main() !void {
|
|
349
|
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
350
|
+
defer _ = gpa.deinit();
|
|
351
|
+
|
|
352
|
+
var app = zeke.App(.{
|
|
353
|
+
Screenshot.bind(screenshotAction),
|
|
354
|
+
Click.bind(clickAction),
|
|
355
|
+
DebugPoint.bind(debugPointAction),
|
|
356
|
+
TypeText.bind(typeTextAction),
|
|
357
|
+
Press.bind(pressAction),
|
|
358
|
+
Scroll.bind(scrollAction),
|
|
359
|
+
Drag.bind(dragAction),
|
|
360
|
+
Hover.bind(hoverAction),
|
|
361
|
+
MouseMove.bind(mouseMoveAction),
|
|
362
|
+
MouseDown.bind(mouseDownAction),
|
|
363
|
+
MouseUp.bind(mouseUpAction),
|
|
364
|
+
MousePosition.bind(mousePositionAction),
|
|
365
|
+
DisplayList.bind(displayListAction),
|
|
366
|
+
WindowList.bind(windowListAction),
|
|
367
|
+
ClipboardGet.bind(clipboardGetAction),
|
|
368
|
+
ClipboardSet.bind(clipboardSetAction),
|
|
369
|
+
}).init(gpa.allocator(), "usecomputer");
|
|
370
|
+
|
|
371
|
+
app.setVersion("0.0.4");
|
|
372
|
+
app.run() catch |err| {
|
|
373
|
+
switch (err) {
|
|
374
|
+
error.CommandFailed, error.InvalidCoordinate, error.MissingArgument => {},
|
|
375
|
+
else => {
|
|
376
|
+
const stderr = getStderr();
|
|
377
|
+
stderr.print("error: {s}\n", .{@errorName(err)}) catch {};
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
std.process.exit(1);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Cross-platform native scroll event helpers for the usecomputer Zig module.
|
|
2
|
+
|
|
3
|
+
const std = @import("std");
|
|
4
|
+
const builtin = @import("builtin");
|
|
5
|
+
|
|
6
|
+
const c_macos = if (builtin.target.os.tag == .macos) @cImport({
|
|
7
|
+
@cInclude("CoreGraphics/CoreGraphics.h");
|
|
8
|
+
@cInclude("CoreFoundation/CoreFoundation.h");
|
|
9
|
+
}) else struct {};
|
|
10
|
+
|
|
11
|
+
const c_windows = if (builtin.target.os.tag == .windows) @cImport({
|
|
12
|
+
@cInclude("windows.h");
|
|
13
|
+
}) else struct {};
|
|
14
|
+
|
|
15
|
+
const c_x11 = if (builtin.target.os.tag == .linux) @cImport({
|
|
16
|
+
@cInclude("X11/Xlib.h");
|
|
17
|
+
@cInclude("X11/extensions/XTest.h");
|
|
18
|
+
}) else struct {};
|
|
19
|
+
|
|
20
|
+
pub const ScrollArgs = struct {
|
|
21
|
+
direction: []const u8,
|
|
22
|
+
amount: f64,
|
|
23
|
+
at_x: ?f64 = null,
|
|
24
|
+
at_y: ?f64 = null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ScrollDirection = enum {
|
|
28
|
+
up,
|
|
29
|
+
down,
|
|
30
|
+
left,
|
|
31
|
+
right,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
pub fn scroll(args: ScrollArgs) !void {
|
|
35
|
+
const direction = try parseDirection(args.direction);
|
|
36
|
+
const steps = try normalizeAmount(args.amount);
|
|
37
|
+
|
|
38
|
+
switch (builtin.target.os.tag) {
|
|
39
|
+
.macos => {
|
|
40
|
+
try scrollMacos(.{ .direction = direction, .steps = steps, .at_x = args.at_x, .at_y = args.at_y });
|
|
41
|
+
},
|
|
42
|
+
.windows => {
|
|
43
|
+
try scrollWindows(.{ .direction = direction, .steps = steps, .at_x = args.at_x, .at_y = args.at_y });
|
|
44
|
+
},
|
|
45
|
+
.linux => {
|
|
46
|
+
try scrollX11(.{ .direction = direction, .steps = steps, .at_x = args.at_x, .at_y = args.at_y });
|
|
47
|
+
},
|
|
48
|
+
else => {
|
|
49
|
+
return error.UnsupportedPlatform;
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn parseDirection(direction: []const u8) !ScrollDirection {
|
|
55
|
+
if (std.ascii.eqlIgnoreCase(direction, "up")) {
|
|
56
|
+
return .up;
|
|
57
|
+
}
|
|
58
|
+
if (std.ascii.eqlIgnoreCase(direction, "down")) {
|
|
59
|
+
return .down;
|
|
60
|
+
}
|
|
61
|
+
if (std.ascii.eqlIgnoreCase(direction, "left")) {
|
|
62
|
+
return .left;
|
|
63
|
+
}
|
|
64
|
+
if (std.ascii.eqlIgnoreCase(direction, "right")) {
|
|
65
|
+
return .right;
|
|
66
|
+
}
|
|
67
|
+
return error.InvalidDirection;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn normalizeAmount(amount: f64) !i32 {
|
|
71
|
+
if (!std.math.isFinite(amount)) {
|
|
72
|
+
return error.InvalidAmount;
|
|
73
|
+
}
|
|
74
|
+
const rounded = @as(i64, @intFromFloat(std.math.round(amount)));
|
|
75
|
+
if (rounded <= 0) {
|
|
76
|
+
return error.InvalidAmount;
|
|
77
|
+
}
|
|
78
|
+
if (rounded > std.math.maxInt(i32)) {
|
|
79
|
+
return error.AmountTooLarge;
|
|
80
|
+
}
|
|
81
|
+
return @as(i32, @intCast(rounded));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn scrollMacos(args: struct {
|
|
85
|
+
direction: ScrollDirection,
|
|
86
|
+
steps: i32,
|
|
87
|
+
at_x: ?f64,
|
|
88
|
+
at_y: ?f64,
|
|
89
|
+
}) !void {
|
|
90
|
+
if (args.at_x != null and args.at_y != null) {
|
|
91
|
+
const point: c_macos.CGPoint = .{ .x = args.at_x.?, .y = args.at_y.? };
|
|
92
|
+
const warp_result = c_macos.CGWarpMouseCursorPosition(point);
|
|
93
|
+
if (warp_result != c_macos.kCGErrorSuccess) {
|
|
94
|
+
return error.CGWarpMouseFailed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var delta_y: i32 = 0;
|
|
99
|
+
var delta_x: i32 = 0;
|
|
100
|
+
switch (args.direction) {
|
|
101
|
+
.up => {
|
|
102
|
+
delta_y = args.steps;
|
|
103
|
+
},
|
|
104
|
+
.down => {
|
|
105
|
+
delta_y = -args.steps;
|
|
106
|
+
},
|
|
107
|
+
.left => {
|
|
108
|
+
delta_x = -args.steps;
|
|
109
|
+
},
|
|
110
|
+
.right => {
|
|
111
|
+
delta_x = args.steps;
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const event = c_macos.CGEventCreateScrollWheelEvent(
|
|
116
|
+
null,
|
|
117
|
+
c_macos.kCGScrollEventUnitLine,
|
|
118
|
+
2,
|
|
119
|
+
delta_y,
|
|
120
|
+
delta_x,
|
|
121
|
+
);
|
|
122
|
+
if (event == null) {
|
|
123
|
+
return error.CGEventCreateFailed;
|
|
124
|
+
}
|
|
125
|
+
defer c_macos.CFRelease(event);
|
|
126
|
+
|
|
127
|
+
if (args.at_x != null and args.at_y != null) {
|
|
128
|
+
const location: c_macos.CGPoint = .{ .x = args.at_x.?, .y = args.at_y.? };
|
|
129
|
+
c_macos.CGEventSetLocation(event, location);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
c_macos.CGEventPost(c_macos.kCGHIDEventTap, event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn scrollWindows(args: struct {
|
|
136
|
+
direction: ScrollDirection,
|
|
137
|
+
steps: i32,
|
|
138
|
+
at_x: ?f64,
|
|
139
|
+
at_y: ?f64,
|
|
140
|
+
}) !void {
|
|
141
|
+
if (args.at_x != null and args.at_y != null) {
|
|
142
|
+
const x = @as(i64, @intFromFloat(std.math.round(args.at_x.?)));
|
|
143
|
+
const y = @as(i64, @intFromFloat(std.math.round(args.at_y.?)));
|
|
144
|
+
if (x < std.math.minInt(i32) or x > std.math.maxInt(i32) or y < std.math.minInt(i32) or y > std.math.maxInt(i32)) {
|
|
145
|
+
return error.InvalidPoint;
|
|
146
|
+
}
|
|
147
|
+
_ = c_windows.SetCursorPos(@as(c_int, @intCast(x)), @as(c_int, @intCast(y)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var flags: u32 = 0;
|
|
151
|
+
var delta: i32 = 0;
|
|
152
|
+
switch (args.direction) {
|
|
153
|
+
.up => {
|
|
154
|
+
flags = c_windows.MOUSEEVENTF_WHEEL;
|
|
155
|
+
delta = args.steps;
|
|
156
|
+
},
|
|
157
|
+
.down => {
|
|
158
|
+
flags = c_windows.MOUSEEVENTF_WHEEL;
|
|
159
|
+
delta = -args.steps;
|
|
160
|
+
},
|
|
161
|
+
.left => {
|
|
162
|
+
flags = c_windows.MOUSEEVENTF_HWHEEL;
|
|
163
|
+
delta = -args.steps;
|
|
164
|
+
},
|
|
165
|
+
.right => {
|
|
166
|
+
flags = c_windows.MOUSEEVENTF_HWHEEL;
|
|
167
|
+
delta = args.steps;
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var event = std.mem.zeroes(c_windows.INPUT);
|
|
172
|
+
event.type = c_windows.INPUT_MOUSE;
|
|
173
|
+
event.Anonymous.mi.dwFlags = flags;
|
|
174
|
+
event.Anonymous.mi.mouseData = @as(c_uint, @intCast(delta * c_windows.WHEEL_DELTA));
|
|
175
|
+
const sent = c_windows.SendInput(1, &event, @sizeOf(c_windows.INPUT));
|
|
176
|
+
if (sent == 0) {
|
|
177
|
+
return error.EventPostFailed;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn scrollX11(args: struct {
|
|
182
|
+
direction: ScrollDirection,
|
|
183
|
+
steps: i32,
|
|
184
|
+
at_x: ?f64,
|
|
185
|
+
at_y: ?f64,
|
|
186
|
+
}) !void {
|
|
187
|
+
const display = c_x11.XOpenDisplay(null) orelse return error.XOpenDisplayFailed;
|
|
188
|
+
defer _ = c_x11.XCloseDisplay(display);
|
|
189
|
+
|
|
190
|
+
if (args.at_x != null and args.at_y != null) {
|
|
191
|
+
const x = @as(i64, @intFromFloat(std.math.round(args.at_x.?)));
|
|
192
|
+
const y = @as(i64, @intFromFloat(std.math.round(args.at_y.?)));
|
|
193
|
+
if (x < std.math.minInt(i32) or x > std.math.maxInt(i32) or y < std.math.minInt(i32) or y > std.math.maxInt(i32)) {
|
|
194
|
+
return error.InvalidPoint;
|
|
195
|
+
}
|
|
196
|
+
_ = c_x11.XWarpPointer(display, 0, c_x11.XDefaultRootWindow(display), 0, 0, 0, 0, @as(c_int, @intCast(x)), @as(c_int, @intCast(y)));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const button_code: c_uint = switch (args.direction) {
|
|
200
|
+
.up => 4,
|
|
201
|
+
.down => 5,
|
|
202
|
+
.left => 6,
|
|
203
|
+
.right => 7,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const repeat_count: u32 = @as(u32, @intCast(args.steps));
|
|
207
|
+
var index: u32 = 0;
|
|
208
|
+
while (index < repeat_count) : (index += 1) {
|
|
209
|
+
_ = c_x11.XTestFakeButtonEvent(display, button_code, c_x11.True, c_x11.CurrentTime);
|
|
210
|
+
_ = c_x11.XTestFakeButtonEvent(display, button_code, c_x11.False, c_x11.CurrentTime);
|
|
211
|
+
}
|
|
212
|
+
_ = c_x11.XFlush(display);
|
|
213
|
+
}
|