Repository navigation
How the Image and PDF Preview Works
Images and PDFs in the work tree open in a preview tab instead of the "Binary file, not shown" message, and a diff of a binary image or PDF shows the old and new version side by side. The user side is in Image and PDF Preview and Diffs.
Repositories hold screenshots, icons and PDF manuals. Seeing them in the app saves a trip to Finder, and both versions side by side are the only useful diff of a logo. Memory is a feature (Architecture), so a preview must cost nothing when it is not on screen.
Both viewers load their files from the gmpreview URL scheme, so the bytes go straight from Rust to WebKit and never enter JavaScript. The scheme, its security rules and its range requests are in How the Preview Scheme Works.
flowchart LR
FileView["FileView.svelte<br/>(file tab)"] -->|"only while active"| Media["MediaPreview.svelte (lazy)"]
DiffView["DiffView.svelte<br/>(binary + previewSides)"] -->|"only while on screen"| Binary["BinaryPreview.svelte (lazy)"]
Media --> Open["openPreview(source, version)"]
Binary --> Open
Open --> Stat["preview_stat"]
Open --> Url["gmpreview URL"]
Url --> Tag["img or iframe"]
-
Picking the preview.
previewOfinviews/files/mediaPreview.tsmaps the extension to a kind (imageorpdf) and a media type. ThenFileView.loaddoes not read the file as text; it only bumpspreviewToken. -
Loading.
MediaPreview.svelteis imported with{#await import(...)}, so its code is a separate chunk loaded the first time. It mounts only whileisActiveis true, so a background tab holds nothing. It callsopenPreview({ kind: "worktree", filePath }, previewToken), shows the size from the stat, and sets the URL on an<img>or<iframe>. -
Images. The view is the UI library's
MediaView. ItsfitZoom(logic/zoom.ts) shrinks big images to the canvas and keeps small ones at 100%;nextZoomwalksZOOM_STEPSfrom wherever the zoom is (a fitted 62% steps to 75% or 50%). Ctrl or Cmd with the wheel zooms, which covers a trackpad pinch too. -
PDFs. An
<iframe>shows the URL and WebKit's own PDF viewer draws it. No PDF library is bundled. -
Reloading. FileView's status effect calls
load(true)when the file may have changed; that bumpspreviewToken, which becomes the URL'sv=number, so WebKit loads the file again.
DiffView takes an optional previewSides: where the old and the new side come from, each a PreviewSource (a work tree path, or a repository, revision and path) or null when that side does not exist. showsBinaryPreview in diff/binaryPreview.ts decides: the diff is binary or too large, it is not a Git LFS pointer (those keep their sizes), the file is an image or PDF, and the caller passed sides. Then BinaryPreview.svelte is imported lazily in place of the "Binary file" message.
| Caller | Left (old) | Right (new) |
|---|---|---|
ChangesDiff, unstaged |
index |
work tree |
ChangesDiff, staged |
HEAD at the old path of a rename |
index |
CommitDetails (Log, commit tabs, file history) |
<commit>^ at the old path; none for a root commit |
the commit |
CompareTab (Compare with Revision) |
the resolved commit; none when the file is not in it | work tree |
WorktreeDiffTab (Show Diff with Working Tree) |
the branch's commit; none when not in it | work tree |
BranchCompareTab |
the base branch's commit, old path | the branch's commit |
ShelfDiffTab |
no preview |
The shelf keeps a binary file only as a patch, and its new blob is usually not in the repository, so the scheme cannot address it: it keeps the "Binary file" message.
sideState turns each side's source and stat into loading, ready, absent or error. An absent old side says "Not in" and the side's label (for example "Not in HEAD"); an absent new side says "Deleted". compareSide hands each side to the library's MediaCompare; its head shows the label, pixel size and file size.
The two images share one zoom. MediaCompare owns the zoom: Fit uses sharedFit (ui/web/src/lib/logic/zoom.ts), the smallest fit of both, so they stay at one scale and a bigger new logo looks bigger. Scrolling one side scrolls the other to the same place in proportion (syncedScroll); an echo flag skips the scroll event that syncing causes. PDFs get two frames with their own viewers.
Callers whose sides can change on disk pass a version (ChangesDiff bumps it on every status change of the repository, the compare tabs on every load). The preview loads again when the sources or the version change, compared as a string key.
DiffView mounts the preview inside a host bound to its width. A hidden tab is display: none, its width drops to 0, and the preview unmounts. The library's releaseMedia action sets every frame to about:blank on destroy, so WebKit drops the PDF document, and removes each image's src. The decoded pictures go with the elements.
| File | What it does |
|---|---|
src/lib/views/files/mediaPreview.ts |
previewOf, tooLargeText
|
src/lib/views/files/MediaPreview.svelte |
The file tab viewer on the library's MediaView (zoom bar, size, freeing) |
src/lib/views/files/FileView.svelte |
Chooses the preview, mounts it only while active, previewToken
|
src/lib/diff/binaryPreview.ts |
PreviewSides, showsBinaryPreview, sideState, compareSide
|
src/lib/diff/BinaryPreview.svelte |
The two sides of a diff, on the library's MediaCompare
|
ui/web/src/lib/logic/zoom.ts, releaseMedia.ts
|
Shared fit, synced scroll, the info line, freeing on unmount |
src/lib/diff/DiffView.svelte |
previewSides; a Stack's onshown keeps the preview only while on screen |
src/lib/views/files/previewScheme.ts |
openPreview |
WebKit's PDF viewer, not pdf.js. pdf.js would add megabytes of code and a worker; the built-in viewer costs nothing until a PDF opens. It is checked on macOS. WebView2 on Windows has its own viewer, but WebKitGTK on Linux may show nothing; Reveal in File Explorer or Open Containing Folder stays as the way out.
Unmount on hide, not only on close. Reading a file again is cheap; keeping decoded 20-megapixel photos in background tabs is not.
SVG stays in the editor. It is text that people edit, so its diff is a text diff.
One scale for both sides. Fitting each image on its own would make a 128 px and a 256 px logo look the same size, which hides the change.
A code chunk cannot be unloaded. Browsers keep an imported module for the life of the page, so the modules hold no data; the pictures and documents are what gets freed.
-
src/lib/views/files/mediaPreview.test.ts: which files get a preview and the too big message. -
src/lib/diff/binaryPreview.test.ts: when the preview shows, absent sides, side states andcompareSide(the library tests the fit, scroll and freeing). - The scheme's own tests are listed in How the Preview Scheme Works.
How the viewers look and that memory goes down after closing a tab need a check in the app, with the status bar's memory readout (How Memory Is Measured).
- Keep the extension list in
mediaPreview.tsandpreview_mimeinmedia.rsthe same. - When a new diff view is added, give it
previewSidesand add it to the table above. - Update Image and PDF Preview and retake
media-preview-image.png,media-preview-pdf.pnganddiff-binary-image.pngwhen the viewers change.diff-binary-image.pngusesextras/brand-kitof the demo, where the logo is replaced by a bigger one. -
media-preview-pdf.pngis taken by hand, since Playwright's WebKit has no PDF viewer andscripts/screenshots.tswould get an empty frame. Opennotes/lorem-ipsum.pdfof the demo (scripts/make-docs-demo.sh, made byscripts/make-demo-pdf.ts) in the real app, capture the window (nibtree cli screenshot, or Cmd+Shift+4 then Space) and crop it to the document, so no tooltip or real path shows.
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