diff --git a/CLAUDE.md b/CLAUDE.md index c0e38f1..ee8e5a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,10 +14,10 @@ A native macOS app that shows video from a capture device (camera / capture card ## Structure - `Sources/` – app code (AppKit + AVFoundation, one `AVCaptureSession`, audio via `AVCaptureAudioPreviewOutput`). The `App` class is split into extensions: `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` – version, permissions (camera, microphone; hardened runtime). +- `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. -- `Makefile` – `make` (builds `build/SecondDisplay.app`), `run`, `check`, `install`, `uninstall`, `clean`, `help`. `SIGN_ID` defaults to ad-hoc (`-`). +- `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 - Settings live in `UserDefaults` (keys: `video`, `audio`, `keepAwake`, `startFull`, `onTop`, `gravity`, `format.`). diff --git a/Config/dmg/Read Me First.txt b/Config/dmg/Read Me First.txt new file mode 100644 index 0000000..0825ff0 --- /dev/null +++ b/Config/dmg/Read Me First.txt @@ -0,0 +1,32 @@ +SECOND DISPLAY — INSTALLATION +============================= + +1. Drag "SecondDisplay" onto the "Applications" folder in this window. + +2. Open Second Display from Applications. + macOS will most likely refuse: this app is not notarized by Apple + (the author does not pay for a developer account). The app is safe to + run if you trust where you got it — the source code is open. + +3. Allow it once (pick ONE way): + + A) System Settings + - Open System Settings > Privacy & Security. + - Scroll to the Security section; you will see + "SecondDisplay was blocked...". Click "Open Anyway". + - Confirm with your password / Touch ID, then click "Open". + + B) Terminal + xattr -dr com.apple.quarantine /Applications/SecondDisplay.app + + You only need to do this once per downloaded copy. + +4. On first launch, allow Camera and Microphone access. + (Change later in System Settings > Privacy & Security.) + +REQUIREMENTS + macOS 26 or newer. + +UNINSTALL + Run this in Terminal to remove the app and every trace it leaves: + /Applications/SecondDisplay.app/Contents/Resources/uninstall.sh diff --git a/Makefile b/Makefile index 71a168f..bfefc4c 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,18 @@ RES := $(shell find Resources -type f) SIGN_ID ?= - BUILDNUM := $(shell git rev-list --count HEAD 2>/dev/null || echo 1) -.PHONY: build run clean install uninstall check help +VERSION := $(shell /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" Config/Info.plist) +DMG := build/SecondDisplay-$(VERSION).dmg + +.PHONY: build run clean install uninstall check dmg help build: $(APP) ## Build build/SecondDisplay.app $(APP): Makefile $(SRC) $(RES) Config/Info.plist Config/SecondDisplay.entitlements rm -rf $(APP) && mkdir -p $(APP)/Contents/MacOS $(APP)/Contents/Resources - swiftc -O $(SRC) -o $(APP)/Contents/MacOS/SecondDisplay + swiftc -O -target arm64-apple-macos26.0 $(SRC) -o $(APP)/Contents/MacOS/SecondDisplay-arm64 + swiftc -O -target x86_64-apple-macos26.0 $(SRC) -o $(APP)/Contents/MacOS/SecondDisplay-x86_64 + lipo -create $(APP)/Contents/MacOS/SecondDisplay-* -output $(APP)/Contents/MacOS/SecondDisplay + rm $(APP)/Contents/MacOS/SecondDisplay-* cp Config/Info.plist $(APP)/Contents/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $(BUILDNUM)" $(APP)/Contents/Info.plist cp -R Resources/en.lproj Resources/pl.lproj Resources/uninstall.sh $(APP)/Contents/Resources/ @@ -36,5 +42,14 @@ 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 + help: ## List targets @grep -E '^[a-z]+:.*##' Makefile | sed 's/:.*##/ -/' diff --git a/README.md b/README.md index 8bc7f1c..0f085bd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ make # builds build/SecondDisplay.app make run # build and launch make check # lint: typecheck, plists, translations, signature make install # copy to ~/Applications +make dmg # build/SecondDisplay-.dmg (universal, not notarized) make help # list targets ``` The app is signed ad-hoc (works on this Mac). For distribution: `make SIGN_ID="Developer ID Application: ..."` (notarization is still required). @@ -25,14 +26,17 @@ open ~/Applications/SecondDisplay.app ``` On first launch macOS asks for camera and microphone access — allow both. You can change this later in System Settings → Privacy & Security. -If you received a prebuilt copy that is not notarized, macOS blocks it. Allow it once in System Settings → Privacy & Security → "Open Anyway", or run `xattr -dr com.apple.quarantine SecondDisplay.app`. +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`). ## 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). ## Layout - `Sources/` – the app (AppKit + AVFoundation): `App` (state, window), `Capture` (devices, formats), `Menu`, `Power`, `Preferences`, `main` (entry point) -- `Config/` – `Info.plist`, entitlements (camera, microphone) +- `Config/` – `Info.plist`, entitlements (camera, microphone), `dmg/` (instructions shipped in the disk image) - `Resources/` – icon (`AppIcon.icon`, Icon Composer), en/pl translations, `uninstall.sh` - `scripts/check-strings.sh` – translation consistency