A fast, lightweight, read-only Markdown viewer.
markdown go markdown-reader markdown-viewer desktop-app

README.md

Sheaf #

Sheaf renders a Markdown file into a clean, instant, read-only window. Point it at a README before you clone, put it beside your editor while you write, or read through notes without touching them. What you get is a calm document with no editing chrome, no toolbars, and no buttons competing for your attention.

Why I built this #

I kept bouncing between a browser tab, a code editor's cramped preview, and a full-featured Markdown editor, just to read documents. Every one of them spent screen space and attention on tools for writing, even when reading was the whole job.

What I wanted was a dedicated surface, opened as fast as a keybinding, that does nothing but read the file. Bind Sheaf to .md files system-wide and you get a viewer that starts fast, stays out of the way, and never opens a document for editing. Every decision in this app comes out of that.

Usage #

sheaf README.md        # open a file directly
sheaf                  # launch empty, then Open (or Ctrl+O)
wl-paste | sheaf -     # render Markdown piped on stdin

Supported extensions:

  • .md
  • .markdown
  • .mdown
  • .mkdn
  • .mkd
  • .mdwn
  • .mdtxt
  • .mdtext
  • .markdn

Features #

  • GFM-style Markdown: tables, fenced code blocks, strikethrough, autolinks, footnotes, heading anchors, smart punctuation
  • Syntax highlighting for fenced code blocks, tokenised on the Go backend in the Digital Rust theme. A language with no recognised lexer renders as a plain fenced block
  • Relative images and inbound Markdown links (README → CHANGELOG, for example) resolve against the directory of the document, so screenshots and sibling pages render and navigate in place. They are served read-only from that directory only, with symlink and .. escape checks (test-enforced)
  • External links open in your system browser
  • In-document anchor links scroll to the section you request
  • Live reload: the view refreshes when the file changes on disk (250 ms polling) and keeps your place, so you do not lose it mid-edit. A file whose contents did not change is not re-rendered at all, so a save with no edits or a touch costs nothing. A fenced code block that did not change is not highlighted again. The find bar, the contents panel and the recents menu stay open across a refresh, and the hit count is recounted against the new text
  • A watched file that disappears stamps GONE at the foot of the sheet and leaves the last rendered content on screen, so a stale view never pretends to be live. Restoring the file clears the stamp and resumes live reload on its own
  • Links to sibling documents open in Sheaf itself, and a link that names a heading lands on that heading
  • A link to a local file Sheaf cannot render (a PDF, an archive, a video) is handed to your system's default handler
  • Images are never fetched from the network. A remote image shows its alt text and URL in place, and not a broken-image mark. Local images render normally
  • Failures are reported. A link or a recent file that cannot be opened says so in the readout at the top of the sheet
  • Find in page (Ctrl+F or /), step through hits with Enter or Shift+Enter, close with Esc
  • Copy the rendered HTML to the clipboard (Copy HTML button)
  • Print or save as PDF (Ctrl+P or the Print button)
  • Table of contents panel (T or F6) built from the document's headings
  • Recent files menu holds the last eight documents. Cycle through them with Ctrl+Tab or Ctrl+Shift+Tab
  • Type zoom (reading size): Ctrl+= / Ctrl+-, Ctrl+0 returns to your system font size
  • Single instance: opening a second file raises the existing window and switches to the new file. A piped launch (sheaf -) opens its own window instead, because the pipe belongs to the process that was started with it
  • Drag-and-drop a file onto the window to open it
  • Dark and light themes follow your system preference
  • It remembers you between sessions: the window size and maximised state, your reading size, and the last eight documents. Launched with no file it opens empty and offers the file you had open last as one button. A document you closed yourself with Ctrl+W is not offered again
  • Ctrl+W closes the document and returns to the empty state. F5 re-reads the file from disk, which is the way out if the live reload misses a change

Read-only by design #

Being read-only is the point and not a gap. There is no way to modify an untrusted document by accident, and nothing competing with your attention.

Sheaf is built so that opening anything in it is safe. Raw HTML in a document is stripped at render time. Unsafe link schemes such as javascript: are neutralised. Every external link opens with rel="nofollow noreferrer noopener". Automated tests enforce all of it on every build.

Nothing leaves your machine. Images are never fetched from the network, so a remote badge or a screenshot shows its alt text and URL instead of a request to a third party.

What Sheaf remembers #

The window size and maximised state, your reading size, and the last eight documents live in one small file. Delete it to start over.

Sheaf opens empty when it is launched without a file. If a document was open when you last quit, the empty state offers it back as one button that names the file. Closing a document with Ctrl+W takes that offer away, because closing it says that you are done with it. Your recent files are still one Ctrl+Tab away either way.

~/.config/sheaf/config.json                      # Linux
~/Library/Application Support/sheaf/config.json  # macOS
%AppData%\sheaf\config.json                      # Windows

Nothing else is stored, and nothing is sent anywhere. Sheaf reports a file that it cannot read on stderr and then ignores it. It clamps a hand-edited window size, so a broken configuration never stops the app from opening.

Getting Sheaf #

Prerequisites: Go 1.26 or newer, Node, pnpm, and the Wails CLI.

wails build                          # produces build/bin/sheaf
wails build -tags webkit2_41         # if your distro ships webkit2gtk-4.1

Linux desktop install #

This installs the binary and the freedesktop integration: a .desktop entry, the app icon, AppStream metadata, and the Markdown MIME association that makes Sheaf appear in "Open With". The .desktop entry is also what sets the titlebar and taskbar icon on Wayland, because Wayland cannot read the runtime window icon.

just desktop-install                 # install to ~/.local
just dest=/usr/local desktop-install # system-wide, with sudo
just desktop-uninstall               # remove

The canonical app icon is packaging/linux/sheaf.svg. To change the icon, edit this SVG and then run bash scripts/render-icons.sh. The script regenerates the PNG set, build/appicon.png, and the Windows .ico from the SVG.

AppImage #

Every release ships an x86_64 AppImage, and the newest one always lives at a stable URL:

curl -LO https://katsuricata.tngl.io/sheaf/latest/sheaf-latest-x86_64.AppImage
chmod +x sheaf-latest-x86_64.AppImage

Release AppImages carry zsync update information, so AppImageUpdate (or any compatible updater) can delta-update the file in place and download only the changed blocks. Like the raw binary, the AppImage links the gtk3 and webkit2gtk-4.1 of the host dynamically and does not bundle them, so it needs a distro that ships webkit2gtk-4.1.

To build one locally (needs appimagetool and zsync):

just appimage    # -> dist/sheaf-0.0.0-dev-x86_64.AppImage, no update info

Arch Linux #

AUR package files for sheaf-git live under aur/ in this repository. To build and publish them, run:

just aur-init       # clone AUR repo, seed PKGBUILD, build
just aur-build      # rebuild after changes

Releases #

Tagged releases are built by Spindle (.tangled/workflows/release.yml) and published to https://katsuricata.tngl.io/sheaf/. A release holds tar and zip archives of the amd64 binary, SHA256SUMS.txt, a source tarball, a prebuilt sheaf-bin pacman package, and an x86_64 AppImage with zsync delta-updates. The raw binary ships inside the archives and not on its own. arm64 is not built yet, because the spindle microVM timeout cannot fit a second native CGO build.

Cut a release with an annotated tag:

git tag -a v1.0.0 -m ...
git push origin v1.0.0

See RELEASE.md for the full pipeline and the one-time setup.

Development #

wails dev                            # live-reload dev
wails dev -tags webkit2_41           # required on distros with webkit2gtk-4.1

If Go tooling fails because frontend/dist is missing, run the frontend build once:

cd frontend && pnpm install && pnpm run build

Testing #

go test ./...                        # backend rendering and asset tests
cd frontend && node --test test/*.test.ts  # frontend unit tests
cd frontend && pnpm run check        # Svelte typecheck
just coverage                        # backend tests + the coverage ratchet
just ci                              # everything CI runs

just coverage runs scripts/check-coverage.sh, which enforces a coverage ratchet for each file. Every Go file is pinned to a floor in coverage-floors.txt. The build fails if a file drops below its pin, which means a covered line regressed. It also fails if the statement count of a file changes. That means the code changed: cover the new lines and then update the pin. The floors sit just under 100% on purpose. A handful of lines are not unit-testable, because the Wails-bound methods and main() run only inside a live app, and a few defensive error branches are unreachable from real input. .coverignore documents those. The suite therefore cannot quietly rot. Add a feature, and you either cover it or the ratchet fails the build.

The frontend tests run on the test runner of Node itself, over the pure modules in frontend/src/lib/. Nothing there touches the DOM, so the tests need no test framework and add no dependencies. They cover search indexing and hit stepping, the scroll anchor maths behind live reload, keyboard routing, link classification, the recent-files queue, zoom stepping, the supported-format list, path display and table-of-contents depth. A component behaviour that genuinely needs a DOM is still tested by hand. Extract logic into src/lib/ when it is worth testing, and do not chase a coverage number. Node 22.18 or newer is required, for native TypeScript type stripping.

Stack #

Wails v2 (Go backend, Svelte 5 frontend in your OS webview). Markdown rendering by blackfriday/v2. Syntax highlighting by Chroma.

License #

MutuaL-1.3. See LICENSE.md.

Project home: https://tangled.org/katsuricata.com/Sheaf · Downloads: https://katsuricata.tngl.io/sheaf/