wry) inside a native window (via tao). The Markdown rendering pipeline runs on the Rust side; the result is injected into the WebView as HTML. All user interaction — opening files, navigating history, adjusting settings — flows through a typed IPC bridge between JavaScript running in the WebView and the Rust host.
Core crates
| Crate | Role |
|---|---|
tao | Native windowing and event loop |
wry | WebView embedding (WKWebView / WebView2 / WebKitGTK) |
pulldown-cmark | CommonMark + GFM Markdown parser |
syntect + two-face | Syntax highlighting for code blocks |
ammonia | HTML sanitization (allowlist-based) |
rusqlite (bundled) | SQLite for the library indexer |
rfd | Native file dialogs |
serde / serde_json | IPC message serialization |
notify-debouncer-mini | Filesystem watcher for live reload |
blake3 | File content hashing in the indexer |
Source files
leaftext’s Rust source is organized into three files:-
src/main.rs— Entry point. Owns thetaoevent loop, theWorkspaceandTabstate, the IPC handler dispatch, theFileWatchlive-reload watcher, and navigation history. EveryUserEventvariant that drives the UI (open, close tab, switch tab, go back/forward, settings changes, indexer results) is handled here. -
src/lib.rs— Core document rendering and app-state helpers. Containsrender_markdown_document(), the HTML/CSS/JS shell generation (app_shell_html()), theme compilation (compiled_theme_css()), settings persistence (load_settings()/save_settings()), the minimap model builder, and thelocal_image_protocol_response()handler for theleaf-image://custom URL scheme. -
src/indexer.rs— Background SQLite-based library indexer. Implements a breadth-first filesystem walk with a parse/hash worker pool (PARSE_WORKERS = 4), incremental fast-path checks onmtime + size, missing-file detection, and a separate read-only connection so tree queries answer promptly during a full crawl.
Rendering pipeline
When a user opens a.md file, the following sequence runs entirely on the Rust side before any content reaches the WebView:
Read the file
load_document() or opened_document_from_markdown() in lib.rs reads the Markdown source from disk.Parse with pulldown-cmark
render_markdown_document() parses the Markdown using pulldown-cmark with Options::ENABLE_TABLES, ENABLE_STRIKETHROUGH, ENABLE_TASKLISTS, ENABLE_GFM, ENABLE_FOOTNOTES, and ENABLE_MATH enabled.Apply GitHub extras
A pipeline of event transformers adds heading IDs, linkifies plain URLs, resolves GitHub issue/PR/commit references and emoji shortcodes, renders syntax-highlighted fenced code blocks via
syntect, and handles footnote back-references.Sanitize with ammonia
The raw rendered HTML is passed through
ammonia with an allowlist of GFM-safe tags and attributes. Scripts, styles, event handlers, and dangerous URLs are stripped before the WebView ever sees the content.Inject initial settings
initial_settings_script() produces window.__leafSettings = {...} from the persisted Settings struct. This script is registered as a WebView initialization script so the theme and library pane render from saved state on the very first paint — no flash of defaults.Load into the WebView
app_shell_html() generates the full HTML/CSS/JS shell. reading_mode_css() assembles the complete style block: Noto fonts + Primer CSS primitives + compiled theme CSS + application CSS. The rendered document HTML is injected into the shell via workspace_state_script() or workspace_switch_script(), which call the appropriate window.leaf* JavaScript entry points.IPC bridge
Communication between the JavaScript running in the WebView and the Rust host useswry’s IPC mechanism. JavaScript calls window.ipc.postMessage(JSON.stringify(message)). Rust deserializes the body into an IpcCommand enum using serde_json, then dispatches it as a UserEvent on the tao event loop.
Key IpcCommand variants include:
| Command | Triggered by |
|---|---|
open | ”Open” button or Ctrl+O / Cmd+O |
openRecent | Recent file list click |
closeTab | Tab close button or Ctrl+W |
switchTab | Tab click |
moveTab | Tab drag-and-drop reorder |
goBack / goForward | History buttons or keyboard shortcuts |
openLink | In-document link click |
setThemeMode | Theme picker in Settings menu |
setMinimapEnabled | Minimap toggle in Settings menu |
setIndexingEnabled | ”Index entire device” toggle |
getFileTree | Boot-time library pane initialization |
webview.evaluate_script(), calling window.leafSetState(), window.leafSwitchTab(), window.leafReloadDocument(), window.leafSetNavigation(), window.leafSetLibraryState(), and related entry points.
Key data structures
The following types inmain.rs model the reader’s stateful document management:
-
Workspace— holdsVec<Tab>(all open tabs) andactive: Option<usize>(the currently visible tab index, orNonewhen the home screen is showing). -
Tab— holds aDocumentHistory, aScrollHistory, atitlestring (cached for the tab bar), and anOption<ScrollAnchor>(the last saved reading position). -
DocumentHistory— aVec<PathBuf>of visited paths with a current index. Supportsgo_back(),go_forward(), andforget_current()(used when a file fails to open). -
ScrollHistory— twoVec<ScrollAnchor>stacks (back_entriesandforward_entries) that record in-document scroll jumps independently from document-level navigation. -
ScrollAnchor—{ section: Option<String>, block: u32, offset_y: f64 }. A render-stable position: the nearest headingidabove the reader’s top edge, the block ordinal within that section, and the signed pixel offset. Survives a full re-render because the same Markdown always produces the same block sequence. -
FileWatch— a debouncednotifywatcher (200 ms window) pointed at the active document’s parent directory. Watching the parent rather than the file itself survives editors that save by atomic rename.
Local image protocol
local_image_protocol_response() in lib.rs serves local image files under the leaf-image:// custom URL scheme (or http://leaf-image.local/ on platforms where custom protocols are restricted, such as Windows and Android).
Before serving any bytes, it validates that the requested path resolves to within the access root — the parent of the currently opened document’s directory. Requests that escape this scope return 403 Forbidden; missing files return 404 Not Found. This scoped access model lets documents reference sibling and parent-directory images via relative paths (including ../) without exposing the full filesystem.