Skip to content

Commit 3457957

Browse files
committed
Add typed browser launch controls
1 parent 973cdef commit 3457957

3 files changed

Lines changed: 184 additions & 32 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,12 @@ standard macOS application bundles, and executable candidates on other
122122
platforms without opening the selected browser.
123123

124124
`Window.openWithBrowser(&running, options)` launches a selected `Browser`
125-
with an optional full executable path and additional argv. Chromium-family
126-
browsers receive an `--app=` URL argument; Firefox receives `-new-window`.
127-
The returned `BrowserProcessId`, also available through
125+
with an optional full executable path, additional argv, and typed kiosk,
126+
window size, window position, and high-contrast controls. Chromium-family
127+
browsers support all four controls; Firefox supports kiosk mode. Unsupported
128+
browser and control combinations return `error.UnsupportedBrowserControl`.
129+
Chromium-family browsers receive an `--app=` URL argument; Firefox receives
130+
`-new-window`. The returned `BrowserProcessId`, also available through
128131
`Window.browserProcessId()`, is a PID on POSIX and a process handle on
129132
Windows. Each window retains at most one launched child; launching another
130133
replaces it, and `Running.stop()` kills and reaps every retained child.

docs/PURE_ZIG_REFACTOR.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ deleted.
3030
| Server and security | HTTP, WebSocket, TLS, loopback/public policy, capabilities, Origin checks, cookies, and protocol limits are implemented. |
3131
| Browser bridge | Bindings, typed arguments and replies, events, deferred replies, JavaScript evaluation, raw data, navigation, and multiple clients are implemented. |
3232
| Content and lifecycle | HTML, directories, custom handlers, external URLs, runtime content replacement, default directories, favicons, directory monitoring, logging, and deterministic shutdown are implemented. |
33-
| Browser integration | Default URL opening, browser discovery, explicit browser selection, custom executables and argv, direct child tracking, replacement, and shutdown cleanup are implemented. |
33+
| Browser integration | Default URL opening, browser discovery, explicit browser selection, typed launch controls, custom executables and argv, direct child tracking, replacement, and shutdown cleanup are implemented. |
3434
| Current validation | `zig build test`, native builds, Windows x86_64 builds, macOS aarch64 builds, and Windows/macOS test-module cross-compilation pass. |
3535

3636
Remaining work is limited to browser window controls and geometry, managed
@@ -321,10 +321,10 @@ implementations.
321321

322322
| Upstream API | Current gap |
323323
|---|---|
324-
| `webui_set_kiosk()`, `webui_focus()`, `webui_minimize()`, `webui_maximize()`, `webui_set_hide()` | Browser window mode and lifecycle controls are not implemented. |
325-
| `webui_set_resizable()`, `webui_set_size()`, `webui_set_minimum_size()`, `webui_set_position()`, `webui_set_center()` | Browser window geometry controls are not implemented. |
324+
| `webui_focus()`, `webui_minimize()`, `webui_maximize()`, `webui_set_hide()` | Runtime browser window lifecycle controls are not implemented. |
325+
| `webui_set_resizable()`, `webui_set_minimum_size()`, `webui_set_center()` | The remaining browser window geometry controls are not implemented. |
326326
| `webui_set_frameless()`, `webui_set_transparent()` | Frameless and transparent browser window modes are not implemented. |
327-
| `webui_set_high_contrast()`, `webui_is_high_contrast()` | High-contrast mode control and detection are not implemented. |
327+
| `webui_is_high_contrast()` | Portable host high-contrast detection is not implemented. |
328328
| `webui_set_profile()`, `webui_delete_profile()`, `webui_delete_all_profiles()` | Managed browser profiles are not implemented. |
329329
| `webui_set_proxy()` | Browser proxy configuration is not implemented. |
330330
| `webui_get_parent_process_id()` | A portable parent-process numeric ID accessor is not implemented. |
@@ -354,6 +354,7 @@ not implementation gaps:
354354
| `webui_open_url()` | `openUrl()` safely passes a non-empty URL as one argument to the platform default opener. |
355355
| `webui_get_best_browser()`, `webui_browser_exist()` | `bestBrowser()` and `browserExists()` discover registered or executable browser candidates through the public `Browser` enum. |
356356
| `webui_show_browser()`, `webui_set_browser_folder()`, `webui_set_custom_parameters()` | `Window.openWithBrowser()` accepts a `BrowserLaunchOptions` value with an explicit browser, optional full executable path, and additional argv. |
357+
| `webui_set_kiosk()`, `webui_set_size()`, `webui_set_position()`, `webui_set_high_contrast()` | Typed `BrowserLaunchOptions` generate supported Chromium-family launch controls; Firefox also supports kiosk mode. Unsupported combinations return an error. |
357358
| `webui_get_child_process_id()` | `Window.openWithBrowser()` returns the retained direct child's `BrowserProcessId`; `Window.browserProcessId()` retrieves it later. |
358359
| `webui_set_default_root_folder()` | `App.Options.default_directory` supplies directory content to windows created without explicit content. |
359360
| `webui_set_config(folder_monitor)` | `App.Options.folder_monitor_interval` enables portable recursive directory polling and reloads the affected window's connected clients. |
@@ -429,11 +430,13 @@ and shutdown cleanup are implemented.
429430
This completes the browser discovery, selection, custom-parameter, and direct
430431
child tracking methods in the ledger.
431432

432-
### Browser window controls
433+
### Browser window controls (partial)
433434

434-
- Implement kiosk, focus, minimize, maximize, hidden, resizable, geometry,
435-
frameless, transparent, and high-contrast controls where the selected
436-
browser and platform support them.
435+
- Typed kiosk, size, position, and high-contrast launch controls are
436+
implemented where the selected browser supports them.
437+
- Implement focus, minimize, maximize, hidden, resizable, minimum-size,
438+
centering, frameless, and transparent controls where the selected browser
439+
and platform support them.
437440
- Implement managed profiles and proxy configuration.
438441

439442
This completes the window control, profile, and proxy methods in the ledger.
@@ -516,6 +519,6 @@ zig build -Dtarget=aarch64-macos
516519

517520
Continue capability parity:
518521

519-
1. Add typed browser launch controls for kiosk, hidden, high-contrast,
520-
resizable, size, and position behavior.
521-
2. Add managed browser profiles and proxy configuration.
522+
1. Add managed browser profiles and proxy configuration.
523+
2. Implement or explicitly reject the remaining platform-specific runtime
524+
window controls.

src/browser.zig

Lines changed: 164 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,29 @@ pub const Browser = enum {
1616
};
1717

1818
pub const LaunchOptions = struct {
19+
pub const Size = struct {
20+
width: u32,
21+
height: u32,
22+
};
23+
24+
pub const Position = struct {
25+
x: i32,
26+
y: i32,
27+
};
28+
1929
browser: Browser,
2030
/// Full path or PATH-resolvable executable name. Null uses discovery.
2131
executable: ?[]const u8 = null,
2232
/// Additional arguments inserted before the browser URL argument.
2333
arguments: []const []const u8 = &.{},
34+
/// Start in kiosk mode. Supported by Chromium-family browsers and Firefox.
35+
kiosk: bool = false,
36+
/// Initial outer window size. Supported by Chromium-family browsers.
37+
size: ?Size = null,
38+
/// Initial window position. Supported by Chromium-family browsers.
39+
position: ?Position = null,
40+
/// Force native high-contrast UI. Supported by Chromium-family browsers.
41+
high_contrast: bool = false,
2442
};
2543

2644
/// PID on POSIX and a process handle on Windows.
@@ -73,6 +91,7 @@ pub fn launch(
7391
if (url.len == 0) return error.InvalidUrl;
7492
if (options.executable) |executable|
7593
if (executable.len == 0) return error.InvalidBrowserExecutable;
94+
try validateLaunchOptions(options);
7695

7796
const discovered = if (options.executable == null)
7897
try resolveExecutable(gpa, io, options.browser) orelse
@@ -82,32 +101,82 @@ pub fn launch(
82101
defer if (discovered) |executable| gpa.free(executable);
83102
const executable = options.executable orelse discovered.?;
84103

85-
var argv: std.ArrayList([]const u8) = .empty;
86-
defer argv.deinit(gpa);
87-
try argv.append(gpa, executable);
88-
try argv.appendSlice(gpa, options.arguments);
89-
const app_url = switch (options.browser) {
90-
.firefox, .safari => null,
91-
else => try std.fmt.allocPrint(gpa, "--app={s}", .{url}),
92-
};
93-
defer if (app_url) |argument| gpa.free(argument);
94-
switch (options.browser) {
95-
.firefox => {
96-
try argv.append(gpa, "-new-window");
97-
try argv.append(gpa, url);
98-
},
99-
.safari => try argv.append(gpa, url),
100-
else => try argv.append(gpa, app_url.?),
101-
}
104+
var arena = std.heap.ArenaAllocator.init(gpa);
105+
defer arena.deinit();
106+
const argv = try buildLaunchArgv(
107+
arena.allocator(),
108+
executable,
109+
url,
110+
options,
111+
);
102112

103113
return std.process.spawn(io, .{
104-
.argv = argv.items,
114+
.argv = argv,
105115
.stdin = .ignore,
106116
.stdout = .ignore,
107117
.stderr = .ignore,
108118
});
109119
}
110120

121+
fn buildLaunchArgv(
122+
allocator: std.mem.Allocator,
123+
executable: []const u8,
124+
url: []const u8,
125+
options: LaunchOptions,
126+
) ![]const []const u8 {
127+
try validateLaunchOptions(options);
128+
129+
var argv: std.ArrayList([]const u8) = .empty;
130+
try argv.append(allocator, executable);
131+
try argv.appendSlice(allocator, options.arguments);
132+
if (options.kiosk) try argv.append(allocator, "--kiosk");
133+
if (options.size) |size| try argv.append(
134+
allocator,
135+
try std.fmt.allocPrint(
136+
allocator,
137+
"--window-size={d},{d}",
138+
.{ size.width, size.height },
139+
),
140+
);
141+
if (options.position) |position| try argv.append(
142+
allocator,
143+
try std.fmt.allocPrint(
144+
allocator,
145+
"--window-position={d},{d}",
146+
.{ position.x, position.y },
147+
),
148+
);
149+
if (options.high_contrast)
150+
try argv.append(allocator, "--force-high-contrast");
151+
switch (options.browser) {
152+
.firefox => {
153+
try argv.append(allocator, "-new-window");
154+
try argv.append(allocator, url);
155+
},
156+
.safari => try argv.append(allocator, url),
157+
else => try argv.append(
158+
allocator,
159+
try std.fmt.allocPrint(allocator, "--app={s}", .{url}),
160+
),
161+
}
162+
return argv.toOwnedSlice(allocator);
163+
}
164+
165+
fn validateLaunchOptions(options: LaunchOptions) !void {
166+
if (options.size) |size|
167+
if (size.width == 0 or size.height == 0)
168+
return error.InvalidWindowSize;
169+
switch (options.browser) {
170+
.firefox => if (options.size != null or
171+
options.position != null or options.high_contrast)
172+
return error.UnsupportedBrowserControl,
173+
.safari => if (options.kiosk or options.size != null or
174+
options.position != null or options.high_contrast)
175+
return error.UnsupportedBrowserControl,
176+
else => {},
177+
}
178+
}
179+
111180
fn commandSucceeds(
112181
gpa: std.mem.Allocator,
113182
io: std.Io,
@@ -334,6 +403,83 @@ fn preferredBrowsers() []const Browser {
334403
};
335404
}
336405

406+
test "typed browser controls build supported argv" {
407+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
408+
defer arena.deinit();
409+
410+
const chromium = try buildLaunchArgv(
411+
arena.allocator(),
412+
"/browser",
413+
"https://127.0.0.1/",
414+
.{
415+
.browser = .chromium,
416+
.arguments = &.{"--guest"},
417+
.kiosk = true,
418+
.size = .{ .width = 1280, .height = 720 },
419+
.position = .{ .x = -20, .y = 30 },
420+
.high_contrast = true,
421+
},
422+
);
423+
const expected_chromium: []const []const u8 = &.{
424+
"/browser",
425+
"--guest",
426+
"--kiosk",
427+
"--window-size=1280,720",
428+
"--window-position=-20,30",
429+
"--force-high-contrast",
430+
"--app=https://127.0.0.1/",
431+
};
432+
try std.testing.expectEqualDeep(expected_chromium, chromium);
433+
434+
const firefox = try buildLaunchArgv(
435+
arena.allocator(),
436+
"/firefox",
437+
"https://127.0.0.1/",
438+
.{ .browser = .firefox, .kiosk = true },
439+
);
440+
const expected_firefox: []const []const u8 = &.{
441+
"/firefox",
442+
"--kiosk",
443+
"-new-window",
444+
"https://127.0.0.1/",
445+
};
446+
try std.testing.expectEqualDeep(expected_firefox, firefox);
447+
448+
try std.testing.expectError(
449+
error.InvalidWindowSize,
450+
buildLaunchArgv(
451+
arena.allocator(),
452+
"/browser",
453+
"https://127.0.0.1/",
454+
.{
455+
.browser = .chromium,
456+
.size = .{ .width = 0, .height = 720 },
457+
},
458+
),
459+
);
460+
try std.testing.expectError(
461+
error.UnsupportedBrowserControl,
462+
buildLaunchArgv(
463+
arena.allocator(),
464+
"/firefox",
465+
"https://127.0.0.1/",
466+
.{
467+
.browser = .firefox,
468+
.position = .{ .x = 0, .y = 0 },
469+
},
470+
),
471+
);
472+
try std.testing.expectError(
473+
error.UnsupportedBrowserControl,
474+
buildLaunchArgv(
475+
arena.allocator(),
476+
"/safari",
477+
"https://127.0.0.1/",
478+
.{ .browser = .safari, .kiosk = true },
479+
),
480+
);
481+
}
482+
337483
test "browser candidates and preference order cover every browser" {
338484
try std.testing.expectError(
339485
error.InvalidUrl,

0 commit comments

Comments
 (0)