A Windows .NET presentation app that displays web content and images as slides.
This is a vibe-coded port of Simon Willison's dream macOS presentation app. It is entirely built in OpenCode, mostly using OpenAI GPT-5.3 Codex and MiniMax-M2.5 Free models. I have never opened the project in Visual Studio.
For contributor/agent workflow guidance, see AGENTS.md.
- Edit Mode: Left sidebar with slide URL list (numbered, drag-to-reorder); right pane WebView2 preview of the selected slide
- Play Mode: Fullscreen presentation window with black background, WebView2 content, slide counter overlay, arrow key navigation (wraps around), Escape to exit
- Auto-persist: Slide list is saved automatically and restored on relaunch (
%APPDATA%\Present.NET\slides.txt) - File I/O: Open/Save slide lists as plain text files (one URL per line)
- Zoom Controls:
Ctrl+=/Ctrl+-/Ctrl+0to zoom in/out/reset (applies to both preview and fullscreen) - Theme Modes:
View -> ThemesupportsUse System,Light, andDarkfor app UI chrome; preference is saved across launches - Image Slides: URLs ending in
.png,.gif,.jpg,.jpeg,.webp,.svgare rendered as full-window images on a black background - Slide Cache: Slides are cached on load for faster navigation; toolbar actions support clear, re-cache, and reload of the selected slide
- Remote Control Server: Embedded HTTP server on port 9123 with a mobile-friendly HTML control page
- Windows 10 / 11 (WPF is Windows-only)
- .NET 8 SDK — Download
- Microsoft Edge WebView2 Runtime — Usually pre-installed on Windows 10/11. If not, download here
If you are new to .NET, follow these steps exactly.
- Install the .NET 8 SDK: https://dotnet.microsoft.com/download/dotnet/8.0
- Open a terminal in this repository root (
Present.NET). - Verify .NET is installed:
dotnet --versionYou should see a version that starts with 8..
- Restore dependencies:
dotnet restore- Build the app:
dotnet build Present.NET.sln -c Release- Run the app:
dotnet run --project src/Present.NET/Present.NET.csproj# Clone and build
git clone https://github.com/charlesroper/Present.NET.git
cd Present.NET
dotnet restore
dotnet build Present.NET.sln -c Release
dotnet format Present.NET.sln
# Run
dotnet run --project src/Present.NET/Present.NET.csprojOr open Present.NET.sln in Visual Studio 2022 and press F5.
Releases are automated via GitHub Actions. To create a release:
git tag v1.0.0-beta1
git push --tagsThis triggers the workflow to build a self-contained Windows exe, zip it, and create a GitHub Release.
See .github/workflows/release.yml and AGENTS.md for details.
This repository includes automated tests in two projects:
tests/Present.NET.Tests- unit and integration tests for core logic and servicestests/Present.NET.UiTests- gated desktop UI smoke tests (FlaUI)
Run all tests:
dotnet test Present.NET.slnRun only unit/integration tests:
dotnet test tests/Present.NET.Tests/Present.NET.Tests.csprojRun UI smoke tests (opt-in):
$env:PRESENT_UI_TESTS = "1"
dotnet test tests/Present.NET.UiTests/Present.NET.UiTests.csprojIf PRESENT_UI_TESTS is not set to 1, UI smoke tests are skipped by design.
Development follows red/green/refactor TDD:
- Write a failing test first (red)
- Implement the smallest change to pass (green)
- Refactor with tests still passing (refactor)
Recent test-related changes in this repo were implemented as one commit per TDD slice to keep history explicit and auditable.
- Click + Add Slide to add a new slide URL
- Type or paste a URL into the text field (e.g.
https://example.comorhttps://example.com/image.png) - Select a slide to preview it in the right pane
- Drag slides up/down to reorder them
- Use ↑/↓ toolbar buttons or drag-and-drop to reorder
- Use File → Open / Save / Save As to manage slide list files
- Use View → Theme to switch app UI between Use System, Light, and Dark
Press F5 or click ▶ Play to start the presentation:
| Key | Action |
|---|---|
→ / ↓ / Space / PgDn |
Next slide |
← / ↑ / PgUp |
Previous slide |
Ctrl+= |
Zoom in |
Ctrl+- |
Zoom out |
Ctrl+0 |
Reset zoom |
F |
Toggle slide counter |
Esc |
Exit fullscreen |
Navigation wraps around (last slide → first, first slide → last).
URLs ending in .png, .gif, .jpg, .jpeg, .webp, or .svg are automatically detected and rendered as full-window images on a black background, scaled to fit while preserving aspect ratio.
When you open a slide list, Present.NET starts caching images in the background.
- Selecting an image slide also triggers caching if it is not cached yet.
- Reload Slide purges and re-caches the selected slide.
- Re-cache clears and rebuilds cache for all slides.
- Clear Cache removes all cached slide content.
- Web pages are always "Live" — they are not cached because they are rendered directly from the network.
Cache status is shown next to each slide in the sidebar:
- Cached — image saved locally, loads instantly offline
- Live — web page loads from the internet each time
- Failed — image could not be cached (check URL and internet connection)
An HTTP server runs on port 9123. Open http://<your-ip>:9123/ on your phone or tablet for a mobile-friendly remote control page. The IP address is shown in the toolbar.
The remote server is hosted with Kestrel and does not require netsh URL ACL setup.
In the toolbar remote field:
- Click the copy icon to copy only the remote URL (for example
http://<your-ip>:9123/) - Double-click anywhere on the remote field to copy only the remote URL
If conference or guest Wi-Fi blocks device-to-device LAN traffic, use Tailscale for a more reliable remote connection.
- What it is: Tailscale gives your laptop and phone stable private IP addresses (typically
100.x.y.z) on a shared tailnet. - Why use it: It avoids local network quirks where
http://<lan-ip>:9123/works on localhost but fails from another device.
Quick setup:
- Install Tailscale on your laptop and phone.
- Sign in on both devices with the same Tailscale account (same tailnet).
- On your laptop, find the Tailscale IPv4 address in the Tailscale app.
- On your phone, open
http://<tailscale-ip>:9123/.
Example:
http://100.101.102.103:9123/
API Endpoints:
| Endpoint | Description |
|---|---|
GET / |
Mobile HTML remote control page |
GET /next |
Go to next slide |
GET /prev |
Go to previous slide |
GET /play |
Start fullscreen presentation |
GET /stop |
Stop presentation (close fullscreen) |
GET /zoomin |
Zoom in |
GET /zoomout |
Zoom out |
GET /scroll?dy=200 |
Scroll page by dy pixels |
GET /status |
JSON status (currentIndex, slideCount, isPlaying, currentUrl, zoomFactor) |
All endpoints (except /) return a JSON status object.
| Shortcut | Action |
|---|---|
F5 |
Play presentation |
Ctrl+O |
Open file |
Ctrl+S |
Save file |
Ctrl+Shift+S |
Save As |
Ctrl+= |
Zoom in |
Ctrl+- |
Zoom out |
Ctrl+0 |
Reset zoom |
Present.NET/
├── Present.NET.sln
├── README.md
├── tests/
│ ├── Present.NET.Tests/
│ └── Present.NET.UiTests/
└── src/
└── Present.NET/
├── Present.NET.csproj
├── App.xaml / App.xaml.cs
├── MainWindow.xaml / MainWindow.xaml.cs ← Edit mode UI
├── FullscreenWindow.xaml / FullscreenWindow.xaml.cs ← Play mode
├── Models/
│ ├── SlideItem.cs ← Data model for a slide
│ └── SlideHelper.cs ← URL detection & image HTML
└── Services/
├── PersistenceService.cs ← Save/load slide lists
└── RemoteControlServer.cs ← HTTP remote control server
Slide lists are plain text files with one URL per line:
https://example.com/slide1
https://example.com/slide2.png
https://mypresentation.com/deck
Blank lines are ignored. The auto-save file is located at %APPDATA%\Present.NET\slides.txt.
