Skip to content

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Leaf

A minimal PDF reader for macOS. Native SwiftUI + PDFKit — no Electron, no bundled runtime, ~1 MB binary.

Build

./build.sh

Produces build/Leaf.app (ad-hoc signed). Install it with:

cp -R build/Leaf.app /Applications/

Requires macOS 15+ and the Xcode command line tools.

There are no binary releases. Leaf is signed ad-hoc rather than notarised, so a downloaded build would be refused by Gatekeeper with the misleading message "Leaf is damaged and can't be opened" — building it yourself avoids that entirely. If you do end up with a downloaded copy, clear the quarantine flag:

xattr -d com.apple.quarantine /Applications/Leaf.app

The two test documents are generated rather than committed, so a fresh clone has none. Recreate them with:

swift Resources/makesample.swift Sample.pdf     # has a table of contents
swift Resources/makenotes.swift LectureNotes.pdf # has none, for the detector

Screenshots

Every shot uses the generated sample documents above, never a real book.

Reading — tabs, thumbnails, and the workspace switcher in the toolbar, under the Soft White light setting.

Reading a document

Split — two documents side by side, each pane with its own tabs and its own scroll position. The contents panel follows whichever pane has focus.

Split view

Search — whole-document search with the surrounding sentence for each match and word-level highlighting on the page.

Search results

Candlelight, two-page — the warmest of the four light settings. Light and layout are remembered per workspace.

Candlelight, two-page layout

Reading mode (⇧⌘R) — sidebar, tabs and toolbar gone, the page running to the window edge. Esc brings them back.

Reading mode

Detected structure — a document with no table of contents of its own, with its headings recovered from the text and filterable by kind.

Detected document structure

What it does

Workspaces

A workspace is a saved reading environment: the documents you have open for a subject, which pane each one sits in, the page you left each of them on, and how they are being displayed. Switch workspaces and the whole setup comes back — "Electrodynamics" might be a textbook beside your lecture notes in a side-by-side split, while "Linear Algebra" is one book in two-page sepia.

Workspaces update themselves as you read, so there is nothing to save. Switch from the toolbar, from View ▸ Workspace, or from the welcome screen; ⇧⌘N starts a new one.

Documents open as tabs, one bar per pane. Open one with the + on the tab bar, ⌘O, or by dropping a PDF onto the window. Drag a tab to the other pane's bar to move it there, or reorder it within its own. A tab's position is what tells you which pane it's in — there is nothing else to read.

The window is split exactly when the second pane has tabs. ⌥⌘S sends the current tab across to start a split; closing the last tab on that side ends it. Side-by-side vs. stacked is remembered per workspace — use the chevron beside the split button, or View ▸ Arrangement.

Reading

  • Three page layouts: single page, continuous scroll, and two-page spread (with the cover shown alone, as in a real book)
  • Fit width, fit page, actual size, and free zoom
  • Light: Daylight, Soft White, Warm White or Candlelight — a warmth ramp rather than one sepia setting, since how much is comfortable depends on the display, the room and the time of day
  • Text selection and copy
  • Reading mode (⇧⌘R): sidebar, tabs and toolbar all go, leaving the page and nothing else. Esc brings them back
  • Split view (⌥⌘S): two independently scrolling panes, side by side or stacked, each with its own document — a textbook beside its solutions, or a paper beside your notes. Each pane fits and zooms itself. Right-click any thumbnail, contents entry, bookmark or search result and choose Open in Split Pane to park it beside what you're reading — the point being to keep a theorem, a figure or a definition in view while you read the text that refers to it

Navigating

  • Sidebar with four panels — thumbnails, contents, bookmarks, and search results — all following the tab you are reading
  • Detected contents for documents that ship no outline, which is most lecture notes and preprints. Leaf reads the text for the labels a technical document prints anyway — Theorem 4.1, Definition 2.3, 4.2 Boundary conditions, Chapter 7 — and builds the table of contents that should have been there. Filter it by kind to get just the theorems, or just the exercises. It is heuristic and says so: when a document has a real outline, that one is shown and the detected list sits behind a second tab
  • Bookmarks: ⌘D marks the current page, auto-named after the page's first line of text and renameable. Bookmarked pages show a marker on their thumbnail, and the whole set is reachable from Go ▸ Go to Bookmark
  • Page box in the toolbar accepts either the printed page label (roman numerals included) or a plain page number
  • Full-document search with per-match context, word-level highlighting on the page, and next/previous stepping
  • Recent documents, each reopening on the page you left it

Elsewhere

  • Drag a PDF onto the window to open it
  • Leaf some.pdf works from a shell; so does double-clicking in the Finder

Keyboard

⌘O Open
⌘N New window
⇧⌘N New workspace
⌘D Add / remove bookmark
⌘F Search
⌘G / ⇧⌘G Next / previous match
⌘↑ / ⌘↓ Previous / next page
⌘1 / ⌘2 / ⌘0 Fit width / fit page / actual size
⌘+ / ⌘- Zoom in / out
⌃Tab / ⌃⇧Tab Next / previous tab
⌥⌘S Split view
⇧⌘R Reading mode (Esc to leave)
⌘B Toggle sidebar
⌥⌘R Show in Finder

Layout

Sources/Leaf/LeafApp.swift App entry, menu bar, file-open routing
Sources/Leaf/ReaderModel.swift Per-window state, workspace binding, search
Sources/Leaf/Workspace.swift Workspace model and storage
Sources/Leaf/Pane.swift One pane: its tabs, position and outline
Sources/Leaf/TabBar.swift Per-pane tab bar, with drag between panes
Sources/Leaf/PDFController.swift PDFKit bridge (paging, zoom, highlights)
Sources/Leaf/ReaderWindow.swift Window layout and toolbar
Sources/Leaf/Sidebar.swift Thumbnails, contents, bookmarks, search
Sources/Leaf/ThumbnailStore.swift Lazy background thumbnail rendering
Sources/Leaf/Bookmarks.swift Bookmarks, tracked across renames and moves
Sources/Leaf/Recents.swift Recent files and resume positions
Resources/makeicon.swift Draws the app icon at build time
Sources/Leaf/DocumentStructure.swift Recovers headings from a document's text
Resources/makesample.swift Regenerates Sample.pdf, the test fixture
Resources/makenotes.swift Regenerates LectureNotes.pdf, an outline-less fixture

Notes

  • PDFKit and SwiftUI disagree about who owns a view. SwiftUI calls makeNSView more than once per pane and installs only one of the views it gets, so the controller binds to whichever view reports itself inside a window rather than to the last one created. frameDidChange notifications never arrive for PDFView and layout() is not reliably called, so re-fitting hangs off setFrameSize. And layerUsesCoreImageFilters, which the page tints need, is ignored unless it is set before the view's layer exists — so it is set at construction, not when a tint is chosen. All four are commented at their call sites.
  • Bookmarks live in preferences, not in the PDF — Leaf never writes to your files. Each record carries a macOS file bookmark alongside the path, so renaming or moving a document keeps its bookmarks: the record is found by that bookmark and re-keyed the next time the file is opened. Records for files that have genuinely gone are left alone rather than pruned, since an unmounted volume looks identical to a deleted file.
  • File access goes through FileAccess/ScopedAccess, which mint and resolve security-scoped bookmarks. The scope is held for as long as a PDFDocument is cached, because PDFKit reads pages lazily and would otherwise lose access mid-read once the app is sandboxed.
  • ./build.sh produces an ad-hoc signed, unsandboxed app with the hardened runtime. SANDBOX=1 ./build.sh adds the App Sandbox entitlements, which is what a store build needs. The sandbox is off by default because a sandboxed app runs against its own container: workspaces from an unsandboxed build start empty, and each document has to be picked once before it can be reopened.
  • Resources/Leaf.entitlements and Resources/PrivacyInfo.xcprivacy exist in case anyone wants to take this to the Mac App Store. That would additionally need an Apple Developer account, distribution certificates and an Xcode project to produce a signed archive. There are no plans to do it.
  • Window restoration is deliberately disabled; relaunching otherwise resurrects every window from the previous session.

Licence

MIT — see LICENSE.

A personal project, written for my own physics and maths reading. Use it freely; no support is promised.

About

A minimal PDF reader for macOS. Workspaces, split panes with independent documents, and a table of contents recovered from documents that ship none.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages