Skip to content

feat(macos-ql): Spotlight MDImporter for .sav/.dat/.fla Quick Look dispatch#940

Merged
codemonkey85 merged 16 commits into
mainfrom
feat/sav-spotlight-mdimporter
May 27, 2026
Merged

feat(macos-ql): Spotlight MDImporter for .sav/.dat/.fla Quick Look dispatch#940
codemonkey85 merged 16 commits into
mainfrom
feat/sav-spotlight-mdimporter

Conversation

@codemonkey85
Copy link
Copy Markdown
Owner

Problem

.sav, .dat, and .fla files 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 .sav file in Finder showed the generic icon instead of our preview.

Solution

Adds PkmdsSpotlight.mdimporter — a Spotlight metadata importer that writes kMDItemContentType = com.bondcodes.pkmds.save-file into the Spotlight database for .sav, .dat, and .fla files. 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. Exports MetadataImporterPluginFactory; the GetMetadataForFile callback simply sets kMDItemContentType to our save-file UTI. No PKHeX parsing needed — extension matching is handled by the UTImportedTypeDeclarations in Info.plist.
  • PkmdsSpotlight/Info.plist — declares UTImportedTypeDeclarations for .sav/.dat/.flacom.bondcodes.pkmds.save-file, plus the CFPlugin factory wiring (CFPlugInFactories / CFPlugInTypes).
  • project.yml — adds PkmdsSpotlight as a bundle target with WRAPPER_EXTENSION: mdimporter, embedded in PkmdsHost.app/Contents/Resources/.
  • build-extension.sh — installs the importer to ~/Library/Spotlight/ (no sudo), calls mdimport -r to reload it, and runs mdimport on all TestFiles/*.sav after each build.

Verified

  • mdls -name kMDItemContentType moon.savcom.bondcodes.pkmds.save-file
  • Quick Look preview and thumbnail dispatch expected to work in Finder after files are indexed.

Notes

  • New .sav files added after install will be indexed automatically by Spotlight's background pass. For immediate effect, run mdimport <file>.
  • qlmanage -p/-t still cannot test sandboxed extensions for prohibited extensions; Finder is the only test surface.

🤖 Generated with Claude Code

codemonkey85 and others added 14 commits May 27, 2026 00:33
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IThumbnailProvider registration, 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.

Comment thread tools/macos-quicklook-poc/xcode/PkmdsSpotlight/Info.plist Outdated
Comment thread tools/windows-preview-poc/install.ps1
Comment thread tools/windows-preview-poc/PkmdsPreviewShim/PkmdsPreviewShim.cpp Outdated
codemonkey85 and others added 2 commits May 27, 2026 00:38
- 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>
@codemonkey85 codemonkey85 enabled auto-merge May 27, 2026 04:43
@codemonkey85 codemonkey85 merged commit ecaf24a into main May 27, 2026
6 checks passed
@codemonkey85 codemonkey85 deleted the feat/sav-spotlight-mdimporter branch May 27, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants