# SkimDown for Windows
SkimDown for Windows is a Windows port of [SkimDown](https://github.com/07JP27/SkimDown) — a native macOS reader for Markdown folders generated by AI agents, developer tools, and teams.
Open a folder, and SkimDown shows only the Markdown files in a sidebar tree and renders the selected file in a calm, read-only preview — no editor chrome, no accidental edits.
> **Original macOS app:** Swift 6 + AppKit + `WKWebView`, by
> [@07JP27](https://github.com/07JP27).
> **This port:** WinUI 3 + Windows App SDK + WebView2 + CommunityToolkit.Mvvm,
> on .NET 10.
## Highlights
- Open a folder with **File → Open Folder…**, **Ctrl+O**, **Window → New Window** (Ctrl+N), by dragging a folder onto the window, or by passing a folder path on the command line — once the app is installed, `skimdown` and `skim` execution aliases are registered automatically, so `skimdown D:\notes` / `skim D:\notes` (or `skimdown .` / `skim .` in any folder) works from any terminal
- **Open a single Markdown file** by double-clicking a `.md` / `.markdown` file in Explorer (after choosing "Open with → SkimDown"), by `skimdown README.md` or `skim README.md` on the command line, or by dragging a `.md` file onto the window — opens a focused **single-file mode** window with the sidebar hidden; one process is reused across activations
- Multiple windows for multiple folders (dropping a folder onto a window that already has one opens a new window)
- Sidebar position is swappable left / right (**View → Move Sidebar to Right / Left**)
- Folder-first, case-insensitive Markdown tree (recursive `.md` / `.markdown` discovery)
- Switch the sidebar between the folder tree and a **flat list sorted by date modified** (newest first) — an app-wide toggle in the sidebar header; each row shows the file name, its last-modified time, and its subfolder
- Hidden-file and ignored-directory filtering (`.git`, `node_modules`, `.build`, `DerivedData`, …)
- Read-only WebView2 preview with bundled rendering assets (no CDN required, works fully offline), including rendered YAML front matter
- Markdown rendering bundled: markdown-it + highlight.js + DOMPurify + KaTeX + Mermaid + emoji; GitHub-style alerts and inline color swatches included
- Table of Contents pane on the right side of the Markdown preview, generated from rendered headings and toggleable from **View → Table of Contents** or Settings
- In-document search for the current file (**Ctrl+F**), plus **Edit → Use Selection for Find** (Ctrl+E)
- Zoom the preview with **Ctrl+`+`** / **Ctrl+`-`** / **Ctrl+`0`** (View → Zoom), or smoothly with **Ctrl+mouse wheel** and **precision-touchpad pinch** — the zoom level is persisted (clamped to 50–300 %)
- Live reload — Markdown add / delete / rename / update events refresh the tree and preview (also works in single-file mode for the open file)
- **VS Code-compatible custom color schemes** — drop a `*.json` theme into the Themes folder and pick it from **View → Theme** (see [Custom color schemes](#custom-color-schemes))
- Persists sidebar position, view mode (tree / by date modified), visibility, width, preview Table of Contents visibility, theme, zoom level, recent folders, last selected file, and tree expansion state (single-file mode never updates RecentFolders / LastFolderPath, so explorer double-clicks don't pollute folder history)
- Local-only — no telemetry, no Markdown text leaves your machine
## Single-file mode
When you open a Markdown file directly (instead of a folder), SkimDown enters **single-file mode**:
- The sidebar (folder tree) is hidden; the preview takes the full window
- Window title shows the file name: `README.md — SkimDown`
- **Toggle Sidebar** (Ctrl+B) and **Move Sidebar** are disabled
- The parent folder is still watched, so external edits to the open file refresh the preview live
- RecentFolders, LastFolderPath, and per-folder expansion state are **not** updated
- Relative Markdown links (`[next](./next.md)`) open in a **new** single-file window
- The persisted sidebar visibility is preserved as-is — open another folder later (File → Open Folder…) and your sidebar comes back as configured
How to enter single-file mode:
- **Explorer** — right-click a `.md` / `.markdown` file → **Open with → SkimDown** (set as default after the first time)
- **Command line** — `skimdown README.md` / `skim README.md`
- **Drag-drop** — drag a `.md` file onto a SkimDown window. If the window already has a folder or another single file, the dropped file opens in a new window
- **Multiple files** — select several `.md` files in Explorer and choose **Open with → SkimDown**, and each file gets its own window
### Single-instance behavior
SkimDown runs as a **single process** per user. The second time you double-click a Markdown file, the activation is redirected to the existing process (rather than spawning a duplicate). This keeps settings (`settings.json`) from being clobbered by competing writes and matches the upstream macOS behavior.
## Architecture
**WinUI 3 + Windows App SDK 2.0.1** + WebView2 + CommunityToolkit.Mvvm on **.NET 10**, with **Microsoft.Extensions.DependencyInjection** for composition. WebView2 serves bundled renderer assets from one virtual host (`https://skimdown-app.example/`) while the opened folder is mapped to a separate origin (`https://skimdown-content.example/`), so a renderer bug cannot reach the user's content from the asset origin. Markdown is delivered to the renderer through `CoreWebView2.PostWebMessageAsJson` — never `NavigateToString`.
The solution is split into 4 + 1 projects following clean-architecture-style layering. Dependencies flow inward only.
| Project | TFM | Role |
|---|---|---|
| **`SkimDownForWindows.Domain`** | `net10.0` | Pure value objects / enums (`AppTheme`, `SidebarPosition`, `SidebarViewMode`, `LinkKind`, `LinkClassification`). No external dependencies. |
| **`SkimDownForWindows.Application`** | `net10.0` | Use case layer: abstractions (`IFileSystem`, `IFolderWatcher`, `ISettingsRepository`, `IClipboardService`, `IShellService`, `ISystemThemeProvider`, `IUiDispatcher`, `IExternalUriLauncher`, `IWindowService`, `IAppLogger`, `IMarkdownFileReader`), pure services (`MarkdownScanner`, `MarkdownTreeBuilder`, `RecentMarkdownListBuilder`, `InitialSelectionPicker`, `LinkResolver`, `CommandLineLauncher`), ViewModels (`MainPageViewModel`), persistence DTOs (`AppSettings`, `FolderState`), UI-bound models (`MarkdownTreeItem`, `LoadRequest`, `RecentFolderEntry`), and path utilities. |
| **`SkimDownForWindows.Infrastructure`** | `net10.0-windows10.0.26100.0` | Platform implementations of the abstractions — `LocalFileSystem`, `FileSystemFolderWatcher`, `JsonSettingsRepository`, `WindowsClipboardService`, `ExplorerShellService`, `UiSettingsThemeProvider`, `LauncherExternalUriService`, `FileAppLogger`. References WinRT only — **does not depend on `Microsoft.WindowsAppSDK`**. |
| **`SkimDownForWindows`** (App / Presentation) | `net10.0-windows10.0.26100.0` | XAML pages / windows / `MarkdownPreview` UserControl, the DI composition root (`App.Services`), and WindowsAppSDK-specific service implementations (`DispatcherQueueUiDispatcher`, `WindowService`). |
| **`SkimDownForWindows.Tests`** | `net10.0` | Unit + opt-in integration tests against Domain + Application. Stays cross-platform-neutral (no Infrastructure reference). |
Each `MainWindow` owns its own `IServiceScope`, so window-scoped services (`IFolderWatcher`, `MainPageViewModel`, Markdown pure services, `CommandLineLauncher`) are deterministically disposed when the window is closed.
The Windows-port behavior spec — keyboard shortcuts, menus, single-file mode, single-instance redirect, the WebView2 two-origin model, persistence keys, and Windows-specific deltas from the macOS app — is in [`design/SPEC.md`](design/SPEC.md).
A current-state technical snapshot of the codebase (project layout, DI registrations, message protocols, persistence model, theme resolution, activation flow) lives under [`docs/`](docs/). Use it when you need to know "how is the code wired right now" rather than "what should it do" (SPEC) or "why was it built this way" (ADR).
Design decisions (the "why") are recorded as ADRs under [`.github/adr/`](.github/adr/). The repository-specific coding guide for humans and Copilot lives at [`.github/copilot-instructions.md`](.github/copilot-instructions.md).
Detailed design documents for the upstream macOS SkimDown are in [`07JP27/SkimDown/design/`](https://github.com/07JP27/SkimDown/tree/main/design).
## Development
### Prerequisites
- Windows 10 v1903+ (Windows 11 recommended)
- Developer Mode enabled (Settings → System → For developers → On)
- .NET 10 SDK
- WinUI 3 templates: `dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`
- `winapp` CLI: `winget install Microsoft.WinAppCLI`
### Build commands
```powershell
dotnet build SkimDownForWindows.slnx # Debug build
dotnet test src\SkimDownForWindows.Tests # Run unit tests
```
Building and launching the WinUI 3 app requires the `winapp` CLI:
```powershell
cd src\SkimDownForWindows
dotnet build SkimDownForWindows.csproj
winapp run .\bin\<Platform>\Debug\<TargetFramework>\win-<arch> --debug-output
```
For a single-command build + launch with auto platform / packaging detection, use the `BuildAndRun.ps1` script that ships with the [`awesome-copilot/winui`](https://github.com/microsoft/win-dev-skills) plugin:
```powershell
copilot plugin install winui@awesome-copilot
cd src\SkimDownForWindows
~\.copilot\installed-plugins\awesome-copilot\winui\skills\winui-dev-workflow\BuildAndRun.ps1
```
The unit test project is plain `net10.0` (no UI dependencies) and re-includes the UI-agnostic source files from the main project. To additionally run the upstream-samples integration tests, point `SKIM_SAMPLES_PATH` at a cloned copy of the SkimDown `samples/` directory:
```powershell
$env:SKIM_SAMPLES_PATH = "C:\path\to\SkimDown\samples"
dotnet test src\SkimDownForWindows.Tests
```
## Custom color schemes
SkimDown for Windows can load user-defined themes for the preview area. Themes are written as JSON files in the [VS Code color theme format](https://code.visualstudio.com/api/references/theme-color), so existing VS Code theme assets can be reused.
### Where themes live
Drop `*.json` files into the Themes folder under your local app data:
- **Packaged build**: `%LOCALAPPDATA%\Packages\<package-family>\LocalState\Themes\`
- **Unpackaged build**: `%LOCALAPPDATA%\SkimDownForWindows\Themes\`
The fastest way to open it: **View → Theme → Open Themes Folder**. After editing or adding a file choose **View → Theme → Reload Themes** to refresh the menu — the folder is **not** watched automatically.
### JSON format
Each file is a stand-alone VS Code color theme:
```json
{
"$schema": "vscode://schemas/color-theme",
"name": "My Theme",
"type": "dark",
"colors": {
"editor.background": "#1e1e1e",
"editor.foreground": "#d4d4d4",
"textLink.foreground": "#3794ff"
}
}
```
- `name` — label shown in **View → Theme**. Falls back to the file name when omitted.
- `type` — `"light"` or `"dark"`. Picks the light/dark code-highlight CSS, decides whether to use the dark CSS fallback block, and chooses the Mermaid theme.
- `colors` — VS Code color keys. Only the subset listed below is consumed by SkimDown; the rest are ignored.
- `tokenColors` (syntax highlighting) is **not yet supported**. Code blocks use GitHub's light or dark palette based on `type`.
### Supported color key mapping
SkimDown reads each row top-down and uses the first VS Code key found. Missing keys fall back to the built-in `--skim-*` palette appropriate for the theme `type`.
| SkimDown CSS variable | VS Code keys (in priority order) |
| ------------------------- | ----------------------------------------------------------------------------- |
| `--skim-bg` | `editor.background` |
| `--skim-fg` | `editor.foreground`, `foreground` |
| `--skim-muted` | `descriptionForeground`, `disabledForeground` |
| `--skim-border` | `panel.border`, `editorGroup.border`, `editorWidget.border`, `contrastBorder` |
| `--skim-soft` | `editorGroupHeader.tabsBackground`, `editor.lineHighlightBackground`, `sideBar.background` |
| `--skim-soft-strong` | `editorWidget.background`, `editor.background` |
| `--skim-code-bg` | `editor.lineHighlightBackground`, `editorGroupHeader.tabsBackground` |
| `--skim-table-stripe` | `editorGroupHeader.tabsBackground`, `editor.lineHighlightBackground` |
| `--skim-link` | `textLink.foreground`, `editorLink.activeForeground`, `focusBorder` |
| `--skim-blockquote` | `descriptionForeground`, `editor.foreground` |
| `--skim-mark-bg` | `editor.findMatchHighlightBackground` |
| `--skim-mark-current-bg` | `editor.findMatchBackground` |
Accepted color value formats are restricted to safe CSS forms: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `rgba(...)`, `hsl(...)`, `hsla(...)`, and `transparent`. Anything containing `var()`, `calc()`, `url()`, `;`, `{`, or `}` is rejected and falls back to the built-in palette.
### Selecting & switching
Open **View → Theme** to see the built-ins (**System / Light / Dark**), registered custom themes, and the **Open Themes Folder** / **Reload Themes** actions. The currently active theme is checked. If the JSON file backing the selected custom theme is deleted, SkimDown silently falls back to **System** the next time the app starts or **Reload Themes** is invoked.
## Microsoft Store submission
SkimDown is published under the reserved app name **SkimDown** in [Microsoft Partner Center](https://partner.microsoft.com/dashboard/products/9NHTZMM0XMMF/overview). The `Package.appxmanifest` is pre-wired to that product:
| Field | Value |
|---|---|
| `Identity/Name` | `45014okazuki.SkimDown` |
| `Identity/Publisher` | `CN=57A8C5FA-395A-4109-91A0-CF1B93556B5D` |
| `Properties/PublisherDisplayName` | `okazuki` |
### Build the `.msixupload`
```powershell
.\scripts\Build-StorePackage.ps1
```
This produces `bin\StorePackage\45014okazuki.SkimDown_<version>.msixupload` containing a multi-architecture `.msixbundle` (x64 + ARM64 by default). Add `-IncludeX86` (or `-Architectures x64,arm64,x86`) for a tri-arch bundle. The `.msixupload` is **unsigned by design** — the Microsoft Store re-signs every package during ingestion.
Internally the script:
1. Runs `dotnet publish -c Release -p:PublishTrimmed=false` per architecture (trimming is disabled for Store builds because WinUI 3 / `CommunityToolkit.Mvvm` reflection code is not fully trim-safe; `ReadyToRun` stays on)
2. Calls `winapp package` on each published layout to produce per-arch `.msix`
3. Calls `MakeAppx.exe bundle` (via `winapp tool`) to combine them into a `.msixbundle`
4. Wraps the unsigned bundle into a `.msixupload` zip with the bundle at the archive root
### Upload to Partner Center
1. Sign in to [Partner Center → SkimDown](https://partner.microsoft.com/dashboard/products/9NHTZMM0XMMF/overview)
2. Start a new submission and open **Packages**
3. Drag-drop `bin\StorePackage\*.msixupload`
4. Fill in store listing, age rating, screenshots, and submit
### Bumping the version
Edit `Identity/@Version` in `src/SkimDownForWindows/Package.appxmanifest`. The last segment **must remain `0`** for Store submissions (the Store reserves the revision component for its own re-signing pipeline).
### Local install validation / sideload distribution (optional)
For sideload testing or GitHub Releases distribution, run with `-Sign`:
```powershell
.\scripts\Build-StorePackage.ps1 -Sign
```
This produces the **same** unsigned `.msixupload` (for Store) **plus** these extra sideload artifacts:
- `bin\StorePackage\<pkg>_<version>_sideload.msixbundle` — a separately-signed copy of the multi-arch bundle
- `bin\StorePackage\devcert.pfx` — the generated dev certificate (private key, **never publish**)
- `bin\StorePackage\devcert.cer` — the public certificate only (safe to publish on GitHub Releases)
To install the sideload bundle locally:
```powershell
certutil -addstore -f "TrustedPeople" .\bin\StorePackage\devcert.cer # admin, one-time
Add-AppxPackage .\bin\StorePackage\45014okazuki.SkimDown_1.0.0.0_sideload.msixbundle
```
> ⚠️ Sideload builds use a self-signed Publisher certificate. Microsoft Store builds use a different signing chain — once a Store build of the same `Identity/Name` is installed, the sideload version cannot be upgraded in place. Uninstall the sideload version before installing the Store version.
The unsigned `.msixupload` and the signed `_sideload.msixbundle` are produced side-by-side — only the `.msixupload` should be uploaded to the Store.
## License
Mirrors the upstream SkimDown project licensing — see [`07JP27/SkimDown`](https://github.com/07JP27/SkimDown) ([GNU General Public License v3.0](https://github.com/07JP27/SkimDown/blob/main/LICENSE)) for the source SPEC, design notes, and macOS reference implementation.
Lire le README source