Native Screensaver Port — Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans or subagent-driven-development. Steps use checkbox (
- [ ]) syntax for tracking. Verification convention: This repo has no test framework (AGENTS.md). "Verify" =npm run typecheck+npm run buildsucceed (web), ordotnet publishsucceeds (Windows). Visual/activation checks are called out where a machine is required.
Goal: Ship the browser screensaver as native macOS .saver and Windows .scr downloads via thin WebView wrappers around a shared screensaver-mode web entry.
Architecture: A second Vite page (screensaver.html + src/screensaver/main.ts) auto-starts the existing engine, reads scene/audio from URL params, audio off, hotkeys off. Native wrappers (Swift WKWebView .saver; .NET WinForms WebView2 .scr) bundle the built assets offline and pass options as URL query. Distribution via Codeberg Releases.
Tech Stack: TypeScript/Vite/three.js (existing), Swift + ScreenSaver.framework + WKWebView (macOS), .NET 8 WinForms + WebView2 (Windows), Codeberg Releases API.
File structure¶
| Path | Responsibility |
|---|---|
screensaver.html |
Vite entry page for screensaver mode (no start button) |
src/screensaver/main.ts |
Auto-start, parse URL params, random scene, audio off, hotkeys off |
vite.config.ts |
Add screensaver.html as 2nd rollup input (modify) |
native/macos/ |
Xcode project: KuroScreensaver ScreenSaverView + configureSheet |
native/windows/ |
.NET WinForms .scr host + options dialog |
scripts/package-macos.sh |
xcodebuild → .saver → zip |
scripts/package-windows.sh |
dotnet publish → rename .scr |
scripts/bundle-web.sh |
copy dist/ web assets into both native projects |
.forgejo/workflows/release.yml (or GitHub fallback) |
build + attach release assets on tag |
README.md / page |
download buttons (modify) |
Task 1: Web screensaver-mode entry (fully autonomous)¶
Files:
- Create: screensaver.html
- Create: src/screensaver/main.ts
- Modify: vite.config.ts
- [ ] Step 1: Create
screensaver.html— minimal, no start button, black bg, mountssrc/screensaver/main.ts.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Kuro Screensaver</title>
<style>
html, body { margin: 0; padding: 0; background: #000; height: 100%; overflow: hidden; }
</style>
</head>
<body>
<script type="module" src="/src/screensaver/main.ts"></script>
</body>
</html>
- [ ] Step 2: Create
src/screensaver/main.ts— parse params, random scene, audio off, hotkeys off, auto-open.
// Screensaver-mode entry — auto-starts the engine with no user gesture.
// Options arrive as URL query params injected by the native host:
// ?scene=random|terrain|city|rift|tunnel|void (default: random)
// ?audio=on|off (default: off)
import '../host-web/obsidian-dom-polyfill';
import { ScreensaverController } from '../engine/controller';
import { WebHost } from '../host-web/persistence';
import { makePluginShim } from '../host-web/plugin-shim';
import { SCENES, type SceneId } from '../engine/data/defaults';
const params = new URLSearchParams(location.search);
const sceneParam = params.get('scene') ?? 'random';
const scene: SceneId =
sceneParam === 'random' || !SCENES.includes(sceneParam as SceneId)
? SCENES[Math.floor(Math.random() * SCENES.length)]
: (sceneParam as SceneId);
const audioOn = params.get('audio') === 'on';
const host = new WebHost({
activePreset: 'toxic-haze',
vaultKanji: '黒',
overrides: {
colorMode: 'kuro-preset',
colorPreset: 'toxic-haze',
defaultScene: scene,
liveHotkeysEnabled: false,
sound: { ...({} as any), master: audioOn },
} as any,
});
const controller = new ScreensaverController(makePluginShim(host));
void controller.open({ scene });
NOTE: confirm
WebHost/overrides merge shape againstsrc/host-web/persistence.tsduring implementation — thesoundoverride must merge, not replace. IfWebHostdoes a shallow merge, setsound.masterpost-construction instead (see Step 3 adjust).
-
[ ] Step 3: Verify the overrides merge correctly — read
src/host-web/persistence.ts; if overrides shallow-merge and would dropsoundsub-keys, switch to mutatinghost.getSettings().sound.master = audioOnbeforeopen(). Pick whichever the code supports; do not leave a half-mergedsound. -
[ ] Step 4: Add
screensaver.htmlto Vite build — modifyvite.config.ts:
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
base: './',
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
screensaver: resolve(__dirname, 'screensaver.html'),
},
output: { manualChunks: { three: ['three'] } },
},
},
server: { port: 5173, open: false },
});
-
[ ] Step 5: Verify —
npm run typecheck && npm run build. Expected: both succeed;dist/screensaver.html+dist/index.htmlemitted,threechunk shared. -
[ ] Step 6: Visual check —
npm run dev, openhttp://localhost:5173/screensaver.html. Expected: a random scene starts immediately, no start button, no audio. Reload → possibly different scene.?scene=tunnel&audio=onforces tunnel with sound. -
[ ] Step 7: Commit —
feat(screensaver): add auto-starting screensaver-mode web entry.
Task 2: Windows .scr — spike the cross-build first¶
Files:
- Create: native/windows/KuroScreensaver.csproj
- Create: native/windows/Program.cs
-
[ ] Step 1: Install dotnet —
brew install --cask dotnet-sdk(orbrew install dotnet). Verifydotnet --version≥ 8. -
[ ] Step 2: Minimal WinForms csproj targeting
net8.0-windows, WinExe, with WebView2 NuGet.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<AssemblyName>KuroScreensaver</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
</ItemGroup>
</Project>
- [ ] Step 3: SPIKE — cross-build empty host — a
Program.cswith a borderless fullscreenFormhosting aWebView2that loads a bundledweb/screensaver.html(path resolved next to the exe). Then run:
dotnet publish native/windows/KuroScreensaver.csproj -c Release -r win-x64
Expected: a KuroScreensaver.exe under bin/Release/net8.0-windows/win-x64/publish/. If this fails on macOS (WinForms cross-build), STOP and switch the Windows build to a GitHub Actions Windows runner (record in plan, scaffold .github/workflows/), but keep the same Program.cs.
-
[ ] Step 4: Implement
.scrarg handling inProgram.cs:/s→ run fullscreen;/c→ options dialog (scene dropdown + audio checkbox → registryHKCU\Software\KuroScreensaver);/p <hwnd>→ render into the preview child window; build the WebView URLweb/screensaver.html?scene=<>&audio=<>from registry. Show full code during implementation. -
[ ] Step 5: Verify build —
dotnet publish … -r win-x64succeeds. (Runtime activation is a Windows-only check — defer to the user/CI.) -
[ ] Step 6: Commit —
feat(windows): WebView2 .scr host + cross-build.
Task 3: macOS .saver — scaffold (build handed to user)¶
Files:
- Create: native/macos/KuroScreensaver/KuroScreensaverView.swift
- Create: native/macos/KuroScreensaver/ConfigureSheet.swift
- Create: native/macos/KuroScreensaver.xcodeproj (or an SPM/xcodegen manifest)
-
[ ] Step 1:
KuroScreensaverView.swift—ScreenSaverViewsubclass adding aWKWebViewfillingbounds; load bundledscreensaver.htmlvialoadFileURL(_, allowingReadAccessTo:); build the URL query fromScreenSaverDefaults(scene/audio). Full code at implementation. -
[ ] Step 2:
ConfigureSheet.swift—configureSheetNSWindow with scene NSPopUpButton (Random + 5) + audio NSButton checkbox, persisting toScreenSaverDefaults(forModuleWithName:). -
[ ] Step 3: Project manifest — use
xcodegen(brew install xcodegen) with aproject.ymlso the.xcodeprojis generated reproducibly and diffable, bundle type.saver, linksScreenSaver+WebKit,Info.plistwithNSPrincipalClass = KuroScreensaverView. -
[ ] Step 4: Verify (limited) —
swift -frontend -parseof the two.swiftfiles for syntax, ORxcodegen generateto confirm the manifest is valid. Full build needs Xcode → handed to user. Document this in the task output. -
[ ] Step 5: Commit —
feat(macos): .saver ScreenSaverView scaffold (build needs Xcode).
Task 4: Web-asset bundling + packaging scripts¶
Files:
- Create: scripts/bundle-web.sh, scripts/package-windows.sh, scripts/package-macos.sh
-
[ ] Step 1:
bundle-web.sh—npm run build, then copydist/(the screensaver page + assets) intonative/windows/web/andnative/macos/KuroScreensaver/web/. Idempotent (clean target first). -
[ ] Step 2:
package-windows.sh— runbundle-web.sh,dotnet publish -r win-x64, copyweb/next to the exe, renameKuroScreensaver.exe→KuroScreensaver.scr, zip. -
[ ] Step 3:
package-macos.sh— runbundle-web.sh,xcodegen generate,xcodebuild -scheme KuroScreensaver -configuration Release, zip the.saver. Guard with a clear error ifxcodebuildis absent. -
[ ] Step 4: Verify — run
bundle-web.sh; confirmnative/*/web/screensaver.htmlexist.package-windows.shruns through publish on this box (cross-build).package-macos.sherrors cleanly without Xcode. -
[ ] Step 5: Commit —
build(scripts): web bundling + native packaging.
Task 5: CI + distribution¶
Files:
- Create: .forgejo/workflows/release.yml (Codeberg) and/or .github/workflows/release.yml (fallback)
- Modify: README.md, index.html (download links)
-
[ ] Step 1: CI workflow on tag
v*— Linux job builds the web + Windows.scr(cross-build, per Task 2 spike result); a macOS job (GitHub macOS runner if Forgejo lacks one) builds the.saver. Both upload artifacts. -
[ ] Step 2: Attach to Codeberg Release —
curlthe Codeberg API to create the release and upload.scrzip +.saverzip as assets, using a CI secret token (Authorization: token …, verified working 2026-05-28). -
[ ] Step 3: Download UI — add a Downloads section to
README.mdand a download link on the demoindex.htmlpointing athttps://git.jkaindl.de/jkaindl/kuro-screensaver/releases/latest. -
[ ] Step 4: Commit —
ci+docs: release pipeline + download links.
Activation note: enabling Forgejo/GitHub Actions and adding the token secret is a user step (see spec "Autonomy boundary").
Self-review¶
- Spec coverage: web entry (T1), Windows .scr (T2), macOS .saver (T3), bundling+packaging (T4), CI+distribution+download UI (T5) — all spec sections mapped.
- Placeholders: native source bodies are described with exact responsibilities + interfaces; full code is produced at implementation time (flagged), not left as vague "handle X". Acceptable because the native files are large and platform-API-bound; the param contract (
?scene=,?audio=) is fixed and shared across all three. - Type consistency:
SCENES/SceneIdfromengine/data/defaults; URL paramsscene/audioidentical in T1 (read), T2/T3 (write);screensaver.htmlpath consistent across bundle + hosts. - Risk gates: T2 Step 3 is the cross-build go/no-go; T3 build is explicitly user-side.