Last active
August 19, 2025 19:09
-
-
Save nwjsmith/7db0be1cbd662c3b13103de436b23368 to your computer and use it in GitHub Desktop.
macOS Dynamic Wallpaper Generator - Create appearance-aware wallpapers from hex colors
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
| { | |
| lib, | |
| swift, | |
| swiftPackages, | |
| }: | |
| swiftPackages.stdenv.mkDerivation { | |
| pname = "wallit"; | |
| version = "1.0.0"; | |
| src = ./Wallit.swift; | |
| dontUnpack = true; | |
| nativeBuildInputs = [ swift ]; | |
| buildPhase = '' | |
| runHook preBuild | |
| cp $src Wallit.swift | |
| swiftc Wallit.swift -o wallit -O | |
| runHook postBuild | |
| ''; | |
| installPhase = '' | |
| runHook preInstall | |
| mkdir -p $out/bin | |
| cp wallit $out/bin/ | |
| runHook postInstall | |
| ''; | |
| meta = with lib; { | |
| description = "macOS dynamic wallpaper generator"; | |
| platforms = platforms.darwin; | |
| maintainers = []; | |
| mainProgram = "wallit"; | |
| }; | |
| } |
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
| import Foundation | |
| import CoreGraphics | |
| import ImageIO | |
| func parseHexColor(_ hex: String) -> (r: CGFloat, g: CGFloat, b: CGFloat)? { | |
| let clean = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) | |
| guard clean.count == 6, let value = Int(clean, radix: 16) else { return nil } | |
| return ( | |
| r: CGFloat((value >> 16) & 0xFF) / 255.0, | |
| g: CGFloat((value >> 8) & 0xFF) / 255.0, | |
| b: CGFloat(value & 0xFF) / 255.0 | |
| ) | |
| } | |
| func createSolidColorImage(_ hex: String) -> CGImage? { | |
| guard let color = parseHexColor(hex) else { return nil } | |
| let width = 5120, height = 2880 | |
| let colorSpace = CGColorSpaceCreateDeviceRGB() | |
| guard let context = CGContext( | |
| data: nil, | |
| width: width, | |
| height: height, | |
| bitsPerComponent: 8, | |
| bytesPerRow: width * 4, | |
| space: colorSpace, | |
| bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue | |
| ) else { return nil } | |
| context.setFillColor(red: color.r, green: color.g, blue: color.b, alpha: 1.0) | |
| context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))) | |
| return context.makeImage() | |
| } | |
| func loadImageFromFile(_ path: String) -> CGImage? { | |
| let url = URL(fileURLWithPath: path) | |
| guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } | |
| return CGImageSourceCreateImageAtIndex(source, 0, nil) | |
| } | |
| func getImage(from input: String) -> CGImage? { | |
| // Try as hex color first | |
| if let image = createSolidColorImage(input) { | |
| return image | |
| } | |
| // Otherwise load as file | |
| return loadImageFromFile(input) | |
| } | |
| func createDynamicWallpaper(light: String, dark: String, output: String) throws { | |
| guard let lightImage = getImage(from: light) else { | |
| throw NSError(domain: "", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot load light image/color: \(light)"]) | |
| } | |
| guard let darkImage = getImage(from: dark) else { | |
| throw NSError(domain: "", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot load dark image/color: \(dark)"]) | |
| } | |
| // Create appearance metadata | |
| let metadata = CGImageMetadataCreateMutable() | |
| CGImageMetadataRegisterNamespaceForPrefix( | |
| metadata, | |
| "http://ns.apple.com/namespace/1.0/" as CFString, | |
| "apple_desktop" as CFString, | |
| nil | |
| ) | |
| let appearanceInfo = ["d": 1, "l": 0] // dark: image 1, light: image 0 | |
| let plistData = try PropertyListSerialization.data( | |
| fromPropertyList: appearanceInfo, | |
| 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) | |
| // Write HEIC file | |
| let outputURL = URL(fileURLWithPath: output) | |
| guard let destination = CGImageDestinationCreateWithURL( | |
| outputURL as CFURL, | |
| "public.heic" as CFString, | |
| 2, | |
| nil | |
| ) else { | |
| throw NSError(domain: "", code: 3, userInfo: [NSLocalizedDescriptionKey: "Cannot create output file"]) | |
| } | |
| CGImageDestinationAddImageAndMetadata(destination, lightImage, metadata, nil) | |
| CGImageDestinationAddImage(destination, darkImage, nil) | |
| guard CGImageDestinationFinalize(destination) else { | |
| throw NSError(domain: "", code: 4, userInfo: [NSLocalizedDescriptionKey: "Cannot write output file"]) | |
| } | |
| } | |
| // Main | |
| let args = CommandLine.arguments | |
| if args.contains("-h") || args.contains("--help") { | |
| print(""" | |
| Usage: \(args[0]) [light] [dark] [output] | |
| Creates a macOS dynamic wallpaper that switches with appearance. | |
| Arguments: | |
| light Hex color or image path for light mode (default: f0f0f0) | |
| dark Hex color or image path for dark mode (default: 1e1e1e) | |
| output Output filename (default: wallpaper.heic) | |
| Examples: | |
| \(args[0]) # Modus theme colors | |
| \(args[0]) ffffff 000000 # Black and white | |
| \(args[0]) day.jpg night.jpg # From images | |
| \(args[0]) f0f0f0 sunset.png custom.heic | |
| """) | |
| exit(0) | |
| } | |
| let light = args.count > 1 ? args[1] : "f0f0f0" | |
| let dark = args.count > 2 ? args[2] : "1e1e1e" | |
| let output = args.count > 3 ? args[3] : "wallpaper.heic" | |
| do { | |
| try createDynamicWallpaper(light: light, dark: dark, output: output) | |
| } 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