mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
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>
This commit is contained in:
@@ -300,7 +300,7 @@ struct QuickActionItems: View {
|
||||
ForEach(QuickAction.all) { action in
|
||||
let reason = store.unavailableReason(action, queue: queue)
|
||||
Button { choose(action) } label: {
|
||||
Label(action.title, systemImage: action.systemImage)
|
||||
Label(action.title(in: store.drivers[queue]), systemImage: action.systemImage)
|
||||
}
|
||||
.disabled(reason != nil)
|
||||
.help(reason ?? action.summary)
|
||||
|
||||
@@ -45,6 +45,9 @@ final class QuickActionModel: Identifiable {
|
||||
|
||||
var currentCode: String? { action.currentCode(context) }
|
||||
|
||||
/// "Use Letter Paper" or "Use Letter Paper, Fit to Nearest Size", depending on the driver.
|
||||
var title: String { action.title(in: state == nil ? nil : context).replacingOccurrences(of: "…", with: "") }
|
||||
|
||||
var validationError: String? { action.validationError(code, context: context) }
|
||||
|
||||
/// What will be written, or why the action can't run here.
|
||||
@@ -85,7 +88,7 @@ final class QuickActionModel: Identifiable {
|
||||
readBack = Dictionary(uniqueKeysWithValues: outcome.readBack.map { ($0.key, $0) })
|
||||
state = try? await OptionState.load(client: store.client, queue: queue)
|
||||
let seconds = elapsedText(since: started)
|
||||
let title = clearing ? "Cleared user code" : action.title.replacingOccurrences(of: "…", with: "")
|
||||
let title = clearing ? "Cleared user code" : self.title
|
||||
if outcome.succeeded {
|
||||
resultMessage = nil
|
||||
store.report("\(title) on \(queue) in \(seconds)")
|
||||
@@ -99,7 +102,7 @@ final class QuickActionModel: Identifiable {
|
||||
phase = .finished(succeeded: false)
|
||||
} catch {
|
||||
resultMessage = PrinterStore.explain(String(describing: error))
|
||||
store.report("ERROR: \(action.title) on \(queue): \(error)")
|
||||
store.report("ERROR: \(title) on \(queue): \(error)")
|
||||
phase = .finished(succeeded: false)
|
||||
}
|
||||
store.noteChange(queue: queue)
|
||||
@@ -114,7 +117,7 @@ struct QuickActionSheet: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label(model.action.title.replacingOccurrences(of: "…", with: ""), systemImage: model.action.systemImage)
|
||||
Label(model.title, systemImage: model.action.systemImage)
|
||||
.font(.title3.weight(.semibold))
|
||||
Text("\(model.queue) · saved as the queue default for every user on this Mac")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -52,8 +52,10 @@ public struct DriverProfile: Decodable, Identifiable {
|
||||
public let match: Match
|
||||
/// Action id -> alternatives, each a list of `keyword=value` pairs applied together.
|
||||
public let actions: [String: [[String]]]
|
||||
/// Action id -> why this driver can't do it (e.g. accounting codes set outside the PPD).
|
||||
public let unavailable: [String: String]
|
||||
|
||||
enum CodingKeys: String, CodingKey { case id, name, match, actions }
|
||||
enum CodingKeys: String, CodingKey { case id, name, match, actions, unavailable }
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@@ -65,6 +67,7 @@ public struct DriverProfile: Decodable, Identifiable {
|
||||
let raw = try c.decode([String: FlexiblePairs].self, forKey: .actions)
|
||||
for (action, pairs) in raw { actions[action] = pairs.alternatives }
|
||||
self.actions = actions
|
||||
unavailable = try c.decodeIfPresent([String: String].self, forKey: .unavailable) ?? [:]
|
||||
}
|
||||
|
||||
private struct FlexiblePairs: Decodable {
|
||||
|
||||
@@ -30,8 +30,8 @@ public struct QuickAction: Identifiable, Hashable {
|
||||
summary: "Two-sided, long edge"),
|
||||
QuickAction(id: "simplex", title: "Default to Single-Sided", systemImage: "doc", input: .none,
|
||||
summary: "One-sided"),
|
||||
QuickAction(id: "letter", title: "Use Letter Paper, Fit to Nearest Size", systemImage: "arrow.up.left.and.arrow.down.right",
|
||||
input: .none, summary: "Letter paper, and fit to the nearest size where the driver can (never prompt at the printer)"),
|
||||
QuickAction(id: "letter", title: "Use Letter Paper", systemImage: "arrow.up.left.and.arrow.down.right",
|
||||
input: .none, summary: "Letter paper, and fit to the nearest size where the driver has that option (never prompt at the printer)"),
|
||||
QuickAction(id: "default", title: "Make Default Printer", systemImage: "star", input: .none,
|
||||
summary: "Server default printer (lpadmin -d)"),
|
||||
]
|
||||
@@ -40,12 +40,25 @@ public struct QuickAction: Identifiable, Hashable {
|
||||
all.first { $0.id == id }
|
||||
}
|
||||
|
||||
/// The title for a queue: Use Letter Paper gains ", Fit to Nearest Size" only when its driver
|
||||
/// profile also sets a fit-to-paper option (e.g. Ricoh RIPaperPolicy), not just the page size.
|
||||
public func title(in context: DriverContext?) -> String {
|
||||
guard id == "letter", let context, case .success(let resolved) = resolve(context),
|
||||
resolved.changes.contains(where: { !Self.paperSizeKeys.contains($0.key) }) else { return title }
|
||||
return title + ", Fit to Nearest Size"
|
||||
}
|
||||
|
||||
static let paperSizeKeys: Set<String> = ["PageSize", "media-default"]
|
||||
|
||||
/// The profile and option changes this action would write on a queue, or why it can't.
|
||||
public func resolve(_ context: DriverContext, value: String? = nil, clearing: Bool = false,
|
||||
profiles: [DriverProfile] = DriverProfiles.builtIn) -> Result<ResolvedQuickAction, QuickActionUnavailable> {
|
||||
let key = clearing ? id + "-clear" : id
|
||||
var firstMissing: String?
|
||||
for profile in DriverProfiles.matching(context, in: profiles) {
|
||||
if profile.actions[key] == nil, let reason = profile.unavailable[id] {
|
||||
return .failure(QuickActionUnavailable(reason: reason))
|
||||
}
|
||||
guard let alternatives = profile.actions[key] else { continue }
|
||||
for pairs in alternatives {
|
||||
let parsed = pairs.compactMap(Self.parse)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"comment": "Quick Action mappings per driver. First matching profile that defines an action AND whose keyword=value pairs this queue supports wins; 'generic' (empty match) must stay last. An action is either one list of pairs (all applied together) or a list of alternative lists. {value} is replaced with the user's input. Find keywords with: cupsadmin ppdreport <queue>",
|
||||
"comment": "Quick Action mappings per driver. First matching profile that defines an action AND whose keyword=value pairs this queue supports wins; 'generic' (empty match) must stay last. An action is either one list of pairs (all applied together) or a list of alternative lists. {value} is replaced with the user's input. 'unavailable' maps an action id to the reason it can't be done with this driver (no later profile is tried). Find keywords with: cupsadmin ppdreport <queue | file.ppd>",
|
||||
"profiles": [
|
||||
{
|
||||
"id": "ricoh-m-c251fw",
|
||||
@@ -27,6 +27,33 @@
|
||||
"letter": ["PageSize=Letter", "RIPaperPolicy=NearestSizeAdjust"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "canon",
|
||||
"name": "Canon UFR II / LIPSLX / CARPS2",
|
||||
"match": { "manufacturer": "^Canon$" },
|
||||
"actions": {
|
||||
"color": ["CNColorMode=color"],
|
||||
"bw": ["CNColorMode=mono"],
|
||||
"duplex": ["CNDuplex=DuplexFront"],
|
||||
"simplex": ["CNDuplex=None"],
|
||||
"letter": ["PageSize=Letter"]
|
||||
},
|
||||
"unavailable": {
|
||||
"usercode": "Canon department IDs are set by the Canon driver, not the PPD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hp",
|
||||
"name": "HP",
|
||||
"match": { "manufacturer": "^HP$" },
|
||||
"actions": {
|
||||
"color": [["HPColorAsGray=False"], ["HPColorMode=colorsmart"]],
|
||||
"bw": [["HPColorAsGray=True"], ["HPColorMode=grayscale"]]
|
||||
},
|
||||
"unavailable": {
|
||||
"usercode": "HP drivers have no accounting code option"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "generic",
|
||||
"name": "Generic (standard PPD keywords, IPP Everywhere)",
|
||||
|
||||
Reference in New Issue
Block a user