macos-wallpaper/Sources/wallpaper/Wallpaper.swift

141 lines
4.0 KiB
Swift

import AppKit
import SQLite
public enum Wallpaper {
public enum Screen {
case all
case main
case index(Int)
case nsScreens([NSScreen])
fileprivate var nsScreens: [NSScreen] {
switch self {
case .all:
return NSScreen.screens
case .main:
guard let mainScreen = NSScreen.main else {
return []
}
return [mainScreen]
case .index(let index):
guard let screen = NSScreen.screens[safe: index] else {
return []
}
return [screen]
case .nsScreens(let nsScreens):
return nsScreens
}
}
}
public enum Scale: String, CaseIterable {
case auto
case fill
case fit
case stretch
case center
}
/**
Works around macOS bug where it sometimes returns a directory instead of an image.
https://openradar.appspot.com/radar?id=4959084113559552
*/
private static func getFromDirectory(_ url: URL) throws -> URL {
let appSupportDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let dbURL = appSupportDirectory.appendingPathComponent("Dock/desktoppicture.db", isDirectory: false)
let table = Table("data")
let column = Expression<String>("value")
let rowID = Expression<Int64>("rowid")
let db = try Connection(dbURL.path)
let maxID = try db.scalar(table.select(rowID.max))!
let query = table.select(column).filter(rowID == maxID)
let image = try db.pluck(query)!.get(column)
return url.appendingPathComponent(image, isDirectory: false)
}
/**
Get the current wallpapers.
*/
public static func get(screen: Screen = .all) throws -> [URL] {
let wallpaperURLs = screen.nsScreens.compactMap { NSWorkspace.shared.desktopImageURL(for: $0) }
return try wallpaperURLs.map { $0.isDirectory ? try getFromDirectory($0) : $0 }
}
/**
Works around a macOS bug where if you set a wallpaper to the same path as the existing wallpaper but with different content, it doesn't update.
https://openradar.appspot.com/radar?id=6095446787227648
*/
private static func forceRefreshIfNeeded(_ image: URL, screen: Screen) throws {
var shouldSleep = false
let currentImages = try get(screen: screen)
for (index, nsScreen) in screen.nsScreens.enumerated() {
if image == currentImages[index] {
shouldSleep = true
try NSWorkspace.shared.setDesktopImageURL(URL(fileURLWithPath: ""), for: nsScreen, options: [:])
}
}
if shouldSleep {
// We need to sleep for a little bit, otherwise it doesn't take effect.
// It works with 0.3, but not with 0.2, so we're using 0.4 just to be sure.
sleep(for: 0.4)
}
}
/**
Set an image URL as wallpaper.
*/
public static func set(_ image: URL, screen: Screen = .all, scale: Scale = .auto, fillColor: NSColor? = nil) throws {
var options = [NSWorkspace.DesktopImageOptionKey: Any]()
switch scale {
case .auto:
break
case .fill:
options[.imageScaling] = NSImageScaling.scaleProportionallyUpOrDown.rawValue
options[.allowClipping] = true
case .fit:
options[.imageScaling] = NSImageScaling.scaleProportionallyUpOrDown.rawValue
options[.allowClipping] = false
case .stretch:
options[.imageScaling] = NSImageScaling.scaleAxesIndependently.rawValue
options[.allowClipping] = true
case .center:
options[.imageScaling] = NSImageScaling.scaleNone.rawValue
options[.allowClipping] = false
}
options[.fillColor] = fillColor
try forceRefreshIfNeeded(image, screen: screen)
for nsScreen in screen.nsScreens {
try NSWorkspace.shared.setDesktopImageURL(image, for: nsScreen, options: options)
}
}
/**
Set a solid color as wallpaper.
*/
public static func set(_ solidColor: NSColor, screen: Screen = .all) throws {
let transparentImage = URL(fileURLWithPath: "/System/Library/PreferencePanes/DesktopScreenEffectsPref.prefPane/Contents/Resources/DesktopPictures.prefPane/Contents/Resources/Transparent.tiff")
try set(transparentImage, screen: screen, scale: .fit, fillColor: solidColor)
}
/**
Names of available screens.
*/
public static var screenNames: [String] {
NSScreen.screens.map(\.name)
}
}