listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

screenshots-mac-desktop.swift (6526B)


      1 #!/usr/bin/env swift
      2 
      3 import AppKit
      4 import CoreGraphics
      5 import CoreText
      6 import Foundation
      7 import ImageIO
      8 
      9 guard CommandLine.arguments.count >= 3 else {
     10     fputs("Usage: screenshots-mac-desktop.swift <window.png> <output.png> [wallpaper]\n", stderr)
     11     exit(1)
     12 }
     13 
     14 let windowPath = CommandLine.arguments[1]
     15 let outputPath = CommandLine.arguments[2]
     16 let wallpaperPath =
     17     CommandLine.arguments.count >= 4
     18     ? CommandLine.arguments[3]
     19     : "Marketing/sequoia-wallpaper.jpg"
     20 
     21 // MacBook Air 13" 4th Gen native resolution
     22 let canvasWidth: CGFloat = 2560
     23 let canvasHeight: CGFloat = 1664
     24 
     25 // Load wallpaper (supports HEIC, JPEG, PNG via ImageIO)
     26 guard let wpData = try? Data(contentsOf: URL(fileURLWithPath: wallpaperPath)),
     27     let wpSource = CGImageSourceCreateWithData(wpData as CFData, nil),
     28     let wallpaper = CGImageSourceCreateImageAtIndex(wpSource, 0, nil)
     29 else {
     30     fputs("Failed to load wallpaper: \(wallpaperPath)\n", stderr)
     31     exit(1)
     32 }
     33 
     34 // Load window screenshot
     35 guard let winProvider = CGDataProvider(filename: windowPath),
     36     let windowImage = CGImage(
     37         pngDataProviderSource: winProvider, decode: nil, shouldInterpolate: true,
     38         intent: .defaultIntent)
     39 else {
     40     fputs("Failed to load window image: \(windowPath)\n", stderr)
     41     exit(1)
     42 }
     43 
     44 // Create canvas
     45 guard
     46     let context = CGContext(
     47         data: nil,
     48         width: Int(canvasWidth),
     49         height: Int(canvasHeight),
     50         bitsPerComponent: 8,
     51         bytesPerRow: 0,
     52         space: CGColorSpaceCreateDeviceRGB(),
     53         bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
     54     )
     55 else {
     56     fputs("Failed to create graphics context\n", stderr)
     57     exit(1)
     58 }
     59 
     60 // Draw wallpaper: scale to fit width, align to top, crop from bottom
     61 let wpWidth = CGFloat(wallpaper.width)
     62 let wpHeight = CGFloat(wallpaper.height)
     63 let wpScale = canvasWidth / wpWidth
     64 let scaledWpHeight = wpHeight * wpScale
     65 // CG origin is bottom-left; align top edge of wallpaper to top of canvas
     66 let wpY = canvasHeight - scaledWpHeight
     67 context.draw(
     68     wallpaper, in: CGRect(x: 0, y: wpY, width: canvasWidth, height: scaledWpHeight))
     69 
     70 // Draw menu bar: translucent strip at the top (@2x: 60px ≈ 30pt)
     71 let menuBarHeight: CGFloat = 60
     72 let menuBarY = canvasHeight - menuBarHeight
     73 context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 0.25))
     74 context.fill(CGRect(x: 0, y: menuBarY, width: canvasWidth, height: menuBarHeight))
     75 
     76 // Helper to draw a menu bar SF Symbol via CTFont
     77 func drawMenuSymbol(_ name: String, at x: CGFloat, size: CGFloat) -> CGFloat {
     78     guard let image = NSImage(systemSymbolName: name, accessibilityDescription: nil) else {
     79         return x
     80     }
     81     let config = NSImage.SymbolConfiguration(pointSize: size, weight: .regular)
     82     let configured = image.withSymbolConfiguration(config) ?? image
     83     // Tint the symbol
     84     let tinted = NSImage(size: configured.size, flipped: false) { rect in
     85         menuTextColor.set()
     86         rect.fill(using: .sourceOver)
     87         configured.draw(in: rect, from: .zero, operation: .destinationIn, fraction: 1.0)
     88         return true
     89     }
     90     let imgWidth = tinted.size.width
     91     let imgHeight = tinted.size.height
     92     let imgY = menuBarY + (menuBarHeight - imgHeight) / 2
     93     guard let cgImage = tinted.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
     94         return x
     95     }
     96     context.draw(cgImage, in: CGRect(x: x, y: imgY, width: imgWidth, height: imgHeight))
     97     return x + imgWidth
     98 }
     99 
    100 // Helper to draw menu bar text
    101 let menuTextColor = NSColor(white: 0.0, alpha: 0.85)
    102 func drawMenuText(_ text: String, at x: CGFloat, size: CGFloat, weight: NSFont.Weight = .regular)
    103     -> CGFloat
    104 {
    105     let font = NSFont.systemFont(ofSize: size, weight: weight)
    106     let attrs: [NSAttributedString.Key: Any] = [
    107         .font: font,
    108         .foregroundColor: menuTextColor,
    109     ]
    110     let attrStr = NSAttributedString(string: text, attributes: attrs)
    111     let line = CTLineCreateWithAttributedString(attrStr)
    112     let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds)
    113     let y = menuBarY + (menuBarHeight - bounds.height) / 2 - bounds.origin.y
    114     context.textPosition = CGPoint(x: x, y: y)
    115     CTLineDraw(line, context)
    116     return x + bounds.width
    117 }
    118 
    119 // Left side: Apple logo, app name, menus
    120 let appleFont = NSFont(name: "SF Pro Display", size: 42) ?? NSFont.systemFont(ofSize: 42)
    121 let appleAttrs: [NSAttributedString.Key: Any] = [
    122     .font: appleFont,
    123     .foregroundColor: menuTextColor,
    124 ]
    125 let appleString = NSAttributedString(string: "\u{F8FF}", attributes: appleAttrs)
    126 let appleLine = CTLineCreateWithAttributedString(appleString)
    127 let appleBounds = CTLineGetBoundsWithOptions(appleLine, .useOpticalBounds)
    128 let appleX: CGFloat = 32
    129 let appleTextY = menuBarY + (menuBarHeight - appleBounds.height) / 2 - appleBounds.origin.y
    130 context.textPosition = CGPoint(x: appleX, y: appleTextY)
    131 CTLineDraw(appleLine, context)
    132 
    133 var curX = appleX + appleBounds.width + 40
    134 curX = drawMenuText("Listless", at: curX, size: 28, weight: .bold) + 36
    135 curX = drawMenuText("File", at: curX, size: 28) + 36
    136 curX = drawMenuText("Edit", at: curX, size: 28) + 36
    137 curX = drawMenuText("View", at: curX, size: 28) + 36
    138 curX = drawMenuText("Window", at: curX, size: 28) + 36
    139 _ = drawMenuText("Help", at: curX, size: 28)
    140 
    141 // Right side: icons, date and time
    142 var rightX = canvasWidth - 260
    143 _ = drawMenuText("Wed 9 Apr  9:41 AM", at: rightX, size: 28)
    144 rightX -= 20
    145 _ = drawMenuSymbol("switch.2", at: rightX - 44, size: 32)
    146 _ = drawMenuSymbol("magnifyingglass", at: rightX - 100, size: 28)
    147 
    148 // Scale and draw window screenshot offset slightly right of centre
    149 let winWidth = CGFloat(windowImage.width)
    150 let winHeight = CGFloat(windowImage.height)
    151 let winScale = (canvasWidth * 0.4) / winWidth
    152 let scaledWinWidth = winWidth * winScale
    153 let scaledWinHeight = winHeight * winScale
    154 let winX = (canvasWidth - scaledWinWidth) / 2 + 400
    155 let availableHeight = canvasHeight - menuBarHeight
    156 let winY = (availableHeight - scaledWinHeight) / 2 + 150
    157 context.draw(
    158     windowImage,
    159     in: CGRect(x: winX, y: winY, width: scaledWinWidth, height: scaledWinHeight))
    160 
    161 // Save
    162 guard let resultImage = context.makeImage(),
    163     let destination = CGImageDestinationCreateWithURL(
    164         URL(fileURLWithPath: outputPath) as CFURL,
    165         "public.png" as CFString,
    166         1,
    167         nil
    168     )
    169 else {
    170     fputs("Failed to create output\n", stderr)
    171     exit(1)
    172 }
    173 
    174 CGImageDestinationAddImage(destination, resultImage, nil)
    175 guard CGImageDestinationFinalize(destination) else {
    176     fputs("Failed to write output\n", stderr)
    177     exit(1)
    178 }