Vorschau liest macOS' eigene Hintergrundverwaltung

NSWorkspace.desktopImageURL ist die alte Schnittstelle: sie meldet das letzte
Bild, das als Datei gesetzt wurde — auch dann noch, wenn es längst gelöscht
ist. Genau das war hier der Fall, der Pfad zeigt auf eine Datei, die es nicht
mehr gibt. Seit macOS 14 führt das System die Auswahl in
com.apple.wallpaper/Store/Index.plist, je Bildschirm und je Schreibtisch, und
Anbieter wie Fotos hinterlegen dort ihre Zwischenspeicher.

Die Datei ist doppelt verpackt: die Konfiguration je Bildschirm steckt als
eigene Property-Liste in einem Datenfeld der äußeren. Ohne den zweiten
Durchgang findet man gar nichts.

Gelesen wird nur der Eintrag dieses Bildschirms, nachgeschlagen über die
Display-Kennung. In der Datei stehen auch alte Bildschirme, der
Bildschirmschoner und alles, was einmal gesetzt war — das Neueste von allem zu
nehmen führt zuverlässig zu einem Bild, das mit dem Schreibtisch nichts zu tun
hat. Ausprobiert: dabei kam ein weißes Platzhalterbild aus einem Kurzbefehl
heraus. Fehlt der Eintrag oder die Datei, ist ein Systembild die ehrlichere
Antwort als ein Rateergebnis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-14 00:08:44 +02:00
parent 152d36c904
commit c6f0a5a848
2 changed files with 163 additions and 3 deletions

View File

@@ -0,0 +1,77 @@
import Testing
import Foundation
@testable import OnyxNotch
@Suite("Hintergrundbild aus macOS' Verwaltung")
struct WallpaperIndexTests {
/// Baut einen Eintrag, wie ihn macOS ablegt.
private func entry(_ path: String) -> [String: Any] {
["type": "imageFile", "url": ["relative": "file://" + path]]
}
private func nested(_ value: Any) throws -> Data {
try PropertyListSerialization.data(fromPropertyList: value, format: .binary, options: 0)
}
@Test("Ein Bild in der äußeren Liste wird gefunden")
func findsPlainEntry() {
var found: [URL] = []
WallpaperLoader.collectImageFiles(["Desktop": entry("/tmp/a.jpg")], into: &found)
#expect(found.map(\.path) == ["/tmp/a.jpg"])
}
@Test("Ein Bild in einer verschachtelten Liste wird auch gefunden")
func findsNestedEntry() throws {
// Genau daran hängt es: die Konfiguration je Bildschirm steckt als
// eigene Property-Liste in einem Datenfeld der äußeren.
var found: [URL] = []
let inner = try nested(entry("/tmp/b.jpg"))
WallpaperLoader.collectImageFiles(
["Displays": ["ABC": ["Desktop": ["Configuration": inner]]]], into: &found)
#expect(found.map(\.path) == ["/tmp/b.jpg"])
}
@Test("Mehrere Bildschirme ergeben mehrere Bilder")
func findsAllScreens() throws {
var found: [URL] = []
WallpaperLoader.collectImageFiles(
["Displays": ["A": try nested(entry("/tmp/a.jpg")),
"B": try nested(entry("/tmp/b.jpg"))]], into: &found)
#expect(Set(found.map(\.path)) == ["/tmp/a.jpg", "/tmp/b.jpg"])
}
@Test("Einträge ohne Datei bleiben liegen")
func ignoresNonFileEntries() {
// Fotos und Aerials stehen als asset" mit einer Kennung darin daraus
// lässt sich keine Datei machen, und ein Eintrag ohne Datei darf nicht
// als Bild durchgehen.
var found: [URL] = []
WallpaperLoader.collectImageFiles(
["Idle": ["type": "asset", "identifier": "3316307E-788F"],
"Web": ["type": "imageFile", "url": ["relative": "https://example.com/a.jpg"]]],
into: &found)
#expect(found.isEmpty)
}
@Test("Nur der eigene Bildschirm zählt")
func onlyOwnScreenCounts() throws {
// In der Datei stehen auch alte Bildschirme und der Bildschirmschoner.
// Das Neueste von allem zu nehmen führt zuverlässig zu einem Bild, das
// mit dem Schreibtisch nichts zu tun hat gemessen war das hier ein
// weißes Platzhalterbild aus einem Kurzbefehl.
var found: [URL] = []
let entry: [String: Any] = ["Desktop": ["Content": ["Choices": [
["Configuration": try nested(entry("/tmp/richtig.jpg"))]]]]]
WallpaperLoader.collectImageFiles(entry["Desktop"] as Any, into: &found)
#expect(found.map(\.path) == ["/tmp/richtig.jpg"])
}
@Test("Unbrauchbare Daten stürzen nicht ab")
func survivesGarbage() {
var found: [URL] = []
WallpaperLoader.collectImageFiles(
["A": Data("bplist00 aber kaputt".utf8), "B": 42, "C": Date()], into: &found)
#expect(found.isEmpty)
}
}