M3 — Unmanaged printing #48

Merged
gronod merged 11 commits from milestone/m3-printing into develop 2026-09-09 07:09:02 +01:00
Owner

Milestone 3: native print panel, ColorSync suppression, and CUPS lp spooling for targets.

Includes:

  • #12 CUPS printer enumeration (lpstat/lpoptions)
  • #13 NSPrintPanel bound to selected queue
  • #14 ColorSync suppression engine + six-layer approach
  • #15 Safe lp argv builder + CupsService.printTarget
  • #17 Stage 2 live print panel + Milestone3UITests

Verification:

  • ICCeryCoreTests 154/154 pass
  • Milestone3UITests 6/6 pass
  • Universal arm64 x86_64 build: ** BUILD SUCCEEDED **

Closes Milestone 3.

Generated with Devin

Milestone 3: native print panel, ColorSync suppression, and CUPS `lp` spooling for targets. Includes: - #12 CUPS printer enumeration (`lpstat`/`lpoptions`) - #13 `NSPrintPanel` bound to selected queue - #14 ColorSync suppression engine + six-layer approach - #15 Safe `lp` argv builder + `CupsService.printTarget` - #17 Stage 2 live print panel + `Milestone3UITests` Verification: - `ICCeryCoreTests` 154/154 pass - `Milestone3UITests` 6/6 pass - Universal `arm64 x86_64` build: `** BUILD SUCCEEDED **` Closes Milestone 3. Generated with [Devin](https://devin.ai)
gronod added 11 commits 2026-09-09 07:08:59 +01:00
- Printer/PrinterStatus/PrinterTray/PrinterPaperSize/PrinterMediaType/
  PrinterCapabilities/PrintOptions/PrintPropertiesResult models
  (docs/10 shared print types).
- CupsParsers: lpstat -e/-p/-d, lpoptions -p (quoted printer-info),
  lpoptions -l Key/Label listings, PPD *Key id/Human: enrichment with
  locale-qualified key support.
- detectMediaTypeKey (CNIJMediaType > EPIJ_Medi > StpMediaType >
  MediaType) and detectDriverColorBypass (Canon Intent2/Intent, Epson
  CCor/CMat, Gutenprint, generic, EpsonColorMode) — docs/11 layer 4.
- CupsService over ProcessManager.runCaptured with injectable
  binaryDir/ppdDir; empty lpstat output is success.
- ProcessID entries for lpstat/lpoptions/lp.

Core tests: 8 parser suites on recorded Epson/Canon fixtures.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- PrintPanelService: PMPrinterCreateFromPrinterID(CUPS queue id) ->
  PMSessionSetCurrentPMPrinter -> session default settings/page format.
- NSPrinter(name: displayName) fallback when PM binding fails; display
  name resolved via lpoptions printer-info (docs/11 #188).
- Default button 'Use Settings' (capture, not print); cancel -> nil.
- PMPrinter released on every path via defer.
- UITestHooks: printPanelResult stub (ICCERY_TEST_PRINT_PANEL=ok/cancel
  + PANEL_OPTIONS/PANEL_PRINTER), printPanelStubbed gate, cupsBinaryDir
  + lpArgvOutURL env seams; CupsService injected via AppEnvironment.

Never opens System Settings / CUPS web UI / NSWorkspace.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- ColorMatchingAttempts (Core): dlsym order Lock -> Mode -> NoLock and
  modes AP_ApplicationColorMatching -> ApplicationColorMatching as data;
  both AP_ print-settings keys; 2-arg (PMPrintSession, CFStringRef)
  signature — never integer 1 (#188).
- ColorSyncSuppressor (app): layer 2 SPI via injectable dlsym resolver,
  first 0 wins; layer 3 PMPrintSettingsSetValue both keys locked;
  layer 4 driver bypass from lpoptions -l keys, unlocked; layer 5
  NSPrintInfo.printSettings mirror; layer 6 PMPrintSettingsToOptions
  (dlsym'd) -> filter -> captured cupsOptions + mediaType.
- CupsOptionsFilter (Core): drops com.apple.*, collate/copies/
  job-sheets/pserrorhandler-requested, empty values, both AP_* keys;
  keeps relevant + unknown non-com.* keys; extractMediaType prefers
  MediaType then EPIJ_Medi.
- PrintPanelService: suppression runs between session binding and
  runModal on the PM path; capture feeds PrintPropertiesResult.

Tests: filter fixtures + injected-resolver call-order/first-win/
mode-fallback/missing-symbol suites.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- LpArgs.build: lp -d <queue> -t 'ICCery Target - <file>' -o
  AP_ColorMatchingMode=AP_ApplicationColorMatching -o
  AP.ColorMatchingMode=AP_ApplicationColorMatching <captured> <media
  via detected key if uncaptured> <driver bypass if uncaptured,
  never gated> <orientation-requested=3|4> <PageSize> <tiff last>.
- Never -o raw (#92): raw is dropped by the capture filter AND skipped
  at build time.
- Captured options win over derived fields (case-insensitive dedup);
  sanitise rejects ;, newlines, shell metachars — argv array only.
- CupsService.printTarget: TIFF existence check, optionKeys lookup for
  media/bypass detection, runCaptured spawn.
- ProcessID.lp(queue, page).

Tests: 9 argv golden suites — both AP_* always present, captured wins,
sanitise rejects ;, no raw.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- rawPrintPanel: printerSelect (lpstat -e, auto-refresh on manifest),
  printerStatusBadge (idle/printing/stopped), printerTraySelect +
  printerMediaTypeSelect from lpoptions -l capabilities, portrait/
  landscape toggle, btnPrinterProperties (bound NSPrintPanel), Print
  All + per-page btnPrintPage-N, in-panel printNotification (cancel →
  info, not error). No cupsOptionsGroup/chkPpdFallback — macOS always
  uses the PM-captured path.
- TargetWorkflowViewModel: refreshPrinters, reloadSelectedCapabilities,
  openPrinterPreferences (panel-side queue switch updates the select),
  printAllPages / printPage (sequential, stop-on-first-error),
  capturedCupsOptions per-queue session cache, wizard.printerName set
  on spool (#95).
- CupsError.noPrinterSelected.
- Mock CUPS fixtures: lpstat (2 queues, one idle one disabled),
  lpoptions (tray/media/EPIJ_CMat listings), lp (appends argv to
  ICCERY_TEST_LP_ARGV). Milestone3UITests: enumeration, preferences
  cancel→info, captured options replayed in argv, per-page print,
  lp failure notice, printerName persistence.
- lpoptions parser: skip keyless '=value' tokens instead of truncating.

UI tests written but not executed here: the dev machine's console is
locked (IOConsoleLocked=true) so XCUIApplication.activate() cannot
bring the app to front — same failure on M2 baseline. Suite must be
run unlocked / on CI.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Ensure unstructured Task closures in TargetWorkflowViewModel and
  Stage2View run on @MainActor after awaiting CupsService / spool.
  Without this,  updates in the  failure path were
  not reaching the UI in time, failing Milestone3UITests.
- lpoptions parser: a  token no longer truncates the whole
  parse.

UI test results: Milestone3UITests 6/6 pass; Core tests 154/154 pass;
universal arm64 x86_64 build passes.
gronod merged commit bb6ca957ba into develop 2026-09-09 07:09:02 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#48