Desktop app (Electron) for macOS and Windows - #1
Open
timbogdanov wants to merge 4 commits into
Open
Conversation
The editor becomes a real application rather than a wrapped web page: native menus, .patternfront documents, Open/Save/Save As, double-click to open from Finder or Explorer, recent files, exports through native Save dialogs, and an unsaved-changes prompt. app/patternfront.html stays the single source of truth. The same file is the browser build and the desktop renderer — nothing is forked. Every native call sits behind a native() feature test, and a test boots the code with no window.pfNative at all and fails if anything reaches for it. Two seams made this small. Exports already funnelled through one download() function, so redirecting them to a Save dialog was a single change. Documents already had serialiseDoc(); restoreDoc() split into loadDocFromJSON() so opening a file and restoring the autosave go down the same tested path. Security posture, since an open-source Electron app gets read by strangers: context isolation on, node integration off, sandbox on, webview disabled, navigation and popups denied, and the renderer served over a registered app:// scheme rather than file:// — a real origin, so localStorage works and a CSP can actually be enforced. The preload exposes ten validated calls and nothing else, asserted by a gate. Saves are atomic. A rename over the target means an interrupted write leaves the previous version intact rather than a truncated file, which is tested by making the directory read-only mid-save. Menu accelerators are owned by the menu. Electron dispatches them before the page sees the keydown, so the renderer skips those combos on desktop — without that, every undo fires twice. Two new suites and a headless launch: 45 checks on the shell's configuration, 19 on document handling, and an Electron smoke test that asserts the editor actually comes up rather than merely that it packaged. Builds are unsigned. Signing is wired but inert; enabling it is setting secrets, not editing config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T
The smoke test aborted with "The SUID sandbox helper binary was found, but is not configured correctly" — chrome-sandbox has to be owned by root with the setuid bit, and npm cannot install it that way. Fixed by setting the ownership rather than passing --no-sandbox. The whole point of the smoke test is to exercise the configuration users actually get, and this app deliberately runs with sandbox: true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T
node_modules/electron/dist is populated lazily on first use, not by npm ci, so the chown ran against a path that did not exist yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T
`npx electron --version` starts the binary, which aborts on the same SUID sandbox error the step exists to fix — so the chown never ran. Calling install.js fetches dist/ without launching anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the editor into a real desktop application for macOS and Windows, without forking it.
app/patternfront.htmlstays the single source of truth. The same file is the browser build andthe desktop renderer — every native call sits behind a
native()feature test, and there is atest that boots the code with no
window.pfNativeat all and fails if anything reaches for it.What you get
Native menus with real accelerators.
.patternfrontdocuments with Open / Save / Save As.Double-click a file in Finder or Explorer and it opens. Recent files in the dock menu and jump
list. Exports through native Save dialogs instead of browser downloads. Remembered window
position, clamped to a display that still exists. An unsaved-changes prompt that will not let you
lose work.
Why the diff is small
Two seams already existed and did the heavy lifting:
download(blob, name). Redirecting them to a Save dialogwas a single change to a single function.
serialiseDoc().restoreDoc()split intoloadDocFromJSON(raw), soopening a file and restoring the autosave now go down the same path — the one the round-trip
tests already covered.
Security
An open-source Electron app gets read by strangers, so this is deliberate:
contextIsolation: true,nodeIntegration: false,sandbox: true, webview disabledhttp(s)ever reachesshell.openExternalapp://scheme, notfile://— a real origin, solocalStorageworks and a CSP can actually be enforced. Path traversal out of
app/is refused.has not grown
Two details worth reviewing
Saves are atomic.
writeAtomicwrites a sibling temp file and renames over the target, so aninterrupted save leaves the previous version intact instead of a truncated file. Tested by making
the directory read-only mid-save and asserting the old contents survive.
Menu accelerators are owned by the menu. Electron dispatches ⌘Z before the page sees the
keydown. The renderer's own handler skips those combos on desktop — without that, every undo,
select-all and deselect fires twice.
MENU_KEYSin the renderer and the accelerator list inelectron/menu.jshave to stay in agreement; both are gated.Verification
./tools/verify-all.sh— eleven suites, all green. Two are new:verify-electron.pyverify-documents.jsPlus a smoke test (
electron . --smoke) that launches headless and asserts the editor reallycame up: 22 commands wired, 54 stamps built, canvas painted, no console errors. CI runs it under
xvfb. That is the difference between proving it packages and proving it works.
CI also fails if the generated stamp table or codec fixtures drift from their generators.
What I could not verify
CI is green on all four checks — verification, headless launch, and packaging on both
macos-latestandwindows-latest. Every artifact builds: two.dmg, two.zip, three NSISinstallers and a portable
.exe. But building is not running, and I am on macOS only:packagejob in CI compiles it; nobodyhas launched it. Worth someone doing on real hardware before the first release.
.dmgdoes not build on my machine, though it does in CI. Locally it fails athdiutil: convert failed - Resource temporarily unavailable, inside thedmgbuildcopyelectron-builder vendors — reproducible, and not caused by the config here (it fails with the
default layout too). The
packagejob settled it: both.dmgslices build fine onmacos-latest, along with both.zips. So this is a local-environment problem, not apackaging one; the README notes the
--mac zipworkaround for anyone who hits it.Also in this branch
--shot <path>renders the window offscreen to a PNG, which is where the README screenshot comesfrom and gives CI a visual artifact on every run.
Builds are unsigned — see the README for the Gatekeeper and SmartScreen steps. The signing config
is wired but inert; enabling it later means setting secrets, not editing workflows.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SPLUTYBHytZJX2BB6kCL9T