Skip to content
ramsribPublic

About

A read-optimized, files-on-disk Markdown notes reader for macOS

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Folio

A read-optimized Markdown app for macOS that opens your existing .md files in place. Think Obsidian, but tuned for reading a project's docs rather than authoring them. Native Swift / SwiftUI, files-on-disk as the source of truth.

The use case it's built for: agents (Claude Code & friends) author, write, and edit most of the docs in a project; Folio is the fast, calm reader you use to browse and absorb all of them efficiently. Writing is supported — but reading is the default, the most polished path, and what every interaction is optimized around.

Status: early. macOS is the built target and is genuinely usable day to day. iOS exists but is a partial reader. Signed, notarized builds ship through GitHub Releases and a Homebrew tap; the test suite covers the parser, tags, link routing, and the store, but not the UI. See Limitations before you point it at anything you can't afford to lose.

Built for read-heavy use

  • Reading is the default. Opening a note, following a link, or switching tabs always lands you in the fully-rendered Reading view. Writing is opt-in — the Read | Write switch in the title bar or ⌘E — and Esc always returns you to reading. There is deliberately no double-click-to-edit, so selecting text can never flip the mode.
  • Optimized for scanning many docs. A fuzzy quick switcher, vault-wide content search, back/forward history, tabs with session restore, a #tags browser, backlinks, and a hover outline — built to move through a whole project's notes quickly, not to sit in one document.
  • Calm, low-chrome rendering. Centered reading column, frontmatter as a typed Properties panel, thin auto-hiding scrollbars, no syntax symbols — so the docs, not the editor, are what you look at.

What works today (macOS)

Everything in this list is implemented. Anything not in it, assume it isn't.

Reading

  • Fully-rendered Reading mode: headings, bold/italic/strike/==highlight==, nested lists, clickable task checkboxes (toggling writes back to the file), blockquotes, titled/colored callouts (> [!note], > [!warning], …), fenced code with syntax highlighting, tables, dividers, and local + remote images.
  • YAML frontmatter renders as a read-only Properties card, with per-key icons, formatted dates, and tag chips — collapsed by default to a one-line "Properties · N" row, so opening a note lands you on the content; click to expand.
  • Vim-style scrolling: j/k/h/l, d/u, gg/G, space / ⇧space.
  • Tables size their columns to their content, and one wider than the reading column pans sideways on its own while vertical scrolling still belongs to the note.
  • Selection runs the length of the note. Reading mode is a single TextKit text stream, so selection, ⌘A, copy, Look Up and drag-out cross headings, lists, code, callouts and tables alike. Copying a table or the Properties card yields its Markdown source.

Navigating & search

  • ⌘O / ⌘K — quick switcher. Fuzzy name/path matching with highlighted hits, spaces as word separators, recency-weighted ranking, recently-opened notes on an empty query, ⇧↵ to create a note from the query, ⌘↵ to open in a new tab, and a # prefix to jump to a heading in the current note.
  • ⇧⌘F — vault-wide content search. Space-separated words AND together (all must appear in a file, any position — auth db phase finds files containing all three); quote for an exact phrase. Case-insensitive, no regex/operators. Live results grouped per file, snippets favor lines where terms co-occur with every hit bolded; ↵ opens the note with the find bar on the clicked hit.
  • ⌘[ / ⌘] (or ⌘⌥←/→, or a two-finger swipe left/right) — back/forward history through visited notes, with title-bar chevrons.
  • ⌘P — command palette: every app action, fuzzy-matched, including Switch Vault and Set Theme.
  • ⇧⌘Y — tag browser: all inline #tags and frontmatter tags: with counts; click through to notes. A tag has to carry a letter or _, so issue references like #1 or #1303/#1305 stay out of the list; matching is Unicode, so #café and #日本語 are tags.
  • ⌘F — find in page, in both reading and writing modes, with case toggle and ⌘G / ⇧⌘G.
  • Hover-reveal outline at the right edge; click a heading to scroll to it.
  • Collapsible backlinks at the bottom of a note, each with a line of context.
  • A pane with no note open shows the moves you'd make next — search files, new note, search in vault, command palette — each row clickable and naming its key.
  • ⌘/ — a cheat sheet of every shortcut. It is the authoritative list.

Vault & files

  • ⇧⌘O opens any folder of Markdown as a vault (an Obsidian vault works). Recent vaults in the File menu; ⇧⌘R reloads.
  • Collapsible folder tree in a resizable sidebar (⌃⌘S to toggle). Skips hidden dirs and node_modules, build, dist, target, etc. The tree reveals the current note (expands and scrolls to it), remembers folder expansion per vault, and has a filter field — every space-separated word must appear in the file name.
  • Live external-change watching via FSEvents — edits from git, Obsidian, or an agent show up without a manual reload, and the open note reloads in place (as long as you have no unsaved local edit pending).
  • New note (⌘N), rename, Move to Trash, Reveal in Finder. Right-click a file for Open in New Tab, Copy Relative / Absolute Path, Copy Wikilink, and Copy Folio Link; right-click a folder for New Note in Folder and the path copies.
  • Open a .md from Finder ("Open With → Folio") or the terminal (open -a Folio note.md): if the file is inside the current vault or a recent one Folio switches to it, otherwise it opens the file's parent folder as a vault. folio://open?vault=…&file=… deep links (what Copy Folio Link puts on the clipboard) jump straight to a note — or a whole vault — from another app, a terminal, or an agent. See Opening from the terminal.
  • Right-click the Dock icon for recent notes and recent vaults.

Links

  • [[wikilinks]] resolve by basename or path, render resolved vs unresolved, click to open, and create the note on click when unresolved.
  • Typing [[ autocompletes note names.
  • Renaming a note rewrites [[old]], [[old|alias]], and [[old#heading]] across the vault.
  • Hovering a wikilink in the editor previews the target note.

Tabs

  • Navigation reuses the current tab (Obsidian-style); ⌘-click a note or wikilink — or ⌘↵ in a palette — to open a new tab. Double-click a sidebar file to keep both: the note you were reading keeps its tab and the new one opens beside it (VS Code's double-click-to-keep-open). Creating a note, or opening one from outside (Finder, CLI, a folio:// link), always gets its own tab — an external open never evicts what you're reading.
  • Open/close (⌘W), drag to reorder, reopen closed (⇧⌘T), cycle (⌃⇥ / ⌃⇧⇥), ⌘1–⌘8 to jump to a tab and ⌘9 to the last, close-others / close-tabs-to-the-right / close-all, and a right-click menu with the same copy actions (relative/absolute path, wikilink) as the explorer. Open tabs and the active note are restored per vault on relaunch.
  • A note whose file goes away (deleted, moved, a branch switch) gets its tab struck through rather than closed. Clicking it opens the last text Folio read, read-only, above a line saying why; it heals back into an ordinary note the moment the file returns.

Writing

  • A Live Preview editor (TextKit NSTextView) that styles the literal Markdown buffer in place, so the file stays byte-for-byte lossless. Syntax markers are dimmed, and revealed at full contrast on the cursor's line — they are not concealed. Smart quotes, dashes, and text replacement are disabled so typing can never mutate the file.
  • Autosave, debounced ~500 ms, written atomically. A save only ever updates a note — it never creates one — so a file deleted out from under an open tab is not resurrected by a pending write.
  • The page title is editable and renames the file on commit (when the inline title is enabled in ⌘,).

Vaults & windows

  • One vault per window. Several vaults can be open at once, each in its own window.
  • ⇧⌘O opens the vault switcher — or click the vault name at the top of the sidebar. Choosing a vault focuses its window if it is already open, and never replaces the vault in the window you are reading.
  • ⌥⌘N opens an empty window; ⌘N is New Note.
  • Opening a note from Finder or the terminal goes to the window whose vault contains it.
  • The set of open vaults is the session, restored on relaunch.

Appearance (⌘,)

  • Themes: System, Light, Dark, Paper (warm), Frosted (glass).
  • Reading font (System / Serif / Mono), body font size (⌘+ / ⌘− / ⌘0 work anywhere, or pinch on the trackpad), readable line width (default ~70 chars/line) or full width (fills the pane — wider lines, less scrolling), an optional inline title, and a text rendering choice (Crisp / Smooth / Smoother — browser-style stem darkening; applies on relaunch).
  • Line breaks — Auto (default) decides per paragraph: text wrapped at a column reflows, deliberate short breaks are kept. Preserve keeps every newline (Obsidian); Reflow joins them (CommonMark). Auto is right more often than either, because wrapping is a property of the document rather than of the reader.
  • All applied live except text rendering.

Opening from the terminal

A note opens with plain open; a vault needs the folio:// scheme, because open -a Folio on a directory does nothing (it only accepts Markdown files).

open -a Folio note.md                       # a note — Folio finds or opens a vault around it

open "folio://open?vault=$(pwd)"            # open this folder as the vault
open "folio://open?vault=$(pwd)&file=docs/plan.md"   # vault + a note in it (file is vault-relative)
open "folio://open?file=$(pwd)/README.md"   # a note by absolute path, Folio picks the vault

The host must be open and the path travels as a query value: folio://$(pwd) is not a valid link (empty host) and just beeps. Query values are percent-encoded, so a path containing a space, &, or # has to be encoded — the string Copy Folio Link puts on the clipboard always is:

folio() {                  # folder → open it as the vault; note → open the note; no arg → cwd
  local a="${1:-.}" p key=file
  if [[ -d $a ]]; then p=$(cd -- "$a" && pwd); key=vault
  elif [[ -f $a ]]; then p="$(cd -- "$(dirname -- "$a")" && pwd)/$(basename -- "$a")"
  else echo "folio: no such file or directory: $a" >&2; return 1; fi
  open "folio://open?$key=$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$p")"
}

folio and folio . open the current folder as a vault, folio notes/plan.md opens that note (finding it a vault by the 3-tier rule), and a path that doesn't exist fails in the shell instead of beeping at you.

The cd … && pwd is what makes relative arguments safe: these paths must be absolute. Folio resolves a relative path against its own working directory (/ for a Launch Services app), so a literal folio://open?vault=. quietly opens the root of the filesystem as a vault.

A link Folio can't act on — a missing path, a vault= that isn't a directory, a file= that isn't Markdown, a host other than open — beeps rather than summoning a window or claiming an idle one. Everything opened this way lands in its own tab — an external open never evicts what you're reading.

iOS

An iOS target exists and builds, but it is a partial reader, not a peer of the Mac app.

  • Opens a cloud-synced folder (practically iCloud Drive) through the system folder picker and remembers it with a security-scoped bookmark. It does not browse the local filesystem.
  • Folder browser with note-name search, the shared Reading view, a tag browser, and appearance settings.
  • Editing is a plain TextEditor, saving through the same lossless debounced write.

Not on iOS: the Live Preview editor, external-change watching (pull to refresh instead), find-in-page, backlinks, outline, tabs, and the command palette. iOS also still runs the older block-per-view reader, so selection there stops at a block boundary.

make ios-test runs unit tests (the shared store compiled for iOS) and UI tests (the real app driven through the screen) on the simulator.

Build & run

Requires macOS 26+ and a Swift 6.2+ toolchain (Xcode 26). The macOS app is a plain SwiftPM executable — no Xcode project, no signing ceremony.

make build      # swift build
make run        # dev loop — run straight from SwiftPM
make test       # swift test — parser, tag syntax, deep-link + window routing, missing notes
make app        # package build/Folio.app (ad-hoc signed)
make open       # package, then launch it
make install    # package and install to /Applications
make clean

Then ⇧⌘O to open a folder of .md files.

Note that make install force-quits any running Folio and replaces /Applications/Folio.app. A locally packaged app falls back to ad-hoc signing when no Developer ID certificate is in the keychain. Proper releases (signed, notarized, published to GitHub Releases and the Homebrew tap) are cut with make release — see RELEASE.md.

iOS

The iOS app can't be expressed in plain SwiftPM, so it needs XcodeGen to generate the Xcode project from project.yml:

brew install xcodegen
make ios-run               # xcodegen generate + build + install + launch in the simulator
make ios-test              # unit tests (hosted in the app) + UI tests, on the simulator
open FolioiOS.xcodeproj    # or drive it from Xcode; the project is gitignored and regenerated

Both targets default to an iPhone 17 Pro simulator; pass SIM="iPad Pro 13-inch (M4)" for another. Xcode needs the iOS platform matching its SDK installed (xcodebuild -downloadPlatform iOS) — older runtimes alone don't count. Code signing is disabled in project.yml, so running on a physical device means setting your own team and identity first.

Tests live in Tests/FolioiOSTests (the shared store compiled for iOS: tree, tags, open, edit, save, bookmark restore) and Tests/FolioiOSUITests (the real app driven through the screen: browse, search, read, edit, tags). UI tests launch the app with --ui-testing --vault <path>, a debug-only hook that opens a folder the test just wrote instead of the saved bookmark.

Limitations

Stated plainly, because the alternative is you finding out later:

  • Search is literal. ⇧⌘F matches exact (case-insensitive) text — no regex, no operators (tag:, path:), no boolean queries. In Reading mode, jumping to a hit can land on a neighboring occurrence in notes with heavy frontmatter/tables before the match (exact in writing mode).
  • Selection stops at rich blocks on iOS. The Mac reader is one text stream and selects straight through; iOS still stacks a view per block, and macOS/iOS can't extend a selection across separate views. iOS gets the same TextKit treatment eventually.
  • Test coverage is partial. make test covers the Markdown parser, tag syntax, deep-link and window routing, and the missing-note paths in the store; make ios-test adds the iOS store and a UI pass. The macOS UI, the reading renderer, and search have no automated coverage, and file writes beyond those suites are exercised by hand. Keep backups.
  • No Source mode, and Live Preview dims Markdown syntax rather than hiding it.
  • File management is thin: no folder create/rename/delete, no move, no drag-and-drop, no delete confirmation. Explorer sorting is by name only.
  • Rename-updates-links is quiet: it rewrites wikilinks (including [[folder/Note]]) and Markdown links ([text](folder/Note.md)) across the vault, but you aren't told what changed — only a failed rewrite speaks up, with a beep. Links inside fenced code blocks are rewritten too.
  • Properties are read-only, and parsed by a line-based heuristic rather than a real YAML parser — multiline values, quoting, and nested mappings won't render faithfully.
  • Markdown gaps: no math, footnotes, Mermaid, comments (%% %%), custom checkbox states, heading folding, or note/section/block embeds. ![[thing]] is treated as an image, not a transclusion.
  • Not built at all: graph view, bookmarks, daily notes, templates, split panes, attachment management, export/print.
  • No conflict resolution. If the same note is edited on two devices at once, Folio will not arbitrate — a local save can overwrite an external change that lands mid-debounce.
  • Reading mode fetches remote images (![](https://…)) over the network, which discloses your IP to that host. Not currently opt-in.
  • macOS 26 / iOS 26 only. UTF-8 Markdown only.

Why files-on-disk (not a database)

The point is to read and edit notes you already have, so the file system is the source of truth — no SwiftData/CloudKit store, no import/export step. Sync is whatever the folder already uses (iCloud Drive, git, Dropbox). The editor keeps the literal Markdown buffer and only ever sets attributes on it, so it can look styled while remaining byte-for-byte lossless — it never reformats your files or drops syntax it doesn't understand.

Where it's going

VISION.md is the aspirational north star — a design brief for the app Folio is being built toward. It is not a feature reference: most of what it describes is deliberately not implemented yet, and it says so section by section. This README is the accurate account of what works.

Layout

A SwiftPM executable, not an Xcode project — fully scriptable, no manual setup.

Sources/Folio/
  Models/    file identity, vault tree, link index
  Storage/   vault store, FSEvents watcher, settings, UI state, platform bridge
  Reading/   Markdown parser, reading view, code + inline highlighters, find
  Editors/   Live Preview editor (TextKit), Markdown highlighter, theme
  Views/     macOS UI — sidebar, tabs, palettes, outline, backlinks, settings
iOS/         iOS-only UI (shares Models/, Storage/, Reading/)

Design reasoning — how search/caching/rendering work and why, plus the SwiftUI/AppKit traps this codebase has already paid for — lives in ARCHITECTURE.md.

Sibling projects using the same shape: recall-cli, recall-app, raven.

License

MIT — see LICENSE.

About

A read-optimized, files-on-disk Markdown notes reader for macOS

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages