Style the DMG: dmgbuild layout, background, large icons

Layout is written by dmgbuild (Config/dmg/settings.py) instead of Finder
AppleScript, which proved unreliable. Background is generated by
scripts/make-dmg-background.swift.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mmacierzankaandClaude Bot committed 2026-11-03 03:42:15 +01:00
1 parent 313be025d6
commit 6273a5b8c5
6 files changed
+68 -10

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ A native macOS app that shows video from a capture device (camera / capture card
`main.swift` (entry point), `App.swift` (state, window, lifecycle), `Capture.swift` (devices, formats, hot-plug), `Menu.swift` (menus and actions), `Power.swift` (sleep prevention), `Preferences.swift` (UserDefaults-backed settings).
- `Config/Info.plist`, `Config/SecondDisplay.entitlements`, `Config/dmg/` – version, permissions (camera, microphone; hardened runtime).
- `Resources/` – `AppIcon.icon` (Icon Composer, Liquid Glass; compiled by `actool`), `{en,pl}.lproj` (`Localizable.strings`, `InfoPlist.strings`), `uninstall.sh` (copied into the bundle; removes the app, preferences, caches, TCC permissions). Bundle ID: `com.mmacierzanka.seconddisplay`.
- `scripts/check-strings.sh` – en/pl translation consistency.
- `scripts/check-strings.sh` – en/pl translation consistency. `scripts/make-dmg-background.swift` regenerates `Config/dmg/background.png`; keep its size and icon coordinates in sync with `Config/dmg/settings.py` (dmgbuild layout, installed into `build/venv` on first `make dmg`).
- `Makefile` – `make` (builds `build/SecondDisplay.app`), `run`, `check`, `install`, `uninstall`, `dmg`, `clean`, `help`. Builds are universal (arm64 + x86_64, macOS 26+). `make dmg` makes an un-notarized DMG with `Config/dmg/Read Me First.txt`. `SIGN_ID` defaults to ad-hoc (`-`).
## Conventions
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+23
View File
@@ -0,0 +1,23 @@
# dmgbuild settings: writes the Finder window layout directly (no Finder/AppleScript needed).
# Keep coordinates in sync with scripts/make-dmg-background.swift.
app = defines['app'] # noqa: F821 (injected by dmgbuild -D)
format = 'UDZO'
filesystem = 'HFS+'
files = [app, 'Config/dmg/Read Me First.txt']
symlinks = {'Applications': '/Applications'}
background = 'Config/dmg/background.png'
window_rect = ((200, 120), (726, 720))
default_view = 'icon-view'
show_toolbar = False
show_status_bar = False
show_sidebar = False
icon_size = 128
text_size = 13
arrange_by = None
icon_locations = {
'SecondDisplay.app': (198, 195),
'Applications': (529, 195),
'Read Me First.txt': (363, 435),
}
+10 -8
View File
@@ -7,6 +7,7 @@ BUILDNUM := $(shell git rev-list --count HEAD 2>/dev/null || echo 1)
VERSION := $(shell /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" Config/Info.plist)
DMG := build/SecondDisplay-$(VERSION).dmg
VOLNAME ?= Second Display
.PHONY: build run clean install uninstall check dmg help
build: $(APP) ## Build build/SecondDisplay.app
@@ -42,14 +43,15 @@ check: build ## Lint sources, plists, strings and the built bundle
scripts/check-strings.sh
codesign --verify --strict $(APP)
dmg: build ## Build a distributable disk image (not notarized)
rm -rf build/dmg $(DMG) && mkdir build/dmg
cp -R $(APP) build/dmg/
ln -s /Applications build/dmg/Applications
cp "Config/dmg/Read Me First.txt" build/dmg/
hdiutil create -volname "Second Display" -srcfolder build/dmg -ov -format UDZO $(DMG)
hdiutil verify $(DMG)
rm -rf build/dmg
DMGBUILD := build/venv/bin/dmgbuild
$(DMGBUILD):
python3 -m venv build/venv && build/venv/bin/pip install -q dmgbuild
dmg: build $(DMGBUILD) ## Build a distributable disk image (not notarized)
rm -f $(DMG)
$(DMGBUILD) -s Config/dmg/settings.py -D app=$(APP) "$(VOLNAME)" $(DMG)
hdiutil verify $(DMG) | tail -1
help: ## List targets
@grep -E '^[a-z]+:.*##' Makefile | sed 's/:.*##/ -/'
+1 -1
View File
@@ -29,7 +29,7 @@ On first launch macOS asks for camera and microphone access — allow both. You
If you received a prebuilt copy (the `.dmg`), drag the app to Applications. It is not notarized, so macOS blocks it on first launch: allow it once in System Settings → Privacy & Security → "Open Anyway", or run `xattr -dr com.apple.quarantine /Applications/SecondDisplay.app`. The same steps are in `Read Me First.txt` inside the disk image.
## Releasing a DMG
`make dmg` builds a universal (arm64 + x86_64) app and packs it with an `Applications` shortcut and `Read Me First.txt` (source: `Config/dmg/`). The disk image is not signed with a Developer ID or notarized; the instructions in it explain how recipients allow it. Bump the version in `Config/Info.plist` before releasing and tag it (`git tag -a vX.Y.Z -m vX.Y.Z`).
`make dmg` builds a universal (arm64 + x86_64) app and packs it with an `Applications` shortcut and `Read Me First.txt` (source: `Config/dmg/`) in a styled Finder window: large icons and a background with an arrow (`Config/dmg/background.png`, generated by `scripts/make-dmg-background.swift`). The layout is written directly into the image by [dmgbuild](https://github.com/dmgbuild/dmgbuild) (settings in `Config/dmg/settings.py`), so no Finder automation is needed. The first `make dmg` installs it into `build/venv` (needs `python3` and network access). The disk image is not signed with a Developer ID or notarized; the instructions in it explain how recipients allow it. Bump the version in `Config/Info.plist` before releasing and tag it (`git tag -a vX.Y.Z -m vX.Y.Z`).
## Usage
Menus at the top of the screen: **Video** (device, format), **Audio** (source or off), **Options** (prevent display sleep, start in full screen, always on top, mute ⇧⌘M, scaling), **Window** (tiling, full screen ⌃⌘F).
+33
View File
@@ -0,0 +1,33 @@
// Usage: swift scripts/make-dmg-background.swift -> writes Config/dmg/background.png (727x626 pt @2x)
import AppKit
let w = 727.0, h = 626.0, scale = 2.0
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(w * scale), pixelsHigh: Int(h * scale), bitsPerSample: 8,
samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
rep.size = NSSize(width: w, height: h) // 144 dpi, so Finder shows it at 727x626 pt
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
NSGradient(starting: NSColor(red: 0.93, green: 0.95, blue: 0.99, alpha: 1), ending: NSColor(red: 0.80, green: 0.86, blue: 0.96, alpha: 1))!
.draw(in: NSRect(x: 0, y: 0, width: w, height: h), angle: -90)
func text(_ s: String, size: CGFloat, weight: NSFont.Weight, y: CGFloat, alpha: CGFloat) {
let a: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: size, weight: weight),
.foregroundColor: NSColor(red: 0.10, green: 0.15, blue: 0.30, alpha: alpha)]
let sz = (s as NSString).size(withAttributes: a)
(s as NSString).draw(at: NSPoint(x: (w - sz.width) / 2, y: h - y - sz.height), withAttributes: a)
}
text("Drag Second Display to Applications", size: 20, weight: .semibold, y: 28, alpha: 1)
text("Blocked on first launch? Open “Read Me First.txt”", size: 13, weight: .regular, y: 330, alpha: 0.65)
// arrow between the two icons (icon centres at x=198 and x=529, y=195 from top)
let ay = h - 195
let arrow = NSBezierPath()
arrow.lineWidth = 6; arrow.lineCapStyle = .round; arrow.lineJoinStyle = .round
arrow.move(to: NSPoint(x: 290, y: ay)); arrow.line(to: NSPoint(x: 430, y: ay))
arrow.move(to: NSPoint(x: 405, y: ay + 22)); arrow.line(to: NSPoint(x: 432, y: ay)); arrow.line(to: NSPoint(x: 405, y: ay - 22))
NSColor(red: 0.25, green: 0.35, blue: 0.95, alpha: 0.8).setStroke()
arrow.stroke()
NSGraphicsContext.restoreGraphicsState()
try! rep.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: "Config/dmg/background.png"))