ReviewOS

also looking at this

craft-native/craft

feat: add Linux and Windows named-window routing

#325
Merged glennmichael123 wants to merge codex/multi-window-linux-windows into main
29 files +2250 -200

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/zig/build.zigmodified+10-0
Changes to packages/zig/build.zig
@@ -1287,6 +1287,14 @@ pub fn build(b: *std.Build) void {
12871287 }),
12881288 });
12891289
1290 const desktop_window_reads_tests = b.addTest(.{
1291 .root_module = b.createModule(.{
1292 .root_source_file = b.path("src/desktop_window_reads.zig"),
1293 .target = target,
1294 .optimize = optimize,
1295 }),
1296 });
1297
12901298 const desktop_script_encoding_tests = b.addTest(.{
12911299 .root_module = b.createModule(.{
12921300 .root_source_file = b.path("src/desktop_script_encoding.zig"),
@@ -1753,6 +1761,7 @@ pub fn build(b: *std.Build) void {
17531761 const run_desktop_bridge_envelope_tests = b.addRunArtifact(desktop_bridge_envelope_tests);
17541762 const run_desktop_bridge_text_tests = b.addRunArtifact(desktop_bridge_text_tests);
17551763 const run_desktop_window_events_tests = b.addRunArtifact(desktop_window_events_tests);
1764 const run_desktop_window_reads_tests = b.addRunArtifact(desktop_window_reads_tests);
17561765 const run_desktop_script_encoding_tests = b.addRunArtifact(desktop_script_encoding_tests);
17571766 const run_external_link_tests = b.addRunArtifact(external_link_tests);
17581767 const run_webview_recovery_tests = b.addRunArtifact(webview_recovery_tests);
@@ -1907,6 +1916,7 @@ pub fn build(b: *std.Build) void {
19071916 test_step.dependOn(&run_desktop_bridge_envelope_tests.step);
19081917 test_step.dependOn(&run_desktop_bridge_text_tests.step);
19091918 test_step.dependOn(&run_desktop_window_events_tests.step);
1919 test_step.dependOn(&run_desktop_window_reads_tests.step);
19101920 test_step.dependOn(&run_desktop_script_encoding_tests.step);
19111921 test_step.dependOn(&run_external_link_tests.step);
19121922 test_step.dependOn(&run_webview_recovery_tests.step);
packages/zig/src/desktop_window_reads.zigadded+70-0
Changes to packages/zig/src/desktop_window_reads.zig
@@ -0,0 +1,70 @@
1//! Results for read-only desktop window actions. The native caller supplies
2//! live OS values; this module preserves the SDK's JSON reply shapes.
3
4const std = ;
5const bridge_error = ;
6const desktop_window_registry = ;
7
8pub fn string(allocator: std.mem.Allocator, value: ?[]const u8) ![]u8 {
9 const text = value orelse return allocator.dupe(u8, "null");
10 var json: std.ArrayListUnmanaged(u8) = .empty;
11 errdefer json.deinit(allocator);
12 try json.append(allocator, '"');
13 try bridge_error.appendJsonEscaped(allocator, &json, text);
14 try json.append(allocator, '"');
15 return json.toOwnedSlice(allocator);
16}
17
18pub fn geometry(allocator: std.mem.Allocator, action: []const u8, bounds: desktop_window_registry.Geometry) ![]u8 {
19 if (std.mem.eql(u8, action, "getSize"))
20 return std.fmt.allocPrint(allocator, "", .{ bounds.width, bounds.height });
21 if (std.mem.eql(u8, action, "getPosition"))
22 return std.fmt.allocPrint(allocator, "", .{ bounds.x, bounds.y });
23 if (std.mem.eql(u8, action, "getBounds"))
24 return std.fmt.allocPrint(allocator, "", .{ bounds.x, bounds.y, bounds.width, bounds.height });
25 return error.UnknownAction;
26}
27
28/// Never return a name for an OS window that has already been destroyed or
29/// belongs to another process. Every page calls its own window `main`.
30pub fn focusedName(
31 live: *const desktop_window_registry.Registry,
32 sender: usize,
33 focused: usize,
34 named: ?[]const u8,
35) ?[]const u8 {
36 if (live.byWindow(focused) == null) return null;
37 if (sender == focused) return "main";
38 return named;
39}
40
41test "read results preserve SDK shapes and escape names" {
42 const allocator = std.testing.allocator;
43 const name = try string(allocator, "set\"tings");
44 defer allocator.free(name);
45 try std.testing.expectEqualStrings("\"set\\\"tings\"", name);
46 const absent = try string(allocator, null);
47 defer allocator.free(absent);
48 try std.testing.expectEqualStrings("null", absent);
49 const bounds: desktop_window_registry.Geometry = .{ .x = -20, .y = 30, .width = 800, .height = 600 };
50 const size = try geometry(allocator, "getSize", bounds);
51 defer allocator.free(size);
52 try std.testing.expectEqualStrings("{\"width\":800,\"height\":600}", size);
53 const position = try geometry(allocator, "getPosition", bounds);
54 defer allocator.free(position);
55 try std.testing.expectEqualStrings("{\"x\":-20,\"y\":30}", position);
56 const full = try geometry(allocator, "getBounds", bounds);
57 defer allocator.free(full);
58 try std.testing.expectEqualStrings("{\"x\":-20,\"y\":30,\"width\":800,\"height\":600}", full);
59}
60
61test "focused name is scoped to a live window and the sender alias" {
62 var live: desktop_window_registry.Registry = .{};
63 _ = live.remember(0x1000, 0x1001);
64 _ = live.remember(0x2000, 0x2001);
65 try std.testing.expectEqualStrings("main", focusedName(&live, 0x1000, 0x1000, null).?);
66 try std.testing.expectEqualStrings("settings", focusedName(&live, 0x1000, 0x2000, "settings").?);
67 try std.testing.expect(focusedName(&live, 0x1000, 0x3000, "other") == null);
68 _ = live.forgetWindow(0x2000);
69 try std.testing.expect(focusedName(&live, 0x1000, 0x2000, "settings") == null);
70}
packages/zig/src/linux.zigmodified+42-0
Changes to packages/zig/src/linux.zig
@@ -3,6 +3,7 @@ const bridge_error = ;
33const desktop_bridge_envelope = ;
44const desktop_window_registry = ;
55const desktop_window_events = ;
6const desktop_window_reads = ;
67const json_utils = ;
78const request_context = ;
89const window_context = ;
@@ -33,6 +34,8 @@ pub extern "c" fn gtk_widget_show(widget: *anyopaque) void;
3334pub extern "c" fn gtk_window_set_position(window: *anyopaque, x: c_int, y: c_int) void;
3435pub extern "c" fn gtk_window_get_position(window: *anyopaque, x: *c_int, y: *c_int) void;
3536pub extern "c" fn gtk_window_get_size(window: *anyopaque, width: *c_int, height: *c_int) void;
37pub extern "c" fn gtk_window_get_title(window: *anyopaque) ?[*:0]const u8;
38pub extern "c" fn gtk_window_is_active(window: *anyopaque) c_int;
3639pub extern "c" fn gtk_widget_get_window(widget: *anyopaque) ?*anyopaque;
3740pub extern "c" fn gdk_window_get_state(window: *anyopaque) c_uint;
3841
@@ -416,9 +419,48 @@ fn targetWindow(data: ?[]const u8) !desktop_window_registry.Entry {
416419 return desktop_windows.byWindow(window_context.current() orelse return error.WindowHandleNotSet) orelse error.WindowHandleNotSet;
417420}
418421
422fn windowGeometry(window: *anyopaque) !desktop_window_registry.Geometry {
423 var x: c_int = 0;
424 var y: c_int = 0;
425 var width: c_int = 0;
426 var height: c_int = 0;
427 gtk_window_get_position(window, &x, &y);
428 gtk_window_get_size(window, &width, &height);
429 if (width < 0 or height < 0) return error.NativeCallFailed;
430 return .{ .x = x, .y = y, .width = @intCast(width), .height = @intCast(height) };
431}
432
433fn sendWindowRead(action: []const u8, data: ?[]const u8) !void {
434 const allocator = std.heap.c_allocator;
435 const entry = try targetWindow(data);
436 const window: *anyopaque = @ptrFromInt(entry.window);
437 const json = if (std.mem.eql(u8, action, "getTitle")) blk: {
438 const title = gtk_window_get_title(window);
439 break :blk try desktop_window_reads.string(allocator, if (title) |text| std.mem.span(text) else "");
440 } else if (std.mem.eql(u8, action, "getFocused")) blk: {
441 var focused: usize = 0;
442 for (desktop_windows.entries) |slot| {
443 if (slot) |candidate| {
444 if (gtk_window_is_active(@ptrFromInt(candidate.window)) != 0) {
445 focused = candidate.window;
446 break;
447 }
448 }
449 }
450 const name = desktop_window_reads.focusedName(&desktop_windows, entry.window, focused, window_registry.nameOf(focused));
451 break :blk try desktop_window_reads.string(allocator, name);
452 } else try desktop_window_reads.geometry(allocator, action, try windowGeometry(window));
453 defer allocator.free(json);
454 bridge_error.sendResultToJS(allocator, action, json);
455}
456
419457fn handleWindowAction(action: []const u8, data: ?[]const u8) !void {
420458 if (std.mem.eql(u8, action, "open") or std.mem.eql(u8, action, "create"))
421459 return openNamedWindow(action, data);
460 if (std.mem.eql(u8, action, "getTitle") or std.mem.eql(u8, action, "getSize") or
461 std.mem.eql(u8, action, "getPosition") or std.mem.eql(u8, action, "getBounds") or
462 std.mem.eql(u8, action, "getFocused"))
463 return sendWindowRead(action, data);
422464
423465 const entry = try targetWindow(data);
424466 const window: *anyopaque = @ptrFromInt(entry.window);
packages/zig/src/windows.zigmodified+50-2
Changes to packages/zig/src/windows.zig
@@ -4,6 +4,7 @@ const desktop_bridge_envelope = ;
44const desktop_bridge_text = ;
55const desktop_script_encoding = ;
66const desktop_window_events = ;
7const desktop_window_reads = ;
78const desktop_window_registry = ;
89const json_utils = ;
910const request_context = ;
@@ -123,6 +124,9 @@ pub extern "user32" fn DefWindowProcW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lPa
123124pub extern "user32" fn PostQuitMessage(nExitCode: c_int) callconv(.c) void;
124125pub extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.c) BOOL;
125126pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.c) BOOL;
127pub extern "user32" fn GetWindowTextLengthW(hWnd: HWND) callconv(.c) c_int;
128pub extern "user32" fn GetWindowTextW(hWnd: HWND, lpString: LPWSTR, nMaxCount: c_int) callconv(.c) c_int;
129pub extern "user32" fn GetForegroundWindow() callconv(.c) ?HWND;
126130pub extern "user32" fn SetWindowPos(hWnd: HWND, hWndInsertAfter: ?HWND, X: c_int, Y: c_int, cx: c_int, cy: c_int, uFlags: UINT) callconv(.c) BOOL;
127131pub extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: LPCWSTR) callconv(.c) ?*anyopaque;
128132pub extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.c) BOOL;
@@ -979,9 +983,53 @@ fn targetWindow(data: ?[]const u8) !desktop_window_registry.Entry {
979983 return desktop_windows.byWindow(window_context.current() orelse return error.WindowHandleNotSet) orelse error.WindowHandleNotSet;
980984}
981985
986fn windowGeometry(hwnd: HWND) !desktop_window_registry.Geometry {
987 var rect: RECT = undefined;
988 if (GetWindowRect(hwnd, &rect) == 0) return error.NativeCallFailed;
989 const width = @as(i64, rect.right) - @as(i64, rect.left);
990 const height = @as(i64, rect.bottom) - @as(i64, rect.top);
991 if (width < 0 or height < 0 or width > std.math.maxInt(u32) or height > std.math.maxInt(u32))
992 return error.NativeCallFailed;
993 return .{ .x = @intCast(rect.left), .y = @intCast(rect.top), .width = @intCast(width), .height = @intCast(height) };
994}
995
996fn windowTitle(allocator: std.mem.Allocator, hwnd: HWND) ![:0]u8 {
997 const length = GetWindowTextLengthW(hwnd);
998 if (length < 0) return error.NativeCallFailed;
999 if (@as(usize, @intCast(length)) >= desktop_bridge_envelope.max_message_bytes)
1000 return error.MessageTooLarge;
1001 const wide = try allocator.alloc(u16, @as(usize, @intCast(length)) + 1);
1002 defer allocator.free(wide);
1003 const copied = GetWindowTextW(hwnd, @ptrCast(wide.ptr), @intCast(wide.len));
1004 if (copied < 0) return error.NativeCallFailed;
1005 const utf8 = try desktop_bridge_text.fromUtf16(allocator, wide[0..@intCast(copied)]);
1006 return utf8;
1007}
1008
1009fn sendWindowRead(action: []const u8, data: ?[]const u8) !void {
1010 const allocator = std.heap.c_allocator;
1011 const entry = try targetWindow(data);
1012 const hwnd: HWND = @ptrFromInt(entry.window);
1013 const json = if (std.mem.eql(u8, action, "getTitle")) blk: {
1014 const title = try windowTitle(allocator, hwnd);
1015 defer allocator.free(title);
1016 break :blk try desktop_window_reads.string(allocator, title);
1017 } else if (std.mem.eql(u8, action, "getFocused")) blk: {
1018 const focused = if (GetForegroundWindow()) |window| @intFromPtr(window) else 0;
1019 const name = desktop_window_reads.focusedName(&desktop_windows, entry.window, focused, window_registry.nameOf(focused));
1020 break :blk try desktop_window_reads.string(allocator, name);
1021 } else try desktop_window_reads.geometry(allocator, action, try windowGeometry(hwnd));
1022 defer allocator.free(json);
1023 bridge_error.sendResultToJS(allocator, action, json);
1024}
1025
9821026fn handleWindowAction(action: []const u8, data: ?[]const u8) !void {
9831027 if (std.mem.eql(u8, action, "open") or std.mem.eql(u8, action, "create"))
9841028 return openNamedWindow(action, data);
1029 if (std.mem.eql(u8, action, "getTitle") or std.mem.eql(u8, action, "getSize") or
1030 std.mem.eql(u8, action, "getPosition") or std.mem.eql(u8, action, "getBounds") or
1031 std.mem.eql(u8, action, "getFocused"))
1032 return sendWindowRead(action, data);
9851033
9861034 const entry = try targetWindow(data);
9871035 const hwnd: HWND = @ptrFromInt(entry.window);
@@ -1007,7 +1055,7 @@ fn handleWindowAction(action: []const u8, data: ?[]const u8) !void {
10071055 defer std.heap.c_allocator.free(title);
10081056 if (std.mem.indexOfScalar(u8, title, 0) != null) return error.InvalidParameter;
10091057 const title_wide = utf8ToUtf16Z(256, title) catch return error.InvalidParameter;
1010 if (SetWindowTextW(hwnd, &title_wide) == 0) return error.NativeCallFailed;
1058 if (SetWindowTextW(hwnd, @ptrCast(&title_wide)) == 0) return error.NativeCallFailed;
10111059 } else if (std.mem.eql(u8, action, "setSize")) {
10121060 const json = data orelse return error.MissingData;
10131061 const width = json_utils.getInt(u32, json, "width") orelse return error.InvalidParameter;
@@ -1264,7 +1312,7 @@ pub const Window = struct {
12641312 pub fn setTitle(self: *Window, title: []const u8) void {
12651313 if (self.liveEntry() == null) return;
12661314 const title_wide_buf = utf8ToUtf16Z(256, title) catch return;
1267 _ = SetWindowTextW(self.hwnd, &title_wide_buf);
1315 _ = SetWindowTextW(self.hwnd, @ptrCast(&title_wide_buf));
12681316 self.title = title;
12691317 }
12701318