Files
cupsadmin/Sources/CupsKit/Format.swift
kfattic eba94aae4e cupsadmin: CLI, CupsKit, app in progress
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:13:07 -05:00

102 lines
3.8 KiB
Swift

import Foundation
public enum Format {
public static func printerState(_ state: Int?) -> String {
switch state {
case 3: return "idle"
case 4: return "processing"
case 5: return "stopped"
case let s?: return "state-\(s)"
case nil: return "?"
}
}
public static func jobState(_ state: Int?) -> String {
switch state {
case 3: return "pending"
case 4: return "held"
case 5: return "processing"
case 6: return "stopped"
case 7: return "canceled"
case 8: return "aborted"
case 9: return "completed"
case let s?: return "state-\(s)"
case nil: return "?"
}
}
/// CUPS reports most *-time attributes as integer seconds since the epoch.
public static func epoch(_ seconds: Int?) -> String {
guard let seconds, seconds > 0 else { return "-" }
return timestamp(Date(timeIntervalSince1970: TimeInterval(seconds)))
}
public static func timestamp(_ date: Date) -> String {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
return f.string(from: date)
}
public static func kilobytes(_ k: Int?) -> String {
guard let k else { return "-" }
if k < 1024 { return "\(k)K" }
return String(format: "%.1fM", Double(k) / 1024)
}
public static func yesNo(_ b: Bool?) -> String {
guard let b else { return "?" }
return b ? "yes" : "no"
}
/// state-reasons without the "none" placeholder.
public static func reasons(_ values: [String]) -> String {
let real = values.filter { $0 != "none" }
return real.isEmpty ? "none" : real.joined(separator: ",")
}
public static func value(_ value: IPPValue, attribute: String) -> String {
switch value {
case .integer(let i):
if attribute.hasSuffix("-time") || attribute.hasPrefix("time-at-"), i > 946_684_800 {
return "\(i) (\(epoch(Int(i))))"
}
return String(i)
case .enumeration(let e):
switch attribute {
case "printer-state": return printerState(Int(e))
case "job-state": return jobState(Int(e))
case "print-quality-default", "print-quality-supported":
return [3: "draft", 4: "normal", 5: "high"][e] ?? String(e)
default: return String(e)
}
case .boolean(let b): return b ? "true" : "false"
case .string(_, let s): return s
case .stringWithLanguage(_, let language, let s): return "\(s) [\(language)]"
case .octetString(let d):
if let s = String(data: d, encoding: .utf8), !s.contains(where: { $0.isASCII && $0.asciiValue! < 0x20 }) {
return s
}
return "<\(d.count) bytes>"
case .dateTime(let date): return timestamp(date)
case .resolution(let x, let y, let units):
let unit = units == 3 ? "dpi" : units == 4 ? "dpcm" : "units\(units)"
return x == y ? "\(x)\(unit)" : "\(x)x\(y)\(unit)"
case .range(let lower, let upper): return "\(lower)-\(upper)"
case .collection(let members):
let inner = members.map { m in
"\(m.name)=" + m.values.map { self.value($0, attribute: m.name) }.joined(separator: ",")
}
return "{" + inner.joined(separator: " ") + "}"
case .outOfBand(let tag):
switch tag {
case .unsupported: return "(unsupported)"
case .unknown: return "(unknown)"
case .noValue: return "(no-value)"
default: return "(out-of-band 0x\(String(tag.rawValue, radix: 16)))"
}
case .other(let tag, let d):
return "<tag 0x\(String(tag.rawValue, radix: 16)), \(d.count) bytes>"
}
}
}