import AppKit @MainActor protocol WallpaperManaging { func download(from url: URL) async throws -> URL func setDesktopImage(from fileURL: URL, screen: NSScreen?) throws func saveToFolder(from sourceURL: URL, to folderURL: URL) throws } @MainActor protocol DesktopImageSetting { func setDesktopImageURL( _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any] ) throws } extension NSWorkspace: DesktopImageSetting {} class WallpaperManager: WallpaperManaging { static let shared = WallpaperManager() private let session: URLSession private let desktopSetter: @MainActor (URL, NSScreen) throws -> Void private let mainScreen: () -> NSScreen? private let firstScreen: () -> NSScreen? init( session: URLSession = .shared, desktopSetter: (@MainActor (URL, NSScreen) throws -> Void)? = nil, workspace: DesktopImageSetting = NSWorkspace.shared, mainScreen: @escaping () -> NSScreen? = { NSScreen.main }, firstScreen: @escaping () -> NSScreen? = { NSScreen.screens.first } ) { self.session = session self.desktopSetter = desktopSetter ?? { url, screen in try workspace.setDesktopImageURL( url, for: screen, options: [:] ) } self.mainScreen = mainScreen self.firstScreen = firstScreen } /// Downloads the file to a temporary location and returns the local URL. /// /// Each call writes to a fresh, unique temp filename so that distinct /// wallpapers sharing a `lastPathComponent` cannot overwrite one another /// before they are consumed (e.g. before `setDesktopImage` or /// `saveToFolder` reads the temp file). The caller owns the returned file /// and is responsible for removing it when no longer needed. func download(from url: URL) async throws -> URL { let (data, response) = try await session.data(from: url) guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { throw WallhavenError.apiError("Wallpaper download failed") } let fileURL = Self.makeTempURL(for: url) try data.write(to: fileURL) return fileURL } /// Returns a temp URL unique to this call. The original `url.pathExtension` /// is preserved when present so downstream consumers can identify the kind /// of file, but the stem is a fresh UUID — never derived from the input /// filename — to guarantee no two concurrent or sequential downloads of /// distinct wallpapers collide in the shared temp directory. static func makeTempURL(for url: URL) -> URL { let fileExtension = url.pathExtension.isEmpty ? "img" : url.pathExtension let stem = UUID().uuidString let filename = "haven-\(stem).\(fileExtension)" return FileManager.default.temporaryDirectory.appendingPathComponent(filename) } /// Sets the desktop image from a local file URL. @MainActor func setDesktopImage(from fileURL: URL, screen: NSScreen? = nil) throws { if let screen = screen { try desktopSetter(fileURL, screen) return } // Pick the screen we should target when the caller didn't pass one. // The previous fallback force-unwrapped `NSScreen.screens.first!`, which // would trap on a headless launch (no screens at all) or a transient // screen-disconnect window. Throw a real error instead so callers can // react. let target: NSScreen if let main = mainScreen() { target = main } else if let first = firstScreen() { target = first } else { throw WallhavenError.apiError("No screen is available to set the desktop image") } try desktopSetter(fileURL, target) } /// Copies the file to the specified destination directory. /// /// Policy for an existing destination with the same name: /// - if its contents match the source, leave it untouched (idempotent /// re-save of the same logical wallpaper, preserves modification time); /// - if its contents differ, replace it with the source bytes. /// Either branch avoids the prior behavior of unconditionally deleting the /// destination before copying, which would destroy the source if the caller /// passed the same path for both arguments. func saveToFolder(from sourceURL: URL, to folderURL: URL) throws { // Ensure folder exists try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true) let destinationURL = folderURL.appendingPathComponent(sourceURL.lastPathComponent) if FileManager.default.fileExists(atPath: destinationURL.path) { let sourceData = try Data(contentsOf: sourceURL) let existingData = try Data(contentsOf: destinationURL) if sourceData == existingData { return } try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.copyItem(at: sourceURL, to: destinationURL) } // Legacy support wrapper (optional, but cleaner to update call sites) @MainActor func setWallpaper(from url: URL, screen: NSScreen? = nil) async throws { let localURL = try await download(from: url) try setDesktopImage(from: localURL, screen: screen) } }