summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml860
1 files changed, 860 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml b/raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml
new file mode 100644
index 0000000..a88655d
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Services/CupsService.qml
@@ -0,0 +1,860 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+
+Singleton {
+ id: root
+
+ property int refCount: 0
+
+ onRefCountChanged: {
+ if (refCount > 0) {
+ ensureSubscription();
+ } else if (refCount === 0 && DMSService.activeSubscriptions.includes("cups")) {
+ DMSService.removeSubscription("cups");
+ }
+ }
+
+ function ensureSubscription() {
+ if (refCount <= 0)
+ return;
+ if (!DMSService.isConnected)
+ return;
+ if (DMSService.activeSubscriptions.includes("cups"))
+ return;
+ if (DMSService.activeSubscriptions.includes("all"))
+ return;
+ DMSService.addSubscription("cups");
+ if (cupsAvailable) {
+ getState();
+ }
+ }
+
+ property var printerNames: []
+ property var printers: []
+ property string selectedPrinter: ""
+ property string expandedPrinter: ""
+
+ property bool cupsAvailable: false
+ property bool stateInitialized: false
+
+ property var devices: []
+ property var ppds: []
+ property var printerClasses: []
+
+ readonly property var filteredDevices: {
+ if (!devices || devices.length === 0)
+ return [];
+ const bareProtocols = ["ipp", "ipps", "http", "https", "lpd", "socket", "beh", "dnssd", "mdns", "smb", "file", "cups-brf"];
+
+ // First pass: filter out invalid/bare protocol entries
+ const validDevices = devices.filter(d => {
+ if (!d.uri)
+ return false;
+ const uriLower = d.uri.toLowerCase();
+ for (let proto of bareProtocols) {
+ if (uriLower === proto || uriLower === proto + ":")
+ return false;
+ }
+ if (d.class === "network" && d.info === "Backend Error Handler")
+ return false;
+ return true;
+ });
+
+ // Second pass: prefer IPP over LPD for the same printer
+ // _printer._tcp (LPD) doesn't work well with driverless printing
+ // _ipp._tcp or _ipps._tcp (IPP) should be preferred
+ const ippDeviceHosts = new Set();
+ for (const d of validDevices) {
+ if (!d.uri)
+ continue;
+ // Extract hostname from dnssd URIs like dnssd://Name%20[mac]._ipp._tcp.local
+ const ippMatch = d.uri.match(/dnssd:\/\/[^/]*\._ipps?\._tcp/);
+ if (ippMatch) {
+ // Extract the unique identifier (usually MAC address in brackets)
+ const macMatch = d.uri.match(/\[([a-f0-9]+)\]/i);
+ if (macMatch)
+ ippDeviceHosts.add(macMatch[1].toLowerCase());
+ }
+ }
+
+ // Filter out _printer._tcp devices when we have _ipp._tcp for the same printer
+ return validDevices.filter(d => {
+ if (!d.uri)
+ return true;
+ // If this is an LPD device, check if we have an IPP alternative
+ if (d.uri.includes("._printer._tcp")) {
+ const macMatch = d.uri.match(/\[([a-f0-9]+)\]/i);
+ if (macMatch && ippDeviceHosts.has(macMatch[1].toLowerCase())) {
+ return false; // Skip LPD device, we have IPP
+ }
+ }
+ return true;
+ });
+ }
+
+ function decodeUri(str) {
+ if (!str)
+ return "";
+ try {
+ return decodeURIComponent(str.replace(/\+/g, " "));
+ } catch (e) {
+ return str;
+ }
+ }
+
+ function getDeviceDisplayName(device) {
+ if (!device)
+ return "";
+ let name = "";
+ if (device.info && device.info.length > 0) {
+ name = decodeUri(device.info);
+ } else if (device.makeModel && device.makeModel.length > 0) {
+ name = decodeUri(device.makeModel);
+ } else {
+ return decodeUri(device.uri);
+ }
+ if (device.ip)
+ return name + " (" + device.ip + ")";
+ return name;
+ }
+
+ function getDeviceSubtitle(device) {
+ if (!device)
+ return "";
+ const parts = [];
+ switch (device.class) {
+ case "direct":
+ parts.push(I18n.tr("Local"));
+ break;
+ case "network":
+ parts.push(I18n.tr("Network"));
+ break;
+ case "file":
+ parts.push(I18n.tr("File"));
+ break;
+ default:
+ if (device.class)
+ parts.push(device.class);
+ }
+ if (device.location)
+ parts.push(decodeUri(device.location));
+ return parts.join(" • ");
+ }
+
+ function suggestPrinterName(device) {
+ if (!device)
+ return "";
+ let name = device.info || device.makeModel || "";
+ name = name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
+ return name.substring(0, 32) || "Printer";
+ }
+
+ function getMatchingPPDs(device) {
+ if (!device || !ppds || ppds.length === 0)
+ return [];
+ const isDnssd = device.uri && (device.uri.startsWith("dnssd://") || device.uri.startsWith("ipp://") || device.uri.startsWith("ipps://"));
+ if (isDnssd) {
+ const driverless = ppds.filter(p => p.name === "driverless" || p.name === "everywhere" || (p.makeModel && p.makeModel.toLowerCase().includes("driverless")));
+ if (driverless.length > 0)
+ return driverless;
+ }
+ if (!device.makeModel)
+ return [];
+ const makeModelLower = device.makeModel.toLowerCase();
+ const words = makeModelLower.split(/[\s_-]+/).filter(w => w.length > 2);
+ return ppds.filter(p => {
+ if (!p.makeModel)
+ return false;
+ const ppdLower = p.makeModel.toLowerCase();
+ return words.some(w => ppdLower.includes(w));
+ }).slice(0, 10);
+ }
+
+ property bool loadingDevices: false
+ property bool loadingPPDs: false
+ property bool loadingClasses: false
+ property bool creatingPrinter: false
+
+ signal cupsStateUpdate
+
+ readonly property string socketPath: Quickshell.env("DMS_SOCKET")
+
+ Component.onCompleted: {
+ if (socketPath && socketPath.length > 0) {
+ checkDMSCapabilities();
+ }
+ }
+
+ Connections {
+ target: DMSService
+
+ function onConnectionStateChanged() {
+ if (DMSService.isConnected) {
+ checkDMSCapabilities();
+ ensureSubscription();
+ }
+ }
+ }
+
+ Connections {
+ target: DMSService
+ enabled: DMSService.isConnected
+
+ function onCupsStateUpdate(data) {
+ console.log("CupsService: Subscription update received");
+ getState();
+ }
+
+ function onCapabilitiesChanged() {
+ checkDMSCapabilities();
+ }
+ }
+
+ function checkDMSCapabilities() {
+ if (!DMSService.isConnected)
+ return;
+ if (DMSService.capabilities.length === 0)
+ return;
+ cupsAvailable = DMSService.capabilities.includes("cups");
+
+ if (cupsAvailable && !stateInitialized) {
+ stateInitialized = true;
+ getState();
+ }
+ }
+
+ function getState() {
+ if (!cupsAvailable)
+ return;
+ DMSService.sendRequest("cups.getPrinters", null, response => {
+ if (response.result) {
+ updatePrinters(response.result);
+ fetchAllJobs();
+ }
+ });
+ }
+
+ function updatePrinters(printersData) {
+ printerNames = printersData.map(p => p.name);
+
+ let printersObj = {};
+ for (var i = 0; i < printersData.length; i++) {
+ let printer = printersData[i];
+ printersObj[printer.name] = {
+ "name": printer.name,
+ "uri": printer.uri || "",
+ "state": printer.state,
+ "stateReason": printer.stateReason,
+ "location": printer.location || "",
+ "info": printer.info || "",
+ "makeModel": printer.makeModel || "",
+ "accepting": printer.accepting !== false,
+ "jobs": []
+ };
+ }
+ printers = printersObj;
+
+ if (printerNames.length > 0) {
+ if (selectedPrinter.length > 0) {
+ if (!printerNames.includes(selectedPrinter)) {
+ selectedPrinter = printerNames[0];
+ }
+ } else {
+ selectedPrinter = printerNames[0];
+ }
+ }
+ }
+
+ function fetchAllJobs() {
+ for (var i = 0; i < printerNames.length; i++) {
+ fetchJobsForPrinter(printerNames[i]);
+ }
+ }
+
+ function fetchJobsForPrinter(printerName) {
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.getJobs", params, response => {
+ if (response.result && printers[printerName]) {
+ let updatedPrinters = Object.assign({}, printers);
+ updatedPrinters[printerName].jobs = response.result;
+ printers = updatedPrinters;
+ }
+ });
+ }
+
+ function getSelectedPrinter() {
+ return selectedPrinter;
+ }
+
+ function setSelectedPrinter(printerName) {
+ if (printerNames.length > 0) {
+ if (printerNames.includes(printerName)) {
+ selectedPrinter = printerName;
+ } else {
+ selectedPrinter = printerNames[0];
+ }
+ }
+ }
+
+ function getPrintersNum() {
+ if (!cupsAvailable)
+ return 0;
+
+ return printerNames.length;
+ }
+
+ function getPrintersNames() {
+ if (!cupsAvailable)
+ return [];
+
+ return printerNames;
+ }
+
+ function getTotalJobsNum() {
+ if (!cupsAvailable)
+ return 0;
+
+ var result = 0;
+ for (var i = 0; i < printerNames.length; i++) {
+ var printerName = printerNames[i];
+ if (printers[printerName] && printers[printerName].jobs) {
+ result += printers[printerName].jobs.length;
+ }
+ }
+ return result;
+ }
+
+ function getCurrentPrinterState() {
+ if (!cupsAvailable || !selectedPrinter)
+ return "";
+
+ var printer = printers[selectedPrinter];
+ return printer.state;
+ }
+
+ function getCurrentPrinterStatePrettyShort() {
+ if (!cupsAvailable || !selectedPrinter)
+ return "";
+
+ var printer = printers[selectedPrinter];
+ return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")";
+ }
+
+ function getCurrentPrinterStatePretty() {
+ if (!cupsAvailable || !selectedPrinter)
+ return "";
+
+ var printer = printers[selectedPrinter];
+ return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")";
+ }
+
+ function getCurrentPrinterJobs() {
+ if (!cupsAvailable || !selectedPrinter)
+ return [];
+
+ return getJobs(selectedPrinter);
+ }
+
+ function getJobs(printerName) {
+ if (!cupsAvailable)
+ return "";
+
+ var printer = printers[printerName];
+ return printer.jobs;
+ }
+
+ function getJobsNum(printerName) {
+ if (!cupsAvailable)
+ return 0;
+
+ var printer = printers[printerName];
+ return printer.jobs.length;
+ }
+
+ function pausePrinter(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.pausePrinter", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to pause printer"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function resumePrinter(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.resumePrinter", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to resume printer"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function cancelJob(printerName, jobID) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName,
+ "jobID": jobID
+ };
+
+ DMSService.sendRequest("cups.cancelJob", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to cancel selected job"), response.error);
+ } else {
+ fetchJobsForPrinter(printerName);
+ }
+ });
+ }
+
+ function purgeJobs(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.purgeJobs", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to cancel all jobs"), response.error);
+ } else {
+ fetchJobsForPrinter(printerName);
+ }
+ });
+ }
+
+ function getDevices() {
+ if (!cupsAvailable)
+ return;
+ loadingDevices = true;
+ DMSService.sendRequest("cups.getDevices", null, response => {
+ loadingDevices = false;
+ if (response.result) {
+ devices = response.result;
+ }
+ });
+ }
+
+ function getPPDs() {
+ if (!cupsAvailable)
+ return;
+ loadingPPDs = true;
+ DMSService.sendRequest("cups.getPPDs", null, response => {
+ loadingPPDs = false;
+ if (response.result) {
+ ppds = response.result;
+ }
+ });
+ }
+
+ function getClasses() {
+ if (!cupsAvailable)
+ return;
+ loadingClasses = true;
+ DMSService.sendRequest("cups.getClasses", null, response => {
+ loadingClasses = false;
+ if (response.result) {
+ printerClasses = response.result;
+ }
+ });
+ }
+
+ function testConnection(host, port, protocol, callback) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "host": host,
+ "port": port,
+ "protocol": protocol
+ };
+
+ DMSService.sendRequest("cups.testConnection", params, response => {
+ if (callback)
+ callback(response);
+ });
+ }
+
+ function createPrinter(name, deviceURI, ppd, options) {
+ if (!cupsAvailable)
+ return;
+ creatingPrinter = true;
+ const params = {
+ "name": name,
+ "deviceURI": deviceURI,
+ "ppd": ppd
+ };
+ if (options) {
+ if (options.shared !== undefined)
+ params.shared = options.shared;
+ if (options.location)
+ params.location = options.location;
+ if (options.information)
+ params.information = options.information;
+ if (options.errorPolicy)
+ params.errorPolicy = options.errorPolicy;
+ }
+
+ DMSService.sendRequest("cups.createPrinter", params, response => {
+ creatingPrinter = false;
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to create printer"), response.error);
+ } else {
+ ToastService.showInfo(I18n.tr("Printer created successfully"));
+ getState();
+ }
+ });
+ }
+
+ function deletePrinter(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.deletePrinter", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to delete printer"), response.error);
+ } else {
+ ToastService.showInfo(I18n.tr("Printer deleted"));
+ if (selectedPrinter === printerName) {
+ selectedPrinter = "";
+ }
+ getState();
+ }
+ });
+ }
+
+ function acceptJobs(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.acceptJobs", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to enable job acceptance"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function rejectJobs(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.rejectJobs", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to disable job acceptance"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function setPrinterShared(printerName, shared) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName,
+ "shared": shared
+ };
+
+ DMSService.sendRequest("cups.setPrinterShared", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to update sharing"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function setPrinterLocation(printerName, location) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName,
+ "location": location
+ };
+
+ DMSService.sendRequest("cups.setPrinterLocation", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to update location"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function setPrinterInfo(printerName, info) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName,
+ "info": info
+ };
+
+ DMSService.sendRequest("cups.setPrinterInfo", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to update description"), response.error);
+ } else {
+ getState();
+ }
+ });
+ }
+
+ function printTestPage(printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.printTestPage", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to print test page"), response.error);
+ } else {
+ ToastService.showInfo(I18n.tr("Test page sent to printer"));
+ fetchJobsForPrinter(printerName);
+ }
+ });
+ }
+
+ function moveJob(jobID, destPrinter) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "jobID": jobID,
+ "destPrinter": destPrinter
+ };
+
+ DMSService.sendRequest("cups.moveJob", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to move job"), response.error);
+ } else {
+ fetchAllJobs();
+ }
+ });
+ }
+
+ function restartJob(jobID) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "jobID": jobID
+ };
+
+ DMSService.sendRequest("cups.restartJob", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to restart job"), response.error);
+ } else {
+ fetchAllJobs();
+ }
+ });
+ }
+
+ function holdJob(jobID, holdUntil) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "jobID": jobID
+ };
+ if (holdUntil) {
+ params.holdUntil = holdUntil;
+ }
+
+ DMSService.sendRequest("cups.holdJob", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to hold job"), response.error);
+ } else {
+ fetchAllJobs();
+ }
+ });
+ }
+
+ function addPrinterToClass(className, printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "className": className,
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.addPrinterToClass", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to add printer to class"), response.error);
+ } else {
+ getClasses();
+ }
+ });
+ }
+
+ function removePrinterFromClass(className, printerName) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "className": className,
+ "printerName": printerName
+ };
+
+ DMSService.sendRequest("cups.removePrinterFromClass", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to remove printer from class"), response.error);
+ } else {
+ getClasses();
+ }
+ });
+ }
+
+ function deleteClass(className) {
+ if (!cupsAvailable)
+ return;
+ const params = {
+ "className": className
+ };
+
+ DMSService.sendRequest("cups.deleteClass", params, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Failed to delete class"), response.error);
+ } else {
+ getClasses();
+ }
+ });
+ }
+
+ function getPrinterData(printerName) {
+ if (!printers || !printers[printerName])
+ return null;
+ return printers[printerName];
+ }
+
+ function getJobStateTranslation(state) {
+ switch (state) {
+ case "pending":
+ return I18n.tr("Pending");
+ case "pending-held":
+ return I18n.tr("Held");
+ case "processing":
+ return I18n.tr("Processing");
+ case "processing-stopped":
+ return I18n.tr("Stopped");
+ case "canceled":
+ return I18n.tr("Canceled");
+ case "aborted":
+ return I18n.tr("Aborted");
+ case "completed":
+ return I18n.tr("Completed");
+ default:
+ return state;
+ }
+ }
+
+ readonly property var states: ({
+ "idle": I18n.tr("Idle"),
+ "processing": I18n.tr("Processing"),
+ "stopped": I18n.tr("Stopped")
+ })
+
+ readonly property var reasonsGeneral: ({
+ "none": I18n.tr("None"),
+ "other": I18n.tr("Other")
+ })
+
+ readonly property var reasonsSupplies: ({
+ "toner-low": I18n.tr("Toner Low"),
+ "toner-empty": I18n.tr("Toner Empty"),
+ "marker-supply-low": I18n.tr("Marker Supply Low"),
+ "marker-supply-empty": I18n.tr("Marker Supply Empty"),
+ "marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"),
+ "marker-waste-full": I18n.tr("Marker Waste Full")
+ })
+
+ readonly property var reasonsMedia: ({
+ "media-low": I18n.tr("Media Low"),
+ "media-empty": I18n.tr("Media Empty"),
+ "media-needed": I18n.tr("Media Needed"),
+ "media-jam": I18n.tr("Media Jam")
+ })
+
+ readonly property var reasonsParts: ({
+ "cover-open": I18n.tr("Cover Open"),
+ "door-open": I18n.tr("Door Open"),
+ "interlock-open": I18n.tr("Interlock Open"),
+ "output-tray-missing": I18n.tr("Output Tray Missing"),
+ "output-area-almost-full": I18n.tr("Output Area Almost Full"),
+ "output-area-full": I18n.tr("Output Area Full")
+ })
+
+ readonly property var reasonsErrors: ({
+ "paused": I18n.tr("Paused"),
+ "shutdown": I18n.tr("Shutdown"),
+ "connecting-to-device": I18n.tr("Connecting to Device"),
+ "timed-out": I18n.tr("Timed Out"),
+ "stopping": I18n.tr("Stopping"),
+ "stopped-partly": I18n.tr("Stopped Partly")
+ })
+
+ readonly property var reasonsService: ({
+ "spool-area-full": I18n.tr("Spool Area Full"),
+ "cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"),
+ "cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning")
+ })
+
+ readonly property var reasonsConnectivity: ({
+ "offline-report": I18n.tr("Offline Report"),
+ "moving-to-paused": I18n.tr("Moving to Paused")
+ })
+
+ readonly property var severitySuffixes: ({
+ "-error": I18n.tr("Error"),
+ "-warning": I18n.tr("Warning"),
+ "-report": I18n.tr("Report")
+ })
+
+ function getPrinterStateTranslation(state) {
+ return states[state] || state;
+ }
+
+ function getPrinterStateReasonTranslation(reason) {
+ let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity);
+
+ let basReason = reason;
+ let suffix = "";
+
+ for (let s in severitySuffixes) {
+ if (reason.endsWith(s)) {
+ basReason = reason.slice(0, -s.length);
+ suffix = severitySuffixes[s];
+ break;
+ }
+ }
+
+ let translation = allReasons[basReason] || basReason;
+ return suffix ? translation + " (" + suffix + ")" : translation;
+ }
+}