Skip to content

Pick the backdrop by path, seeded with the wallpaper - #4

Merged
winebarrel merged 2 commits into
mainfrom
custom-backdrop-picture
Aug 10, 2026
Merged

winebarrel merged 2 commits into
mainfrom
custom-backdrop-picture

Conversation

@winebarrel

Copy link
Copy Markdown
Owner

The Desktop picture backdrop showed the wrong picture whenever the wallpaper was
a dynamic or video one. This replaces it with a checkbox and a path field.

Why the old one could not work

NSWorkspace.desktopImageURL(for:) predates WallpaperKit and only reports still
pictures. With the com.apple.NeptuneOneExtension provider set — Sonoma's dynamic
and video wallpapers — the wallpaper store holds no file at all:

provider: com.apple.NeptuneOneExtension
Files:    []

and the API answers with a placeholder,
/System/Library/CoreServices/DefaultDesktop.heic, plus an empty options
dictionary. The saver was drawing that placeholder faithfully. No public API
reports those wallpapers, so there is nothing to fix on the reading side.

What it does instead

There is one source for the backdrop now — a file — and the desktop's wallpaper is
only where its value starts. The path is filled in once, on first run, and is an
ordinary saved setting from then on: changing your wallpaper later leaves the saver
alone.

That removes the awkward middle case the old control had, where Desktop picture
silently followed an API that could not see the wallpaper. The placeholder can
still turn up as the seeded value, but now it is sitting in a field, visible and
correctable, rather than deciding the backdrop from behind the scenes.

Cats:        [ ^ v ]                              1
Size:        [---|------]                     64 pt
Speed:       [----|-----]                     1.00×
[x] Picture: [ …reServices/DefaultDesktop.heic ] [ Choose… ]
Grey level:  [|---------]                        0%
             [x] Paw prints
             [x] Scratch marks

Unchecking the box, or emptying the field, leaves the grey.

Bookmarks dropped

Storing a path rather than a bookmark drops the security-scoped bookmark
machinery added in the previous change. A typed path is not a user-selected file
in the sandbox's sense, but legacyScreenSaver.appex can read anywhere under /,
so the read succeeds either way and there is nothing left for a bookmark to buy.

Layout

The desktop's own scaling, clipping and fill colour still apply when the path
happens to name the very picture the desktop is showing, which is the usual case
given where the value comes from. Anything else is laid out as Fill Screen.

Verified

  • Both schemes build and analyse; swiftlint --strict, swiftlint analyze --strict
    and swiftformat --lint are clean
  • Seeding from an empty preference domain writes the wallpaper path — here, the
    DefaultDesktop.heic placeholder, which is the case this change exists for
  • Offscreen renders for an explicit --picture, and for --grey
  • The Options sheet layout, with the path truncated at the head so the file name
    stays readable

The Choose… panel needs a GUI to exercise, so it is unverified, as is the whole
saver in System Settings on a real machine.

The Desktop picture backdrop shows the wrong picture when the wallpaper is a
dynamic or video one. NSWorkspace.desktopImageURL(for:) predates WallpaperKit and
only reports still pictures: with the com.apple.NeptuneOneExtension provider set,
the wallpaper store holds no file at all and the API answers with a placeholder,
/System/Library/CoreServices/DefaultDesktop.heic, and an empty options dictionary.
The saver was drawing that placeholder faithfully. No public API reports those
wallpapers, so there is nothing to fix on the reading side.

The Desktop picture checkbox therefore becomes a Backdrop menu — Solid grey,
Desktop picture, Custom picture… — and the last of those opens a file panel and
keeps what you chose. Choosing that item always opens the panel, so one menu entry
also serves to swap one picture for another; cancelling with nothing chosen puts
the menu back.

The picture is stored as a bookmark rather than a path, so renaming or moving it
does not break the backdrop. Security-scoped where the host can make one — the
preview app, being unsandboxed, cannot — and a plain bookmark otherwise; the read
succeeds either way, since legacyScreenSaver.appex can read anywhere under `/`.

The grey slider is now labelled Grey level, which is what it does, and no longer
sits one row under Backdrop meaning something else.
The three-way Backdrop menu becomes a checkbox and a path field. There is one
source for the backdrop now — a file — and the desktop's wallpaper is only where
its value starts.

The path is filled in once, on first run, from NSWorkspace.desktopImageURL(for:),
and is an ordinary saved setting from then on: changing the wallpaper later leaves
the saver alone. That removes the awkward middle case the menu had, where
"Desktop picture" silently followed an API that cannot see dynamic or video
wallpapers and answered with a placeholder. The placeholder can still turn up as
the seeded value, but now it is sitting in a field, visible and correctable,
rather than deciding the backdrop from behind the scenes.

Storing a path rather than a bookmark drops the security-scoped bookmark
machinery. A typed path is not a user-selected file in the sandbox's sense, but
legacyScreenSaver.appex can read anywhere under `/`, so the read succeeds either
way and there is nothing left for a bookmark to buy.

The desktop's layout options still apply when the path happens to name the very
picture the desktop is showing, which is the usual case; anything else is laid
out as Fill Screen.
@winebarrel
winebarrel enabled auto-merge August 10, 2026 03:00
@winebarrel
winebarrel merged commit 7716737 into main Aug 10, 2026
3 checks passed
@winebarrel
winebarrel deleted the custom-backdrop-picture branch August 10, 2026 03:01
winebarrel added a commit that referenced this pull request Aug 10, 2026
A picture chosen from Documents never appeared. The saver stored its path and
reopened it by path, and the kernel refused:

    Sandbox: legacyScreenSaver deny(1) file-read-data
      /Users/…/Documents/Wallpaper/…png

#4 dropped the security-scoped bookmark on the grounds that legacyScreenSaver.appex
holds a read-only exception for `/`, so a path could reach anything. It cannot.
Documents, Desktop and Downloads sit behind a consent macOS will never ask a
screen saver for, and the sandbox exception says nothing about them. The read is
refused outright, the image comes back nil, and the backdrop falls to black with
nothing said — which is how a setting that saved correctly still did nothing.

So the bookmark comes back. Choosing a file is what grants access to it; the
bookmark is how that grant survives into the run that draws it. A bare path still
serves everywhere else — a picture seeded from the wallpaper, or one typed into
the field — and stands in when there is no bookmark or it no longer resolves.
Typing a path clears the bookmark, which described a different file.

The file is read into Data inside the security scope rather than handed to
NSImage as a URL: NSImage may defer the read, and by then the scope is shut.

Left as it is: a wallpaper seeded into the field is not something the user picked,
so a wallpaper living in Documents still cannot be read.
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.

1 participant