Skip to content

feat(windows): open a local Markdown file offline - #28

Open
kamilkaczmareksolutions wants to merge 14 commits into
ashtree74:mainfrom
kamilkaczmareksolutions:port/windows-winui
Open

kamilkaczmareksolutions wants to merge 14 commits into
ashtree74:mainfrom
kamilkaczmareksolutions:port/windows-winui

Conversation

@kamilkaczmareksolutions

@kamilkaczmareksolutions kamilkaczmareksolutions commented Oct 4, 2026 •

Copy link
Copy Markdown

What changed?

One WinUI 3 window opens a local .md or .markdown file and renders it offline. The build copies Sources/PreviewMD/Resources/Renderer/ unchanged and checks every file against ports/windows/renderer.sha256. Relative images stay inside the open document's folder. http, https, and mailto links open with Launcher.LaunchUriAsync.

The first screen is empty, with Open and Showcase. The window title is PreviewMD (Windows experiment). A file can also be dropped from Explorer. Ctrl+O opens the picker. Ctrl+W closes the document. Opened files appear in the taskbar Recent list.

Why this approach?

Issue 22 names WinUI 3 and WebView2. The renderer already rewrites local images to previewmd-local-image, so the shell registers that scheme and serves the bytes. A second virtual host that rewrites img.src after render would race the diagram pass.

An unpackaged build copies the Windows App SDK next to the executable, so dotnet run does not need a separate runtime install. -p:WindowsPackageType=MSIX selects the package build. That package expects the Windows App Runtime.

How was it tested?

  • dotnet test ports/windows/PreviewMD.Windows.sln passed, 17 tests, 0 failed
  • dotnet build ports/windows/PreviewMD.Windows.sln passed, 0 warnings
  • swift test — this change does not touch the macOS app
  • python3 -m unittest discover -s site -p 'test_*.py' — the site is unchanged
  • ./scripts/build-app.sh — packaging of the macOS app is unchanged
  • Manual run of PreviewMD.Windows.exe on Windows 11

The empty window showed Open and Showcase. A local note showed a relative image, a KaTeX formula, and a highlighted Swift block. A second note drew a Mermaid flowchart with no network. Showcase loaded the bundled welcome.md, including its badge images. Open chose a file. A drag from Explorer opened another file. An https link opened Brave on Example Domain. The taskbar Recent list showed the opened names.

Visual evidence

The runs above were on this machine. Screenshots stayed local. They are not in the branch.

Contributor checklist

  • The change is focused and does not rewrite unrelated code.
  • New behavior has tests or an explanation of why a test is impractical.
  • Offline rendering and document privacy remain intact.
  • Third-party code includes source, version, license, and notices.
  • No credentials, private documents, subscriber data, or local paths were added.
  • Every commit has a DCO Signed-off-by line (git commit -s).
  • Public use of the PreviewMD name/icon follows TRADEMARKS.md.

Follow-ups or limitations

Tabs and Ctrl+Tab stay out. This issue leaves a TabView until after the slice works.

A directory junction inside the document folder can still point outside it. Path.GetFullPath does not resolve that.

The MSIX was produced and not installed. The running build was unpackaged, with the same copied renderer.

Fixes #22

A Windows slice has to show a relative image and refuse a path
that leaves the document folder. The decision is a pure function
so the tests can name the refusal.

Signed-off-by: kamilkaczmareksolutions <kamil.kaczmarek.us@gmail.com>
Issue 22 asks for one offline window. The build copies the
renderer unchanged and checks it against a checksum. External
links leave the app.

Signed-off-by: kamilkaczmareksolutions <kamil.kaczmarek.us@gmail.com>
Ctrl+W returns to the empty screen. A Markdown file dropped on
the window opens. Opened files stay on the taskbar Jump List.

Signed-off-by: kamilkaczmareksolutions <kamil.kaczmarek.us@gmail.com>
@kamilkaczmareksolutions

Copy link
Copy Markdown
Author

Pushed feat(windows): close a document, drop a file, and remember it.

Ctrl+W returned the window to the empty screen. A local note still renders. Clicking the http link in the document opened Brave on Example Domain.

dotnet publish -p:WindowsPackageType=MSIX produced PreviewMD.Windows_1.0.0.0_x64.msix. That package is not committed and was not installed.

Tabs and Ctrl+Tab stay out, as the issue leaves tabs until after this slice.

Explorer hands a file to the window through the normal drop
target. The taskbar Recent list uses the same app id as the
Start menu shortcut.

Signed-off-by: kamilkaczmareksolutions <kamil.kaczmarek.us@gmail.com>
@kamilkaczmareksolutions

Copy link
Copy Markdown
Author

The three remaining checks are now visible on a running window.

  • Open chose picker.md. The window showed “Opened from the picker”.
  • A drag from Explorer of explorer.md opened that file. The window showed “Dropped from Explorer”.
  • A right-click on the taskbar button showed Recent: explorer.md, picker.md, link.md, and opened.md.

@ashtree74 ashtree74 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the WinUI 3 experiment and the Windows smoke-test results. The macOS fixes have now landed in #29; this port remains separate and #22 stays open until the Windows slice is ready to merge.

Before merging:

  1. Sync with current main and refresh the renderer manifest with deterministic checkout line endings. I compared all 77 entries: 15 pins match CRLF-converted text rather than the Git blob bytes, and editor.js, renderer.css and renderer.js have changed in 1.8. The checksum checks must continue to verify the actual current renderer.
  2. Address the resource-confinement limitation already documented in the PR/README, with a Windows regression test matching the document-folder restriction promised in #22.
  3. Re-run the Windows tests/build on the updated branch and verify installation/launch of the MSIX, which is currently recorded as produced but not installed. Update the README's excluded-feature list, which still excludes drag/drop and the Jump List even though this PR implements them.

This is a source review and checksum comparison; I have not independently run WinUI/MSIX on Windows. No CI checks were returned for this revision, so the existing macOS release validation does not validate this port.

Comment thread ports/windows/renderer.sha256 Resolved
cursoragent and others added 10 commits October 6, 2026 22:56
Current main includes the 1.8 renderer from ashtree74#29 and the later renderer
edits through 1.9. The Windows slice has to pin those bytes.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
Fifteen text-file pins matched a CRLF conversion, so an LF checkout
failed BundledRendererMatchesTheManifest. Renderer text now checks
out as LF, and pin-renderer.py rewrites the manifest from HEAD.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
A relative image inside the folder is allowed. A parent path, an
absolute path, a file link, and a directory link must be refused.
The file link is still allowed, which is the hole this test pins.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
The jail compared the lexical path, so a symlink or junction inside
the folder could still open a file outside it. The decision now
follows the final target and refuses that path. A relative image in
the folder still loads.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
The window already accepts a dropped Markdown file, and recent files
already appear in the taskbar Jump List.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
verify-renderer.ps1 trimmed only backslash. PowerShell on Linux kept
a leading slash, so matching file bytes still failed the manifest.
Windows still uses backslash, which is DirectorySeparatorChar there.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
The line-ending attribute and the blob compare already state the
rule. The notes above them repeated it.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
A Windows machine that can create symbolic links would otherwise
never build a directory junction. The junction is the case the
README used to leave open. Linux still uses a directory symlink.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
ResolveLinkTarget on C:\ throws DirectoryNotFoundException even
though the drive exists. Decide treated that as InvalidRequest, so
every image under the drive was refused and the junction test never
ran. A volume root stays unchanged, and that exception from the
link query does too. A real junction is still followed.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
Windows PowerShell 5.1 does not find Get-FileHash when PSModulePath
lists PowerShell 7 modules first. The build uses pwsh when it is on
PATH, and powershell.exe only when pwsh is absent. The script and
the checksum comparison are unchanged.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Kamil Kaczmarek <contact@kamilkaczmareksolutions.com>
@kamilkaczmareksolutions

Copy link
Copy Markdown
Author

Thanks for the review. Follow-up is on tip 38598fa (also mirrored on fork draft kamilkaczmareksolutions#2). The head of this PR (port/windows-winui) is now at that tip.

1. Sync + pins. Current upstream main is merged (incl. 1.8/1.9 renderer from #29+). ports/windows/renderer.sha256 is regenerated from Git blob bytes; text renderer files check out LF (.gitattributes). python3 ports/windows/pin-renderer.py exits 1 if the work tree differs from HEAD. Checksum gate stays on. On Windows, dotnet test and verify-renderer.ps1 pass; the build log shows Renderer checksum shell is pwsh.

2. Confinement. ImagePolicy follows the final target of a symlink/junction, then applies the document-folder prefix check. DocumentFolderAllowsARelativeImageAndRefusesEscape passed on Win11 with mklink /J. Volume-root ResolveLinkTarget(@"C:\") throwing DirectoryNotFoundException is treated as identity (not InvalidRequest), so relative images under the document folder work again.

3. Build / MSIX / README. dotnet build and unpackaged Release launch succeed on Win11 (window title PreviewMD (Windows experiment)). MSIX package builds; sideload of the unsigned test package fails with 0x800B0100 (needs a signing cert — same on prior tip). README no longer lists drag/drop or Jump List as excluded.

Happy to rebase onto #28’s head / adjust signing if you want a different packaging path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Port]: PreviewMD on Windows

3 participants