Files
cupsadmin/CLAUDE.md
kfattic 795ab7a981 Canon and HP driver profiles for quick actions
Canon: CNColorMode color/mono, CNDuplex, PageSize=Letter. HP: HPColorAsGray or
HPColorMode; duplex and Letter from the generic profile. Profiles can mark an action
unavailable with a reason (Canon department IDs, HP accounting codes). Use Letter
Paper only says Fit to Nearest Size when the profile sets a fit option. Offline tests
sweep every Canon and HP PPD found in CUPSKIT_PPD_DIRS or /Library/Printers/PPDs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:04:02 -05:00

95 lines
7.3 KiB
Markdown

# 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/<queue>.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 <queue> -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.<value>` 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). Use Letter Paper is
titled ", Fit to Nearest Size" only when the profile also sets a fit option. `cupsadmin ppdreport
<queue | file.ppd[.gz]>` 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=<queue>`, `CUPSKIT_LIVE_JOBS=<a>,<b>`,
`CUPSKIT_LIVE_QUICK=<ricoh>,<generic>`. 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.