mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
Vendor-neutral Quick Actions: driver profiles, generic fallback, ppdreport
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,8 +101,8 @@ final class PrinterStore {
|
||||
private(set) var revision = 0
|
||||
/// The server default printer (CUPS-Get-Default), if any.
|
||||
private(set) var defaultPrinter: String?
|
||||
/// Parsed PPDs, for enabling Quick Actions per printer. A queue with no PPD maps to nil.
|
||||
private(set) var ppds: [String: PPD?] = [:]
|
||||
/// Driver PPD + IPP attributes per queue, for enabling Quick Actions.
|
||||
private(set) var drivers: [String: DriverContext] = [:]
|
||||
var failure: Failure?
|
||||
|
||||
let client = CupsClient()
|
||||
@@ -190,31 +190,28 @@ final class PrinterStore {
|
||||
}
|
||||
|
||||
func noteChange(queue: String? = nil) {
|
||||
if let queue { ppds[queue] = nil }
|
||||
if let queue { drivers[queue] = nil }
|
||||
revision += 1
|
||||
}
|
||||
|
||||
// MARK: PPDs for Quick Actions
|
||||
// MARK: drivers for Quick Actions
|
||||
|
||||
/// Fetches PPDs not yet cached, in the background of a refresh.
|
||||
/// Loads driver info (PPD, IPP attributes) not yet cached, in the background of a refresh.
|
||||
func loadPPDs() async {
|
||||
for printer in printers where ppds[printer.name] == nil {
|
||||
let text = try? await client.getPPD(queue: printer.name)
|
||||
ppds[printer.name] = .some(text.map(PPD.init(text:)))
|
||||
for printer in printers where drivers[printer.name] == nil {
|
||||
if let context = try? await DriverContext.load(client: client, queue: printer.name) {
|
||||
drivers[printer.name] = context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// nil when the action can run on the queue, else why not (shown as the menu item's tooltip).
|
||||
func unavailableReason(_ action: QuickAction, queue: String) -> String? {
|
||||
if case .defaultPrinter = action.effect {
|
||||
if action.isDefaultPrinter {
|
||||
return defaultPrinter == queue ? "\(queue) is already the default printer" : nil
|
||||
}
|
||||
guard let cached = ppds[queue] else { return "Checking \(queue)’s PPD…" }
|
||||
if action.isAvailable(for: cached) { return nil }
|
||||
if case .failure(let unavailable) = action.changes(for: cached, value: action.input == .userCode ? "0" : nil) {
|
||||
return unavailable.reason
|
||||
}
|
||||
return "Not available on \(queue)"
|
||||
guard let context = drivers[queue] else { return "Checking \(queue)’s driver…" }
|
||||
return action.unavailableReason(in: context)
|
||||
}
|
||||
|
||||
// MARK: per-printer actions
|
||||
@@ -256,7 +253,7 @@ final class PrinterStore {
|
||||
report("ERROR: delete \(queue): \(error)")
|
||||
fail("Couldn’t delete \(queue)", String(describing: error))
|
||||
}
|
||||
ppds[queue] = nil
|
||||
drivers[queue] = nil
|
||||
noteChange()
|
||||
await refreshQuietly()
|
||||
return deleted
|
||||
|
||||
@@ -39,24 +39,34 @@ final class QuickActionModel: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var currentCode: String? { QuickAction.currentUserCode(state?.ppd) }
|
||||
var context: DriverContext {
|
||||
DriverContext(ppd: state?.ppd, attributes: state?.snapshot.attributes)
|
||||
}
|
||||
|
||||
var validationError: String? { action.validationError(code, ppd: state?.ppd) }
|
||||
var currentCode: String? { action.currentCode(context) }
|
||||
|
||||
var validationError: String? { action.validationError(code, context: context) }
|
||||
|
||||
/// What will be written, or why the action can't run here.
|
||||
func plannedChanges(clearing: Bool = false) -> Result<[OptionChange], QuickActionUnavailable> {
|
||||
action.changes(for: state?.ppd, value: clearing ? "" : code)
|
||||
action.resolve(context, value: clearing ? nil : code, clearing: clearing).map(\.changes)
|
||||
}
|
||||
|
||||
/// "Ricoh", "Generic (…)" — which driver profile supplied the keywords.
|
||||
var profileName: String? {
|
||||
guard !action.isDefaultPrinter, case .success(let resolved) = action.resolve(context, value: code.isEmpty ? "0" : code) else { return nil }
|
||||
return resolved.profile.name
|
||||
}
|
||||
|
||||
var command: String {
|
||||
if case .defaultPrinter = action.effect { return CupsTools.commandLine(CupsTools.lpadminPath, ["-d", queue]) }
|
||||
if action.isDefaultPrinter { return CupsTools.commandLine(CupsTools.lpadminPath, ["-d", queue]) }
|
||||
guard case .success(let changes) = plannedChanges() else { return "" }
|
||||
return OptionApplier.displayCommand(queue: queue, changes: changes)
|
||||
}
|
||||
|
||||
/// `lpstat` and apps use ~/.cups/lpoptions' Default line over the server default.
|
||||
var userDefaultNote: String? {
|
||||
guard case .defaultPrinter = action.effect,
|
||||
guard action.isDefaultPrinter,
|
||||
let text = try? String(contentsOf: LpOptionsFile.userFileURL, encoding: .utf8),
|
||||
let line = text.split(whereSeparator: \.isNewline).first(where: { $0.hasPrefix("Default ") }) else { return nil }
|
||||
let name = line.split(separator: " ")[1].split(separator: "/")[0]
|
||||
@@ -70,8 +80,8 @@ final class QuickActionModel: Identifiable {
|
||||
phase = .running
|
||||
let started = Date()
|
||||
do {
|
||||
let outcome = try await action.run(queue: queue, value: clearing ? "" : (action.input == .userCode ? code : nil),
|
||||
client: store.client)
|
||||
let outcome = try await action.run(queue: queue, value: action.input == .userCode && !clearing ? code : nil,
|
||||
clearing: clearing, client: store.client)
|
||||
readBack = Dictionary(uniqueKeysWithValues: outcome.readBack.map { ($0.key, $0) })
|
||||
state = try? await OptionState.load(client: store.client, queue: queue)
|
||||
let seconds = elapsedText(since: started)
|
||||
@@ -108,6 +118,11 @@ struct QuickActionSheet: View {
|
||||
.font(.title3.weight(.semibold))
|
||||
Text("\(model.queue) · saved as the queue default for every user on this Mac")
|
||||
.foregroundStyle(.secondary)
|
||||
if let driver = model.state.map({ _ in model.context.driverName }), let profile = model.profileName {
|
||||
Text("\(driver) · driver profile: \(profile)")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
switch model.phase {
|
||||
@@ -184,7 +199,7 @@ struct QuickActionSheet: View {
|
||||
.frame(height: 130)
|
||||
}
|
||||
|
||||
if case .defaultPrinter = model.action.effect {
|
||||
if model.action.isDefaultPrinter {
|
||||
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
|
||||
GridRow {
|
||||
Text("Server default").foregroundStyle(.secondary)
|
||||
|
||||
Reference in New Issue
Block a user