Windows · v0.1.2 · MIT
Notepad for .txt.
kunang for .md.
A fast, simple Markdown viewer and editor for Windows. Double-click a
.md file and it opens rendered — in about 47 milliseconds,
not the one to three seconds a full editor takes. One keystroke gets you a real
two-pane live-preview editor. Another gets you back.
Prefer an installer? kunang-setup-0.1.2.exe · All releases
The whole product
One key between
reading and writing.
Most of the time you want to glance at a rendered document, occasionally fix a line, and close it. kunang is built around exactly that, and nothing else.
Rendered view mode, immediately. Headings, tables, code, links — a real document, not a wall of asterisks. No splash screen, no workspace to pick.
Source left, live preview right. A CodeMirror 6 editor with synced scrolling, so the line you are typing is the line you are watching.
Back to reading. Esc backs out one level at a time — the find bar, then edit mode, then the tab, then the window. It never surprises you.
Double-click → painted
A race that
ends early.
Read the numbers as a table
| Path | Time to a visible document | Condition |
|---|---|---|
| kunang | 47 ms (p50) · 57 ms (p95) | Warm host, measured by npm run bench |
| kunang | ~960 ms | Cold — no host running yet |
| Notepad | 80–200 ms | Cold, plain text only |
| VS Code / Typora / Obsidian | 1,000–3,000 ms | Cold Electron-class boot |
Linear scale. kunang's figure is the warm path and is stated as such — the cold path is in the table too.
The trick
The double-click path
never boots Electron.
A cold Electron window costs 800 ms to 2 s. So the .md file
association does not point at Electron. It points at a 2.4 MB Go stub whose only
job is to reach an already-running host over a named pipe. The host stays resident
after you close a window, so every open after the first is warm.
\\.\pipe\kunang.<secret>Grants foreground rights, writes argv + cwd + t013.6 msMeasured, not estimated
| Hop | p50 | p95 |
|---|---|---|
| stub entry → pipe read | 13.6 ms | 16.7 ms |
| → dispatched to renderer | +0.0 ms | — |
| → renderer painted | +15.4 ms | — |
| → window shown | +17.5 ms | — |
| Total | 47.0 ms | 57.3 ms |
npm run bench launches the real stub 50 times — the same binary Explorer
runs on a double-click — so the numbers include process creation and the pipe
handshake. Figures move a few milliseconds with machine load; the gate is the
contract, not the exact number.
Cold, with no host running, bringing the host up takes about 960 ms; the very next open is 59.6 ms. The installer adds a logon task that warms the host 30 seconds after you sign in, so even the first open of the day is fast.
Then it gets out of the way.
Everything it does
Small app.
Not a simple one.
Fast is the headline. But a document editor earns trust in the details — what happens to your encoding, your line endings, your unsaved buffer when the file changes underneath you.
Tabs that behave
Every open lands as a tab in the window you were last looking at. Drag to reorder; drag one off the strip and it gets a window of its own — a move, never a copy. The strip only appears once there is more than one document.
Two-pane live preview
Source on the left in CodeMirror 6, rendered output on the right, updated through morphdom so the preview never flickers or loses your place. Scroll one pane, the other follows.
Outline sidebar
Ctrl+\ opens a heading outline for long documents. Click to jump;
the position you were at is remembered per file, so reopening lands you where
you left off.
Encoding survives the round trip
UTF-8, UTF-16 LE/BE with BOM sniffing, CP1252 detected by validating the bytes — each written back as it was found, along with the file's dominant line ending. Your git diffs stay clean.
Atomic saves
Writes go through Windows' own ReplaceFileW, preserving ACLs and
alternate data streams. A CP1252 file that gains a character it cannot represent
is promoted to UTF-8 rather than losing it to a ?.
It watches the file
Changed on disk? A clean buffer reloads silently; a dirty one tells you and waits. Delete the file and the buffer stays — at that point it is the only copy left. Rename it and kunang follows.
GFM, math and diagrams
markdown-it with GitHub-flavoured Markdown and highlight.js. KaTeX for
$…$, Mermaid for ```mermaid — both imported
only when a document actually contains them, so the fast path never pays
for features it does not use.
Export self-contained HTML
Ctrl+Shift+E writes one HTML file with the styles and images inlined —
something you can email, or open on a machine that has never heard of Markdown.
Themes, zoom and your own CSS
Ctrl+Shift+T cycles auto / light / dark. Ctrl with
+, − or 0 handles zoom. Drop a stylesheet at
%LOCALAPPDATA%\kunang\custom.css and restyle the document entirely —
F5 re-reads it.
Drag and drop
Drop .md files anywhere to open them, one tab each. Drop an image
while editing and it is inserted as Markdown, linked relative to the open document.
Relative links between documents open in a tab.
Sessions come back
Open documents are remembered, in the order you arranged them. Restored tabs are created empty and only read when you return to one — so a fifty-file session costs nothing on the open path. Files that vanished are dropped quietly.
Find, print, and a 5 MB guard
Ctrl+F finds in the rendered page; edit mode hands over to CodeMirror's
own search. Ctrl+P prints. Documents over 5 MB skip syntax
highlighting and say so, rather than wedging the renderer.
Every shortcut
Learn it in
one screen.
Hover or tab through a key to see what it does. That is the entire keymap — there is no second page.
The Tools menu also carries Register / Unregister as default .md viewer.
Try the idea
This is what
Ctrl+E looks like.
A rough sketch of edit mode, running right here. Type on the left; the right side keeps up. In the real app the editor is CodeMirror 6 with syntax highlighting, and the preview is full GitHub-flavoured Markdown.
And it stays quiet.
Safe by default
A Markdown file
is untrusted input.
A single remote <img> in a document is enough to report that you opened
it, and from which IP address. So kunang blocks every remote request by
default and asks first.
That consent follows the active tab, and switching documents withdraws it. It is deliberately never persisted — the safe state is the one you get by doing nothing.
The renderer is a sandbox
contextIsolation, sandbox: true, no Node integration.
It talks to the main process through one narrow contextBridge API and
nothing else. Rendered HTML goes through DOMPurify on the way in.
Local files, local protocol
Images and links inside a document are served over a custom mdfile://
protocol rather than raw file access. External links open in your real browser,
not inside the app.
Hostile content, handled
Mermaid runs with securityLevel: 'strict' and KaTeX with
throwOnError: false — both are handed text straight from a document
nobody vouched for. The test suite includes a deliberately hostile corpus.
The pipe is unguessable
The named pipe carries a random 32-hex secret stored in
%LOCALAPPDATA%\kunang\pipe. Node cannot set pipe ACLs, so an
unguessable name is what stops another user's process from driving your host.
Install
Pick one.
Not both.
Portable — a single file
- Download
kunang-portable-0.1.2.exe(~144 MB) and run it. - It unpacks itself to
%LOCALAPPDATA%\kunang\app\<version>\in about 4 seconds. - Starts the resident host, then registers the
.mdassociation once the host is warm. - After that the exe has done its job. Double-clicking a
.mdruns only the 2.4 MB stub. - Run it again carrying a newer build and it upgrades in place — keyed on a hash of the payload, not the version number.
NSIS installer
kunang-setup-0.1.2.exe— a per-user install, no admin rights.- Same result, plus a logon task that re-warms the host 30 seconds after you sign in.
- That makes even the first open after a reboot fast. The portable build deliberately does not create that task.
- Uninstalls through Windows the way you would expect.
.md
association and each wants a resident host at its own path, so the two would compete
for the same named pipe.
.md
for you. But on a machine where you have ever picked a default app for
.md, Windows stores a salted hash under
…\FileExts\.md\UserChoice that Microsoft made unforgeable specifically to
stop apps hijacking associations — so no program can set it, not kunang, not an
installer running as admin. Set it yourself: right-click any .md →
Open with → Choose another app → kunang → Always.
npm install and npm run package:portable.
The full build notes are in the README.
Before you download
What it is not.
kunang is pre-1.0 and feature-complete against its original plan. Here is the honest list, the same one that sits in the README.
- Windows only. The stub is Win32-specific — named pipes,
ReplaceFileW,AllowSetForegroundWindow. There is no macOS or Linux build and the architecture does not port cheaply. - Expect a SmartScreen prompt. The portable exe is unsigned, self-extracting and writes to HKCU. That will keep tripping SmartScreen until it is code-signed. The source is public if you would rather build it yourself.
- It is not small on disk. ~370 MB unpacked, ~200 MB resident at idle plus 60–80 MB per window — which is exactly why opens go to tabs in an existing window rather than a window each. That is the price of TypeScript instead of C++.
- Rename detection is a heuristic. Windows reports a rename as an unrelated delete followed by an add, so they are paired by proximity in time within a directory. Deleting one file and creating another in that window looks identical. It errs toward following the file.
- Not a zero-footprint portable app. A resident host and a file
association both need a stable path on disk, so it unpacks once and stays.
--uninstallplus deleting%LOCALAPPDATA%\kunangremoves it completely. - Still on the list: multiple-window stress testing, taskbar
grouping, network-drive and WSL paths, and
idleTimeoutMinutes— the setting is read but nothing acts on it yet.