TDD must be used always, no exceptions.
Every feature, bugfix, or change must follow the Red-Green-Refactor cycle:
- Red — Write a failing test first that describes the expected behavior
- Green — Write the minimum code to make the test pass
- Refactor — Clean up the code while keeping tests green
If you are about to write implementation code and no test exists for it yet, STOP and write the test first.
GithubSaver is a Windows screensaver (.scr) that displays GitHub activity as D3.js visualizations. It uses C# .NET 8 with WebView2 to host web content fullscreen.
GithubSaver.scr (C# .NET 8 + WebView2)
├── Program.cs — Entry point, parses /s /c /p args
├── ScreensaverForm.cs — Fullscreen WebView2, exits on any input
├── ConfigForm.cs — WebView2 settings dialog (HTML-rendered)
├── PreviewForm.cs — Tiny WebView2 for Windows preview
├── Services/
│ ├── GitHubDataService.cs — GitHub REST + GraphQL API + JSON caching
│ ├── SettingsManager.cs — JSON settings at %LOCALAPPDATA%\GithubSaver\
│ └── CredentialStore.cs — Windows Credential Manager for GitHub PAT
└── wwwroot/
├── shared/ — D3.js, common CSS, data-loader.js
├── modules/ — Screensaver visualization modules (HTML/JS/D3)
└── config/ — Settings UI (HTML/JS)
Each module is a self-contained index.html in wwwroot/modules/{name}/:
| Module | Description |
|---|---|
heatmap-multidata |
4 stacked grids: Commits (green), PRs (blue), Issues (purple), Reviews (orange) |
heatmap-years |
7 years of contribution grids stacked vertically |
rain-days |
365 columns, chronological year sweep |
rain-repos |
Each column = a repo, rain intensity = activity level |
heatmap-rain-weeks |
Matrix-style contribution rain with real week data highlights |
.scr /s starts → SettingsManager loads config
→ GitHubDataService fetches data (or reads cache)
→ Writes JSON to wwwroot/data/
→ WebView2 loads module's index.html
→ data-loader.js provides data to D3 visualization
→ Any user input → screensaver exits
- Use file-scoped namespaces (
namespace GithubSaver;) - Nullable reference types enabled — handle nulls explicitly
- Async/await for all I/O operations
- Never throw exceptions in the screensaver runtime path — catch and degrade gracefully
- Use
System.Text.Jsonfor serialization (not Newtonsoft)
- All module JS and CSS inline in the single
index.htmlfile - Use
requestAnimationFramefor all animations — target 60fps - Use Canvas for particle-heavy effects, SVG for structured visualizations
- Pure black background (#000) — OLED burn-in prevention
- No static text — all labels must fade in/out and drift position
- Modules must work with demo data (no GitHub connection required)
- Reference shared assets via relative paths:
../../shared/d3.v7.min.js
- Must exit immediately on any user input (mouse move >5px, click, key, wheel, touch)
- No static UI elements that could cause OLED burn-in
- All animations must be continuous — no frozen frames
- Must work offline (cached data or demo data fallback)
- Must never show an error dialog or crash visibly
- Use xUnit as the test framework
- Test project at
tests/GithubSaver.Tests/ - Mock external dependencies (HTTP, file system, Credential Manager)
- Key areas to test:
SettingsManager: Load/Save round-trip, missing file handling, corrupt JSONGitHubDataService: API response parsing, cache TTL, offline fallbackProgram.cs: Command-line argument parsing (/s, /c, /p)Settings: Default values, serialization
- Use a browser-based test runner or Node.js with jsdom
- Key areas to test:
data-loader.js: Demo data generation, fetch fallback, postMessage handling- Module data consumption: Verify each module handles missing/partial data
This project uses Semantic Versioning:
- MAJOR.MINOR.PATCH (e.g.
1.2.3) - Pre-release: append
-beta,-alpha, etc. (e.g.0.1.0-beta) - MAJOR: Breaking changes (settings format, module API)
- MINOR: New features (new modules, new settings)
- PATCH: Bug fixes, performance improvements
Currently in beta (0.x) — the API and settings format may change.
Tag releases with v prefix: v0.1.0-beta, v1.0.0.
Important: Always bump the version in GithubSaver.csproj AND installer/Package.wxs before building a new MSI. Windows Installer uses the version to detect upgrades — same version = skipped.
dist/— Development builds for local testing. Rebuilt frequently during development. Use for quick iteration and debugging. Gitignored.releases/v{version}+{build}/— Release builds (e.g.releases/v0.2.4-beta+3/). Each build gets its own folder. Gitignored.
Key difference: dist/ is throwaway — rebuild it anytime. releases/ are versioned snapshots, built by the release script.
# Dev build (fast iteration)
dotnet publish src/GithubSaver/GithubSaver.csproj -c Release -r win-x64 --self-contained -o dist
copy dist\GithubSaver.exe dist\GithubSaver.scr
# Release build (automated — preferred)
.\build-release.ps1 -Version 0.2.5-beta
# Release build (skip tests for quick rebuild)
.\build-release.ps1 -Version 0.2.5-beta -SkipTests
# Run tests
dotnet test tests/GithubSaver.Tests/GithubSaver.Tests.csproj
npx jest tests/js/Always use build-release.ps1 to create release candidates. It automates the full pipeline:
- Auto-increments a build number (4th digit) for MSI upgrade detection
- Updates version in
GithubSaver.csprojandinstaller/Package.wxs - Runs all tests (C# + JS) — fails fast on any failure
- Publishes self-contained single-file build to
releases/v{version}+{build}/ - Copies
GithubSaver.exe→GithubSaver.scr - Builds MSI installer with WiX
- Prints artifact summary and next steps
The build number auto-increments within the same base version and resets when the version changes. This ensures every MSI build is upgradable without burning real semver numbers on test iterations.
- Assembly version:
0.2.4-beta+3(semver + build metadata) - MSI version:
0.2.4.3(4-digit numeric for Windows Installer) - Build number stored in
.build-number(gitignored, local per machine)
Use -SkipTests only when rebuilding a version whose tests have already passed.
- Run
.\build-release.ps1 -Version {version}— this handles steps 2-4 automatically - User has tested the build and confirmed it works ← REQUIRED
- Only then: create GitHub Release with release notes
# Push tags
git push origin master --tags
# Create GitHub Release via gh CLI (or via github-mcp-server)
gh release create v0.1.0-beta --title "v0.1.0-beta" --notes "Release notes here" --prerelease## What's New
- Feature 1
- Feature 2
## Bug Fixes
- Fix 1
## Screensaver Modules
- heatmap-multidata, heatmap-years, rain-days, rain-repos, heatmap-rain-weeks
## Installation
1. Download and extract the release ZIP
2. Copy `GithubSaver.scr` to `C:\Windows\System32\`
3. Open Screen Saver Settings and select GithubSaver
## Requirements
- Windows 10/11
- WebView2 Runtime (pre-installed on Windows 11)This project follows GitHub Flow:
mainis always deployable — never commit directly to main- Create a branch — for every feature, bugfix, or change, create a descriptively named branch from
main(e.g.feature/multi-monitor,fix/mouse-exit-threshold) - Commit often — make small, focused commits with clear messages
- Open a Pull Request — when work is ready for review, open a PR against
main - Review and discuss — get feedback, iterate
- Merge to main — after approval, merge the PR (squash or merge commit)
- Delete the branch — clean up after merge
Branch naming: feature/, fix/, chore/, docs/ prefixes followed by a short kebab-case description.
- Remote: https://github.com/pedrofuentes/github-saver
- License: MIT
- Branch strategy: GitHub Flow —
mainalways deployable, feature branches for all work
FetchAndCacheAllAsyncmust returnDictionary<string,string>— callers need in-memory data, not just file side-effects. File writes are for JS fallback only.- Never cache empty API responses (
{},[]). A 2-byte cached file treated as "fresh" blocks all subsequent fetches until TTL expires. - GraphQL requires authentication — public REST endpoints work without a token, but GraphQL returns 401. Always warn when no token is configured.
- The 15-second fetch timeout is necessary for first-time users fetching 7 years of GraphQL data. 5 seconds is too short.
- MSI version must be numeric 4-digit (
MAJOR.MINOR.PATCH.BUILD). Pre-release suffixes like-betamust be stripped. - Same MSI version = upgrade skipped. Use auto-incrementing build number (4th digit) during development.
MajorUpgraderuns uninstall-then-install. UseREMOVE="ALL" AND NOT UPGRADINGPRODUCTCODEto run cleanup only on full uninstall, not upgrade.- WiX v4 Custom action conditions use
Condition=""attribute, not inner text. - WiX regex for Package Version must not match
InstallerVersionor XML declarationversion=. - The installed
.scrruns fromC:\Windows\System32— it reads modules fromC:\Program Files\GithubSaver\wwwrootvia registry path. Must reinstall MSI to pick up new code.
EnsureCoreWebView2Asynccan hang forever if WebView2 runtime is missing. Always use a timeout (10s) with fallback UI.- Data is injected via
AddScriptToExecuteOnDocumentCreatedAsyncbefore navigation. UseJSON.parse()wrapper to prevent code injection from malicious JSON. - Each form needs a separate
userDataFolderto avoid locking conflicts between screensaver and config.
- Use
InternalsVisibleToto test internal methods without making them public. - When extracting shared JS functions to a separate file, update ALL module
<script>tags and run ALL JS tests — partial extraction breaks modules silently. - Static classes are hard to test. Use a static facade pattern:
Logger.Log()delegates toFileLoggerinstance,Logger.InstanceexposesILoggerfor DI.