Repository navigation
How the Preview Scheme Works
The image and PDF previews, in a file tab and in a diff, load their files from a URL scheme of our own: gmpreview. This page covers the scheme: its URLs, what it may serve, range requests and memory. The viewers that use it are in How the Image and PDF Preview Works.
The first preview read the whole file through a command and made a blob: URL of it. Every byte crossed the IPC bridge (the channel between the web page and Rust) and sat in the page as a Blob while the preview was open: a 90 MB PDF meant 90 MB in the web view, and a diff of two versions twice that.
With a URL scheme, the page only sets img.src or iframe.src. WebKit asks Rust for the bytes itself and lets go when the element goes; no bytes enter JavaScript. WebKit's PDF viewer can also ask for a document piece by piece.
sequenceDiagram
participant View as MediaPreview or BinaryPreview
participant Js as previewScheme.ts
participant Cmd as preview_stat
participant WebKit
participant Scheme as preview_scheme.rs
View->>Js: openPreview(source, version)
Js->>Js: wait for the last mcp_set_workspace
Js->>Cmd: previewStat(source)
Cmd-->>Js: exists, size, limit
Js-->>View: URL, or a message (missing, too big)
View->>WebKit: img.src or iframe.src = URL
WebKit->>Scheme: GET, maybe with Range
Scheme->>Scheme: decode, check folders and type, read
Scheme-->>WebKit: 200 or 206 with the bytes
View->>View: unmount: iframe to about:blank, img src removed
previewUrl in src/lib/views/files/previewSource.ts builds them, and parse_target in src-tauri/src/preview_scheme.rs reads them. Each part is percent-encoded on its own with encodeURIComponent, so a / inside a path never splits it:
-
<base>worktree/<absolute file path>: a file on disk. -
<base>revision/<repository root>/<revision>/<repo-relative path>: a file in git. The revision isHEAD,index(stage 0 of the index), a full commit id, or a full id with^for its first parent.
The base comes from Tauri's convertFileSrc: gmpreview://localhost/ on macOS and Linux, http://gmpreview.localhost/ on Windows. The backend only reads the path, so both work. A ?v= number at the end is ignored by Rust; a new one makes WebKit load the file again after it changed on disk.
Short ids and branch names are refused on purpose: Oid::from_str pads a short id with zeros, and a branch name would change meaning when the branch moves.
The scheme answers only for the folders open in the window. App.svelte sends them with mcp_set_workspace whenever the workspace changes; the command stores them in AppState.preview_folders (a PreviewFolders) as well as for MCP. Before the first call the list is empty and every request is refused. openPreview waits for the latest call, so a preview tab restored at start does not race it.
-
Work tree files go through
checked_pathfrommcp/paths.rs: the path must be absolute,..is refused, and symlinks are resolved before the folder check, so a link that leads out is outside. On Unix the file is opened withO_NOFOLLOW, so a symlink swapped in after the check is not followed. -
Revisions go through
checked_repo: the repository must be inside a folder, or be the one a folder was opened in. The path must be relative and stay in the repository. Only regular files are served, never a committed symlink or a submodule. -
Types: only what
media::preview_mimeknows (PNG, JPEG, GIF, WebP, BMP, ICO, AVIF, PDF). The answer always says that type, withX-Content-Type-Options: nosniff.
A refusal is 403 (not allowed), 404 (not there) or 413 (too big), with an empty body, so no path or reason leaks into the page.
parse_range reads one bytes= range: 0-99, 100- or -500 (the last 500 bytes). The answers:
- no
Range: 200 with the whole file andContent-Length; - a range inside the file: 206 with
Content-Range: bytes start-end/size, at mostMAX_RANGE_BYTES(4 MB); a longer or open range gets the first 4 MB and the client asks again; - a range past the end or a broken one: 416 with
Content-Range: bytes */size; - another unit or several ranges: the whole file, as RFC 9110 allows.
Every answer has Accept-Ranges: bytes and Cache-Control: no-store, so WebKit keeps no copy in its cache.
A work tree range is read with seek and read of just those bytes. Tauri sends a response body in one piece, so a request without a range holds the whole file once; that is why a whole PDF keeps the 100 MB limit while ranged requests of it have none. Images keep the 50 MB limit either way, since the decoded picture is several times bigger.
git stores blobs compressed, so there is no way to seek into one. A revision request reads the blob with git2, answers its range and drops it right away. The blob is in memory once per request, which is why revisions keep both limits. preview_stat gets a blob's size from the object header (odb.read_header) without inflating it.
The handler runs with register_asynchronous_uri_scheme_protocol and spawn_blocking, off the main thread, and caches nothing.
| File | What it does |
|---|---|
src-tauri/src/preview_scheme.rs |
PreviewFolders, parse_target, parse_range, respond, stat, the Tauri wrapper http_response
|
src-tauri/src/media.rs |
preview_mime and the size limits |
src-tauri/src/commands/files.rs |
preview_stat |
src-tauri/src/commands/mcp.rs |
mcp_set_workspace, which also sets the preview folders |
src-tauri/src/lib.rs |
registers the scheme |
src-tauri/tauri.conf.json |
gmpreview: and http://gmpreview.localhost in img-src and frame-src
|
src/lib/views/files/previewSource.ts |
the URL format and the special revisions |
src/lib/views/files/previewScheme.ts |
the base, syncWorkspaceFolders, openPreview
|
A scheme, not bytes over IPC. See "Why we need it". It also removed blob: from the CSP (Content Security Policy, the list of places the page may load from), which nothing else used.
A stat command next to the scheme. An img that fails only says "error". preview_stat tells missing from too big, so a diff side can say "Not in HEAD" or "Deleted". It shares resolve with the scheme. A fetch with HEAD would have needed CORS headers and connect-src for the scheme.
The MCP folder list. The window already sends the open folders for MCP. Reusing that call keeps one source of truth; the scheme keeps its own copy, so it works with MCP off.
One testable function. respond takes a method, a path and a Range value and returns status, headers and body. The Tauri closure only converts types, so everything that matters runs in plain unit tests.
src-tauri/src/preview_scheme/tests.rs covers decoding, every URL form, the ranges, the 4 MB cap, a PDF over 100 MB that still streams, every refusal, HEAD, index, a commit and its parent in a real repository, blob ranges, stat, the folder registry and the Tauri wrapper with both URL forms. src/lib/views/files/previewSource.test.ts covers the URLs.
Whether WebKit's PDF viewer really asks for ranges from a custom scheme, and the CSP on each platform, need a check in the app.
- Change
previewSource.tsandparse_targettogether, with their tests. - The screenshot script answers
http://gmpreview.localhostfrom the demo itself (previewResponseinscripts/screenshots.ts), since the browser page has no scheme; keep it in step with the URL format.
None yet.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode