{"id":52010785,"url":"https://github.com/webui-dev/pure-zig-webui","last_synced_at":"2026-07-31T19:30:31.172Z","repository":{"id":373753435,"uuid":"1316163802","full_name":"webui-dev/pure-zig-webui","owner":"webui-dev","description":"Use any web browser or WebView as GUI, with Zig in the backend and modern web technologies in the frontend.","archived":false,"fork":false,"pushed_at":"2026-07-29T18:58:07.000Z","size":86,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-29T20:06:21.833Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/webui-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null,"disclosure":null}},"created_at":"2026-07-29T13:26:47.000Z","updated_at":"2026-07-29T18:58:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/webui-dev/pure-zig-webui","commit_stats":null,"previous_names":["webui-dev/pure-zig-webui"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/webui-dev/pure-zig-webui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webui-dev%2Fpure-zig-webui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webui-dev%2Fpure-zig-webui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webui-dev%2Fpure-zig-webui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webui-dev%2Fpure-zig-webui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webui-dev","download_url":"https://codeload.github.com/webui-dev/pure-zig-webui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webui-dev%2Fpure-zig-webui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36131433,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-31T02:00:06.731Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-31T19:30:27.838Z","updated_at":"2026-07-31T19:30:31.148Z","avatar_url":"https://github.com/webui-dev.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zig-webui\n\n\u003e [!WARNING]\n\u003e This is an experimental project under active development and is not suitable\n\u003e for production use.\n\nzig-webui is being rebuilt as a pure Zig WebUI implementation. The core no\nlonger compiles or links the upstream WebUI C library or CivetWeb.\n[Linsang](https://github.com/jinzhongjia/Linsang) provides HTTP and WebSocket\nsupport.\n\nThe current phase provides:\n\n- Zig 0.16;\n- one `App`, multiple isolated windows, and automatic port selection;\n- embedded HTML, static directories, custom resources, external URLs, and a\n  built-in JavaScript bridge;\n- application-wide default static directories for windows without content;\n- optional recursive directory monitoring with per-window browser reloads;\n- inline and file-backed per-window favicons;\n- runtime content and resource-handler replacement through\n  `Window.setContent()`;\n- targeted runtime content replacement through `Client.show()`;\n- explicit browser connection waiting and timeout through\n  `Window.waitForConnection()`;\n- window connected/shown state through `Window.isShown()`;\n- JavaScript calls to Zig bindings with return values;\n- typed integer, float, and boolean call arguments and replies;\n- owned one-shot delayed binding replies through `Call.deferReply()`;\n- window and targeted `Call.client` calls to JavaScript with results, errors,\n  timeouts, and stale-client detection;\n- targeted client navigation, close, and raw binary delivery;\n- bounded multi-client windows through `WindowOptions.max_clients`;\n- bounded concurrent evaluations through\n  `WindowOptions.max_pending_evals`;\n- bounded delayed replies through `WindowOptions.max_pending_replies`;\n- explicit connection, WebSocket message, call, argument, binding, event, and\n  script limits through `App.Options.limits`;\n- window navigation, close, raw-data, and JavaScript broadcasts with\n  per-client results;\n- targeted and broadcast fire-and-forget JavaScript through `Client.run` and\n  `Window.run`;\n- connected, disconnected, click, and intercepted navigation events through\n  `Window.onEvent`;\n- per-window serial or concurrent binding and event execution through\n  `Window.setEventMode()`;\n- bounded concurrent handlers through `WindowOptions.max_pending_events`;\n- caller-provided internal logging through `App.Options.logger`;\n- same-origin WebSocket validation for hosted content and external-page Origin\n  validation for `.external_url`;\n- optional path-scoped `HttpOnly` cookie authorization through\n  `App.Options.use_cookies`;\n- optional Deno, Node.js, or Bun interpretation of served `.js` and `.ts`\n  files through `App.WindowOptions.runtime`;\n- loopback-only listening by default and caller-provided TLS for explicit\n  public listening;\n- app-mode window launching through installed-browser discovery, with a\n  managed per-browser profile and OS URL opening as the fallback;\n- explicit browser launching with custom executable paths and argv;\n- per-window kiosk and headless modes plus persistent initial and runtime size\n  and position;\n- per-window Chromium forced-color control and browser-native high-contrast\n  detection;\n- per-window browser profile directories with deletable managed profiles,\n  and Chromium-family proxy rules;\n- per-window browser child identifiers and deterministic process cleanup;\n- current backend process ID through `parentProcessId()`;\n- default-browser launching and deterministic shutdown.\n\n```zig\nconst std = @import(\"std\");\nconst webui = @import(\"webui\");\n\nfn hello(call: *webui.Call, _: ?*anyopaque) !void {\n    try call.reply(\"Hello from Zig\");\n}\n\npub fn main(init: std.process.Init) !void {\n    const gpa = init.gpa;\n    const io = init.io;\n\n    var app = webui.App.init(gpa, .{});\n    defer app.deinit();\n    const window = try app.createWindow(.{\n        .content = .{\n            .html =\n            \\\\\u003cbutton onclick=\"webui.call('hello').then(alert)\"\u003eCall Zig\u003c/button\u003e\n            \\\\\u003cscript src=\"webui.js\"\u003e\u003c/script\u003e\n            ,\n        },\n    });\n    try window.bind(\"hello\", hello, null);\n\n    var running = try app.start(io);\n    defer running.stop() catch {};\n    try window.open(io, \u0026running);\n\n    var result_buffer: [64]u8 = undefined;\n    const result = try window.eval(\n        io,\n        \"return 6 * 7\",\n        \u0026result_buffer,\n        .fromSeconds(5),\n    );\n    switch (result) {\n        .value =\u003e |value| std.debug.print(\"JavaScript: {s}\\n\", .{value}),\n        .javascript_error =\u003e |message| std.log.err(\"JavaScript: {s}\", .{message}),\n    }\n    try running.wait();\n}\n```\n\n```sh\nzig build test\nzig build\nzig build run\n```\n\n`zig build test` uses Node's built-in test runner for the browser bridge when\nNode is available and otherwise skips those tests with a warning. Building and\nusing the library does not require Node or npm. CI and release validation\nshould run `zig build test-bridge`, which fails when Node is unavailable.\n`Window.evalAll` returns owned results; call `deinit` on them after consuming\nevery per-client outcome.\n\n`Window.open()` discovers the best installed browser and launches it as a\nstandalone app window, exactly like `Window.openWithBrowser()` with that\nbrowser. It returns immediately. When no known browser is installed it hands\nthe URL to the OS default handler, which opens an ordinary tab and therefore\nreturns `error.ExplicitBrowserRequired` when any window control is active.\nCall `Window.waitForConnection(io, timeout)` when startup must wait for a\nbrowser; it returns the first connected `Client`. `Window.eval()` uses the same\ntotal timeout for connection waiting and JavaScript execution.\n\nCall `openUrl(gpa, io, url)` to open any non-empty URL with the OS default\nhandler. `browserExists(gpa, io, browser)` checks an explicit `Browser`, while\n`bestBrowser(gpa, io)` returns the first installed browser in the preferred\nplatform order or `null`. Discovery probes Windows application registration,\nstandard macOS application bundles, and executable candidates on other\nplatforms without opening the selected browser.\n\n`Window.openWithBrowser(\u0026running, options)` launches a selected `Browser`\nwith an optional full executable path and additional argv. Chromium-family\nbrowsers receive an `--app=` URL argument; Firefox receives `-new-window`.\nWhen `options.arguments` is empty, Chromium-family browsers also receive a set\nof default arguments that suppress first-run interstitials, extensions,\nbackground services, translation, sync, and proxies; a non-empty\n`options.arguments` replaces those defaults entirely.\nThe returned `BrowserProcessId`, also available through\n`Window.browserProcessId()`, is a PID on POSIX and a process handle on\nWindows. Each window retains at most one launched child; launching another\nreplaces it, and `Running.stop()` kills and reaps every retained child.\n\n`parentProcessId()` returns the numeric ID of the current Zig backend process,\nwhich is the parent of browsers launched directly by this package. It is\nprocess-wide and does not require a `Window`. Targets without a supported\nprocess-ID API return `error.UnsupportedPlatform`.\n\nSet `.kiosk`, `.hide`, `.size`, or `.position` in `App.WindowOptions` to\ncontrol the initial browser window. `.hide` launches the browser headless.\nChromium-family browsers support all four controls. Firefox supports kiosk,\nhide, and size but returns `error.UnsupportedBrowserControl` for position;\nSafari returns the same error for any of these controls. Width and height must\nbe non-zero, while positions may be negative for secondary displays. Only the\nOS-handler fallback inside `Window.open()` cannot honour these controls, and it\nreturns `error.ExplicitBrowserRequired` instead of ignoring them.\n\nSet `.high_contrast = false` in `App.WindowOptions` to disable Chromium's\nforced-color feature for that window. Firefox and Safari return\n`error.UnsupportedBrowserHighContrast` instead of ignoring this setting.\nThe browser-side `webui.isHighContrast()` detects active forced colors or a\nstronger contrast preference through native media queries and requires no\nexternal OS program.\n\nSet `.profile_directory` in `App.WindowOptions` to launch Chromium-family\nbrowsers with `--user-data-dir` or Firefox with `--profile`. Set\n`.proxy_server` for Chromium-family browsers to pass one `--proxy-server`\nargument without invoking a shell. The app copies both strings. Profile\ndirectories remain caller-managed and are never deleted by zig-webui.\nFirefox returns `error.UnsupportedBrowserProxy` for proxy configuration;\nSafari returns `error.UnsupportedBrowserProfile` or\n`error.UnsupportedBrowserProxy` instead of silently ignoring either option.\n\nWithout `.profile_directory`, Chromium-family launches get a managed profile\nunder the system temporary directory, such as\n`/tmp/.WebUI/WebUIChromeProfile`. A dedicated profile is what makes the app\nwindow independent: an already running browser instance otherwise adopts the\nURL, ignores every window argument, and lets the launched process exit\nimmediately. The browser creates the directory on first use and reuses it\nacross runs.\n\n`Window.deleteProfile(\u0026running)` removes the managed profile of the browser\nthat window launched and reports whether one existed; `deleteManagedProfile`\nand `deleteAllManagedProfiles` do the same without a `Window`, and\n`managedProfileDirectory(gpa, browser)` returns the path. Deletion only ever\ntouches the generated path: a window configured with `.profile_directory`\nreturns `error.CallerManagedProfile`, and caller-owned directories are never\nremoved.\n\n`Window.setSize(io, size)` and `Window.setPosition(io, position)` persist new\ngeometry, return the number of currently notified clients, and replay the\nlatest values to clients that connect later. Subsequent\n`Window.openWithBrowser()` calls use the updated values. Connected external\nbrowsers receive `window.resizeTo()` or `window.moveTo()` requests; browser\nsecurity policy may ignore those requests for ordinary tabs.\n\n`Window.setCenter(io)`, and `.center = true` in `App.WindowOptions`, centre\nthe window on the primary display. Only the browser knows the screen\ngeometry, so it computes the coordinates itself, which means centring takes\neffect once a client connects rather than at launch. Centring and an explicit\nposition are mutually exclusive: each one clears the other, and setting both\nin `App.WindowOptions` returns `error.ConflictingWindowPlacement`.\n\nServe a directory by setting\n`.content = .{ .directory = \"path/to/public\" }`. The path is opened when the\napp starts and closed when it stops. Custom resources receive `webui.Request`\nand `webui.Response` directly.\n\nSet `.runtime = .deno`, `.node_js`, or `.bun` in `App.WindowOptions` to run\nserved `.js` and `.ts` files through an external interpreter instead of\nsending them to the browser. A request for a directory resolves `index.ts`\nand then `index.js`. The interpreter is spawned as argv, never through a\nshell, and receives the script path followed by the raw query string, so a\nquery can never become a command. Standard output is answered as\n`text/plain` and bounded by `Limits.max_runtime_output`; a run is abandoned\nafter 30 seconds. Matching upstream, a missing interpreter, a timeout, or\noversized output answers an empty `200` so one absent runtime does not break\nthe page — every such case is reported through the window logger, as is a\nnon-zero exit status. Percent escapes in the request path are never decoded,\nso they cannot become path separators or hide a traversal.\n\nSet `App.Options.default_directory` to let windows created without `.content`\ninherit one static directory. Explicit window content takes precedence. A\nwindow without either setting returns `error.MissingContent`.\n\nSet `App.Options.folder_monitor_interval` to a positive `std.Io.Duration` to\nrecursively poll active directory content. A changed tree sends\n`location.reload();` to that window's connected clients. Monitoring is\ndisabled by default and stops with `Running.stop()`.\n\nUse `Window.setIcon(io, data, mime_type)` for in-memory favicon data or\n`Window.setIconFile(io, path)` for SVG, PNG, ICO, JPEG, GIF, WebP, or AVIF\nfiles. Embedded HTML receives a relative favicon link automatically.\nDirectory and custom pages can reference `favicon.ico` relative to the window\ncapability root.\n\n`Window.setContent(\u0026running, content)` prepares and installs new content, then\nnavigates every connected client to it and returns the number notified. An\ninvalid replacement leaves the current content unchanged. If client\nnotification fails, the prepared replacement remains installed.\n\n`Client.show(\u0026running, content)` installs the same window-wide content but\nnavigates only the selected client, matching upstream `webui_show_client()`.\nOther connected pages are not reloaded; later resource requests use the new\nwindow content.\n\n`Window.onEvent` installs one handler for browser lifecycle, click, and\nnavigation events. `Event.data` contains the element ID for clicks, the target\nURL for navigation, and is empty for connected or disconnected events.\nNavigation attempts are intercepted while an event handler is installed; call\n`Event.client.navigate` from the handler to continue them.\n\nHandlers run in `.serial` mode by default. `Window.setEventMode(.concurrent)`\nchanges newly received binding calls and browser events to independent tasks.\nConcurrent tasks own their event data, are bounded by\n`WindowOptions.max_pending_events`, and are canceled and joined by\n`Running.stop()`.\n\nSet `App.Options.logger` and optional `logger_user_data` to receive formatted\ninternal messages with a `std.log.Level`. The message slice is valid only\nduring the callback. The callback must be thread-safe when concurrent event\nhandling is enabled. Without a callback, messages use `std.log`.\n\n`Window.bind(\"button\", ...)` also dispatches clicks from elements with\n`id=\"button\"`, including elements added after the bridge loads. DOM click\nhandlers receive no arguments and their replies are ignored; explicit\n`webui.call(\"button\", ...)` remains available.\n\nBinding handlers can transfer an explicit `webui.call()` response beyond the\nhandler lifetime with `Call.deferReply()`. Complete the owned `PendingReply`\nonce with `reply()`, `replyInt()`, `replyFloat()`, or `replyBool()`, or call\n`deinit()` to abandon it. All pending replies must be completed or abandoned\nbefore `App.deinit()`.\n\nThe browser-side `webui` object also provides connection events, runtime\nlogging, Base64 helpers, navigation control, and native high-contrast media\nquery detection.\n\nUse `Client.run` or `Window.run` when JavaScript results and errors are not\nneeded. These methods use the protocol's `JS_QUICK` command and do not consume\npending evaluation slots.\n\nExternal pages use `.content = .{ .external_url = \"http://...\" }`.\n`Window.url` returns the external page, while `Window.bridgeUrl` returns the\ncapability-scoped script URL that the caller-owned page must load. The bridge\nconnects its WebSocket to the script's origin instead of the page's origin,\nand the server accepts the external page's Origin for that window.\n\nNon-loopback listening requires both explicit public mode and TLS:\n\n```zig\nvar app = webui.App.init(gpa, .{\n    .address = \"0.0.0.0\",\n    .public = true,\n    .use_cookies = true,\n    .tls = .{\n        .certificate_pem = @embedFile(\"certificate.pem\"),\n        .private_key_pem = @embedFile(\"private-key.pem\"),\n    },\n    .limits = .{\n        .max_connections = 128,\n        .max_unauthenticated_connections = 16,\n        .max_ws_message_size = 1 \u003c\u003c 20,\n    },\n});\n```\n\nThe certificate and private key are parsed by `App.start()` and released by\n`Running.stop()`. zig-webui never generates a self-signed certificate.\n\nSee the\n[pure Zig refactor plan](docs/PURE_ZIG_REFACTOR.md) for the complete scope and\nimplementation order.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebui-dev%2Fpure-zig-webui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebui-dev%2Fpure-zig-webui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebui-dev%2Fpure-zig-webui/lists"}