Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4c34f3f
feat: add the windows win32 ffi foundation with wstr and an import-sa…
ipfizz Jun 20, 2026
3583d2c
feat: add the windows win32 window primitive and cooperative message …
ipfizz Jun 20, 2026
d709ce2
feat: add the wincairo webkit2 ffi loader and string marshalling
ipfizz Jun 20, 2026
fad5206
feat: add the wincairo webkit view hosted in an hwnd with a renderer-…
ipfizz Jun 20, 2026
c4dec5f
feat: add the native-wndproc windows window with pump-routed close ha…
ipfizz Jun 21, 2026
9b30254
feat: assemble the windows backend so browserwindow and ipc work end …
ipfizz Jun 21, 2026
263c749
feat: surface windows lifecycle events via the pump poll
ipfizz Jun 21, 2026
5fa5634
feat: wire windows zoom, user-agent, and automatic webview resize
ipfizz Jun 21, 2026
1ed10be
feat: wire windows navigation lifecycle events via the webkit page na…
ipfizz Jun 21, 2026
fc8553d
fix: close the windows webkit window without crashing by clearing web…
ipfizz Jun 21, 2026
11f38e7
fix: exit the windows app cleanly by hard-terminating past webkit sta…
ipfizz Jun 21, 2026
159ab20
feat: wire windows fullscreen, opacity, resizable, and center
ipfizz Jun 21, 2026
20857af
feat: recognize windows as a supported platform and artifact os
ipfizz Jun 21, 2026
6cd5dbb
feat: add a windows distributable builder to the cli
ipfizz Jun 21, 2026
c12e415
feat: resolve the windows webkit engine from the store
ipfizz Jun 21, 2026
e7e69e9
feat: add a windows clipboard backend for text and html
ipfizz Jun 21, 2026
8407dab
fix: resolve windows app paths with native conventions instead of xdg
ipfizz Jun 21, 2026
ec8521c
feat: add a windows safestorage backend using dpapi
ipfizz Jun 21, 2026
621cfa3
test: cover single-instance argv hand-off on windows
ipfizz Jun 21, 2026
6967b41
feat: detect windows dark mode for nativetheme via the registry
ipfizz Jun 21, 2026
9f1a7c3
feat: add a windows globalshortcut backend via registerhotkey
ipfizz Jun 21, 2026
34e90ac
feat: add a windows screen backend via enumdisplaymonitors
ipfizz Jun 21, 2026
56234b9
feat: add a windows shell backend via shellexecutew
ipfizz Jun 21, 2026
83b19ac
feat: add a windows powersaveblocker via setthreadexecutionstate
ipfizz Jun 21, 2026
eed5926
fix: re-apply showwindow when the launcher suppresses the first show
ipfizz Jun 21, 2026
3ac3e65
chore: validate the windows backend on a windows-latest ci runner
ipfizz Jun 21, 2026
1707077
feat: add a windows powermonitor for suspend resume and lock events
ipfizz Jun 21, 2026
66f6e55
feat: add a windows tray backend via shell_notifyicon
ipfizz Jun 21, 2026
ff01c6d
docs: reflect the Windows backend (WinCairo, in development) across r…
ipfizz Jun 21, 2026
8ce3d16
feat(web): add theme-color meta for light and dark browser chrome
ipfizz Jun 21, 2026
87293b5
feat: add a windows dialog backend with file pickers and message boxes
ipfizz Jun 21, 2026
cbf72ae
feat: add a windows menu backend with context menus via trackpopupmenu
ipfizz Jun 21, 2026
42e2aaf
feat: add a windows nativeimage backend via gdi+
ipfizz Jun 21, 2026
cff8b24
feat: add a windows notification backend via tray balloons
ipfizz Jun 21, 2026
6e874b3
feat: bundle the webkit engine into windows builds for env-free launch
ipfizz Jun 21, 2026
199bb7c
feat: add a windows session backend that clears cookies and caches
ipfizz Jun 21, 2026
9be455f
feat: add windows clipboard image read and write via cf_dib
ipfizz Jun 21, 2026
0e13582
docs: clarify printtopdf and capturepage are blocked by the wincairo …
ipfizz Jun 21, 2026
0a6918f
fix: return an empty image when decoding a zero-length buffer on windows
ipfizz Jun 21, 2026
ade7020
refactor: join platform-target paths with explicit posix or win32 sep…
ipfizz Jun 21, 2026
453815b
test: make host-relative path assertions separator-agnostic for windo…
ipfizz Jun 21, 2026
9c178a2
feat: add a windows application menu bar dispatched via a jscallback …
ipfizz Jun 21, 2026
561bb9c
feat: add a wincairo engine build pipeline that relocates into the st…
ipfizz Jun 21, 2026
4979c3c
feat: build the windows engine from wincairo source with a serializer…
ipfizz Jun 22, 2026
9c30357
fix: assert windows app-path conventions with win32.join so posix ci …
ipfizz Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Normalise line endings to LF in the working tree on every platform. The repo's
# Biome config enforces `lineEnding: lf`, so a Windows checkout (where Git would
# otherwise apply core.autocrlf and produce CRLF) must keep LF — both for the
# Windows CI `biome format` check and to silence the "LF will be replaced by CRLF"
# warnings. Binary files are auto-detected by `text=auto` and left untouched.
* text=auto eol=lf
146 changes: 146 additions & 0 deletions .github/workflows/build-engine-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: build-engine-windows

# Builds OUR OWN WinCairo WebKit engine FROM SOURCE on a Windows runner, relocates
# it into the Bunmaska engine-store layout, and proves it loads from the store —
# the Windows peer of `build-engine.yml` (which relocates apt's WebKitGTK on Linux).
# Windows has no system WebKit and we never ship Playwright's build, so the engine
# is our own from-source binary.
#
# This recipe mirrors the one proven locally end-to-end (a real BrowserWindow loaded
# the from-source engine from the store: STORE_ENGINE_OK). Notes baked in from that:
# * WinCairo dropped MSVC — it builds with clang-cl, so we install LLVM 20.
# * WebKit's code-gen needs gperf on PATH.
# * A short WEBKIT_OUTPUTDIR avoids Windows' 250-char object-path limit in vcpkg.
# * patch-webkit-wincairo.py routes ~300 serializer inputs through a response file
# (otherwise the inline command line overflows cmd.exe's ~8191-char limit).
# * `ninja -k 0` finishes past a broken dev-tooling target (compile_commands.json)
# that no product DLL depends on; success = WebKit2.dll exists.
#
# Heavy (~hours, GBs): runs on demand / on the engine branch only.
on:
workflow_dispatch:
inputs:
webkit_tag:
description: 'WebKit git tag to build (e.g. wpewebkit-2.46.0)'
required: true
default: 'wpewebkit-2.46.0'
push:
branches: [feat/windows-engine]

jobs:
build:
name: build WinCairo from source (windows)
runs-on: windows-latest
timeout-minutes: 350
env:
WEBKIT_TAG: ${{ github.event.inputs.webkit_tag || 'wpewebkit-2.46.0' }}
# Short build-output root (WebKit honours WEBKIT_OUTPUTDIR): vcpkg's ICU build
# generates pathologically deep try-compile object paths that blow past
# Windows' 250-char limit when nested under the workspace.
WEBKIT_OUTPUTDIR: C:\wkb
LLVM_DIR: C:\llvm20\clang+llvm-20.1.8-x86_64-pc-windows-msvc
steps:
- name: Checkout bunmaska
uses: actions/checkout@v4
with:
path: bunmaska

# Clone WebKit to a SHORT path (C:\WebKit) at the pinned tag; shallow (history
# is enormous, the build only needs the tree at that revision).
- name: Checkout WebKit at the pinned tag
run: git clone --depth 1 --branch "$env:WEBKIT_TAG" https://github.com/WebKit/WebKit.git C:\WebKit
shell: pwsh

- name: Apply Bunmaska's WinCairo build patch (serializers response file)
run: python bunmaska/packages/bunmaska/tools/engine/patch-webkit-wincairo.py C:\WebKit
shell: pwsh

# cmake + VS are preinstalled; add ninja, perl, ruby, gperf.
- name: Install build tools (ninja, perl, ruby, gperf)
run: |
choco install -y ninja strawberryperl ruby
curl.exe -L -A "Mozilla/5.0" -o "$env:TEMP\gperf.zip" `
"https://master.dl.sourceforge.net/project/ezwinports/gperf-3.1-w32-bin.zip?viasf=1"
Expand-Archive "$env:TEMP\gperf.zip" -DestinationPath C:\gperf -Force
shell: pwsh

# WinCairo builds with clang-cl: install LLVM 20 (the find_library for
# clang_rt.builtins resolves relative to the clang-cl compiler).
- name: Install LLVM 20 (clang-cl)
run: |
curl.exe -L -o "$env:TEMP\llvm.tar.xz" `
"https://github.com/llvm/llvm-project/releases/download/llvmorg-20.1.8/clang%2Bllvm-20.1.8-x86_64-pc-windows-msvc.tar.xz"
New-Item -ItemType Directory -Force -Path C:\llvm20 | Out-Null
tar.exe -xf "$env:TEMP\llvm.tar.xz" -C C:\llvm20
shell: pwsh

- name: Set up MSVC (x64)
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64

# build-webkit configures (auto-building the vcpkg deps) and compiles; it stops
# on the broken dev-tooling target, so a follow-up `ninja -k 0` finishes the
# product DLLs. clang-cl is the compiler (CC/CXX); gperf + clang on PATH.
- name: Build WebKit (WinCairo, Release, clang-cl)
working-directory: C:\WebKit
env:
CC: ${{ env.LLVM_DIR }}\bin\clang-cl.exe
CXX: ${{ env.LLVM_DIR }}\bin\clang-cl.exe
run: |
$env:Path = "$env:LLVM_DIR\bin;C:\gperf\bin;$env:Path"
perl Tools/Scripts/build-webkit --wincairo --release
if (-not (Test-Path "$env:WEBKIT_OUTPUTDIR\Release\bin\WebKit2.dll")) {
Write-Host "finishing past the dev-tooling target with ninja -k 0 ..."
ninja -C "$env:WEBKIT_OUTPUTDIR\Release" -k 0
}
if (-not (Test-Path "$env:WEBKIT_OUTPUTDIR\Release\bin\WebKit2.dll")) {
Write-Error "WebKit2.dll was not produced"; exit 1
}
shell: pwsh

- name: Compute the engine id from the tag
id: id
run: |
$ver = "$env:WEBKIT_TAG" -replace '^(wpe|webkit)webkit-?|^webkitgtk-', ''
"engine_id=webkit-2-$ver-bunmaska1-windows-x64" >> $env:GITHUB_OUTPUT
shell: pwsh

# Relocate the from-source closure (Release/bin) into the store, mark installed.
- name: Relocate into the engine store
run: |
$store = "$env:RUNNER_TEMP\store"
& bunmaska/packages/bunmaska/tools/engine/build-wincairo-windows.ps1 `
-Source "$env:WEBKIT_OUTPUTDIR\Release\bin" -OutDir $store `
-EngineId "${{ steps.id.outputs.engine_id }}"
New-Item -ItemType File -Force `
-Path "$store\${{ steps.id.outputs.engine_id }}\INSTALLATION_COMPLETE" | Out-Null
shell: pwsh

- uses: oven-sh/setup-bun@v2
with:
bun-version-file: bunmaska/.bun-version

- name: Install bun deps
working-directory: bunmaska
run: bun install --frozen-lockfile

# Prove the relocated engine works resolved purely from the store (no
# BUNMASKA_WEBKIT_PATH): a real BrowserWindow + executeJavaScript.
- name: Prove it loads from the store
working-directory: bunmaska/packages/bunmaska
env:
BUNMASKA_ENGINES_PATH: ${{ runner.temp }}\store
BUNMASKA_WEBKIT_ID: ${{ steps.id.outputs.engine_id }}
run: |
$out = bun run tools/engine/windows-engine-load-probe.ts 2>&1 | Out-String
Write-Host $out
if ($out -notmatch 'STORE_ENGINE_OK') { Write-Error "engine did not load from the store"; exit 1 }
shell: pwsh

- name: Upload the engine artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.id.outputs.engine_id }}
path: ${{ runner.temp }}\store\${{ steps.id.outputs.engine_id }}
retention-days: 7
13 changes: 12 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest]
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -53,3 +53,14 @@ jobs:
- name: Validate (macOS)
if: runner.os == 'macOS'
run: bun run validate

# Windows runs a SCOPED validation: format + lint + type-check (all
# platform-agnostic) plus the Windows backend tests, which pass on a real
# windows-latest runner (engine-gated WebKit tests skip without
# BUNMASKA_WEBKIT_PATH). The full cross-platform suite is not yet
# POSIX/Windows path-portable; running it on Windows is a separate
# test-hardening follow-up (see .admin/WINDOWS.md).
- name: Validate (Windows — backend tests + lint + type-check)
if: runner.os == 'Windows'
working-directory: packages/bunmaska
run: bun run validate:windows
11 changes: 7 additions & 4 deletions apps/web/src/content/docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ description: Exactly which operating systems and CPU architectures Bunmaska runs
order: 4
---

Bunmaska is a **macOS + Linux** framework. It is not cross-platform until Windows works, and we'd rather tell you that on the first page than have you find out three weeks into a port.
Bunmaska ships on **macOS and Linux** today. A **Windows** backend (WinCairo WebKit) is in active development - it's real in the code and runs on CI, but not yet shippable end-to-end (it needs a hosted WinCairo engine). The honest matrix, on the first page rather than three weeks into a port:

## The support matrix

| Platform | Status | Architectures | Engine |
| --- | --- | --- | --- |
| **macOS** | ✅ Shipping | Apple Silicon (ARM64) + Intel (x64) | AppKit + `WKWebView` |
| **Linux** | ✅ Shipping | x64 + ARM64 (incl. Raspberry Pi) | GTK 4 + WebKitGTK 6 |
| **Windows** | ⏳ Planned | - | WinCairo WebKit (see [roadmap](/roadmap)) |
| **Windows** | 🚧 In development | x64 + ARM64 | WinCairo WebKit (from the store) |

## macOS

Expand All @@ -28,9 +28,12 @@ Bunmaska is a **macOS + Linux** framework. It is not cross-platform until Window

## Windows

Not supported yet - and deliberately so. The easy route (WebView2) is Chromium, which is exactly what Bunmaska exists to avoid. The real route is **WinCairo**, WebKit's Windows port; when it's reliably embeddable, Bunmaska's architecture ports to it cleanly. Full reasoning on the [roadmap](/roadmap).
**In active development.** A from-scratch Win32 backend is built on pure `bun:ffi` - native windows + a cooperative message pump, the **WinCairo WebKit** view (WebKit's real Windows port, *not* WebView2/Chromium), the renderer↔main IPC bridge, and ~10 modules (clipboard, tray, `safeStorage` via DPAPI, screen, shell, global shortcuts, power, native theme). It validates on a `windows-latest` CI runner.

If your project needs Windows today, Bunmaska isn't the tool for that target yet.
- **Architectures:** `x64` and `ARM64`. 32-bit (x86) is not supported, on purpose.
- **Engine:** Windows ships no system WebKit, so an app loads **WinCairo `WebKit2.dll` from the engine store** - the same pinned-engine mechanism as the other platforms, with the engine directory put on the DLL search path so its dependency closure resolves beside it.
- **The catch:** we don't host prebuilt WinCairo engines yet, so a Windows app needs one provided locally (`BUNMASKA_WEBKIT_PATH` or a local store install). Hosting those builds is the last step before Windows ships end-to-end - the same step Linux's pinned tier is waiting on.
- **Known gaps:** `printToPDF` / `capturePage`, DevTools, clipboard images, and the tray context menu aren't wired yet - they throw a clear error rather than silently no-op. Full picture on the [roadmap](/roadmap).

## Requirements (all platforms)

Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const jsonLd = {
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#fcfcfb" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0c0c0b" media="(prefers-color-scheme: dark)" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href={canonical} />
<title>{fullTitle}</title>
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const features = [
{ icon: "lucide:globe", t: "WebKit, not Chromium", d: "Renders on WebKit - WKWebView on macOS, WebKitGTK on Linux. No browser engine bundled into your app." },
{ icon: "lucide:blocks", t: "Electron-shaped API", d: "app, BrowserWindow, ipcMain, Menu, Tray, dialog… the names you already know. Drop-in shim included." },
{ icon: "lucide:terminal", t: "A real CLI", d: "bunmaska init / dev / run / build. Scaffold, hot-reload, and package to .dmg or .deb." },
{ icon: "lucide:monitor", t: "macOS + Linux", d: "Two platforms that actually work. Windows is on the list, not in the build. We'll be honest about it." },
{ icon: "lucide:monitor", t: "macOS + Linux, Windows next", d: "macOS and Linux ship today. A Windows backend - WinCairo WebKit, never Chromium - is in the build and running on CI." },
{ icon: "lucide:gauge", t: "Fast cold start", d: "No Chromium to boot, no V8 snapshot to thaw. Bun + JavaScriptCore, up before your splash screen would've loaded." },
];

Expand All @@ -68,12 +68,12 @@ const compare = [
["Compile step", "Yes, and it'll fail somewhere", "None"],
["Runtime deps", "Several", "Zero"],
["Runtime", "Node + V8", "Bun + JavaScriptCore"],
["Platforms", "Win / macOS / Linux", "macOS + Linux (Windows: not yet)"],
["Platforms", "Win / macOS / Linux", "macOS + Linux (Windows in dev)"],
["Familiar API", "The original", "Drop-in, ~70-80% parity"],
];

const faqs = [
["Windows?", "macOS and Linux today. Windows is on the list, not in the build - we'd rather ship two platforms that work than three that sort of do. Windows folks: we see you. Hang tight."],
["Windows?", "macOS and Linux ship today. Windows is now in the build - a WinCairo WebKit backend (never Chromium), running on CI. It still needs a hosted engine before it ships end-to-end, but it's no longer 'someday.' Windows folks: getting close."],
["Production-ready?", "It says alpha for a reason. Use it for the thing you were going to rewrite anyway."],
["Why no Chromium?", "Because it's already on your computer, and shipping a second one is how we got here."],
["What's the catch?", "~70-80% of Electron's surface, and we publish the parity matrix so you can check before you commit."],
Expand Down Expand Up @@ -115,7 +115,7 @@ const DISPLAY = "font-serif leading-[1.04] text-balance text-[clamp(2.5rem,5.5vw
</div>

<p class="mt-6 text-base text-text-muted">
<span class="text-[1.1em] italic" style="font-family:var(--font-serif);">Alpha - and we'll admit it.</span> macOS + Linux.
<span class="text-[1.1em] italic" style="font-family:var(--font-serif);">Alpha - and we'll admit it.</span> macOS + Linux, Windows in the build.
</p>
</div>
</section>
Expand Down
33 changes: 17 additions & 16 deletions apps/web/src/pages/roadmap.astro
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ const phases: { status: Status; title: string; note?: string; items: string[] }[
],
},
{
status: "future",
status: "in-progress",
title: "Windows",
note: "deliberately last - and on our terms.",
note: "now in the build - and on our terms.",
items: [
"A from-scratch Win32 backend (~5k LOC - the real work, and it's the same regardless of engine).",
"WinCairo WebKit, brought via the same engine store. Never WebView2 - that's Chromium.",
"A from-scratch Win32 backend - windows, IPC, and ~10 modules - running on a windows-latest CI runner.",
"WinCairo WebKit, loaded from the same engine store. Never WebView2 - that's Chromium.",
"ARM64 + x64 only. Never 32-bit.",
],
},
Expand Down Expand Up @@ -156,8 +156,8 @@ const phases: { status: Status; title: string; note?: string; items: string[] }[
>
</h2>
<p class="mt-4 text-text-muted">
We're not on Windows yet, and we're not going to fudge why. It's a deliberate choice, and the reasoning is
worth stating plainly.
Windows used to be the "someday" platform. It's now in the build - a from-scratch Win32 backend with
WinCairo WebKit. Here's the honest state, gaps and all.
</p>
</div>

Expand All @@ -167,22 +167,22 @@ const phases: { status: Status; title: string; note?: string; items: string[] }[
{
icon: "lucide:shield-x",
t: "Not WebView2",
d: "The easy Windows path is WebView2 - which is Microsoft Edge, i.e. Chromium. Shipping that would break the one promise the whole project is built on. So that door is closed on purpose.",
d: "The easy Windows path is WebView2 - Microsoft Edge, i.e. Chromium. We didn't take it. Windows renders on WinCairo WebKit, the real WebKit port, loaded from the same engine store as every other platform.",
},
{
icon: "lucide:package",
t: "WinCairo, brought along",
d: "Windows ships no system WebKit (Safari for Windows died in 2012). So unlike mac and Linux, we can't borrow one - we bring WinCairo WebKit through the same engine store as the pinned tier.",
icon: "lucide:wrench",
t: "A real Win32 backend",
d: "Built from scratch on bun:ffi - window + message pump, the WinCairo WebKit view and IPC bridge, and ~10 modules (clipboard, tray, safeStorage via DPAPI, screen, shell, global shortcuts, power, theme). It runs on a windows-latest CI runner.",
},
{
icon: "lucide:cpu",
t: "ARM64 + x64. Never 32-bit.",
d: "Windows is moving to ARM (Snapdragon X, Copilot+ PCs, NVIDIA's 2026 laptops). ARM64 + x64 covers the present and the future; x86 is not on the list.",
},
{
icon: "lucide:wrench",
t: "It's the most work, not the least",
d: "There's no Win32 backend yet (~5k lines), and we'd own the WinCairo build + its CVE treadmill forever. Windows is the expensive platform - so it goes last, once macOS + Linux are solid.",
icon: "lucide:package",
t: "Still the expensive platform",
d: "Windows has no system WebKit, so we ship and maintain the WinCairo engine ourselves - CVE treadmill and all. That's why the last piece is hosting the WinCairo builds, so a Windows app can fetch its engine.",
},
].map((c) => (
<div class="group bg-bg p-7 transition-colors duration-200 hover:bg-bg-subtle">
Expand All @@ -198,9 +198,10 @@ const phases: { status: Status; title: string; note?: string; items: string[] }[
))
}
</div>
<p class="mx-auto mt-6 max-w-[60ch] text-center text-sm text-text-faint">
Need Windows today? Use Electron for that target, and check back here. We'd rather ship two platforms that
work than three that sort of do.
<p class="mx-auto mt-6 max-w-[64ch] text-center text-sm text-text-faint">
Honest status: the backend is built and runs on a Windows CI runner, but a Windows app still needs a WinCairo
engine in the store - and we don't host those builds yet. So Windows is real in the code; shipping it
end-to-end waits on the hosted engine, the same step Linux is waiting on.
</p>
</div>
</section>
Expand Down
Loading
Loading