[plasma/print-manager] src: libkcups/kded: Consolidate marker level helpers into KCupsPrinter

Mike Noe <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit c9aad2cacd0eb7baced440291bbb4bcb6ab8eca7 by Mike Noe.
Committed on 20/07/2026 at 13:50.
Pushed by noee into branch 'master'.

libkcups/kded: Consolidate marker level helpers into KCupsPrinter

And simplify the consumables checker logic

- Add markers* getters/helpers
- Use KCupsPrinter::marker-* helpers from kded
- Check negative level earlier
- Remove not-needed div-by-zero check and ratio calculation
- Compare level to CUPS-reported low value directly
- Clean up and expand debug prints a bit

M  +38   -204  src/kded/MarkerLevelChecker.cpp
M  +2    -1    src/kded/MarkerLevelChecker.h
M  +230  -28   src/libkcups/KCupsPrinter.cpp
M  +30   -4    src/libkcups/KCupsPrinter.h

https://invent.kde.org/plasma/print-manager/-/commit/c9aad2cacd0eb7baced440291bbb4bcb6ab8eca7

diff --git a/src/kded/MarkerLevelChecker.cpp b/src/kded/MarkerLevelChecker.cpp
index 3d9095fa..45405571 100644
--- a/src/kded/MarkerLevelChecker.cpp
+++ b/src/kded/MarkerLevelChecker.cpp
@@ -33,8 +33,38 @@ MarkerLevelChecker::MarkerLevelChecker(QObject *parent)
     connect(KCupsConnection::global(), &KCupsConnection::jobCreated, this, &MarkerLevelChecker::jobHandler);
 }
 
-void MarkerLevelChecker::checkMarkerLevels(const QString &printerName)
+void MarkerLevelChecker::checkMarkerLevels(const KCupsPrinter &printer)
 {
+    const auto msgs = printer.checkMarkerLevels();
+
+    if (!msgs.isEmpty()) {
+        auto notify = new KNotification(u"MarkerLevel"_s, KNotification::Persistent);
+        notify->setComponentName(u"printmanager"_s);
+        notify->setTitle(printer.info());
+        notify->setText(msgs.join(u"\n"_s));
+
+        auto checkMarkers = notify->addDefaultAction(i18nc("@action:button check printer ink levels", "Check Levels…"));
+        connect(checkMarkers, &KNotificationAction::activated, this, [pn = printer.name(), token = notify->xdgActivationToken().toUtf8()]() {
+            ProcessRunner::kcmConfigurePrinter(pn, token);
+        });
+
+        notify->sendEvent();
+    }
+}
+
+void MarkerLevelChecker::jobHandler([[maybe_unused]] const QString &text,
+                                    [[maybe_unused]] const QString &printerUri,
+                                    [[maybe_unused]] const QString &printerName,
+                                    [[maybe_unused]] uint printerState,
+                                    [[maybe_unused]] const QString &printerStateReasons,
+                                    [[maybe_unused]] bool printerIsAcceptingJobs,
+                                    [[maybe_unused]] uint jobId,
+                                    [[maybe_unused]] uint jobState,
+                                    [[maybe_unused]] const QString &jobStateReasons,
+                                    [[maybe_unused]] const QString &jobName,
+                                    [[maybe_unused]] uint jobImpressionsCompleted)
+{
+    qCDebug(PMKDED) << text << printerName << jobId << jobStateReasons;
     static const QStringList s_attrs({KCUPS_MARKER_NAMES,
                                       KCUPS_MARKER_LEVELS,
                                       KCUPS_MARKER_HIGH_LEVELS,
@@ -43,215 +73,19 @@ void MarkerLevelChecker::checkMarkerLevels(const QString &printerName)
                                       KCUPS_PRINTER_INFO,
                                       KCUPS_PRINTER_TYPE});
 
-    /**
-     *  Marker low level indicates the near empty warning value, but it can be 0.
-     *  Let's be more conservative and warn if actual level is at a higher pct
-     *  https://openprinting.github.io/cups/doc/spec-ipp.html#marker-low-levels
-     */
-    static const int s_threshold = 3;
-
-    qCDebug(PMKDED) << "checkMarkerLevels: getting printer attributes" << printerName;
-
     const auto request = new KCupsRequest;
     connect(request, &KCupsRequest::finished, this, [this](KCupsRequest *req) {
-        // let's make sure to not leave dangling requests
-        auto cleanupReq = qScopeGuard([req] {
-            req->deleteLater();
-        });
-
-        // it's possible attributes from a temporary queue could be empty
-        if (req->printers().isEmpty()) {
-            qCDebug(PMKDED) << "No printers found for marker level check";
-            return;
-        }
-
-        const auto printer = req->printers().at(0);
-
-        // QVariant::toList converts an int to a null list
-        // So, create a QList<int> if only one entry (int)
-        const auto getLevels = [printer](const QString &key) -> QList<QVariant> {
-            QList<QVariant> list;
-            const auto levels = printer.argument(key);
-            if (levels.isValid()) {
-                if (levels.canConvert<QList<int>>()) {
-                    list = levels.toList();
-                    qCDebug(PMKDED) << "Valid levels list" << key << list;
-                } else {
-                    list = QList<QVariant>{levels};
-                    qCDebug(PMKDED) << "Valid level value set to a list" << key << list;
-                }
-            } else {
-                // Valid levels entry not found
-                qCDebug(PMKDED) << "Invalid levels entry:" << key;
-            }
-
-            return list;
-        };
-
-        const auto currentLevels = getLevels(KCUPS_MARKER_LEVELS);
-        const auto lowLevels = getLevels(KCUPS_MARKER_LOW_LEVELS);
-        const auto highLevels = getLevels(KCUPS_MARKER_HIGH_LEVELS);
-        const auto typesList = printer.argument(KCUPS_MARKER_TYPES).toStringList();
-        const auto namesList = printer.argument(KCUPS_MARKER_NAMES).toStringList();
-
-        // All lists should have values for a valid marker check
-        if (currentLevels.isEmpty() || highLevels.isEmpty() || lowLevels.isEmpty() || typesList.isEmpty() || namesList.isEmpty()) {
-            qCWarning(PMKDED) << "At least one marker level attribute is empty, aborting level check" << currentLevels << lowLevels << highLevels << typesList
-                              << namesList;
-            return;
-        }
-
-        // We address all lists by index so they had better be of uniform length.
-        const auto levelCounts = {currentLevels.count(), lowLevels.count(), highLevels.count(), typesList.count(), namesList.count()};
-        const auto levelsMaxCount = std::max(levelCounts);
-        if (std::ranges::any_of(levelCounts, [levelsMaxCount](auto count) {
-                return count != levelsMaxCount;
-            })) {
-            QString message;
-            QDebug(&message) << "Marker level lists have different counts, aborting level check" << currentLevels << lowLevels << highLevels << typesList
-                             << namesList;
-            qCWarning(PMKDED) << message;
-            // This is a fairly serious problem we should inspect. Fail an assertion on it!
-            // Can be safely demoted when we have some more confidence that we understand why the check may fail.
-            Q_ASSERT_X(false, Q_FUNC_INFO, message.toUtf8().constData());
-            return;
-        }
-
-        /** CUPS supports devices with both types of markers, consumables and receptacles
-         *
-         *  https://openprinting.github.io/cups/doc/spec-ipp.html#marker-types
-         *
-         *  Consumables are generally toners/cartridges
-         *  levels decrease from 100 => 0 (high to low)
-         *
-         *  Receptacles are generally waste containters
-         *  levels increase from 0 => 100 (low to high)
-         */
-        QStringList msgs;
-        constexpr int empty = 0;
-        constexpr int full = 100;
-        // For each marker
-        for (uint i = 0; i < namesList.count(); ++i) {
-            int levelIndex = -1;
-            int checkerValue = 0;
-
-            const auto level = currentLevels.at(i).toInt();
-            const auto low = lowLevels.at(i).toInt();
-            const auto high = highLevels.at(i).toInt();
-            // per CUPS
-            // low-level == 0 && high-level != 0 && high-level < 100 => waste receptacle
-            // high-level == 100 && low-level != 100 && low-level >= 0 => consumable
-            bool consumable = false;
-            if (high == full && low != full && low >= empty) {
-                consumable = true;
-                qCDebug(PMKDED) << "Found consumable, checking" << typesList.at(i);
-            } else if (low == empty && high != empty && high < full) {
-                consumable = false;
-                qCDebug(PMKDED) << "Found receptacle, checking" << typesList.at(i);
-            } else {
-                qCDebug(PMKDED) << "low/high range invalid (" << low << "," << high << "), failure to determine marker type:" << typesList.at(i);
-                continue;
-            }
-
-            if (consumable) {
-                // Because printers, level can be 0 and low boundary can be > zero
-                // Also, level can be < low and not zero, ie. low=2, level=1
-                // level < 0 is unknown or unavailable, so ignore
-                if (level == empty || (level <= low && level > empty)) {
-                    levelIndex = i;
-                    if (level <= low) {
-                        checkerValue = level;
-                    }
-                } else {
-                    // CUPS seems to handle this, but just in case don't divide by zero
-                    if (high == empty) {
-                        qCWarning(PMKDED) << "Found a high boundary == 0";
-                        break;
-                    }
-                    if (level > low && level <= high) {
-                        auto result = div((level - low) * 100, high);
-                        if (result.quot <= s_threshold) {
-                            levelIndex = i;
-                            checkerValue = level;
-                        }
-                    }
-                }
-                // Receptacle
-            } else {
-                if (level >= high) {
-                    levelIndex = i;
-                    checkerValue = level;
-                }
-            }
-
-            // found a marker level at or below the threshold pct
-            if (levelIndex >= 0) {
-                const auto name = namesList.at(levelIndex);
-                const auto type = typesList.at(levelIndex);
-
-                if (consumable) {
-                    if (checkerValue == 0) {
-                        msgs << i18nc("@info:usagetip The name and type of the ink cartridge", "%1 (%2) appears to be empty.", name, type);
-                    } else {
-                        msgs << i18nc("@info:usagetip The name and type of the ink cartridge and percent ink remaining",
-                                      "%1 (%2) appears to be low (%3% remaining).",
-                                      name,
-                                      type,
-                                      checkerValue);
-                    }
-                } else {
-                    msgs << i18nc("@info:usagetip The name, type, and percentage full of the waste receptacle",
-                                  "%1 (%2) is almost full (%3%)",
-                                  name,
-                                  type,
-                                  checkerValue);
-                }
-                qCDebug(PMKDED) << "Found marker-level at threshold" << type << name << checkerValue;
-            }
-        }
-
-        if (!msgs.isEmpty()) {
-            auto notify = new KNotification(u"MarkerLevel"_s, KNotification::Persistent);
-            notify->setComponentName(u"printmanager"_s);
-            notify->setTitle(printer.info());
-            notify->setText(msgs.join(u"\n"_s));
-
-            auto checkMarkers = notify->addDefaultAction(i18nc("@action:button check printer ink levels", "Check Levels…"));
-            connect(checkMarkers, &KNotificationAction::activated, this, [pn = printer.name()]() {
-                ProcessRunner::kcmConfigurePrinter(pn);
-            });
-
-            notify->sendEvent();
+        if (!req->printers().isEmpty()) {
+            const auto printer = req->printers().at(0);
+            checkMarkerLevels(printer);
+        } else {
+            // it's possible attributes from a temporary queue could be empty
+            qCDebug(PMKDED) << "No printers found for checking";
         }
+        req->deleteLater();
     });
 
     request->getPrinterAttributes(printerName, false, s_attrs);
 }
 
-void MarkerLevelChecker::jobHandler(const QString &text,
-                                    const QString &printerUri,
-                                    const QString &printerName,
-                                    uint printerState,
-                                    const QString &printerStateReasons,
-                                    bool printerIsAcceptingJobs,
-                                    uint jobId,
-                                    uint jobState,
-                                    const QString &jobStateReasons,
-                                    const QString &jobName,
-                                    uint jobImpressionsCompleted)
-{
-    Q_UNUSED(text)
-    Q_UNUSED(printerUri)
-    Q_UNUSED(printerState)
-    Q_UNUSED(printerStateReasons)
-    Q_UNUSED(printerIsAcceptingJobs)
-    Q_UNUSED(jobId)
-    Q_UNUSED(jobState)
-    Q_UNUSED(jobStateReasons)
-    Q_UNUSED(jobName)
-    Q_UNUSED(jobImpressionsCompleted)
-
-    checkMarkerLevels(printerName);
-}
-
 #include "moc_MarkerLevelChecker.cpp"
diff --git a/src/kded/MarkerLevelChecker.h b/src/kded/MarkerLevelChecker.h
index 3d3ba41a..8f24653c 100644
--- a/src/kded/MarkerLevelChecker.h
+++ b/src/kded/MarkerLevelChecker.h
@@ -8,6 +8,7 @@
 
 #include <QObject>
 
+class KCupsPrinter;
 /**
  * Check for at least one marker level being below the threshold.
  *
@@ -27,7 +28,7 @@ public:
     ~MarkerLevelChecker() = default;
 
 private:
-    void checkMarkerLevels(const QString &printerName);
+    void checkMarkerLevels(const KCupsPrinter &printer);
 
     /**
      * @brief Generic handler for job signals
diff --git a/src/libkcups/KCupsPrinter.cpp b/src/libkcups/KCupsPrinter.cpp
index 1ef8f4bc..3705d1b5 100644
--- a/src/libkcups/KCupsPrinter.cpp
+++ b/src/libkcups/KCupsPrinter.cpp
@@ -1,10 +1,14 @@
 /*
     SPDX-FileCopyrightText: 2010-2018 Daniel Nicoletti <[email protected]>
+    SPDX-FileCopyrightText: 2026 Mike Noe <[email protected]>
 
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 
+#include "kcupslib_log.h"
+
 #include "KCupsPrinter.h"
+#include <KLocalizedString>
 
 KCupsPrinter::KCupsPrinter()
     : m_isClass(false)
@@ -17,11 +21,154 @@ KCupsPrinter::KCupsPrinter(const QString &printer, bool isClass)
 {
 }
 
-KCupsPrinter::KCupsPrinter(const QVariantMap &arguments)
-    : m_printer(arguments[QLatin1String(KCUPS_PRINTER_NAME)].toString())
-    , m_isClass(arguments[QLatin1String(KCUPS_PRINTER_TYPE)].toInt() & CUPS_PRINTER_CLASS)
-    , m_arguments(arguments)
+KCupsPrinter::KCupsPrinter(const QVariantMap &attributes)
+    : m_printer(attributes[KCUPS_PRINTER_NAME].toString())
+    , m_isClass(attributes[KCUPS_PRINTER_TYPE].toInt() & CUPS_PRINTER_CLASS)
+    , m_attributes(attributes)
+{
+}
+
+QStringList KCupsPrinter::checkMarkerLevels() const
 {
+    const auto currentLevels = markerLevels();
+    const auto lowLevels = markerLowLevels();
+    const auto highLevels = markerHighLevels();
+    const auto typesList = markerTypes();
+    const auto namesList = markerNames();
+
+    // All lists should have values for a valid marker check
+    if (currentLevels.isEmpty() || highLevels.isEmpty() || lowLevels.isEmpty() || typesList.isEmpty() || namesList.isEmpty()) {
+        qCWarning(LIBKCUPS) << "At least one marker level attribute is empty, aborting level check" << currentLevels << lowLevels << highLevels << typesList
+                            << namesList;
+        return {};
+    }
+
+    // We address all lists by index so they had better be of uniform length.
+    const auto levelCounts = {currentLevels.count(), lowLevels.count(), highLevels.count(), typesList.count(), namesList.count()};
+    const auto levelsMaxCount = std::max(levelCounts);
+    if (std::ranges::any_of(levelCounts, [levelsMaxCount](auto count) {
+            return count != levelsMaxCount;
+        })) {
+        QString message;
+        QDebug(&message) << "Marker level lists have different counts, aborting level check" << currentLevels << lowLevels << highLevels << typesList
+                         << namesList;
+        qCWarning(LIBKCUPS) << message;
+        // This is a fairly serious problem we should inspect. Fail an assertion on it!
+        // Can be safely demoted when we have some more confidence that we understand why the check may fail.
+        Q_ASSERT_X(false, Q_FUNC_INFO, message.toUtf8().constData());
+        return {};
+    }
+
+    QStringList msgs;
+    constexpr int empty = 0;
+    constexpr int nearEmpty = 3;
+    constexpr int full = 100;
+    // For each marker
+    for (uint i = 0; i < namesList.count(); ++i) {
+        // Per OpenPrinting, level < 0 indicates unknown or unavailable
+        const auto level = currentLevels.at(i);
+        if (level < empty) {
+            qCDebug(LIBKCUPS, "Invalid level (%d), ignore checking: %s, %s", level, qPrintable(typesList.at(i)), qPrintable(namesList.at(i)));
+            continue;
+        }
+
+        int levelIndex = -1;
+        int checkerValue = 0;
+
+        auto low = lowLevels.at(i);
+        auto high = highLevels.at(i);
+
+        /**
+         * Per OpenPrinting
+         * marker-low-levels = 0 AND marker-high-levels < 100 -> receptacle
+         * "near full" warning at marker-high-levels value
+         *
+         * marker-low-levels > 0 AND marker-high-levels = 100 -> consumable
+         * "near empty"/"low" warning at marker-low-levels value
+         *
+         * marker-low-levels = 0 AND marker-high-levels = 100 is a firmware bug
+         * assume marker-low-levels = 3 (nearEmpty) which turns it into a consumable
+         * and a "near empty"/"low" warning is issued when 3% of it is left.
+         *
+         * marker-low-levels > 0 AND marker-high-levels < 100 is also a firmware bug
+         * assume marker-high-levels = 100 which turns it into a consumable and a
+         * "near empty"/"low" warning is issued when its level reaches the marker-low-levels value
+         *
+         * This assures that in case of correct firmware with correct supply-level reporting
+         * everything is done exactly as the printer manufacturer expects it and in case of
+         * buggy firmware which reports supply level info which does not allow to determine
+         * whether a supply is a consumable or a receptacle we assume that it is a consumable
+         * as most supplies are consumables.
+         */
+        bool consumable = false;
+
+        // probable firmware bugs
+        if (low == empty && high == full) {
+            // default low, set as a consumable
+            qCDebug(LIBKCUPS, "%s: possible firmware bug (%d,%d): setting low-level to: %d", qPrintable(namesList.at(i)), low, high, nearEmpty);
+            low = nearEmpty;
+        } else if (low > empty && high < full) {
+            // default high, set as a consumable
+            qCDebug(LIBKCUPS, "%s: possible firmware bug (%d,%d): setting high-level to: %d", qPrintable(namesList.at(i)), low, high, full);
+            high = full;
+        }
+
+        // first, check for receptacle
+        if (low == empty && high < full) {
+            // receptacle
+            consumable = false;
+            qCDebug(LIBKCUPS, "Found receptacle: checking %s, current level %d => (%d,%d)", qPrintable(namesList.at(i)), level, low, high);
+            if (level >= high) {
+                levelIndex = i;
+                checkerValue = level;
+            }
+        } else if (low > empty && high == full) {
+            // consumable
+            consumable = true;
+            qCDebug(LIBKCUPS, "Found consumable: checking %s, current level %d => (%d,%d)", qPrintable(namesList.at(i)), level, low, high);
+            if (level <= low) {
+                levelIndex = i;
+                checkerValue = level;
+            }
+        } else {
+            qCDebug(LIBKCUPS, "low/high range invalid (%d,%d): failure to determine marker type for: %s", low, high, qPrintable(typesList.at(i)));
+            continue;
+        }
+
+        // found a marker level at or below the warning boundary
+        if (levelIndex >= 0) {
+            const auto name = namesList.at(levelIndex);
+            const auto type = typesList.at(levelIndex);
+
+            if (consumable) {
+                if (checkerValue == 0) {
+                    msgs << i18nc("@info:usagetip The name and type of the ink cartridge", "%1 (%2) appears to be empty.", name, type);
+                } else {
+                    msgs << i18nc("@info:usagetip The name and type of the ink cartridge and percent ink remaining",
+                                  "%1 (%2) appears to be low (%3% remaining).",
+                                  name,
+                                  type,
+                                  checkerValue);
+                }
+            } else {
+                msgs << i18nc("@info:usagetip The name, type, and percentage full of the waste receptacle",
+                              "%1 (%2) is almost full (%3%)",
+                              name,
+                              type,
+                              checkerValue);
+            }
+            qCDebug(LIBKCUPS, "Send level notify on: %s, %s, level=%d", qPrintable(type), qPrintable(name), checkerValue);
+        }
+    }
+
+    return msgs;
+}
+
+void KCupsPrinter::setAttribute(const QString &key, const QVariant &value)
+{
+    if (!key.isEmpty()) {
+        m_attributes[key] = value;
+    }
 }
 
 QString KCupsPrinter::name() const
@@ -36,32 +183,32 @@ bool KCupsPrinter::isClass() const
 
 bool KCupsPrinter::isDefault() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_TYPE)].toUInt() & CUPS_PRINTER_DEFAULT;
+    return m_attributes[KCUPS_PRINTER_TYPE].toUInt() & CUPS_PRINTER_DEFAULT;
 }
 
 bool KCupsPrinter::isShared() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_IS_SHARED)].toBool();
+    return m_attributes[KCUPS_PRINTER_IS_SHARED].toBool();
 }
 
 bool KCupsPrinter::isAcceptingJobs() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_IS_ACCEPTING_JOBS)].toBool();
+    return m_attributes[KCUPS_PRINTER_IS_ACCEPTING_JOBS].toBool();
 }
 
 cups_ptype_e KCupsPrinter::type() const
 {
-    return static_cast<cups_ptype_e>(m_arguments[QLatin1String(KCUPS_PRINTER_TYPE)].toUInt());
+    return static_cast<cups_ptype_e>(m_attributes[KCUPS_PRINTER_TYPE].toUInt());
 }
 
 QString KCupsPrinter::location() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_LOCATION)].toString();
+    return m_attributes[KCUPS_PRINTER_LOCATION].toString();
 }
 
 QString KCupsPrinter::info() const
 {
-    const QString printerInfo = m_arguments[QLatin1String(KCUPS_PRINTER_INFO)].toString();
+    const QString printerInfo = m_attributes[KCUPS_PRINTER_INFO].toString();
     if (printerInfo.isEmpty()) {
         return name();
     }
@@ -70,92 +217,147 @@ QString KCupsPrinter::info() const
 
 QString KCupsPrinter::makeAndModel() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_MAKE_AND_MODEL)].toString();
+    return m_attributes[KCUPS_PRINTER_MAKE_AND_MODEL].toString();
 }
 
 QStringList KCupsPrinter::commands() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_COMMANDS)].toStringList();
+    return m_attributes[KCUPS_PRINTER_COMMANDS].toStringList();
 }
 
 QStringList KCupsPrinter::memberNames() const
 {
-    return m_arguments[QLatin1String(KCUPS_MEMBER_NAMES)].toStringList();
+    return m_attributes[KCUPS_MEMBER_NAMES].toStringList();
 }
 
 QString KCupsPrinter::deviceUri() const
 {
-    return m_arguments[QLatin1String(KCUPS_DEVICE_URI)].toString();
+    return m_attributes[KCUPS_DEVICE_URI].toString();
 }
 
 QStringList KCupsPrinter::errorPolicy() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_ERROR_POLICY)].toStringList();
+    return m_attributes[KCUPS_PRINTER_ERROR_POLICY].toStringList();
 }
 
 QStringList KCupsPrinter::errorPolicySupported() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_ERROR_POLICY_SUPPORTED)].toStringList();
+    return m_attributes[KCUPS_PRINTER_ERROR_POLICY_SUPPORTED].toStringList();
 }
 
 QStringList KCupsPrinter::opPolicy() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_OP_POLICY)].toStringList();
+    return m_attributes[KCUPS_PRINTER_OP_POLICY].toStringList();
 }
 
 QStringList KCupsPrinter::opPolicySupported() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_OP_POLICY_SUPPORTED)].toStringList();
+    return m_attributes[KCUPS_PRINTER_OP_POLICY_SUPPORTED].toStringList();
 }
 
 QStringList KCupsPrinter::jobSheetsDefault() const
 {
-    return m_arguments[QLatin1String(KCUPS_JOB_SHEETS_DEFAULT)].toStringList();
+    return m_attributes[KCUPS_JOB_SHEETS_DEFAULT].toStringList();
 }
 
 QStringList KCupsPrinter::jobSheetsSupported() const
 {
-    return m_arguments[QLatin1String(KCUPS_JOB_SHEETS_SUPPORTED)].toStringList();
+    return m_attributes[KCUPS_JOB_SHEETS_SUPPORTED].toStringList();
 }
 
 QStringList KCupsPrinter::requestingUserNameAllowed() const
 {
-    return m_arguments[QLatin1String(KCUPS_REQUESTING_USER_NAME_ALLOWED)].toStringList();
+    return m_attributes[KCUPS_REQUESTING_USER_NAME_ALLOWED].toStringList();
 }
 
 QStringList KCupsPrinter::requestingUserNameDenied() const
 {
-    return m_arguments[QLatin1String(KCUPS_REQUESTING_USER_NAME_DENIED)].toStringList();
+    return m_attributes[KCUPS_REQUESTING_USER_NAME_DENIED].toStringList();
 }
 
 QStringList KCupsPrinter::authInfoRequired() const
 {
-    return m_arguments[QLatin1String(KCUPS_AUTH_INFO_REQUIRED)].toStringList();
+    return m_attributes[KCUPS_AUTH_INFO_REQUIRED].toStringList();
 }
 
 QString KCupsPrinter::uriSupported() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_URI_SUPPORTED)].toString();
+    return m_attributes[KCUPS_PRINTER_URI_SUPPORTED].toString();
 }
 
 KCupsPrinter::Status KCupsPrinter::state() const
 {
-    return static_cast<Status>(m_arguments[QLatin1String(KCUPS_PRINTER_STATE)].toUInt());
+    return static_cast<Status>(m_attributes[KCUPS_PRINTER_STATE].toUInt());
 }
 
 QString KCupsPrinter::stateMsg() const
 {
-    return m_arguments[QLatin1String(KCUPS_PRINTER_STATE_MESSAGE)].toString();
+    return m_attributes[KCUPS_PRINTER_STATE_MESSAGE].toString();
 }
 
 int KCupsPrinter::markerChangeTime() const
 {
-    return m_arguments[QLatin1String(KCUPS_MARKER_CHANGE_TIME)].toInt();
+    return m_attributes[KCUPS_MARKER_CHANGE_TIME].toInt();
+}
+
+QStringList KCupsPrinter::markerColors() const
+{
+    return m_attributes[KCUPS_MARKER_COLORS].toStringList();
+}
+
+QStringList KCupsPrinter::markerNames() const
+{
+    return m_attributes[KCUPS_MARKER_NAMES].toStringList();
+}
+
+QStringList KCupsPrinter::markerTypes() const
+{
+    return m_attributes[KCUPS_MARKER_TYPES].toStringList();
+}
+
+static QList<int> toIntList(const QVariant &val)
+{
+    if (val.isValid()) {
+        if (val.canConvert<QList<int>>()) {
+            return val.value<QList<int>>();
+        } else {
+            return QList<int>{val.value<int>()};
+        }
+    }
+    return {};
+}
+
+QList<int> KCupsPrinter::markerLowLevels() const
+{
+    return toIntList(m_attributes.value(KCUPS_MARKER_LOW_LEVELS));
+}
+
+QList<int> KCupsPrinter::markerHighLevels() const
+{
+    return toIntList(m_attributes.value(KCUPS_MARKER_HIGH_LEVELS));
+}
+
+QList<int> KCupsPrinter::markerLevels() const
+{
+    return toIntList(m_attributes.value(KCUPS_MARKER_LEVELS));
+}
+
+QVariantMap KCupsPrinter::markers() const
+{
+    const auto levels = markerLevels();
+    if (levels.isEmpty()) {
+        return {};
+    } else {
+        return {{KCUPS_MARKER_LEVELS, QVariant::fromValue(levels)},
+                {KCUPS_MARKER_COLORS, markerColors()},
+                {KCUPS_MARKER_NAMES, markerNames()},
+                {KCUPS_MARKER_TYPES, markerTypes()}};
+    }
 }
 
 QVariant KCupsPrinter::argument(const QString &name) const
 {
-    return m_arguments.value(name);
+    return m_attributes.value(name);
 }
 
 QIcon KCupsPrinter::icon() const
diff --git a/src/libkcups/KCupsPrinter.h b/src/libkcups/KCupsPrinter.h
index e78859bc..e59c93f4 100644
--- a/src/libkcups/KCupsPrinter.h
+++ b/src/libkcups/KCupsPrinter.h
@@ -1,5 +1,6 @@
 /*
     SPDX-FileCopyrightText: 2010-2012 Daniel Nicoletti <[email protected]>
+    SPDX-FileCopyrightText: 2026 Mike Noe <[email protected]>
 
     SPDX-License-Identifier: GPL-2.0-or-later
 */
@@ -50,27 +51,52 @@ public:
     int markerChangeTime() const;
     QVariant argument(const QString &name) const;
 
+    QVariantMap markers() const;
+    QList<int> markerLevels() const;
+    QList<int> markerLowLevels() const;
+    QList<int> markerHighLevels() const;
+    QStringList markerColors() const;
+    QStringList markerNames() const;
+    QStringList markerTypes() const;
+
     /**
      * Requires enum PrinterType to work properly
-     *
      */
     QIcon icon() const;
     static QIcon icon(cups_ptype_e type);
     QString iconName() const;
     static QString iconName(cups_ptype_e type);
 
+    /**
+     * @brief checkMarkerLevels - Check all marker levels for the printer
+     *  CUPS supports devices with both classes of markers, consumables and receptacles
+     *
+     *  https://openprinting.github.io/cups/doc/spec-ipp.html#marker-types
+     *
+     *  Consumables are generally toners/cartridges
+     *  levels decrease from full to empty with warning level at near-empty
+     *
+     *  Receptacles are generally waste containters
+     *  levels increase from empty to full with warning level at near-full
+     *
+     * @return list of message strings, one for each marker found at warning threshold
+     */
+    QStringList checkMarkerLevels() const;
+
 protected:
-    KCupsPrinter(const QVariantMap &arguments);
+    explicit KCupsPrinter(const QVariantMap &attributes);
+    void setAttribute(const QString &key, const QVariant &value);
 
 private:
     friend class KCupsRequest;
+    friend class PrinterModel;
 
     QString m_printer;
     bool m_isClass;
-    QVariantMap m_arguments;
+    QVariantMap m_attributes;
 };
 
-typedef QList<KCupsPrinter> KCupsPrinters;
+using KCupsPrinters = QList<KCupsPrinter>;
 Q_DECLARE_METATYPE(KCupsPrinters)
 Q_DECLARE_METATYPE(KCupsPrinter)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.