-
-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathWallpaper.swift
More file actions
192 lines (162 loc) · 5.43 KB
/
Copy pathWallpaper.swift
File metadata and controls
192 lines (162 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import AppKit
import SQLite
// https://github.com/stephencelis/SQLite.swift/issues/1277
typealias Expression = SQLite.Expression
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
Note: This workaround is only needed on macOS versions prior to macOS 26. On macOS 26+, the database schema may have changed or may not exist, and NSWorkspace.shared.desktopImageURL appears to return proper file paths.
*/
private static func getFromDirectory(_ url: URL) throws -> URL {
// On macOS 26+, skip the database workaround as it may not be available
// and the underlying bug appears to be fixed
if #available(macOS 26, *) {
return 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 wallpaperURLs.map { url in
if url.isDirectory {
// Try to get specific image from directory, fall back to directory if it fails (e.g., in sandbox)
return (try? getFromDirectory(url)) ?? url
} else {
return url
}
}
}
/**
Validates that a file or directory exists and is accessible.
*/
private static func validateFile(_ url: URL) throws {
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) else {
throw NSError(
domain: "WallpaperError",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "The file doesn't exist."]
)
}
// For files, ensure they're actually accessible
if !isDirectory.boolValue {
guard (try? url.checkResourceIsReachable()) == true else {
throw NSError(
domain: "WallpaperError",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "The file exists but is not accessible."]
)
}
}
}
/**
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 {
// Validate that the file or directory exists and is accessible
try validateFile(image)
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)
}
}