// 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 } } }