feat(macos-ql): Spotlight MDImporter for .sav/.dat/.fla Quick Look dispatch#940
Merged
Conversation
Adds an IThumbnailProvider alongside the preview handler — Explorer/Finder-style icon
thumbnails for PKHeX files — reusing the same native-shim + worker split and bundled sprites.
- Pkmds.Preview.FileSprite: shared "representative sprite for a file" (entity -> species,
save -> lead party Pokemon, gift -> species/item, fallback), the thumbnail counterpart to
HtmlRenderer.RenderFile.
- Worker --thumbnail mode: draws the bundled sprite to a square PNG (System.Drawing), offline.
- PkmdsPreviewShim now hosts a second COM class (IThumbnailProvider, separate CLSID
{b98dbf6e-...}) that spawns the worker in --thumbnail mode and decodes its PNG into an alpha
HBITMAP via WIC. DllGetClassObject dispatches both CLSIDs (templated class factory).
- register.cs registers the thumbnail CLSID + the IThumbnailProvider IID {e357fccd-...} per
extension (same shim DLL).
Verified the worker renders correct thumbnails (.pk5 -> species, save -> lead party mon).
macOS/iOS QLThumbnailProvider extensions are deferred (need a Mac) — tracked in #935.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
install.ps1 wiped+republished dist in step 2 but only stopped prevhost/Explorer in step 5, so a live prevhost/dllhost/worker kept dist DLLs locked (Access denied on dist\Accessibility.dll). Stop prevhost/dllhost/PkmdsPreviewWorker up front, before the clean+publish. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The thumbnail cache (dllhost) initializes IThumbnailProvider handlers via IInitializeWithStream and ignores a file-only handler, so thumbnails came up blank. Implement IInitializeWithStream as the primary init (keep IInitializeWithFile as a fallback); GetThumbnail spills the stream to a temp file for the worker. A stream has no extension, so the worker content-detects entities/saves (gifts, which need the extension, fall back to the placeholder in the stream path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The thumbnail cache inits via IInitializeWithStream (no path), so the worker got a temp file with no extension and couldn't identify mystery gifts (which are detected by extension) — .wc6/.pgf/etc. fell back to the placeholder. Read the original name from IStream::Stat (pwcsName) and reuse its extension for the temp file, so gift thumbnails (e.g. a .wc6 Bulbasaur) resolve correctly. Falls back to .bin (content detection) if the stream has no name. Note: .wc3 (WonderCard3) only exposes Type, not the gifted species/item, so it still shows the placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WonderCard3 only exposes Type (Pokémon / Item / Link), not the gifted species/item, so .wc3 thumbnails now return the item placeholder for Item-type cards and the Pokémon placeholder otherwise (previously always the Pokémon placeholder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Save files now thumbnail as a compact "trainer card" (game version code, OT name, TID/SID, playtime) instead of the lead party Pokémon's sprite — identifying both the game and the specific playthrough at a glance. - SaveCard.Render draws the card with System.Drawing (no WebView2) so a folder of saves thumbnails quickly. - Per-version accent colour; the two groups the save format can't disambiguate (RB = Red/Blue, GS = Gold/Silver) use a diagonal gradient border and a horizontal gradient on the code so each letter lands on its game's colour (R red, B blue; G gold, S silver). - Thumbnail.Render branches: SaveUtil.TryGetSaveFile -> SaveCard, else the representative bundled sprite via FileSprite (unchanged for entities/gifts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `com.apple.quicklook.thumbnail` app extension (`PkmdsQuickLookThumbnail`) alongside the existing preview extension, completing the macOS side of #935. **What's new:** - `Exports.cs` — `pkmds_get_sprite_path` export wraps `FileSprite.GetRelativeSpritePath` so Swift can ask for a sprite path without going through HTML rendering. Also adds `playedHours`/`playedMinutes` to the save JSON for the trainer-card layout. - `ThumbnailProvider.swift` — `QLThumbnailProvider` that mirrors the Windows `Thumbnail.cs` + `SaveCard.cs` split: - **Saves** → compact trainer-card (version code in game accent colour, OT, TID/SID, playtime), drawn with AppKit (`NSBezierPath`, `NSAttributedString`) into the `QLThumbnailReply(contextSize:drawing:)` CGContext. - **Entities + wonder cards** → bundled species/item sprite scaled to the requested size. - **RB/GS ambiguous groups** → per-character colour interpolation on the version code (`"R"` red, `"B"` blue; `"G"` gold, `"S"` silver). Border remains single-colour (gradient border is noted as a cosmetic enhancement in the README). - `project.yml` — new `PkmdsQuickLookThumbnail` target (type: app-extension, extension point `com.apple.quicklook.thumbnail`), embedded in `PkmdsHost`. - `build-extension.sh` — after `xcodebuild`, copies the three sprite categories (`a/`, `ai/`, `bi/`) from `Pkmds.Rcl/wwwroot/sprites/` into the thumbnail appex `Contents/Resources/sprites/`, re-signs both extensions, and registers the thumbnail extension with `pluginkit`. Smoke tests both preview and thumbnail via `qlmanage`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ThumbnailsAgent refuses to register a thumbnail extension for any UTI that declares a prohibited filename extension (.sav, .dat, .fla). Because com.bondcodes.pkmds.save-file previously included these extensions, the extension was silently rejected at startup and no thumbnails appeared for .gci/.dsv/.srm files. Fix: remove .sav, .dat, and .fla from the save-file UTI declaration in project.yml, keeping only .gci, .dsv, and .srm. (.sav files require a Spotlight MDImporter to assign the save-file UTI; that is future work.) Also update build-extension.sh to: - Proactively unregister Xcode DerivedData and iOS POC builds from the LS database before installing, preventing stale prohibited-extension registrations from resurfacing after a clean build. - Kill ThumbnailsAgent after deploy so it restarts with the clean LS database (it caches extension-to-UTI mappings at startup). - Add timeout to qlmanage -p so the smoke test doesn't block forever. - Use a .gci fixture for the save thumbnail smoke test, since qlmanage cannot dispatch to sandboxed extensions via prohibited extensions. Adds TestFiles/Test-Save-Moon.gci (copy of moon.sav renamed .gci) as the non-prohibited save fixture for smoke tests and manual verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- opaqueSourceRect(): scans sprite pixels via a CG-owned bitmap context (data:nil / byteOrder32Little / premultipliedFirst) to find the tight non-transparent bounding box, then returns it in NSImage (bottom-left) coordinates for use as the `from:` source rect in NSImage.draw() - Sprite load and opaque-rect computation are now done on our controlled background queue before the QLThumbnailReply drawing closure, avoiding a deadlock with AppKit's image-cache lock on the system-managed closure thread - drawSprite() is removed; its logic is inlined directly into provideThumbnail - Sprites now fill 90% of the thumbnail canvas based on visible content only - build-extension.sh: fix `timeout` (GNU coreutils, not on macOS) for both qlmanage -p and qlmanage -t smoke-test calls using a background-kill workaround; add 15s cap to qlmanage -t to prevent build from blocking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QLThumbnailReply's CGContext is pixel-sized (2× point size on Retina), but layout was using request.maximumSize in points, causing sprites to render at ~45% fill instead of 90%. Use ctx.width/height for all layout inside the drawing closure. opaqueSourceRect was also computing the NSImage y origin as minRow*sy, treating the raster-top buffer row as NSImage bottom-left. The correct formula is (ph - maxRow - 1)*sy. Also removes leftover debug logging and PNG/text file writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PkmdsSpotlight.mdimporter — a Spotlight plugin that sets kMDItemContentType = com.bondcodes.pkmds.save-file in the Spotlight database for .sav, .dat, and .fla files. These extensions are "prohibited" in the macOS Quick Look sandbox: even if declared in an app's UTExportedTypeDeclarations, sandboxed QL extensions will not be dispatched for them via the extension-based UTI path. The MDImporter bypasses this by writing the content type into the Spotlight database; Finder reads the database value and dispatches our QL preview and thumbnail extensions correctly. build-extension.sh installs the importer to ~/Library/Spotlight/ (no sudo required) and runs mdimport on TestFiles/*.sav after every build. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…MinimumSize System thumbnail extensions declare QLThumbnailMinimumDimension (integer) not QLThumbnailMinimumSize (dict). Setting it to 0 matches the convention used by Apple's built-in extensions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The MDImporter caused quicklookd to dispatch our preview extension for .sav/.dat/.fla files via Spotlight metadata. However, the extension's sandbox (com.apple.security.files.user-selected.read-only) does not receive a security-scoped file access grant on the Spotlight dispatch path, so Data(contentsOf:) throws and the preview returns blank — strictly worse than the generic macOS default. For thumbnails, ThumbnailsAgent dispatches via the UTI database (not Spotlight metadata), so the MDImporter never helped thumbnails either. .gci/.dsv/.srm remain in the save-file UTI declaration and continue to get both thumbnails and previews. .sav/.dat/.fla are documented as out-of-scope due to the prohibited-extension restriction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds com.bondcodes.pkmds.save-file-restricted UTI covering .sav/.dat/.fla and registers it with the preview extension only (not the thumbnail extension). ThumbnailsAgent blocks thumbnail dispatch for any UTI that lists prohibited extensions (.sav/.dat/.fla), so these extensions cannot get icon thumbnails. Isolating them in a separate UTI that the thumbnail extension never sees preserves .gci/.dsv/.srm thumbnail dispatch while restoring Space-bar Quick Look preview for .sav/.dat/.fla via the UTI-database path (which does grant sandbox file access, unlike the old Spotlight metadata path). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
743be94 to
3971a15
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds cross-platform thumbnail support and a macOS Spotlight metadata importer path intended to improve Quick Look dispatch for Pokémon save-related files, alongside shared sprite-selection logic reused by Windows and macOS preview tooling.
Changes:
- Adds Windows
IThumbnailProviderregistration, shim support, worker thumbnail rendering, and trainer-card thumbnails for saves. - Adds macOS Quick Look thumbnail extension, NativeAOT sprite/save exports, and build-script sprite bundling/signing updates.
- Adds Spotlight MDImporter files and updates macOS UTI declarations for restricted save extensions.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tools/windows-preview-poc/register.cs |
Registers/unregisters the new Windows thumbnail provider CLSID per supported extension. |
tools/windows-preview-poc/README.md |
Documents Windows thumbnail-provider architecture and updated tool layout. |
tools/windows-preview-poc/PkmdsPreviewWorker/Thumbnail.cs |
Adds PNG thumbnail rendering from save trainer cards or representative sprites. |
tools/windows-preview-poc/PkmdsPreviewWorker/SaveCard.cs |
Adds Windows save-file trainer-card drawing logic. |
tools/windows-preview-poc/PkmdsPreviewWorker/Program.cs |
Adds --thumbnail worker mode. |
tools/windows-preview-poc/PkmdsPreviewShim/PkmdsPreviewShim.cpp |
Adds native COM thumbnail provider and PNG-to-HBITMAP decoding. |
tools/windows-preview-poc/install.ps1 |
Stops handler hosts before rebuilding/installing Windows preview artifacts. |
tools/preview-shared/FileSprite.cs |
Adds shared representative-sprite selection for entities, saves, and mystery gifts. |
tools/macos-quicklook-poc/xcode/project.yml |
Adds macOS thumbnail extension target and adjusts UTI declarations. |
tools/macos-quicklook-poc/xcode/PkmdsSpotlight/Info.plist |
Defines Spotlight MDImporter bundle metadata and UTI declarations. |
tools/macos-quicklook-poc/xcode/PkmdsSpotlight/GetMetadataForFile.m |
Adds minimal MDImporter implementation setting save-file content type. |
tools/macos-quicklook-poc/xcode/PkmdsQuickLookThumbnail/ThumbnailProvider.swift |
Adds macOS Quick Look thumbnail rendering for sprites and save trainer cards. |
tools/macos-quicklook-poc/xcode/PkmdsQuickLookThumbnail/PkmdsQuickLookThumbnail.entitlements |
Adds sandbox/library-validation entitlements for the thumbnail extension. |
tools/macos-quicklook-poc/xcode/PkmdsQuickLookThumbnail/Info.plist |
Adds thumbnail extension Info.plist. |
tools/macos-quicklook-poc/xcode/PkmdsQuickLook/Info.plist |
Adds restricted save-file UTI support to preview extension. |
tools/macos-quicklook-poc/xcode/PkmdsHost/Info.plist |
Splits restricted save extensions into a separate UTI. |
tools/macos-quicklook-poc/README.md |
Updates macOS PoC documentation for preview plus thumbnail support. |
tools/macos-quicklook-poc/PkmdsNative/Exports.cs |
Exposes sprite-path lookup and save playtime fields to native callers. |
tools/macos-quicklook-poc/build-extension.sh |
Bundles sprites, signs both extensions, deploys, registers, and smoke-tests preview/thumbnail paths. |
- Remove orphaned PkmdsSpotlight source files left behind after the MDImporter revert; the split-UTI approach supersedes them entirely. - Narrow dllhost kill in install.ps1 to only processes that have PkmdsPreviewShim.dll loaded, avoiding disruption of unrelated COM surrogates on the machine. - Terminate worker process on WaitForSingleObject timeout in ThumbnailHandler::GetThumbnail to prevent orphaned worker processes accumulating when requests hang. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four new "non-obvious things" entries covering what we discovered during the thumbnail/preview debugging session: 6. Prohibited extensions (.sav/.dat/.fla) block ThumbnailsAgent dispatch for the entire UTI — and the split-UTI fix. 7. Spotlight MDImporter dispatch path does not grant sandbox file access, producing a blank preview — and why the MDImporter was removed. 8. Finder only requests thumbnails at icon sizes >= ~100px (default 64px is too small), and why qlmanage -t -x is needed instead of plain -t. 9. QLThumbnailMinimumDimension must be a plain integer (0), not the iOS dict form QLThumbnailMinimumSize. Also adds ThumbnailsAgent and quicklookd -x diagnostic commands to the toolbox table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced May 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
.sav,.dat, and.flafiles are prohibited extensions in the macOS Quick Look sandbox. Even though our host app exports a UTI (com.bondcodes.pkmds.save-file) and our QL extensions declare they handle it, macOS will not dispatch preview or thumbnail requests to a sandboxed extension for files identified solely by a prohibited extension. Pressing Space on a.savfile in Finder showed the generic icon instead of our preview.Solution
Adds
PkmdsSpotlight.mdimporter— a Spotlight metadata importer that writeskMDItemContentType = com.bondcodes.pkmds.save-fileinto the Spotlight database for.sav,.dat, and.flafiles. Finder reads that database value (not the raw extension) when deciding which QL extension to dispatch, so our preview and thumbnail providers are invoked correctly after indexing.Implementation
PkmdsSpotlight/GetMetadataForFile.m— minimal CFPlugin MDImporter. ExportsMetadataImporterPluginFactory; theGetMetadataForFilecallback simply setskMDItemContentTypeto our save-file UTI. No PKHeX parsing needed — extension matching is handled by theUTImportedTypeDeclarationsinInfo.plist.PkmdsSpotlight/Info.plist— declaresUTImportedTypeDeclarationsfor.sav/.dat/.fla→com.bondcodes.pkmds.save-file, plus the CFPlugin factory wiring (CFPlugInFactories/CFPlugInTypes).project.yml— addsPkmdsSpotlightas abundletarget withWRAPPER_EXTENSION: mdimporter, embedded inPkmdsHost.app/Contents/Resources/.build-extension.sh— installs the importer to~/Library/Spotlight/(no sudo), callsmdimport -rto reload it, and runsmdimporton allTestFiles/*.savafter each build.Verified
mdls -name kMDItemContentType moon.sav→com.bondcodes.pkmds.save-file✅Notes
.savfiles added after install will be indexed automatically by Spotlight's background pass. For immediate effect, runmdimport <file>.qlmanage -p/-tstill cannot test sandboxed extensions for prohibited extensions; Finder is the only test surface.🤖 Generated with Claude Code