In the TargetPrint Inspector sidebar, the "Tray" selection picklist (trayPop) is completely empty with 0 menu items when an OEM printer (such as Epson XP-55 or Canon Pro series) is selected, rendering a blank dropdown control.
Investigation & Root Cause Analysis
1. Swift Nil-Coalescing Bug on Empty Arrays in InspectorView
Because q is a non-nil PrinterQueue, q?.trays evaluates to Optional([]). In Swift, the nil-coalescing operator ?? only evaluates the right-hand expression if the left-hand operand is nil. Since Optional([]) != nil, q?.trays ?? ["Auto"] evaluates to [].
pop.removeAllItems() clears all existing items, and items.forEach iterates 0 times. Consequently, trayPop is left with 0 items, presenting a completely blank/empty control.
2. Missing Vendor-Specific PPD Keywords for Tray / Paper Source
discoverPPDOptions hardcodes keyword: "InputSlot". While generic PostScript and network laser printers use *InputSlot, major inkjet manufacturers use proprietary keywords:
optionChoices extracts only the choice token prior to / ("2", "3", "12"), discarding the human-readable display titles ("Cassette 1", "Cassette 2", "Rear Paper Feed Slot").
PrintEngine hardcodes extras["InputSlot"]. When printing to Epson or Canon queues, setting InputSlot in NSPrintInfo print settings is ignored by the driver because the driver filter listens for EPIJ_FdSo or CNIJMediaSupply.
Steps to Reproduce
Configure an Epson or Canon printer queue (e.g. Epson_XP_55_LPD).
Launch TargetPrint.
In the Inspector sidebar, observe the "Tray" dropdown menu.
Observed: The dropdown contains 0 items and displays a blank selector.
Expected: The dropdown displays the available paper sources (e.g. Cassette 1, Cassette 2, Rear Paper Feed Slot), or defaults to Auto if the printer has a single fixed feed.
Store the discovered tray option keyword on PrinterQueue (e.g. trayKeyword: String?) so PrintEngine can inject the appropriate key.
Parse both the choice code (name, e.g. "2", "12") and human-readable label (title, e.g. "Cassette 1", "Rear Paper Feed Slot"), decoding any PPD hex escapes (<2F>, <2E>).
Lookup the active queue's trayKeyword (defaulting to "InputSlot").
Inject extras[trayKey] = paperSource.
Phase 5: Unit Testing & Verification
Add test cases in Tests/:
Test PPD parsing of Epson *EPIJ_FdSo choices (Cassette 1, Cassette 2, Rear Paper Feed Slot).
Test PPD parsing of Canon *CNIJMediaSupply choices.
Test fallback to ["Auto"] when a queue has no tray options or an empty list.
Verify trayPop UI retains Auto rather than clearing to 0 items.
Run swift test to ensure all tests pass without regression.
### Summary
In the TargetPrint Inspector sidebar, the "Tray" selection picklist (`trayPop`) is completely empty with 0 menu items when an OEM printer (such as Epson XP-55 or Canon Pro series) is selected, rendering a blank dropdown control.
---
### Investigation & Root Cause Analysis
#### 1. Swift Nil-Coalescing Bug on Empty Arrays in InspectorView
In `Sources/Views/InspectorView.swift` (line 188):
```swift
private func refreshDependent() {
let q = selectedQueue
refill(mediaPop, q?.mediaSizes.isEmpty == false ? q!.mediaSizes : Geometry.papers.map { $0.name })
refill(typePop, q?.mediaTypes ?? ["Plain"])
refill(trayPop, q?.trays ?? ["Auto"])
refill(resolutionPop, q?.resolutions ?? [])
updateAirPrint()
}
```
Notice the difference between `mediaPop` and `trayPop`:
- `mediaPop` correctly checks `q?.mediaSizes.isEmpty == false ? ... : Geometry.papers.map { $0.name }`.
- `trayPop` uses `q?.trays ?? ["Auto"]`.
Because `q` is a non-nil `PrinterQueue`, `q?.trays` evaluates to `Optional([])`. In Swift, the nil-coalescing operator `??` only evaluates the right-hand expression if the left-hand operand is `nil`. Since `Optional([]) != nil`, `q?.trays ?? ["Auto"]` evaluates to `[]`.
Inside `refill(_:_:)`:
```swift
private func refill(_ pop: NSPopUpButton, _ items: [String]) {
let current = pop.titleOfSelectedItem
pop.removeAllItems()
items.forEach { pop.addItem(withTitle: $0) }
if let current = current { pop.selectItem(withTitle: current) }
}
```
`pop.removeAllItems()` clears all existing items, and `items.forEach` iterates 0 times. Consequently, `trayPop` is left with 0 items, presenting a completely blank/empty control.
#### 2. Missing Vendor-Specific PPD Keywords for Tray / Paper Source
In `Sources/Printing/CUPSManager.swift` (line 104):
```swift
static func discoverPPDOptions(_ ppdText: String) -> PPDOptions {
var out = PPDOptions()
out.pageSizes = optionChoices(in: ppdText, keyword: "PageSize")
out.mediaTypes = optionChoices(in: ppdText, keyword: "MediaType")
out.trays = optionChoices(in: ppdText, keyword: "InputSlot")
out.resolutions = optionChoices(in: ppdText, keyword: "Resolution")
```
`discoverPPDOptions` hardcodes `keyword: "InputSlot"`. While generic PostScript and network laser printers use `*InputSlot`, major inkjet manufacturers use proprietary keywords:
- **Epson inkjet drivers** (e.g. `Epson_XP_55_LPD.ppd`, XP-600/700/800/900 series):
```ppd
*OpenUI *EPIJ_FdSo/Paper Source: PickOne
*EPIJ_FdSo 2/Cassette 1: ""
*EPIJ_FdSo 3/Cassette 2: ""
*EPIJ_FdSo 12/Rear Paper Feed Slot: ""
*CloseUI: *EPIJ_FdSo
```
- **Canon inkjet drivers** (e.g. `CanonIJPro9500IIseries.ppd`):
```ppd
*OpenUI *CNIJMediaSupply/Paper Source: PickOne
*CNIJMediaSupply 7/Rear Tray: ""
*CNIJMediaSupply 33/Manual Feed: ""
*CloseUI: *CNIJMediaSupply
```
Because neither vendor PPD contains `*InputSlot`, `optionChoices(in: ppdText, keyword: "InputSlot")` returns `[]`. Combined with the nil-coalescing bug above, `trayPop` becomes completely empty.
#### 3. Dropping Human-Readable Display Names
Even if `EPIJ_FdSo` or `CNIJMediaSupply` were queried with `optionChoices(in:keyword:)`:
```swift
let token = rest.split(whereSeparator: { $0 == "/" || $0 == ":" || $0 == " " }).first
```
`optionChoices` extracts only the choice token prior to `/` (`"2"`, `"3"`, `"12"`), discarding the human-readable display titles (`"Cassette 1"`, `"Cassette 2"`, `"Rear Paper Feed Slot"`).
#### 4. PrintEngine PPD Injection Keyword Mismatch
In `Sources/Printing/PrintEngine.swift` (line 92):
```swift
if let paperSource = paperSource { extras["InputSlot"] = paperSource }
```
`PrintEngine` hardcodes `extras["InputSlot"]`. When printing to Epson or Canon queues, setting `InputSlot` in `NSPrintInfo` print settings is ignored by the driver because the driver filter listens for `EPIJ_FdSo` or `CNIJMediaSupply`.
---
### Steps to Reproduce
1. Configure an Epson or Canon printer queue (e.g. `Epson_XP_55_LPD`).
2. Launch `TargetPrint`.
3. In the Inspector sidebar, observe the "Tray" dropdown menu.
4. **Observed:** The dropdown contains 0 items and displays a blank selector.
5. **Expected:** The dropdown displays the available paper sources (e.g. `Cassette 1`, `Cassette 2`, `Rear Paper Feed Slot`), or defaults to `Auto` if the printer has a single fixed feed.
---
### Implementation Plan for Fix
#### Phase 1: Fix Inspector Empty Array Fallback (`InspectorView.swift`)
- Modify `refreshDependent()` to check `.isEmpty`:
```swift
let trays = (q?.trays.isEmpty == false) ? q!.trays : ["Auto"]
refill(trayPop, trays)
```
- Ensure `trayPop` always contains at least `["Auto"]` rather than clearing to 0 items when no trays are discovered.
#### Phase 2: Expand PPD Keyword Discovery (`CUPSManager.swift`)
1. Detect tray options across standard and vendor keywords:
- Check `InputSlot`, `EPIJ_FdSo` (Epson), `CNIJMediaSupply` (Canon), or dynamically inspect `*OpenUI *<Keyword>/Paper Source:` / `/Media Source:`.
2. Store the discovered tray option keyword on `PrinterQueue` (e.g. `trayKeyword: String?`) so `PrintEngine` can inject the appropriate key.
3. Parse both the choice code (`name`, e.g. `"2"`, `"12"`) and human-readable label (`title`, e.g. `"Cassette 1"`, `"Rear Paper Feed Slot"`), decoding any PPD hex escapes (`<2F>`, `<2E>`).
#### Phase 3: Update Inspector UI Selection Logic (`InspectorView.swift`)
1. Populate `trayPop` items with `title = choice.title` and `representedObject = choice.name`.
2. Update `select(trayPop, ...)` to find matching items by either `representedObject as? String == value` or `title == value`.
3. In `push(into engine:)`, set `engine.paperSource` to `trayPop.selectedItem?.representedObject as? String ?? trayPop.titleOfSelectedItem`.
#### Phase 4: Dynamic PPD Key Injection (`PrintEngine.swift`)
1. Update `applyOptionalPPDKeys(to:)`:
- Lookup the active queue's `trayKeyword` (defaulting to `"InputSlot"`).
- Inject `extras[trayKey] = paperSource`.
#### Phase 5: Unit Testing & Verification
1. Add test cases in `Tests/`:
- Test PPD parsing of Epson `*EPIJ_FdSo` choices (`Cassette 1`, `Cassette 2`, `Rear Paper Feed Slot`).
- Test PPD parsing of Canon `*CNIJMediaSupply` choices.
- Test fallback to `["Auto"]` when a queue has no tray options or an empty list.
- Verify `trayPop` UI retains `Auto` rather than clearing to 0 items.
2. Run `swift test` to ensure all tests pass without regression.
gronod
added the Kind/Bug label 2026-09-07 22:21:34 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
In the TargetPrint Inspector sidebar, the "Tray" selection picklist (
trayPop) is completely empty with 0 menu items when an OEM printer (such as Epson XP-55 or Canon Pro series) is selected, rendering a blank dropdown control.Investigation & Root Cause Analysis
1. Swift Nil-Coalescing Bug on Empty Arrays in InspectorView
In
Sources/Views/InspectorView.swift(line 188):Notice the difference between
mediaPopandtrayPop:mediaPopcorrectly checksq?.mediaSizes.isEmpty == false ? ... : Geometry.papers.map { $0.name }.trayPopusesq?.trays ?? ["Auto"].Because
qis a non-nilPrinterQueue,q?.traysevaluates toOptional([]). In Swift, the nil-coalescing operator??only evaluates the right-hand expression if the left-hand operand isnil. SinceOptional([]) != nil,q?.trays ?? ["Auto"]evaluates to[].Inside
refill(_:_:):pop.removeAllItems()clears all existing items, anditems.forEachiterates 0 times. Consequently,trayPopis left with 0 items, presenting a completely blank/empty control.2. Missing Vendor-Specific PPD Keywords for Tray / Paper Source
In
Sources/Printing/CUPSManager.swift(line 104):discoverPPDOptionshardcodeskeyword: "InputSlot". While generic PostScript and network laser printers use*InputSlot, major inkjet manufacturers use proprietary keywords:Epson_XP_55_LPD.ppd, XP-600/700/800/900 series):CanonIJPro9500IIseries.ppd):Because neither vendor PPD contains
*InputSlot,optionChoices(in: ppdText, keyword: "InputSlot")returns[]. Combined with the nil-coalescing bug above,trayPopbecomes completely empty.3. Dropping Human-Readable Display Names
Even if
EPIJ_FdSoorCNIJMediaSupplywere queried withoptionChoices(in:keyword:):optionChoicesextracts only the choice token prior to/("2","3","12"), discarding the human-readable display titles ("Cassette 1","Cassette 2","Rear Paper Feed Slot").4. PrintEngine PPD Injection Keyword Mismatch
In
Sources/Printing/PrintEngine.swift(line 92):PrintEnginehardcodesextras["InputSlot"]. When printing to Epson or Canon queues, settingInputSlotinNSPrintInfoprint settings is ignored by the driver because the driver filter listens forEPIJ_FdSoorCNIJMediaSupply.Steps to Reproduce
Epson_XP_55_LPD).TargetPrint.Cassette 1,Cassette 2,Rear Paper Feed Slot), or defaults toAutoif the printer has a single fixed feed.Implementation Plan for Fix
Phase 1: Fix Inspector Empty Array Fallback (
InspectorView.swift)refreshDependent()to check.isEmpty:trayPopalways contains at least["Auto"]rather than clearing to 0 items when no trays are discovered.Phase 2: Expand PPD Keyword Discovery (
CUPSManager.swift)InputSlot,EPIJ_FdSo(Epson),CNIJMediaSupply(Canon), or dynamically inspect*OpenUI *<Keyword>/Paper Source://Media Source:.PrinterQueue(e.g.trayKeyword: String?) soPrintEnginecan inject the appropriate key.name, e.g."2","12") and human-readable label (title, e.g."Cassette 1","Rear Paper Feed Slot"), decoding any PPD hex escapes (<2F>,<2E>).Phase 3: Update Inspector UI Selection Logic (
InspectorView.swift)trayPopitems withtitle = choice.titleandrepresentedObject = choice.name.select(trayPop, ...)to find matching items by eitherrepresentedObject as? String == valueortitle == value.push(into engine:), setengine.paperSourcetotrayPop.selectedItem?.representedObject as? String ?? trayPop.titleOfSelectedItem.Phase 4: Dynamic PPD Key Injection (
PrintEngine.swift)applyOptionalPPDKeys(to:):trayKeyword(defaulting to"InputSlot").extras[trayKey] = paperSource.Phase 5: Unit Testing & Verification
Tests/:*EPIJ_FdSochoices (Cassette 1,Cassette 2,Rear Paper Feed Slot).*CNIJMediaSupplychoices.["Auto"]when a queue has no tray options or an empty list.trayPopUI retainsAutorather than clearing to 0 items.swift testto ensure all tests pass without regression.