Skip to content

Reach the picture the file panel granted, and stop guessing a default - #12

Merged
winebarrel merged 3 commits into
mainfrom
picture-security-scope
Aug 10, 2026
Merged

winebarrel merged 3 commits into
mainfrom
picture-security-scope

Conversation

@winebarrel

Copy link
Copy Markdown
Owner

A picture chosen from Documents never appeared. The setting saved correctly, the
path in the field was right, and the screen stayed black.

Why

The saver stored the path and reopened it by path. 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 a sandbox exception says nothing
about them. The read is refused, the image comes back nil, and the backdrop falls
to black with nothing said.

That reasoning was mine and it was wrong. The two entitlements I dismissed as
buying nothing —

com.apple.security.files.user-selected.read-only
com.apple.security.files.bookmarks.app-scope

— are exactly the mechanism for this.

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 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.

And the default goes

The picture was seeded from the desktop's wallpaper on first run. That is a lovely
default right up until the wallpaper lives in Documents: seeding cannot grant
access, since nobody picked the file, so the backdrop falls to black while the
field shows a path that looks perfectly fine. A default that works for some people
and silently does nothing for others is worse than no default.

So Picture starts unticked with an empty field, and the screen is black until
someone asks for something else. Ticking it without naming a picture is not a
state worth saving, so OK is disabled until the field has something in it.

Seeding goes, and with it the reason to ask NSWorkspace where the wallpaper is.
Reading the layout of a picture that happens to be the wallpaper stays: pick that
file yourself and it still sits the way the desktop sits it.

Verified

The picture from Documents now draws — confirmed in System Settings on the machine
that reported it.

The Options sheet was rendered in both states:

unticked, empty path   Choose… disabled, OK enabled
ticked,   empty path   Choose… enabled,  OK disabled

Both schemes build and analyse; swiftlint --strict, swiftlint analyze --strict
and swiftformat --lint are clean.

Still true

A picture that has moved since it was chosen, a video, or a path typed by hand
into one of the three closed folders, all end up as a black screen with no
explanation. Saying so in the sheet is worth doing and is not in here.

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.
The picture was seeded from the desktop's wallpaper on first run, which is a
lovely default right up until the wallpaper lives in Documents — a folder a
screen saver cannot read, and cannot be granted by seeding, since nobody picked
the file. The backdrop then falls to black with the field showing a path that
looks perfectly fine. A default that works for some people and silently does
nothing for others is worse than no default.

So there is none. Picture starts unticked with an empty field, and the screen is
black until someone asks for something else. Ticking it without naming a picture
is not a state worth saving, so OK is disabled until the field has something in
it.

Seeding goes, and with it the reason to ask NSWorkspace where the wallpaper is.
Reading the layout of a picture that happens to be the wallpaper stays: pick that
file yourself and it still sits the way the desktop sits it.
Typing in the path field wrote the new value straight into the settings and left
the buttons alone, so clearing the field by hand left OK enabled — the one state
the button is there to refuse.

The check moves out of `refresh` into its own method so the field can call it
while being typed into. Calling `refresh` would do it too, and would also rewrite
the field's contents underneath the cursor.
@winebarrel
winebarrel enabled auto-merge August 10, 2026 07:07
@winebarrel
winebarrel merged commit 4172e60 into main Aug 10, 2026
3 checks passed
@winebarrel
winebarrel deleted the picture-security-scope branch August 10, 2026 07:08
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