For contributors and anyone who wants to build from source. End users should just grab the DMG from the Releases page; see the README.
- macOS 13 or later
- Xcode Command Line Tools (
xcode-select --install). Full Xcode is not required.
git clone https://github.com/hcaiano/lineup.git && cd lineup
swift run lineup-tests # dependency-free test suite (no Xcode/XCTest needed)
./Scripts/setup-signing.sh # one-time: stable signature so the macOS permission sticks
./Scripts/build-app.sh ~/Applications
open ~/Applications/Lineup.appbuild-app.sh produces a universal (arm64 + x86_64) app by default, so it runs on every
supported Mac. For faster local iteration, UNIVERSAL=0 ./Scripts/build-app.sh … builds the
host arch only. (One-shot --arch needs full Xcode; under Command Line Tools each slice is
built with --triple and combined with lipo.)
A locally built app is ad-hoc signed, whose signature changes every build, so macOS keeps asking
you to re-grant Accessibility. setup-signing.sh creates a reused self-signed identity once, so
every build shares one stable signature and you grant Accessibility a single time. The same applies
to releases: build the release DMG on a machine where this has been run, so users authorize once
and updates keep working. Pass REQUIRE_STABLE_SIGNATURE=1 ./Scripts/build-app.sh dist to make a
release build fail loudly rather than ship an ad-hoc signature by accident.
REQUIRE_STABLE_SIGNATURE=1 ./Scripts/build-app.sh dist # refuses to build an ad-hoc release
./Scripts/make-dmg.sh dist # -> dist/Lineup-<version>.dmg; also rejects an ad-hoc appBoth steps refuse an ad-hoc signature so a release can't accidentally ship one (which would
make every update drop the user's Accessibility grant). Run setup-signing.sh first. For a
throwaway local DMG you can bypass with ALLOW_ADHOC_DMG=1 ./Scripts/make-dmg.sh dist.
Sources/LineupCore/ Pure, tested core (no AppKit)
ZoneTree.swift Recursive split-tree model + resolver + editor geometry
LayoutEdit.swift Pure split / merge / resize operations
LineupConfig.swift Per-screen schema-3 config + migration
Shortcuts.swift Shortcut bindings + conflicts + zone actions
Cycle.swift Left/right cycle steps + continuation predicate
Sources/lineup/ AppKit agent
main.swift Menu-bar app, hotkeys, config lifecycle
LayoutEditorOverlay.swift The on-screen layout editor
DragSnap.swift Modifier-drag-to-snap
SettingsWindow.swift Shortcuts + General
WindowMover.swift Accessibility window get/set
ShortcutKit.swift Defaults + Cocoa/Carbon + combo strings
Theme.swift Brand colour + menu-bar logo
Updater.swift Sparkle updater controller (Check for Updates + background checks)
Scripts/ build-app, setup-signing, make-dmg, make-icon, make-icns,
notarize, sparkle-keygen, sparkle-appcast
Config lives at ~/.config/lineup/zones.json (one layout per display, keyed to the monitor). The
editor writes it for you.
With an Apple Developer account, a Developer ID Application certificate in the keychain
makes build-app.sh sign with it automatically (hardened runtime + secure timestamp), which
notarize.sh can then submit to Apple. Notarization removes the "unidentified developer"
prompt on first open. One-time credential setup (keeps secrets out of scripts):
xcrun notarytool store-credentials "lineup-notary" \
--apple-id <your-apple-id> --team-id <TEAMID> --password <app-specific-password>Release flow (the REQUIRE_DEVELOPER_ID_SIGNATURE=1 gate fails fast if no Developer ID
identity is found, so a notarized release can't silently fall back to the self-signed cert):
REQUIRE_DEVELOPER_ID_SIGNATURE=1 ./Scripts/build-app.sh dist # sign the app w/ Developer ID
./Scripts/notarize.sh dist/Lineup.app # notarize + staple the app
./Scripts/make-dmg.sh dist # package it; signs the DMG too
./Scripts/notarize.sh dist/Lineup-<version>.dmg # notarize + staple the DMGStapling the app makes the dragged-out copy pass Gatekeeper offline; notarizing the DMG makes
the download itself open cleanly. notarytool and stapler ship with the Command Line Tools,
so no full Xcode is needed.
Lineup updates in place with Sparkle. build-app.sh embeds
Sparkle.framework and re-signs it inside-out with the same identity as the app; updates are
authenticated with an EdDSA signature so a tampered or man-in-the-middled download is
rejected. The feed is web/appcast.xml, served at https://lineup.caiano.com/appcast.xml
(auto-deployed from web/), and pointed to by SUFeedURL in Resources/Info.plist.
One-time key setup (do this once, ever — losing the key means you can't sign future updates that existing installs will accept):
./Scripts/sparkle-keygen.sh # private key -> your login Keychain (never committed)Paste the printed public key into Resources/Info.plist under SUPublicEDKey (replacing the
placeholder). That's the only Sparkle value that ships in the app. The private key stays in
your Keychain, exactly like the notarization credential.
Per release, after notarizing the DMG and uploading it to its GitHub release, regenerate
the signed feed and commit it (committing web/appcast.xml auto-deploys the feed):
./Scripts/sparkle-appcast.sh dist/Lineup-<version>.dmg # EdDSA-signs the DMG, writes web/appcast.xml
git add web/appcast.xml && git commit -m "Appcast: <version>" # deploys; installs see the updateThe full release sequence is therefore: build-app.sh → notarize.sh (app) → make-dmg.sh
→ notarize.sh (DMG) → publish the GitHub release with the DMG → sparkle-appcast.sh →
commit web/appcast.xml.