Windows v0.10.0 Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: v0.10.0 des Windows-Hosts: UI-loser Kiosk-Saver, Settings-Dialog mit
voller macOS-Parität, Performance-Paket (renderScale + adaptives Downscale),
Multi-Monitor-Modi (Spanning / Je-Monitor) und Wallpaper-Modus /w mit Tray +
PowerPolicy.
Architecture: Spec docs/specs/2026-07-16-windows-v0.10-saver-wallpaper-design.md.
Web-Engine-Patches sind lokal testbar (vitest + typecheck + Vite-Dev);
C++-Host ist MSVC-only → Verifikation ausschließlich über GitHub-CI
(windows-host.yml, gh run watch). Alle Format-Erweiterungen sind ADDITIV:
die 12 bestehenden Registry-Werte/Query-Keys bleiben byte-identisch als Präfix.
Tech Stack: TypeScript/Vite/vitest (web), C++17/Win32/WebView2/CMake (host).
Global Constraints¶
- Die ersten 12 Query-Keys bleiben exakt:
scene,preset,speed,audio,bloom,trails,scan,crt,matrix,terminal,radar,crosshair— Reihenfolge undon/off-Encoding unverändert. - Registry-Pfad
HKCU\Software\KuroScreensaverunverändert; neue Werte additiv, PascalCase. - Numerische Werte laufen als ROHE validierte Strings durch Registry+Query (nie reformatieren → Byte-Parität trivial). Validierung: Regex
^[0-9]+(\.[0-9]+)?$+ Range-Check; Save-Reject ist all-or-nothing wie bisher. npm run typecheckundnpm testmüssen nach jedem Web-Task grün sein.- C++-Tasks: lokal nur schreiben + committen; Kompilier-/Test-Verifikation gesammelt über CI (Task 12).
- Conventional Commits mit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>. - macOS-Native-Code (
native/macos/) wird NICHT angefasst.
Zentrale Kontrakte (Referenz für alle Tasks)¶
Query-/Registry-Erweiterung (Reihenfolge gepinnt)¶
Nach crosshair hängen exakt diese Keys in dieser Reihenfolge an
(Query-Key → Registry-Name → Typ/Range → Default-String):
| # | Query | Registry | Typ | Default |
|---|---|---|---|---|
| 13 | look |
Look |
Slug oder leer (clean\|heavy\|broken\|vapor\|matrix\|leer) |
`` (leer = Eigene) |
| 14 | altitude |
Altitude |
low\|mid\|high |
low |
| 15 | fog |
Fog |
auto\|clear\|dense |
auto |
| 16 | weather |
Weather |
light-fog\|heavy-fog\|storm\|dust\|clear |
light-fog |
| 17 | bank |
Bank |
num 0..2 | 1 |
| 18 | reactive |
Reactive |
bool | on |
| 19 | autocycle |
AutoCycle |
bool | on |
| 20 | cyclemin |
CycleMin |
num 0.5..10 | 5 |
| 21 | termlayout |
TermLayout |
strip\|window |
strip |
| 22 | boot |
Boot |
bool | on |
| 23 | bootspeed |
BootSpeed |
fast\|normal\|cinematic |
normal |
| 24 | daynight |
DayNight |
bool | on |
| 25 | crtintensity |
CrtIntensity |
num 0..1 | 0.35 |
| 26 | curvature |
Curvature |
num 0..0.25 | 0.012 |
| 27 | aperture |
Aperture |
num 0..0.5 | 0.22 |
| 28 | bloomstrength |
BloomStrength |
num 0..3 | 1.4 |
| 29 | trailsamount |
TrailsAmount |
num 0.5..0.95 (= trails.damp) | 0.84 |
| 30 | ntsc |
Ntsc |
num 0..1 | 0 |
| 31 | halation |
Halation |
num 0..0.6 | 0.15 |
| 32 | scale |
Scale |
num 0.25..1 | 1 |
| 33 | perfadapt |
PerfAdapt |
bool | on |
Default-Query-Fixture v0.10 (beidseitig gepinnt):
?scene=random&preset=toxic-haze&speed=norm&audio=off&bloom=on&trails=off&scan=on&crt=on&matrix=off&terminal=on&radar=on&crosshair=on&look=&altitude=low&fog=auto&weather=light-fog&bank=1&reactive=on&autocycle=on&cyclemin=5&termlayout=strip&boot=on&bootspeed=normal&daynight=on&crtintensity=0.35&curvature=0.012&aperture=0.22&bloomstrength=1.4&trailsamount=0.84&ntsc=0&halation=0.15&scale=1&perfadapt=on
Voll-Flip-Fixture v0.10 (Roundtrip-Test, beidseitig):
scene=void&preset=phosphor&speed=fast&audio=on&bloom=off&trails=on&scan=off&crt=off&matrix=on&terminal=off&radar=off&crosshair=off&look=heavy&altitude=high&fog=dense&weather=storm&bank=2&reactive=off&autocycle=off&cyclemin=0.5&termlayout=window&boot=off&bootspeed=cinematic&daynight=off&crtintensity=0.85&curvature=0.022&aperture=0.45&bloomstrength=1.6&trailsamount=0.9&ntsc=0.6&halation=0.4&scale=0.66&perfadapt=off
Weitere Registry-Werte (nicht im Query an screensaver.html)¶
MonitorMode=per(Default) |span— Saver.WallpaperMonitorMode=per(Default) |span— Wallpaper.WallpaperScale= num 0.25..1, Default0.66(ersetztscalein der Wallpaper-URL).- Subkey pro Monitor
Software\KuroScreensaver\Monitors\<Id>:Mode(on|off|random|scene, Defaulton),Scene(Slug),Preset(Slug oder leer),WMode(on|off|random|scene, Default: Primäron, sonstoff),WScene,WPreset. - Monitor-Id: sanitized aus
DISPLAYCONFIG_TARGET_DEVICE_NAME.monitorDevicePath(nur[A-Za-z0-9_-], andere Zeichen →_, max 128); FallbackGetMonitorInfoW.szDevicegleich sanitized.
Save-Message v0.10¶
save:<voller 33-Key-Query ohne '?'> + optional angehängte Monitor-Keys
(&m0id=…&m0mode=…&m0scene=…&m0preset=…&m1id=…, Index 0..7 lückenlos;
mNscene/mNpreset dürfen leer sein). cancel unverändert. NEU Host→Seite:
saveerror (Save abgelehnt, Dialog bleibt offen) via PostWebMessageAsString.
Saver-URLs bekommen zusätzlich konstant &kiosk=on (KEIN Registry-Wert).
Wallpaper-URLs: &kiosk=on&audio=off erzwungen + scale=<WallpaperScale> statt Scale.
Host→Page Power-Messages (nur Wallpaper)¶
power:hidden | power:frozen | power:animating:<fps> als WebMessage-String.
Engine-Settings-Erweiterungen (defaults.ts)¶
renderScale: number; // 0.25..1, default 1 — multipliziert die devicePixelRatio-Cap
kioskMode: boolean; // default false — UI-loser Betrieb im nativen Saver-Host
autoCycle: { on: boolean; intervalMin: number }; // Typweitung von 2|5|10 auf number
Neue/geänderte Engine-APIs¶
// core.ts (Engine)
setAdaptiveScale(f: number): void; // 1|0.85|0.7|0.55 — re-applied pixelRatio+Größen
frameCap: number | null; // rAF-Render-Throttle (Wallpaper-Power-State)
// controller.ts
applyPowerState(state: 'hidden' | 'frozen' | 'animating', fps?: number): void;
Task 1: Engine — Settings-Felder + params.ts v0.10 + main.ts-Konsolidierung¶
Files:
- Modify: src/engine/data/defaults.ts (Interface + DEFAULT_SCREENSAVER)
- Modify: src/screensaver/params.ts (alle neuen Params + kiosk)
- Modify: src/screensaver/main.ts (Reihenfolge/Konsolidierung)
- Test: tests/screensaver-params.test.ts
Interfaces:
- Produces: settings.renderScale, settings.kioskMode, autoCycle.intervalMin: number; applyParamOverrides versteht die Keys 13–33 + kiosk.
- Konsumiert von Task 2 (core/controller lesen renderScale/kioskMode) und Task 5 (Dialog sendet die Keys).
- [ ] Step 1: Failing Tests schreiben — in
tests/screensaver-params.test.tsneue describe-Blöcke:
describe('applyParamOverrides — v0.10 keys', () => {
it('maps enums with whitelist validation', () => {
expect(apply('altitude=high').cityAltitude).toBe('high');
expect(apply('altitude=orbit').cityAltitude).toBe(DEFAULT_SCREENSAVER.cityAltitude);
expect(apply('fog=dense').fogMode).toBe('dense');
expect(apply('weather=storm').weather).toBe('storm');
expect(apply('weather=sharknado').weather).toBe(DEFAULT_SCREENSAVER.weather);
expect(apply('bootspeed=cinematic').bootSpeed).toBe('cinematic');
expect(apply('termlayout=window').terminalLayout).toBe('center-window');
expect(apply('termlayout=strip').terminalLayout).toBe('bottom-strip');
});
it('maps bools', () => {
expect(apply('reactive=off').narrativeReactiveWorld).toBe(false);
expect(apply('autocycle=off').autoCycle.on).toBe(false);
expect(apply('boot=off').bootEnabled).toBe(false);
expect(apply('daynight=off').dayNightCycle.on).toBe(false);
expect(apply('perfadapt=off').perfAdapt).toBe(false);
});
it('maps clamped numerics from raw strings', () => {
expect(apply('bank=2').bankStrength).toBe(2);
expect(apply('bank=9').bankStrength).toBe(2); // clamp
expect(apply('cyclemin=0.5').autoCycle.intervalMin).toBe(0.5);
expect(apply('crtintensity=0.85').crtSim.intensity).toBe(0.85);
expect(apply('curvature=0.022').fx.curvature).toEqual({ on: true, amount: 0.022 });
expect(apply('curvature=0').fx.curvature.on).toBe(false);
expect(apply('aperture=0.45').fx.aperture).toEqual({ on: true, strength: 0.45 });
expect(apply('trailsamount=0.9').fx.trails.damp).toBe(0.9);
expect(apply('ntsc=0.6').fx.ntsc).toEqual({ on: true, amount: 0.6 });
expect(apply('halation=0.4').fx.halation).toEqual({ on: true, amount: 0.4 });
expect(apply('scale=0.66').renderScale).toBe(0.66);
expect(apply('scale=0.1').renderScale).toBe(0.25); // clamp low
expect(apply('bank=abc').bankStrength).toBe(DEFAULT_SCREENSAVER.bankStrength);
});
it('bloomstrength disables theme inheritance so the value sticks', () => {
const s = apply('bloomstrength=1.6');
expect(s.fx.bloom.strength).toBe(1.6);
expect(s.fxInheritFromTheme).toBe(false);
});
it('ignores look (dialog-only key)', () => {
expect(apply('look=heavy')).toEqual(DEFAULT_SCREENSAVER);
});
});
describe('applyParamOverrides — kiosk mode', () => {
it('kiosk=on hides the control bar and survives applyHudPreset', () => {
const s = apply('kiosk=on');
expect(s.kioskMode).toBe(true);
expect(s.hud.controlBar).toBe(false);
expect(s.hudPreset).toBe('custom');
applyHudPreset(s);
expect(s.hud.controlBar).toBe(false);
});
it('defaults stay without the param', () => {
expect(apply('').kioskMode).toBe(false);
expect(apply('').hud.controlBar).toBe(true);
});
});
- [ ] Step 2:
npm test→ neue Tests FAILen (unbekannte Properties / unveränderte Defaults). - [ ] Step 3: defaults.ts erweitern — im Interface nach
seedLock:renderScale: number;undkioskMode: boolean;;autoCycle: { on: boolean; intervalMin: number };(Literal-Union ersetzen). InDEFAULT_SCREENSAVER:renderScale: 1,kioskMode: false,(autoCycle-Default bleibt{ on: false, intervalMin: 5 }). - [ ] Step 4: params.ts erweitern — Helfer + Mappings anhängen (vollständig):
function readNum(params: URLSearchParams, key: string, lo: number, hi: number): number | undefined {
const raw = params.get(key);
if (raw === null || !/^[0-9]+(\.[0-9]+)?$/.test(raw)) return undefined;
const v = parseFloat(raw);
return Math.min(hi, Math.max(lo, v));
}
function readEnum<T extends string>(params: URLSearchParams, key: string, allowed: readonly T[]): T | undefined {
const v = params.get(key);
return v !== null && (allowed as readonly string[]).includes(v) ? (v as T) : undefined;
}
Dann in applyParamOverrides (nach dem bestehenden Block, VOR dem
hudOverridden-Abschluss bzw. mit eigenem Guard):
altitude→cityAltitude, fog→fogMode, weather→weather, bootspeed→bootSpeed
(jeweils readEnum); termlayout: strip→bottom-strip, window→center-window;
reactive→narrativeReactiveWorld, autocycle→autoCycle.on, boot→bootEnabled,
daynight→dayNightCycle.on, perfadapt→perfAdapt (readBool);
bank→bankStrength (readNum 0..2), cyclemin→autoCycle.intervalMin (0.5..10),
crtintensity→crtSim.intensity (0..1),
curvature→fx.curvature = { on: v > 0.001, amount: v } (0..0.25),
aperture→fx.aperture = { on: v > 0.001, strength: v } (0..0.5),
trailsamount→fx.trails.damp (0.5..0.95),
ntsc→fx.ntsc = { on: v > 0.001, amount: v } (0..1),
halation→fx.halation = { on: v > 0.001, amount: v } (0..0.6),
scale→renderScale (0.25..1);
bloomstrength (0..3): fx.bloom.strength = v; settings.fxInheritFromTheme = false;
kiosk: readBool → kioskMode = true; hud.controlBar = false; hudOverridden = true;
look wird bewusst NICHT gelesen (Dialog-only) — Kommentar an die Stelle.
- [ ] Step 5: main.ts konsolidieren — (a)
perfAdapt: falseausoverridesentfernen (Kommentar dazu ersetzen: Steuerung jetzt via?perfadapt=, Engine-Default on); (b)host.getSettings().autoCycle.on = true;VORapplyParamOverrides(...)ziehen, damit?autocycle=offgewinnt; (c) die Zeilen 86–91 (ntsc/halation/weather-Direkt-Handling) ERSATZLOS streichen — params.ts deckt sie validiert ab. - [ ] Step 6:
npm test+npm run typecheck→ PASS (alle, inkl. Bestand). - [ ] Step 7: Commit
feat(engine): v0.10 query params — kiosk, renderScale + full settings bridge
Task 2: Engine — renderScale/adaptScale/frameCap, kiosk in controller, AA off, Scanline-Drift¶
Files:
- Modify: src/engine/engine/core.ts
- Modify: src/engine/controller.ts
- Modify: src/engine/hud/index.ts
Interfaces:
- Consumes: settings.renderScale, settings.kioskMode (Task 1).
- Produces: Engine.setAdaptiveScale(f: number), Engine.frameCap: number|null, ScreensaverController.applyPowerState(state, fps?) (von Task 3 verdrahtet).
- [ ] Step 1: core.ts — (a) Konstruktor:
antialias: true→antialias: false; PixelRatio-Zeile ersetzen durchthis.applyPixelRatio();mit:
private adaptScale = 1;
private applyPixelRatio() {
const base = Math.min(window.devicePixelRatio, 2);
const scale = Math.min(1, Math.max(0.25, this.settings.renderScale ?? 1));
this.renderer.setPixelRatio(base * scale * this.adaptScale);
}
setAdaptiveScale(f: number) {
this.adaptScale = f;
this.applyPixelRatio();
const W = window.innerWidth, H = window.innerHeight;
this.renderer.setSize(W, H);
this.composer.setSize(W, H);
}
frameCap: number | null = null;
private lastFrameT = 0;
(b) onResize: nach setSize zusätzlich this.applyPixelRatio();.
(c) rAF-Loop (start()-Tick): am Anfang if (this.frameCap && now - this.lastFrameT < 1000 / this.frameCap - 1) { schedule next; return; } this.lastFrameT = now; (an die vorhandene Loop-Struktur anpassen — reines Render-Skip, Zeitbasis läuft weiter).
- [ ] Step 2: controller.ts — (a) perfAdapt-Wächter (Z.366–380) erweitern: Stufenzähler perfTier 0..3; wenn nach Bloom/Trails-Drossel weiterhin 3× <40 fps → perfTier++ und engine.setAdaptiveScale([1, 0.85, 0.7, 0.55][perfTier]); Erholung: 13 Intervalle (~20 s) in Folge >55 fps → perfTier-- + re-apply. (b) kiosk in open(): bei s.kioskMode den mousemove-Cursor-Reveal überspringen (this.overlay.style.cursor bleibt none, kein showControlBar-Aufruf), initiales showControlBar (Z.232–235) und requestFullscreen (Z.243–245) überspringen. Parallax bleibt aktiv (harmlos ohne Maus).
- [ ] Step 3: hud/index.ts Scanline-Drift — backgroundPositionY-Mutation (Z.731–733) ersetzen: beim Build des scanlines-Elements eine CSS-Keyframes-Animation injizieren (@keyframes ks-drift { from { background-position-y: 0 } to { background-position-y: 4px } }, Dauer aus 4 / (speed * 0.6 * 60) s, animation: ks-drift linear infinite), Loop-Körper behält nur den seltenen Sync-Tear (transform, 0.2 %). stopLoop/off-Pfad: Animation via animationPlayState pausieren, wenn scanlineDrift.on false ist.
- [ ] Step 4: npm run typecheck + npm test → PASS. Visuelle Stichprobe: npm run dev, http://localhost:5173/screensaver.html?kiosk=on&scale=0.5 — keine Control-Bar (auch nicht initial), Szene läuft.
- [ ] Step 5: Commit feat(engine): renderScale + adaptive downscale tier, kiosk-mode open path, AA off, compositor-friendly scanline drift
Task 3: Engine — Power-Bridge (Host→Page)¶
Files:
- Modify: src/screensaver/main.ts
- Modify: src/engine/controller.ts (applyPowerState)
Interfaces:
- Consumes: Engine.frameCap, engine.stop()/start() (Task 2).
- Produces: WebMessage-Verhalten power:hidden|frozen|animating:<fps> (Host Task 11 sendet).
- [ ] Step 1: controller.applyPowerState —
applyPowerState(state: 'hidden' | 'frozen' | 'animating', fps?: number): void {
if (!this.engine) return;
if (state === 'hidden' || state === 'frozen') { this.engine.frameCap = null; this.engine.stop(); this.hud?.stopLoop(); }
else { this.engine.frameCap = fps && fps > 0 ? fps : null; this.engine.start(); this.hud?.startLoop?.(); }
}
(stop()/start() idempotent verwenden — vorhandene Engine-API prüfen und ggf. Guard if (this.rafId) ergänzen. HUD-Loop-Neustart: exakten Methodennamen aus hud/index.ts übernehmen.)
- [ ] Step 2: main.ts Listener — nach Controller-Erzeugung:
(window as any).chrome?.webview?.addEventListener?.('message', (ev: { data: unknown }) => {
const m = typeof ev.data === 'string' ? ev.data : '';
if (!m.startsWith('power:')) return;
const [, state, fpsRaw] = m.split(':');
if (state === 'hidden' || state === 'frozen' || state === 'animating') {
controller.applyPowerState(state, fpsRaw ? parseInt(fpsRaw, 10) : undefined);
}
});
- [ ] Step 3:
npm run typecheck+npm test→ PASS. - [ ] Step 4: Commit
feat(engine): host→page power bridge (pause/fps-cap) for the wallpaper mode
Task 4: Web — form-state v0.10 (Voll-Format + Monitore) + Fixtures¶
Files:
- Modify: src/settings/form-state.ts
- Test: tests/settings-form.test.ts
Interfaces:
- Produces: SettingsFormState (33 Felder; Numerik als string), MonitorEntry { id, name, w, h, portrait, primary }, MonitorFormState { id, mode, scene, preset }, buildSaveMessage(s, monitors?), readInitial(params), readMonitors(params): MonitorEntry[] (aus monitors=-JSON), FORM_DEFAULTS.
- Kontrakt: Default- und Flip-Fixture aus dem Kontrakt-Block oben, save:-Präfix; Monitor-Anhang &m0id=<id>&m0mode=<mode>&m0scene=<slug|leer>&m0preset=<slug|leer> je Eintrag.
- [ ] Step 1: Failing Tests — die zwei bestehenden Fixture-Tests auf die v0.10-Strings aus dem Kontrakt-Block heben (Default-Serialisierung, Roundtrip mit Flip-String) + neu:
it('appends monitor entries in index order', () => {
const mons = [
{ id: 'DELL_ABC1', mode: 'scene', scene: 'matrix', preset: 'phosphor' },
{ id: 'LAPTOP_0', mode: 'off', scene: '', preset: '' },
];
expect(buildSaveMessage(FORM_DEFAULTS, mons)).toContain(
'&m0id=DELL_ABC1&m0mode=scene&m0scene=matrix&m0preset=phosphor&m1id=LAPTOP_0&m1mode=off&m1scene=&m1preset=',
);
});
it('readMonitors parses the host monitor list', () => {
const json = encodeURIComponent(JSON.stringify([{ id: 'A', name: 'Dell U2720Q', w: 3840, h: 2160, portrait: false, primary: true }]));
expect(readMonitors(new URLSearchParams('?monitors=' + json))[0].name).toBe('Dell U2720Q');
expect(readMonitors(new URLSearchParams(''))).toEqual([]);
});
- [ ] Step 2:
npm test→ FAIL. - [ ] Step 3: Implementieren —
SettingsFormStateum die 21 Felder erweitern (look: string; altitude: string; fog: string; weather: string; bank: string; reactive: boolean; autocycle: boolean; cyclemin: string; termlayout: string; boot: boolean; bootspeed: string; daynight: boolean; crtintensity: string; curvature: string; aperture: string; bloomstrength: string; trailsamount: string; ntsc: string; halation: string; scale: string; perfadapt: boolean;— Numerik als string!);FORM_DEFAULTSexakt mit den Default-Strings der Kontrakt-Tabelle;readInitialliest Strings roh (params.get(key) ?? default) und Bools wie bisher;buildSaveMessage(s, monitors: MonitorFormState[] = [])serialisiert in Kontrakt-Reihenfolge + Monitor-Anhang;readMonitorsmit try/catch-JSON.parse. - [ ] Step 4:
npm test+npm run typecheck→ PASS. - [ ] Step 5: Commit
feat(settings): v0.10 form state — full option set + per-monitor entries (pinned fixtures)
Task 5: Web — Settings-Dialog-Neubau (Sektionen, Looks, Slider, Monitore, saveerror)¶
Files:
- Modify: src/settings/main.ts (Neubau auf Basis von form-state v0.10)
- Modify: settings.html (CSS: 2-Spalten-Grid, Sektionen, Slider, Karten)
Interfaces:
- Consumes: SettingsFormState/buildSaveMessage/readInitial/readMonitors (Task 4), LOOKS/applyLook-Wertetabellen (src/engine/data/looks.ts), SCENES, PRESETS.
- Produces: UI-Verhalten, das Host-Task 10 konsumiert (monitors=-Param rein, save:/cancel raus, saveerror rein).
- [ ] Step 1: UI bauen — Struktur (alles dynamisch in main.ts, Muster wie Bestand):
Look-Zeile (Select
['','clean','heavy','broken','vapor','matrix']mit Label „Eigene" für leer; onChange schreibt die Look-Werte ausLOOKS[key]in die Slider-Felder:crtintensity=String(L.intensity),curvature,aperture,ntsc,halation,bloomstrength=String(L.bloom),trailsamount=String(0.5 + L.trails * 0.45),preset=L.preset,matrix=L.matrix); jede manuelle Slider-Änderung setztstate.look = ''. Sektionen (h2 + Grid): Bild (Scene/Color/Speed-Selects wie bisher + Altitudelow|mid|high, Fogauto|clear|dense, Weather 5er), CRT-Effekte (7input[type=range]mit Live-Wert-Anzeige: crtintensity 0–1 step 0.01, curvature 0–0.25 step 0.001, aperture 0–0.5 step 0.01, bloomstrength 0–3 step 0.1, trailsamount 0.5–0.95 step 0.01, ntsc 0–1 step 0.01, halation 0–0.6 step 0.01 + Toggles scan/crt), Bewegung & Story (bank-Slider 0–2 step 0.1, reactive, autocycle + cyclemin 0.5–10 step 0.5, terminal-Toggle), Terminal (termlayout-Select strip/window — nur aktiv wenn terminal an), Anzeige & Automatik (radar, crosshair, matrix, boot + bootspeed, daynight, audio), Monitore (RadioMonitorModeper/span — NUR wennreadMonitors≥2 Einträge liefert; Karte je Monitor:name — w×h, Badges „Hauptmonitor"/„Hochformat", Selects modeon|off|random|scene, scene (nur bei mode=scene), preset (leer=global)), Leistung (scale-Slider 0.25–1 step 0.01 mit %-Anzeige, perfadapt-Toggle). Hinweiszeile unter Monitore bei span: „Empfehlung: Render-Scale ≤ 66 % bei 3 Monitoren". Slider-Zahlwerte: Anzeige roh (String(v)), state speichert den String des Inputs. - [ ] Step 2: MonitorMode-Transport —
MonitorModekommt als eigener Query-Parammonitormode=per|spanvom Host (Task 10) und geht als&monitormode=<v>im Save-String zurück (Position: direkt nachperfadapt, VOR den mN-Keys; in Task 4-Fixtures ist er absichtlich nicht enthalten — buildSaveMessage bekommt ihn als optionales drittes ArgumentmonitorMode?: string, nur angehängt wenn nicht-leer; Test ergänzen:buildSaveMessage(FORM_DEFAULTS, [], 'span')endet auf&monitormode=span). - [ ] Step 3: saveerror-Empfang — Listener
chrome.webview.addEventListener('message', …): beisaveerrorrote Statuszeile „Speichern abgelehnt — Werte prüfen" einblenden (Dialog bleibt offen; Host schließt bei Erfolg). - [ ] Step 4: settings.html-CSS — Grid
minmax(0,1fr) minmax(0,1fr), Sektions-Rahmen im CRT-Look (Bestand fortschreiben),input[type=range]-Styling,.mon-card,.badge,.error-Zeile. Fenster-Inhalt für ~720×640 ausgelegt, aber flüssig (Overflow-y scroll als Fallback). - [ ] Step 5: Verifikation —
npm run typecheck+npm test;npm run dev+ Playwright-Screenshot:
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); const p = await b.newPage({ viewport: { width: 720, height: 640 } }); await p.goto('http://localhost:5173/settings.html?monitors=' + encodeURIComponent(JSON.stringify([{id:'A',name:'Laptop',w:1920,h:1200,portrait:false,primary:true},{id:'B',name:'Dell',w:2160,h:3840,portrait:true,primary:false}]))); await p.screenshot({ path: '/tmp/settings-v010.png', fullPage: true }); await b.close(); })()"
Screenshot ansehen: alle Sektionen da, Monitor-Karten mit Badges, keine Layout-Brüche.
- [ ] Step 6: Commit feat(settings): full macOS-parity dialog — looks, CRT sliders, monitors, performance
Task 6: C++ — options v0.10 (Struct, Query, Registry, Parse + Monitor-Keys)¶
Files:
- Modify: native/windows/host/src/options.h
- Modify: native/windows/host/src/options.cpp
- Test: native/windows/host/tests/options_test.cpp
Interfaces: - Produces (options.h):
struct SaverOptions {
// …bestehende 12 Felder unverändert…
std::wstring look; // leer = Eigene
std::wstring altitude = L"low";
std::wstring fog = L"auto";
std::wstring weather = L"light-fog";
std::wstring bank = L"1"; // Numerik als roher validierter String
bool reactive = true;
bool autocycle = true;
std::wstring cyclemin = L"5";
std::wstring termlayout = L"strip";
bool boot = true;
std::wstring bootspeed = L"normal";
bool daynight = true;
std::wstring crtintensity = L"0.35";
std::wstring curvature = L"0.012";
std::wstring aperture = L"0.22";
std::wstring bloomstrength = L"1.4";
std::wstring trailsamount = L"0.84";
std::wstring ntsc = L"0";
std::wstring halation = L"0.15";
std::wstring scale = L"1";
bool perfadapt = true;
};
struct MonitorSave { std::wstring id, mode, scene, preset; };
// msg OHNE "save:"; monitors nullable. All-or-nothing wie bisher.
bool ParseSaveMessage(const std::wstring& msg, SaverOptions& out,
std::vector<MonitorSave>* monitors = nullptr,
std::wstring* monitorMode = nullptr);
bool IsValidNumber(const std::wstring& v, double lo, double hi); // ^[0-9]+(\.[0-9]+)?$ + Range
- [ ] Step 1: Failing Tests — options_test.cpp: Default-Fixture und Flip-Roundtrip-Fixture auf die v0.10-Strings aus dem Kontrakt-Block umstellen; neue Fälle:
// Numerik-Validierung
SaverOptions n;
ExpectTrue(!ParseSaveMessage(L"crtintensity=1.5", n), "reject out-of-range");
ExpectTrue(!ParseSaveMessage(L"bank=1,5", n), "reject locale comma");
ExpectTrue(ParseSaveMessage(L"bank=1.25", n) && n.bank == L"1.25", "raw numeric passthrough");
// look darf leer sein
ExpectTrue(ParseSaveMessage(L"look=", n), "empty look ok");
ExpectTrue(!ParseSaveMessage(L"look=Heavy!", n), "reject bad look");
// Monitor-Keys
SaverOptions m; std::vector<MonitorSave> mons; std::wstring mode;
ExpectTrue(ParseSaveMessage(L"scene=city&m0id=DELL_ABC1&m0mode=scene&m0scene=matrix&m0preset=phosphor&m1id=LAPTOP_0&m1mode=off&m1scene=&m1preset=&monitormode=span", m, &mons, &mode), "parse monitors");
ExpectTrue(mons.size() == 2 && mons[0].scene == L"matrix" && mons[1].mode == L"off", "monitor entries");
ExpectTrue(mode == L"span", "monitor mode");
ExpectTrue(!ParseSaveMessage(L"m0id=A&m0mode=warp", m, &mons, &mode), "reject bad monitor mode");
// Registry-Roundtrip v0.10 (Test-Key): Flip-Werte schreiben, lesen, Query vergleichen.
- [ ] Step 2: Implementieren —
BuildQueryStringhängt die 21 Keys in Kontrakt-Reihenfolge an (lookmit EscapeDataString, Numerik roh);LoadOptions/SaveOptionsum die 21 Registry-Werte (Kontrakt-Namen) erweitern;IsValidNumber(manueller Scan, kein swscanf — Locale!);ParseSaveMessage: neue Keys mit exakten Checks (Enums per Whitelist, Numerik per IsValidNumber mit Kontrakt-Ranges, look leer-oder-IsSlug),mN*-Keys (Regex-frei: Präfixm, Ziffer 0–7, dannid|mode|scene|preset) in eine lokale Map, am Ende lückenlos 0..k prüfen und inmonitorsschieben (id: IsSlug-ähnlich, zusätzlichA-Z_erlaubt — eigeneIsMonitorId; mode-Whiteliston|off|random|scene; scene/preset leer-oder-IsSlug);monitormode-Key (Whitelistper|span). Unbekannte Keys → weiterhin false. - [ ] Step 3: Kompiliert nicht lokal — Sichtprüfung + Commit; CI verifiziert in Task 12.
- [ ] Step 4: Commit
feat(windows): options v0.10 — additive query/registry format, numeric passthrough, monitor saves
Task 7: C++ — Monitor-Enumeration + Monitor-Registry-IO¶
Files:
- Create: native/windows/host/src/monitors.h
- Create: native/windows/host/src/monitors.cpp
- Modify: native/windows/host/CMakeLists.txt (host_lib + dxgi-frei; nur user32 — QueryDisplayConfig lebt in user32)
- Test: native/windows/host/tests/options_test.cpp (nur SanitizeMonitorId — API-freie Pure-Funktion)
Interfaces: - Produces:
struct MonitorInfo {
std::wstring id; // stabil (EDID-Pfad, sanitized) — Registry-Subkey-Name
std::wstring name; // friendly name, ggf. leer
RECT rect; // physische Pixel (per-monitor-aware)
bool primary;
bool portrait; // rect höher als breit
};
std::vector<MonitorInfo> EnumMonitors();
std::wstring SanitizeMonitorId(const std::wstring& raw); // [A-Za-z0-9_-], sonst '_', max 128, leer→L"UNKNOWN"
// Registry-IO für Monitors\<Id> (regPath-Override für Tests):
struct MonitorConfig { std::wstring mode = L"on", scene, preset, wmode = L"off", wscene, wpreset; };
MonitorConfig LoadMonitorConfig(const std::wstring& id, const wchar_t* regPath = kRegPath);
void SaveMonitorConfig(const std::wstring& id, const MonitorConfig& c, const wchar_t* regPath = kRegPath);
std::wstring LoadMonitorMode(bool wallpaper, const wchar_t* regPath = kRegPath); // "per"|"span"
void SaveMonitorMode(bool wallpaper, const std::wstring& mode, const wchar_t* regPath = kRegPath);
- [ ] Step 1:
EnumMonitors:EnumDisplayMonitors(HMONITOR + Rect) → je MonitorGetMonitorInfoW(MONITORINFOEXW, primary-Flag, szDevice); Pfad-Auflösung:GetDisplayConfigBufferSizes/QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)+DisplayConfigGetDeviceInfomitDISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME(Match auf szDevice) undDISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME(monitorDevicePath + monitorFriendlyDeviceName). Id =SanitizeMonitorId(monitorDevicePath), FallbackSanitizeMonitorId(szDevice); Kollisions-Suffix_2,_3…portrait = (h > w). - [ ] Step 2: Registry-IO: Subkey
<regPath>\Monitors\<id>; fehlender Subkey → Defaults (wmode: primär on / sonst off entscheidet der AUFRUFER, LoadMonitorConfig liefert neutralwmode=L"off"und der Wallpaper-Pfad wendet den Primär-Default an, wenn der Subkey fehlt — dafür zusätzlichbool MonitorConfigExists(id, regPath)). - [ ] Step 3: SanitizeMonitorId-Tests in options_test.cpp (Umlaute/
\\?\DISPLAY#...-Pfade →_-ersetzt, Kappung, leer). - [ ] Step 4: Commit
feat(windows): stable EDID-based monitor identity + per-monitor registry config
Task 8: C++ — webview_host: LOCALAPPDATA-Profil + WebView-Rückgabe + PostWebMessage¶
Files:
- Modify: native/windows/host/src/webview_host.h
- Modify: native/windows/host/src/webview_host.cpp
Interfaces:
- Produces: onCreated-Signatur wird std::function<void(ICoreWebView2Controller*, ICoreWebView2*)> (Aufrufer: saver_window, settings_window, preview unberührt, wallpaper neu). UserDataDir: %LOCALAPPDATA%\KuroScreensaver\WebView2, Fallback %TEMP%\KuroScreensaverWV2.
- [ ] Step 1:
UserDataDir()aufGetEnvironmentVariableW(L"LOCALAPPDATA")+L"\\KuroScreensaver\\WebView2"umstellen (SHCreateDirectoryExWfür die Kette; bei leerer Env-Var alter TEMP-Pfad).onCreated(controller, webview.Get())durchreichen; alle bestehenden Aufrufer anpassen (Lambda-Signatur + ungenutzten Parameter benennen). - [ ] Step 2: Commit
feat(windows): persistent WebView2 profile under LOCALAPPDATA, expose ICoreWebView2 to hosts
Task 9: C++ — render_policy (pure) + Tests¶
Files:
- Create: native/windows/host/src/render_policy.h
- Create: native/windows/host/src/render_policy.cpp
- Modify: native/windows/host/CMakeLists.txt
- Test: native/windows/host/tests/options_test.cpp (gleiches Test-Target)
Interfaces:
- Produces (Port von native/macos/KuroNativeSaver/Core/RenderPolicy.swift, ohne Thermal):
enum class RenderState { Hidden, Frozen, Animating };
struct PolicyInputs {
bool occluded = false; // Monitor von Vollbild-/maximiertem Fenster verdeckt
bool sessionLocked = false;
bool displayOff = false;
bool fullscreenApp = false; // SHQueryUserNotificationState busy/D3D/presentation
bool onBattery = false;
bool powerSaver = false; // Windows-Stromsparmodus
bool animateOnBattery = false;
int acFps = 30;
int batteryFps = 10;
};
struct PolicyDecision { RenderState state; int fps; };
PolicyDecision DecideRenderPolicy(const PolicyInputs& in);
Prioritäten (wie macOS): Unsichtbarkeit (occluded|sessionLocked|displayOff) → Hidden(0); fullscreenApp → Frozen(0); (onBattery && !animateOnBattery) oder powerSaver → Frozen(0); sonst Animating(onBattery ? batteryFps : acFps).
- [ ] Step 1: Tests zuerst (options_test.cpp, Tabellen-Fälle):
ExpectTrue(DecideRenderPolicy({}).state == RenderState::Animating, "default animates");
ExpectTrue(DecideRenderPolicy({}).fps == 30, "ac fps");
{ PolicyInputs i; i.occluded = true; i.fullscreenApp = true;
ExpectTrue(DecideRenderPolicy(i).state == RenderState::Hidden, "invisibility beats fullscreen"); }
{ PolicyInputs i; i.sessionLocked = true; ExpectTrue(DecideRenderPolicy(i).state == RenderState::Hidden, "locked hides"); }
{ PolicyInputs i; i.fullscreenApp = true; ExpectTrue(DecideRenderPolicy(i).state == RenderState::Frozen, "fullscreen freezes"); }
{ PolicyInputs i; i.onBattery = true; ExpectTrue(DecideRenderPolicy(i).state == RenderState::Frozen, "battery w/o opt-in freezes"); }
{ PolicyInputs i; i.onBattery = true; i.animateOnBattery = true;
auto d = DecideRenderPolicy(i); ExpectTrue(d.state == RenderState::Animating && d.fps == 10, "battery opt-in 10fps"); }
{ PolicyInputs i; i.powerSaver = true; ExpectTrue(DecideRenderPolicy(i).state == RenderState::Frozen, "power saver freezes"); }
- [ ] Step 2: Implementation (~25 Zeilen, keine Win32-Includes im Header).
- [ ] Step 3: Commit
feat(windows): pure render-policy decision table (macOS RenderPolicy port, no thermal signal)
Task 10: C++ — settings_window v0.10 (DPI, monitors-Param, saveerror) + saver_window v0.10 (span/per/off/kiosk)¶
Files:
- Modify: native/windows/host/src/settings_window.cpp
- Modify: native/windows/host/src/saver_window.cpp
Interfaces:
- Consumes: EnumMonitors/LoadMonitorConfig/MonitorConfigExists/LoadMonitorMode/SaveMonitorMode/SaveMonitorConfig (Task 7), ParseSaveMessage(msg, out, &mons, &mode) (Task 6), neue CreateWebView-Signatur (Task 8).
- Produces: Saver-URL-Bau BuildSaverPage(const SaverOptions&, const MonitorConfig*) — Basis-Query + bei Monitor-Override scene/preset ersetzt + &kiosk=on.
- [ ] Step 1: settings_window — (a) Fenstergröße: nach
CreateWindowExWUINT dpi = GetDpiForWindow(hwnd);→ Client 720×640 skaliertMulDiv(720, dpi, 96)etc.,SetWindowPosre-zentriert auf Work-Area; (b) URL:settings.html+BuildQueryString(LoadOptions())+L"&monitormode=" + LoadMonitorMode(false)+L"&monitors=" + EscapeDataString(MonitorListJson())(kleines JSON per Hand:[{"id":"…","name":"…","w":1920,"h":1200,"portrait":false,"primary":true},…]— Name mit Escape für"/\); (c) WebMessage-Callback:save:→ Parse mit&mons,&mode; Erfolg →SaveOptions+SaveMonitorMode(false, mode)falls nicht-leer + jeMonitorSaveinMonitorConfig{mode,scene,preset}(WMode/WScene/WPreset des bestehenden Configs erhalten!)SaveMonitorConfig+DestroyWindow; Fehler →webview->PostWebMessageAsString(L"saveerror"), Fenster bleibt offen (webview-Pointer aus dem neuen onCreated speichern, AddRef/Release symmetrisch zum Controller). - [ ] Step 2: saver_window —
RunSaver():
SaverOptions opts = LoadOptions();
std::vector<MonitorInfo> mons = EnumMonitors();
if (mons.empty()) return 0;
bool span = LoadMonitorMode(false) == L"span";
ShowCursor(FALSE);
if (span) {
RECT vs{ GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), 0, 0 };
vs.right = vs.left + GetSystemMetrics(SM_CXVIRTUALSCREEN);
vs.bottom = vs.top + GetSystemMetrics(SM_CYVIRTUALSCREEN);
CreateSaverWindowWithWebView(vs, BuildSaverPage(opts, nullptr)); // ein Fenster, ein Timer
} else {
for (const MonitorInfo& m : mons) {
MonitorConfig c = LoadMonitorConfig(m.id);
if (c.mode == L"off") { CreateBlackCoverWindow(m.rect); continue; } // Watchdog ja, WebView nein
SaverOptions per = opts;
if (c.mode == L"random") per.scene = L"random";
else if (c.mode == L"scene" && !c.scene.empty()) per.scene = c.scene;
if (!c.preset.empty()) per.preset = c.preset;
CreateSaverWindowWithWebView(m.rect, BuildSaverPage(per, nullptr));
}
}
BuildSaverPage(o) = L"screensaver.html" + BuildQueryString(o) + L"&kiosk=on".
CreateBlackCoverWindow: gleiche Fensterklasse + SaverState + Timer, nur ohne CreateWebView (schwarzer Brush deckt). Bestehende Watchdog-/Exit-Logik unverändert; onCreated-Lambda auf die 2-Parameter-Signatur heben.
- [ ] Step 3: Commit feat(windows): span/per-monitor saver modes, black cover for off-monitors, kiosk URL, DPI-scaled settings dialog with monitor cards
Task 11: C++ — Wallpaper /w + Tray + PowerSignals + TrySuspend¶
Files:
- Create: native/windows/host/src/wallpaper_window.h (int RunWallpaper();)
- Create: native/windows/host/src/wallpaper_window.cpp
- Create: native/windows/host/src/tray.h / tray.cpp
- Modify: native/windows/host/src/main.cpp (Dispatch /w)
- Modify: native/windows/host/CMakeLists.txt (neue Quellen; wtsapi32 linken)
Interfaces:
- Consumes: DecideRenderPolicy (Task 9), EnumMonitors/Monitor-Configs (Task 7), CreateWebView (Task 8), BuildQueryString (Task 6).
- Produces: Tray-Menü-IDs IDM_TRAY_TOGGLE=1, IDM_TRAY_SETTINGS=2, IDM_TRAY_AUTOSTART=3, IDM_TRAY_EXIT=4; Run-Key HKCU\Software\Microsoft\Windows\CurrentVersion\Run\KuroWallpaper.
- [ ] Step 1: WorkerW-Attach —
HWND FindWallpaperHost() {
HWND progman = FindWindowW(L"Progman", nullptr);
SendMessageTimeoutW(progman, 0x052C, 0xD, 0x1, SMTO_NORMAL, 1000, nullptr);
// Strategie A (bis 23H2): WorkerW-Sibling hinter dem SHELLDLL_DefView suchen
HWND workerw = nullptr;
EnumWindows([](HWND top, LPARAM lp) -> BOOL {
if (FindWindowExW(top, nullptr, L"SHELLDLL_DefView", nullptr))
*reinterpret_cast<HWND*>(lp) = FindWindowExW(nullptr, top, L"WorkerW", nullptr);
return TRUE;
}, reinterpret_cast<LPARAM>(&workerw));
if (workerw) return workerw;
// Strategie B (24H2): WorkerW liegt direkt unter Progman
HWND child = FindWindowExW(progman, nullptr, L"WorkerW", nullptr);
return child ? child : progman;
}
Wallpaper-Fenster: WS_CHILD | WS_VISIBLE unter dem Host-HWND, Koordinaten relativ (MapWindowPoints vom virtuellen Screen); per/span analog Task 10 mit WMode-Werten (fehlender Subkey: primär on, sonst off — via MonitorConfigExists); URL screensaver.html + Query mit scale=<WallpaperScale> überschrieben + &kiosk=on&audio=off (audio-Feld vor BuildQueryString auf false zwingen).
- [ ] Step 2: PowerSignals — Message-Only-Window (HWND_MESSAGE): WTSRegisterSessionNotification (WM_WTSSESSION_CHANGE → lock/unlock), RegisterPowerSettingNotification(GUID_CONSOLE_DISPLAY_STATE) (PBT_POWERSETTINGCHANGE → displayOff), GetSystemPowerStatus + SHQueryUserNotificationState + Occlusion (Foreground-Fenster-Rect deckt Monitor-Rect ≥95 %) im 2-s-Poll-Timer. Zustandsänderung → DecideRenderPolicy je Fenster → WebMessage power:hidden|frozen|animating:<fps> an die Page; bei Hidden zusätzlich ICoreWebView2_3::TrySuspend (Callback ignoriert Fehler), bei Verlassen Resume().
- [ ] Step 3: Tray — Shell_NotifyIconW(NIM_ADD) mit App-Icon, WM_APP+1-Callback; Kontextmenü (CreatePopupMenu): Wallpaper anhalten/fortsetzen (Fenster verstecken + TrySuspend vs. zurück), Einstellungen… (RunSettings() blockiert — stattdessen Settings-Fenster nicht-modal im selben Thread öffnen: OpenSettingsWindow()-Refactor: RunSettings behält seine Message-Loop für /c, Tray-Pfad erzeugt nur das Fenster), Autostart-Häkchen (Run-Key lesen/schreiben: "<exe-Pfad>" /w), Beenden (PostQuitMessage). Icon-Tooltip „Kuro Wallpaper".
- [ ] Step 4: main.cpp — Dispatch: else if (flag == L"/w") { rc = RunWallpaper(); } vor dem Saver-Fallback.
- [ ] Step 5: Commit feat(windows): /w animated wallpaper — WorkerW attach (both strategies), tray, power policy enforcement with TrySuspend
Task 12: CI-Loop — kompilieren, Tests, Paket¶
Files: keine neuen (Fix-Commits nach Bedarf)
- [ ] Step 1:
git push origin main && git push github main(bzw. der eingerichtete GitHub-Push-Weg), danngh run watchaufwindows-host.ymlim GitHub-Mirror-Repo (johannes-kaindl/kuro-screensaver). - [ ] Step 2: Compile-/Testfehler lokal fixen, erneut pushen — iterieren bis grün (options_test inkl. render_policy-Fälle läuft im CI-Job).
- [ ] Step 3: Paket-Artefakt herunterladen (
gh run download), Zip-Inhalt prüfen:KuroScreensaver.scr+web/mitsettings.html/screensaver.html-Assets, Größe grob <6 MB. - [ ] Step 4: Commit (nur falls Fixes) — konventionell je Fix.
Task 13: bump-version.sh → app.rc + Doku + TaskNote¶
Files:
- Modify: scripts/bump-version.sh
- Modify: README.md (+ die 3 Linter-Pflichtsektionen Features/Usage/No build step)
- Modify: docs/WINDOWS-INSTALL.md (Wallpaper-Kapitel, Monitor-Modi, Performance-Tipps, neue Settings)
- Create (Vault): …/25_Coding/kuro-screensaver/_Tasks/v0.10.0 on-device-Abnahme Windows.md
- [ ] Step 1: bump-version.sh — dritte Zieldatei
native/windows/host/app.rc:FILEVERSION/PRODUCTVERSION→X, Y, Z, 0, String-ValuesFileVersion→X.Y.Z.0,ProductVersion→X.Y.Z(sed auf Temp-Kopie, gleiche Preflight-/Validierungs-/Atomic-Move-Systematik wie die zwei bestehenden Ziele). - [ ] Step 2: README — Windows-Abschnitt aktualisieren (Settings-Umfang, Multi-Monitor, Wallpaper); Pflichtsektionen
## Features,## Usage,## No build stepergänzen/umbenennen, danachpython3 ../_docs/readme/readme_lint.py kuro-screensaver(vom Repo-Root;_docs= Schwester-Repo) → 0 Fehler. - [ ] Step 3: WINDOWS-INSTALL.md — neue Kapitel: „Animierter Hintergrund (/w)", „Mehrere Monitore", „Leistung" (Render-Scale, adaptive Qualität, Hybrid-GPU-Hinweis auf Windows-Grafikeinstellungen für msedgewebview2).
- [ ] Step 4: TaskNote im Vault anlegen (Frontmatter-Muster der v0.9-Note): Checkliste je Block — Kiosk (keine Bar, 30 s beobachten), Settings-Roundtrip inkl. Slider + HiDPI 150 %, Multi-Monitor per/span/off/random + Hochformat, Wallpaper an/aus/Autostart/Lock/Batterie/Vollbild-App, Upgrade v0.9→v0.10 (Registry überlebt), Win10-MessageBox, SmartScreen.
- [ ] Step 5: Commit
docs+build: v0.10 docs, README lint sections, app.rc in bump-version
Task 14: Release-Vorbereitung (WARTET auf User-Go)¶
- [ ]
bash scripts/bump-version.sh 0.10.0→ Commitchore(release): v0.10.0. - [ ] NACH explizitem Go von Johannes:
git tag v0.10.0 && git push origin main v0.10.0(+ GitHub-Push für CI), Release-Assets von /releases/latest per API gegenprüfen (LESSON „README ist das Schaufenster"). macOS-dmg nur bei Bedarf neu anhängen (macOS unverändert → das v0.9.1-dmg bleibt gültig? NEIN — jedes Release braucht seine eigenen Assets: dmg neu bauen viascripts/package-native-app.sh, da die Engine-Web-Änderungen die Metal-App zwar nicht betreffen, das Release aber vollständig sein muss).
Self-Review (durchgeführt)¶
- Spec-Coverage: §1→Task 1/2/10; §2→Task 1/4/5/6/10; §3→Task 1/2/8 (+GPU-Recherche in Task 12 falls CI-Zeit übrig — bewusst optional); §4→Task 6/7/10; §5→Task 3/9/11; §6→Task 6 (Fixtures), 12 (CI), 13 (Doku/bump), 14 (Release).
- Konsistenz:
ParseSaveMessage-Signatur (Task 6) == Verwendung (Task 10);CreateWebView-Signatur (Task 8) == Aufrufer (Task 10/11); Fixture-Strings identisch in Task 4 und 6;monitormodebewusst NICHT in den 33er-Fixtures (separater Test in Task 4/6). - Platzhalter: keine.