Files
Panama/config/dot/quickshell/modules/settings/PrintersPage.qml
T
Gabriel Brown e9d567aa72 Own printers, driverless only
The fourth panel this desktop handed to GNOME Settings, and the last one
worth owning.

Driverless only, deliberately. Adding a printer that describes its own
capabilities over IPP is supported; choosing a PPD or fetching a vendor
driver is not, and the page says so rather than pretending. That
restraint is the whole design: a wrong driver produces a printer that
accepts jobs, reports success, and prints nothing, which is the worst
failure this page could ship because it looks like it worked. A printer
old enough to need a PPD stays a job for the system printer tool.

Printers and the queue are separate cards because they answer separate
questions. Which printers exist is one; where a document went is the
other, and it is the one that actually brings someone here -- so the
queue is a single list across every printer.

Device URIs are validated by scheme before reaching CUPS, whose backends
run as root. file: and pipe: do not lead to a printer and are refused
here rather than further down.

This machine has no printer, so the page was built against a temporary
CUPS queue that was created, exercised through the service, and removed;
the service was confirmed to observe the removal rather than merely
perform it. Discovery and the driverless add path are verified by their
refusals rather than against hardware.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 15:07:17 -04:00

354 lines
14 KiB
QML

// Printers, and one queue across all of them.
//
// The list answers "which printers exist"; the queue answers "where is my
// document", which is the question that actually brings someone here. They are
// separate cards because they are separate questions -- a job that has not come
// out is not necessarily a problem with the printer it was sent to.
//
// Driverless only. A printer old enough to need a PPD is named as such rather
// than offered and then failing at the moment it is added.
import Quickshell
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
objectName: "printers"
title: "Printers"
lede: "Printers this machine can use, and what they are waiting on."
property string expandedPrinter: ""
property string confirmingRemoval: ""
property bool addingByAddress: false
property string manualUri: ""
property string manualName: ""
readonly property bool manualReady: /^(ipp|ipps|socket):\/\/\S+$/.test(root.manualUri)
&& root.manualName.trim() !== ""
Component.onCompleted: Printers.refresh()
TextRow {
visible: Printers.lastError !== ""
label: "Printing needs attention"
detail: Printers.lastError
value: ""
divider: false
}
// ── Which printers exist ─────────────────────────────────────────────────
SettingsCard {
visible: Printers.anyPrinters
title: "Printers"
subtitle: "Open one to change what it does by default."
Repeater {
model: Printers.printers
delegate: Column {
id: printerBlock
required property var modelData
required property int index
readonly property string name: String(printerBlock.modelData.name ?? "")
readonly property bool open: root.expandedPrinter === printerBlock.name
readonly property bool paused: String(printerBlock.modelData.state ?? "") === "stopped"
width: parent.width
SettingRow {
width: printerBlock.width
icon: printerBlock.paused ? "\u{F0026}" : "\u{F042A}"
label: String(printerBlock.modelData.description ?? printerBlock.name)
+ (printerBlock.modelData.isDefault ? " · Default" : "")
detail: Printers.stateSummary(printerBlock.modelData)
+ " · " + String(printerBlock.modelData.uri ?? "")
value: Printers.jobsFor(printerBlock.name) + " job"
+ (Printers.jobsFor(printerBlock.name) === 1 ? "" : "s")
activatable: !Printers.busy
divider: !printerBlock.open
&& printerBlock.index < Printers.printers.length - 1
onActivated: {
root.confirmingRemoval = "";
root.expandedPrinter = printerBlock.open ? "" : printerBlock.name;
}
}
Column {
width: printerBlock.width
visible: printerBlock.open
TextRow {
width: printerBlock.width
label: "Model"
detail: "Reported by the printer itself"
value: String(printerBlock.modelData.makeAndModel ?? "Unknown")
}
ActionRow {
width: printerBlock.width
visible: !printerBlock.modelData.isDefault
label: "Use by default"
detail: "Applications print here unless they are told otherwise"
action: "Make default"
enabled: !Printers.busy
onTriggered: Printers.setDefault(printerBlock.name)
}
ActionRow {
width: printerBlock.width
label: printerBlock.paused ? "Resume printing" : "Pause printing"
detail: printerBlock.paused
? "Queued jobs start again"
: "Jobs keep queuing, but nothing is sent until it resumes"
action: printerBlock.paused ? "Resume" : "Pause"
enabled: !Printers.busy
onTriggered: printerBlock.paused
? Printers.resume(printerBlock.name)
: Printers.pause(printerBlock.name)
}
ActionRow {
width: printerBlock.width
label: "Print a test page"
detail: "Confirms the printer answers and puts ink on paper"
action: "Print"
enabled: !Printers.busy && !printerBlock.paused
onTriggered: Printers.testPage(printerBlock.name)
}
SettingRow {
width: printerBlock.width
label: "Remove this printer"
detail: root.confirmingRemoval === printerBlock.name
? "Anything still queued for it is cancelled."
: "It can be added again later"
controlWidth: 200
divider: printerBlock.index < Printers.printers.length - 1
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
text: root.confirmingRemoval === printerBlock.name ? "Keep" : "Remove…"
enabled: !Printers.busy
onClicked: root.confirmingRemoval =
root.confirmingRemoval === printerBlock.name ? "" : printerBlock.name
}
SettingsButton {
visible: root.confirmingRemoval === printerBlock.name
text: "Remove"
tone: "danger"
enabled: !Printers.busy
onClicked: {
root.confirmingRemoval = "";
root.expandedPrinter = "";
Printers.remove(printerBlock.name);
}
}
}
}
}
}
}
}
// ── Where my document is ─────────────────────────────────────────────────
SettingsCard {
visible: Printers.anyPrinters
title: "Print queue"
subtitle: Printers.jobs.length === 0
? "Nothing is waiting."
: "Everything waiting, across every printer."
Repeater {
model: Printers.jobs
delegate: ActionRow {
id: jobRow
required property var modelData
required property int index
width: parent.width
label: String(jobRow.modelData.name ?? "Untitled")
detail: String(jobRow.modelData.printer ?? "") + " · "
+ String(jobRow.modelData.state ?? "")
+ (Number(jobRow.modelData.pages ?? 0) > 0
? " · " + jobRow.modelData.pages + " pages" : "")
action: "Cancel"
enabled: !Printers.busy
divider: jobRow.index < Printers.jobs.length - 1
onTriggered: Printers.cancel(Number(jobRow.modelData.id))
}
}
TextRow {
visible: Printers.jobs.length === 0
label: "Queue is empty"
detail: "Jobs appear here while they wait, and can be cancelled from here"
value: ""
divider: false
}
}
// ── Nothing set up yet ───────────────────────────────────────────────────
SettingsCard {
visible: Printers.scanned && !Printers.anyPrinters
title: "No printers yet"
subtitle: "Printers that announce themselves on your network appear here on their own."
ActionRow {
label: "Search the network"
detail: Printers.searching
? "Listening for printers that announce themselves…"
: (Printers.searched
? Printers.addable.length + " found"
: "Looks for printers over mDNS, the same way phones and laptops find them")
action: Printers.searching ? "Searching…" : "Search"
enabled: !Printers.searching
divider: false
onTriggered: Printers.search()
}
}
// ── Adding ───────────────────────────────────────────────────────────────
SettingsCard {
title: "Add a printer"
subtitle: "Driverless printers only. One that needs a manufacturer driver has to be set up with the system printer tool."
ActionRow {
visible: Printers.anyPrinters
label: "Search the network"
detail: Printers.searching
? "Listening for printers that announce themselves…"
: (Printers.searched
? Printers.addable.length + " printer"
+ (Printers.addable.length === 1 ? "" : "s") + " found that are not set up here"
: "Looks for printers over mDNS")
action: Printers.searching ? "Searching…" : "Search"
enabled: !Printers.searching
onTriggered: Printers.search()
}
Repeater {
model: Printers.addable
delegate: ActionRow {
id: foundRow
required property var modelData
width: parent.width
label: String(foundRow.modelData.name ?? "")
detail: foundRow.modelData.driverless === true
? String(foundRow.modelData.uri ?? "") + " · driverless"
: String(foundRow.modelData.uri ?? "") + " · needs a manufacturer driver"
action: foundRow.modelData.driverless === true ? "Add" : "Not supported"
enabled: !Printers.busy && foundRow.modelData.driverless === true
onTriggered: Printers.add(String(foundRow.modelData.uri ?? ""),
Printers.suggestedName(String(foundRow.modelData.name ?? "")))
}
}
TextRow {
visible: Printers.searched && Printers.addable.length === 0 && !Printers.searching
label: "Nothing found"
detail: "No printer announced itself. It may be asleep, on another network, or may not support network discovery."
value: ""
}
ActionRow {
label: "Add by address"
detail: "For a printer that does not announce itself"
action: root.addingByAddress ? "Cancel" : "Enter…"
enabled: !Printers.busy
divider: root.addingByAddress
onTriggered: {
root.addingByAddress = !root.addingByAddress;
root.manualUri = "";
root.manualName = "";
}
}
Column {
width: parent.width
visible: root.addingByAddress
TextFieldRow {
width: parent.width
label: "Address"
detail: "ipp://, ipps://, or socket:// followed by the printer's host"
placeholder: "ipp://printer.local/ipp/print"
text: root.manualUri
onAccepted: value => root.manualUri = value
}
TextFieldRow {
width: parent.width
label: "Name"
detail: "What this printer is called on this machine"
placeholder: "office-printer"
text: root.manualName
onAccepted: value => root.manualName = value
}
ActionRow {
width: parent.width
label: "Add this printer"
detail: "The printer is asked what it can do; if it cannot answer, it is not added"
action: "Add"
enabled: root.manualReady && !Printers.busy
divider: false
onTriggered: {
Printers.add(root.manualUri, Printers.suggestedName(root.manualName));
root.addingByAddress = false;
}
}
}
}
// ── The service underneath ───────────────────────────────────────────────
SettingsCard {
title: "Printing service"
subtitle: "What has to be running for any of this to work."
TextRow {
label: "CUPS"
detail: Printers.service?.running === true
? "Running, so a printer added here works immediately"
: "Not running, so nothing can print"
value: Printers.service?.running === true ? "Running" : "Stopped"
}
TextRow {
label: "Starts at boot"
detail: Printers.service?.startsAtBoot === true
? "Started with the system"
: "Started on demand, when something prints. This is a normal configuration."
value: Printers.service?.startsAtBoot === true ? "Yes" : "On demand"
}
TextRow {
label: "Network discovery"
detail: Printers.service?.discoveryAvailable === true
? "Avahi is running, which is what finds printers that announce themselves"
: "Avahi is not running, so network printers cannot be discovered"
value: Printers.service?.discoveryAvailable === true ? "Available" : "Unavailable"
divider: false
}
}
}