# cupsadmin A native replacement for the CUPS web interface (`http://localhost:631`) that Apple removed in macOS 27. `cupsd`, `lpadmin`, `lpstat`, `lpoptions` and IPP Everywhere queues still work; only the browser admin pages are gone. Two front ends share one library: - **CupsKit** (`Sources/CupsKit`) — IPP client, PPD parser, CUPS tool wrappers, read-back checks. Pure logic: returns values or throws, never prints. - **`cupsadmin`** (`Sources/cupsadmin`) — the CLI, a thin layer over CupsKit. - **CUPS Admin.app** (`Sources/CUPSAdminApp`) — the SwiftUI app. The target is `CUPSAdminApp`, not `CUPSAdmin`: APFS is case-insensitive, so `Sources/CUPSAdmin` is the same folder as `Sources/cupsadmin` and the two executables would collide in `.build/`. Private maintainer notes, if present, are in `CLAUDE.local.md` (gitignored). ## Architecture - **Reads are IPP** with a small hand-written encoder/decoder (RFC 8010, `CupsKit/IPP`): CUPS-Get-Printers, Get-Printer-Attributes, Get-Jobs, Get-Job-Attributes, Cancel-Job, CUPS-Get-Default. This gives typed data (state reasons, `*-default`, `*-supported`, job state) instead of localized `lpstat` text. - **Transport is the Unix socket `/private/var/run/cupsd`, not TCP 631.** On macOS 27 launchd socket-activates cupsd only on that socket; cupsd binds port 631 itself while running and exits after about a minute idle, so TCP fails whenever it's asleep. URLSession can't use AF_UNIX, so `UnixSocketHTTP.swift` speaks minimal HTTP/1.1 over POSIX sockets. PPDs are fetched the same way (`GET /printers/.ppd`). - **Writes shell out** through `CupsTools`: `lpadmin`, `cupsenable`/`cupsdisable`, `cupsaccept`/`cupsreject`, `cancel`, `lp`, `lpmove`, `lpinfo`. `lpadmin` already handles `_lpadmin` authorization and IPP Everywhere PPD generation. - **Queue defaults are written with `lpadmin -p -o key=value`** (into the queue's PPD / printers.conf, for every user) — never `lpoptions`. `~/.cups/lpoptions` is only read, to point out a per-user override. - **Driver filter check** (`DriverFilters.swift`): resolves each `*cupsFilter`/`*cupsFilter2` program (bare names under `/usr/libexec/cups/filter`, skipping `maxsize(n)` and `-`) and reads its Mach-O header directly — any arm64 CPU type counts as native, including subtypes `lipo` can't name. Intel-only → "Driver needs Rosetta"; not found → "Driver filter missing" (shown first). - **PPD options** keep their `*OpenGroup`, UI type and `*ParamCustom` type/range. Custom values are written as `Custom.` and quoted when they contain spaces (unquoted, `lpadmin` silently truncates). - **Quick actions** are vendor-neutral. `QuickActions.swift` defines the actions; what each writes comes from `Resources/driver-profiles.json` (embedded in code — the CLI is a single binary), matched on the PPD's `*Manufacturer`/`*NickName`, vendor profiles first and `generic` (standard PPD keywords, IPP Everywhere `*-default` attributes) last. A pair is used only if that queue supports the keyword and value; otherwise the action is unavailable with the reason. A profile's `unavailable` map gives the reason an action can't be done with that driver (e.g. Canon department IDs); its `inputs` map relabels the typed value and can add an optional second value (`{value2}`, e.g. the Xerox account ID). Use Letter Paper is titled ", Fit to Nearest Size" only when the profile also sets a fit option. `cupsadmin ppdreport ` shows a driver's keywords and which actions its profile enables. ## Build and test ``` swift build # debug build of everything .build/debug/cupsadmin printers # run the CLI ./test.sh # Swift Testing via the Command Line Tools (see flags inside) ./build.sh --build-only # universal CLI + app bundle, ad-hoc signed, no identities needed ./build.sh --screenshots # regenerate docs/screenshots/app-jobs.png from temporary demo queues ./build.sh # release: sign, payload-free PKG, notarize, staple, spctl ``` - Only the Command Line Tools are required. Universal builds are per-triple (`swift build -c release --triple arm64-apple-macosx14.0` and `x86_64-…`) plus `lipo`; `--arch arm64 --arch x86_64` needs full Xcode. - Package: tools-version 6.0, Swift 5 language mode, macOS 14 minimum. - `build.sh` reads `CODESIGN_APP_IDENTITY`, `CODESIGN_PKG_IDENTITY`, `NOTARY_PROFILE` (and optional `CODESIGN_TEAM_ID`) from the environment or an untracked `build.env`. Never hard-code identities. - PKG layout: the app is a payload (`pkgbuild --root` with a staging `Applications/` that is really root:admin 775 and `--ownership preserve`, `BundleIsRelocatable` false), so it gets a receipt and the package records the system's own ownership. `--ownership recommended` would record Applications as root:wheel — don't use it. The CLI is not a payload: the binary ships in Scripts and `pkg/postinstall` copies it, because a `/usr/local/bin` payload entry would reset a Homebrew-owned folder. The build verifies both BOM ownership entries. The app is notarized and stapled before packaging; the PKG after. - The app icon master is `Icon/AppIcon-1024.png`; `build.sh` generates `AppIcon.icns` from it. Don't redraw it. - Live tests are opt-in and write to queues: `CUPSKIT_LIVE_QUEUE=`, `CUPSKIT_LIVE_JOBS=,`, `CUPSKIT_LIVE_QUICK=,`, `CUPSKIT_LIVE_DELETE=1` (deletes a queue name that never existed; writes nothing). Use throwaway queues (e.g. `lpd://127.0.0.1/…`, paused) and delete them afterwards; the live tests restore what they change. Offline tests use vendor PPDs from `CUPSKIT_PPD_DIRS` (colon-separated, e.g. Canon/HP PPDs extracted with `pkgutil --expand-full`, not installed) and `/Library/Printers/PPDs`, and skip when they aren't there. ## Conventions - **US spelling everywhere** — code, UI, CLI output, docs (color, canceled, gray). - **No third-party dependencies.** No Xcode project. - **CLI status lines on every exit path:** `started HH:MM:SS`, exactly one `OK: …` or `ERROR: …`, and `finished HH:MM:SS (total N min)` — on stderr, so tables on stdout pipe cleanly. Exit 0 success, 1 runtime error or not applied, 2 usage error. Existing command output is a contract: don't change it casually. Scripts (`build.sh`, `test.sh`) print the same started/finished lines. - **Read back after every write.** Re-query cupsd and compare; a change that didn't take is reported (`NOT APPLIED`, non-zero exit, or a red X with the reason) — never silent. - **App feedback:** every action updates the bottom status line with its timing ("Set 3 options on Office in 0.4 s", "ERROR: …") and logs the same through `os_log` (subsystem `edu.wku.cupsadmin.app`). Failures show the tool output and, for permission errors, the `_lpadmin` fix. The exact command is visible before it runs. Destructive actions confirm and name the object. - **Secrets:** passcode/password options written as queue defaults land in a world-readable PPD — warn, and mask them in displayed commands. Never commit `build.env`, `dist/`, notary output or `CLAUDE.local.md`. - **Native SwiftUI and macOS idioms only:** `NavigationSplitView`, sortable/resizable `Table`, `Form` with `.formStyle(.grouped)`, standard sheets, alerts and confirmation dialogs, SF Symbols, system fonts, semantic colors (`.primary`, `.secondary`, `.green`/`.blue`/`.red` state dots), `ContentUnavailableView` empty states. No custom chrome, no web-style cards, no emoji. Must look right in light and dark mode. Tables and text are selectable and copyable.