Created
October 2, 2026 23:59
-
-
Save unrevised6419/8eb526fcecbc6821aa9d80d77dbc7970 to your computer and use it in GitHub Desktop.
Adds black rounded corners to a wallpaper so maximized macOS windows blend into the screen edge.
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/env swift | |
| // Adds black rounded corners to a wallpaper so maximized macOS windows blend into the screen edge. | |
| // | |
| // The default corner shape was measured from real maximized windows on macOS 27: | |
| // a continuous ("squircle") corner of ~16.75pt. | |
| // | |
| // Usage: round-wallpaper.swift <image> [-o out.png] [--radius 16.75] [--top 0] | |
| // [--size 2880x1800] [--color 000000] [--set] | |
| import AppKit | |
| import SwiftUI | |
| func fail(_ message: String) -> Never { | |
| FileHandle.standardError.write(Data("error: \(message)\n".utf8)) | |
| exit(1) | |
| } | |
| let usage = """ | |
| usage: round-wallpaper.swift <image> [options] | |
| -o, --output PATH output PNG (default: <image>-rounded.png) | |
| --radius PT corner radius in points (default: 16.75, macOS 27 window radius) | |
| --top PT top inset in points, e.g. menu bar height if windows sit below it (default: 0) | |
| --size WxH output size in pixels (default: main screen's native pixel size) | |
| --color HEX corner color (default: 000000) | |
| --set set the result as wallpaper on all screens | |
| """ | |
| var input: String? | |
| var output: String? | |
| var radius: CGFloat = 16.75 | |
| var topInset: CGFloat = 0 | |
| var size: CGSize? | |
| var color = NSColor.black | |
| var setWallpaper = false | |
| var args = CommandLine.arguments.dropFirst().makeIterator() | |
| func value(_ flag: String) -> String { | |
| guard let v = args.next() else { fail("\(flag) needs a value") } | |
| return v | |
| } | |
| func number(_ flag: String) -> CGFloat { | |
| guard let n = Double(value(flag)) else { fail("\(flag) needs a number") } | |
| return CGFloat(n) | |
| } | |
| while let arg = args.next() { | |
| switch arg { | |
| case "-o", "--output": output = value(arg) | |
| case "--radius": radius = number(arg) | |
| case "--top": topInset = number(arg) | |
| case "--size": | |
| let parts = value(arg).lowercased().split(separator: "x").compactMap { Double($0) } | |
| guard parts.count == 2 else { fail("--size must look like 2880x1800") } | |
| size = CGSize(width: parts[0], height: parts[1]) | |
| case "--color": | |
| guard let hex = UInt32(value(arg).trimmingCharacters(in: ["#"]), radix: 16) else { fail("bad --color") } | |
| color = NSColor( | |
| srgbRed: CGFloat((hex >> 16) & 0xFF) / 255, | |
| green: CGFloat((hex >> 8) & 0xFF) / 255, | |
| blue: CGFloat(hex & 0xFF) / 255, | |
| alpha: 1) | |
| case "--set": setWallpaper = true | |
| case "-h", "--help": print(usage); exit(0) | |
| default: | |
| if arg.hasPrefix("-") || input != nil { fail("unexpected argument \(arg)\n\(usage)") } | |
| input = arg | |
| } | |
| } | |
| guard let input else { print(usage); exit(1) } | |
| let inputURL = URL(fileURLWithPath: (input as NSString).expandingTildeInPath) | |
| guard let source = NSImage(contentsOf: inputURL), | |
| let cgSource = source.cgImage(forProposedRect: nil, context: nil, hints: nil) | |
| else { fail("cannot read image \(input)") } | |
| let screen = NSScreen.main | |
| let scale = screen?.backingScaleFactor ?? 2 | |
| let pixelSize = size ?? screen.map { | |
| CGSize(width: $0.frame.width * scale, height: $0.frame.height * scale) | |
| } ?? CGSize(width: cgSource.width, height: cgSource.height) | |
| let width = Int(pixelSize.width), height = Int(pixelSize.height) | |
| guard let ctx = CGContext( | |
| data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, | |
| space: CGColorSpace(name: CGColorSpace.sRGB)!, | |
| bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) | |
| else { fail("cannot create bitmap context") } | |
| ctx.interpolationQuality = .high | |
| // Fill the screen like macOS does: scale to cover, center, crop. | |
| let fill = max(pixelSize.width / CGFloat(cgSource.width), pixelSize.height / CGFloat(cgSource.height)) | |
| let drawn = CGSize(width: CGFloat(cgSource.width) * fill, height: CGFloat(cgSource.height) * fill) | |
| ctx.draw(cgSource, in: CGRect( | |
| x: (pixelSize.width - drawn.width) / 2, y: (pixelSize.height - drawn.height) / 2, | |
| width: drawn.width, height: drawn.height)) | |
| // Paint everything outside the window-shaped rounded rect (even-odd: full canvas minus the window). | |
| let canvas = CGRect(origin: .zero, size: pixelSize) | |
| let window = CGRect(x: 0, y: 0, width: pixelSize.width, height: pixelSize.height - topInset * scale) | |
| let corners = CGMutablePath() | |
| corners.addRect(canvas) | |
| corners.addPath(RoundedRectangle(cornerRadius: radius * scale, style: .continuous).path(in: window).cgPath) | |
| ctx.addPath(corners) | |
| ctx.setFillColor(color.cgColor) | |
| ctx.fillPath(using: .evenOdd) | |
| guard let result = ctx.makeImage(), | |
| let png = NSBitmapImageRep(cgImage: result).representation(using: .png, properties: [:]) | |
| else { fail("cannot encode PNG") } | |
| let outputURL = output.map { URL(fileURLWithPath: ($0 as NSString).expandingTildeInPath) } | |
| ?? inputURL.deletingPathExtension().appendingPathExtension("rounded.png") | |
| do { try png.write(to: outputURL) } catch { fail("cannot write \(outputURL.path): \(error.localizedDescription)") } | |
| print("wrote \(outputURL.path) (\(width)x\(height), radius \(radius)pt @\(scale)x)") | |
| if setWallpaper { | |
| for screen in NSScreen.screens { | |
| do { | |
| try NSWorkspace.shared.setDesktopImageURL( | |
| outputURL, for: screen, | |
| options: [.imageScaling: NSImageScaling.scaleProportionallyUpOrDown.rawValue, .allowClipping: true]) | |
| } catch { fail("cannot set wallpaper: \(error.localizedDescription)") } | |
| } | |
| print("wallpaper set") | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment