Created
August 19, 2025 16:21
-
-
Save nwjsmith/f2ab354250eeed5f869f21317ee42fac to your computer and use it in GitHub Desktop.
macOS Dynamic Wallpaper Generator - Creates HEIC wallpapers that switch between light/dark modes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/swift | |
| import Foundation | |
| import AppKit | |
| import AVFoundation | |
| import ImageIO | |
| struct Color { | |
| let r: CGFloat | |
| let g: CGFloat | |
| let b: CGFloat | |
| init?(hex: String) { | |
| let clean = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) | |
| guard clean.count == 6, let value = Int(clean, radix: 16) else { return nil } | |
| self.r = CGFloat((value >> 16) & 0xFF) / 255.0 | |
| self.g = CGFloat((value >> 8) & 0xFF) / 255.0 | |
| self.b = CGFloat(value & 0xFF) / 255.0 | |
| } | |
| } | |
| func createImage(color: Color, size: CGSize = CGSize(width: 5120, height: 2880)) -> CGImage? { | |
| let colorSpace = CGColorSpaceCreateDeviceRGB() | |
| let context = CGContext( | |
| data: nil, | |
| width: Int(size.width), | |
| height: Int(size.height), | |
| bitsPerComponent: 8, | |
| bytesPerRow: Int(size.width) * 4, | |
| space: colorSpace, | |
| bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue | |
| ) | |
| context?.setFillColor(red: color.r, green: color.g, blue: color.b, alpha: 1.0) | |
| context?.fill(CGRect(origin: .zero, size: size)) | |
| return context?.makeImage() | |
| } | |
| func createWallpaper(lightHex: String, darkHex: String, outputFile: String) throws { | |
| guard let lightColor = Color(hex: lightHex) else { | |
| throw NSError(domain: "", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid light color: \(lightHex)"]) | |
| } | |
| guard let darkColor = Color(hex: darkHex) else { | |
| throw NSError(domain: "", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid dark color: \(darkHex)"]) | |
| } | |
| guard let lightImage = createImage(color: lightColor) else { | |
| throw NSError(domain: "", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create light image"]) | |
| } | |
| guard let darkImage = createImage(color: darkColor) else { | |
| throw NSError(domain: "", code: 4, userInfo: [NSLocalizedDescriptionKey: "Failed to create dark image"]) | |
| } | |
| // Create metadata for appearance switching | |
| let metadata = CGImageMetadataCreateMutable() | |
| CGImageMetadataRegisterNamespaceForPrefix( | |
| metadata, | |
| "http://ns.apple.com/namespace/1.0/" as CFString, | |
| "apple_desktop" as CFString, | |
| nil | |
| ) | |
| let appearanceData = try JSONEncoder().encode([ | |
| "ap": ["l": 0, "d": 1] // light index: 0, dark index: 1 | |
| ]) | |
| let plistData = try PropertyListSerialization.data( | |
| fromPropertyList: try JSONSerialization.jsonObject(with: appearanceData), | |
| format: .binary, | |
| options: 0 | |
| ) | |
| let tag = CGImageMetadataTagCreate( | |
| "http://ns.apple.com/namespace/1.0/" as CFString, | |
| "apple_desktop" as CFString, | |
| "apr" as CFString, | |
| .string, | |
| plistData.base64EncodedString() as CFString | |
| ) | |
| CGImageMetadataSetTagWithPath(metadata, nil, "apple_desktop:apr" as CFString, tag!) | |
| // Create HEIC file | |
| let outputData = NSMutableData() | |
| let destination = CGImageDestinationCreateWithData( | |
| outputData, | |
| AVFileType.heic as CFString, | |
| 2, | |
| nil | |
| )! | |
| CGImageDestinationAddImageAndMetadata( | |
| destination, | |
| lightImage, | |
| metadata, | |
| [kCGImageDestinationLossyCompressionQuality: 1.0] as CFDictionary | |
| ) | |
| CGImageDestinationAddImage( | |
| destination, | |
| darkImage, | |
| [kCGImageDestinationLossyCompressionQuality: 1.0] as CFDictionary | |
| ) | |
| guard CGImageDestinationFinalize(destination) else { | |
| throw NSError(domain: "", code: 5, userInfo: [NSLocalizedDescriptionKey: "Failed to create HEIC"]) | |
| } | |
| try outputData.write(to: URL(fileURLWithPath: outputFile)) | |
| } | |
| func printUsage() { | |
| print(""" | |
| Usage: \(CommandLine.arguments[0]) [light_color] [dark_color] [output_file] | |
| Creates a macOS dynamic wallpaper that switches between light and dark colors. | |
| Arguments: | |
| light_color Hex color for light mode (default: f0f0f0) | |
| dark_color Hex color for dark mode (default: 1e1e1e) | |
| output_file Output filename (default: wallpaper.heic) | |
| Examples: | |
| \(CommandLine.arguments[0]) # Modus colors | |
| \(CommandLine.arguments[0]) ffffff 000000 # Black and white | |
| \(CommandLine.arguments[0]) fdf6e3 002b36 solarized.heic # Solarized theme | |
| Colors can be specified with or without '#' prefix. | |
| """) | |
| } | |
| // Main | |
| let args = CommandLine.arguments | |
| if args.count > 1 && (args[1] == "-h" || args[1] == "--help") { | |
| printUsage() | |
| exit(0) | |
| } | |
| let lightHex = args.count > 1 ? args[1] : "f0f0f0" // Modus Operandi bg-dim | |
| let darkHex = args.count > 2 ? args[2] : "1e1e1e" // Modus Vivendi bg-dim | |
| let outputFile = args.count > 3 ? args[3] : "wallpaper.heic" | |
| do { | |
| try createWallpaper(lightHex: lightHex, darkHex: darkHex, outputFile: outputFile) | |
| } catch { | |
| print("Error: \(error.localizedDescription)") | |
| exit(1) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment