Architecture¶
KVitals is a KDE Plasma 6 widget (plasmoid) built around a data-driven metric pipeline. Sensor components feed raw values into a central store, which produces a flat list of typed metric objects. The views consume that list without knowing anything about sensor internals.
Data flow¶
Hardware discovery (contents/ui/models/HardwareDiscovery.qml)¶
KVitals separates hardware discovery from sensor interpretation:
- Hardware discovery: What devices and sensor nodes exist on this system.
- Sensor interpretation: What a sensor reading means, how it is polled, filtered, and formatted.
HardwareDiscovery.qml maintains exactly one SensorTreeModel and KDescendantsProxyModel per active QML context (one in the runtime widget, one while the configuration dialog is open). It listens to topology changes (rowsInserted, rowsRemoved, modelReset), debounces updates (350ms), and exposes a minimal generic query API:
count: Total number of registered sensors.revision: Reactive counter incremented when hardware topology changes.allSensorIds: Array of all discovered sensor ID strings.query(pattern): Returns array of matching{ id, name }objects.queryIds(pattern): Returns array of matching sensor ID strings.sensorExists(id): Fast boolean check.
HardwareDiscovery contains zero GPU, disk, fan, network, temperature, or battery specific logic.
Sensor modules (contents/ui/sensors/)¶
Each metric category has its own QML component. Components receive the shared HardwareDiscovery instance, query their relevant sensor IDs, and own all value polling, calculations, and domain heuristics.
| Module | Discovery query | Key exposed properties |
|---|---|---|
CpuSensors.qml |
Static (cpu/all/usage, averageFrequency) |
cpuValue, cpuNumericValue, cpuFreqValue |
MemorySensors.qml |
Static (memory/physical/used, total) |
ramValue, ramPercentValue, ramPercentage |
TempSensors.qml |
PATTERNS.TEMP_LMSENSORS + ISA Super I/O / SPD5118 filtering |
tempValue, cpuTempValue, ramTempValue, ramTempExists |
GpuSensors.qml |
PATTERNS.GPU (gpu/gpu\d+/usage) |
gpuDataList, discoveredGpus |
BatterySensors.qml |
PATTERNS.BATTERY + probe/qdbus fallback |
batValue, batNumericValue, powerValue |
NetworkSensors.qml |
PATTERNS.NETWORK_IFACE (network/[^/]+/download) |
netDownValue, netUpValue, netIpValue |
DiskSensors.qml |
PATTERNS.DISK_READ & PATTERNS.DISK_TEMP + Solid hotplug filter |
diskReadValue, diskWriteValue, diskDataList |
FanSensors.qml |
PATTERNS.FAN + alphabetical sort and max RPM check |
fanDataList, hasFanData, multiFan |
UptimeSensors.qml |
Static (os/system/uptime) |
uptimeValue |
Utils.qml is a singleton providing formatting helpers (formatBytes, formatRate, resolveColor) used by most sensor components.
Performance properties¶
- Exactly one
SensorTreeModelactive at runtime across the entire widget. - No subprocesses during normal execution.
- Reads from the same ksystemstats backend as the official KDE System Monitor.
- Disabling a sensor group stops all its subscriptions immediately.
Models layer (contents/ui/models/)¶
This layer sits between sensors and views and is where metric aggregation lives.
MetricDefinitions.js¶
A .pragma library file (shared singleton) holding:
GROUPS: Metadata per category (id, default label, icon, default sub-metrics).PATTERNS: Canonical discovery regex patterns (GPU,DISK_READ,DISK_TEMP,FAN,NETWORK_IFACE,TEMP_LMSENSORS,BATTERY).DEFINITIONS: One entry per metric keyed by"group.subKey". Each entry declares the sensor path, chart settings, threshold type/key, and direction prefix.MetricStore._createMetricmerges these definitions with runtime overrides from the sensor layer.
MetricConfig.qml¶
A QtObject that wraps every Plasmoid.configuration value and provides typed accessors used by both sensor components and MetricStore. Nothing outside this file reads Plasmoid.configuration directly. It owns:
- Group enable flags and
isGroupEnabled(group) - Sub-metric selection per group and
isSubMetricEnabled(group, subKey) - Visibility target per group (
"compact","widget","both") andisMetricVisible(group, subKey, view) - Labels, icons, and threshold values with fallback defaults
orderedKeys: The final display order, filled from the user'smetricOrdersetting with any missing groups appended fromMetricDefinitions.ALL_GROUP_KEYS.
MetricStore.qml¶
The central aggregator. On every sensor change it recomputes metrics, a readonly property holding a flat array of metric objects conforming to the Metric Contract.
MetricStore also manages chartHistory, a ring buffer (up to 60 samples) per chartKey, written by chartTimer at updateInterval ms.
Metric Contract¶
MetricStore serves as the normalization boundary between sensor modules and generic views:
Sensor QML Modules
↓
normalized metric
↓
MetricStore
↓
generic presentation (ViewHelpers / CompactView / FullView)
KVitals supports arbitrary metric sources as long as sensor modules normalize their data into the Metric Contract. Sensor components convert backend-specific structures (D-Bus objects, sysfs strings, boolean flags, or structured records) into numeric values or display strings before passing them to MetricStore:
D-Bus object / sysfs string / boolean state / structured backend value
↓
sensor-specific interpretation (polling, calculations, formatting)
↓
number or display string
↓
MetricStore (_createMetric normalization)
Supported Metric Categories¶
Every metric emitted by MetricStore falls into one of two categories:
1. Quantitative Metric¶
Represents a scalar numeric measurement (percentages, rates, temperatures, frequencies, capacity, RPM, power).
value: Finite numeric scalar (typeof value === "number" && isFinite(value)). Set toNaNwhen temporarily loading or unavailable.displayValue: Formatted string (e.g." 42%","55°C","16.4/32.0G").status:"ready","loading", or"unavailable".hasChart:trueifchartKeyis configured.chartKey: Buffer identifier inchartHistory.chartMax: Upper bound for chart scaling (0for auto-scale).- Thresholds: Warning and critical thresholds evaluate only against finite numeric values.
MetricStore accepts finite numeric scalars, including negative values. Whether negative values are meaningful depends on the metric's domain semantics (such as battery charge/discharge rates, energy flow, or temperature deltas); sensor modules and metric definitions determine the appropriate interpretation.
2. Display-Only Metric¶
Represents discrete text data without scalar telemetry (IP addresses, uptime strings).
value:NaN(explicit sentinel).displayValue: Formatted string (e.g."192.168.1.10","2d 4h 12m").status:"ready","loading", or"unavailable".hasChart:false(chartKey: "").- Thresholds: Not evaluated; retains base text color.
- Chart history: Never enters
chartHistory.
Normalization Rules¶
MetricStore._createMetric enforces these normalization guarantees:
- Finite number: Preserved as the quantitative
value(including negative numbers). - NaN: Preserved as the non-quantitative or unavailable sentinel.
- Infinity / -Infinity: Normalized to
NaN. - null / undefined: Normalized to
NaN(prevents coercion to 0). - boolean: Normalized to
NaN(prevents coercion to 0 or 1). - objects / arrays: Normalized to
NaN. - numeric strings: Strings are never coerced into quantitative numbers; they are rejected to
NaN. - displayValue: Always normalized to a
string.
Developer Examples¶
Example 1: Quantitative Metric (Swap Usage)¶
-
MetricDefinitions.js:
-
Sensor module (
MemorySensors.qml):Sensors.Sensor { id: swapUsedSensor sensorId: "memory/swap/used" updateRateLimit: root.updateInterval } Sensors.Sensor { id: swapTotalSensor sensorId: "memory/swap/total" updateRateLimit: root.updateInterval } readonly property real swapPercentage: { if (swapUsedSensor.status !== Sensors.Sensor.Ready || swapTotalSensor.status !== Sensors.Sensor.Ready) return NaN; if (swapTotalSensor.value <= 0) return NaN; return (swapUsedSensor.value / swapTotalSensor.value) * 100; } readonly property string swapValue: { if (isNaN(swapPercentage)) return "..."; return Math.round(swapPercentage) + "%"; } -
MetricStore integration (
MetricStore.qml): -
Normalized metric object in
MetricStore.metrics:{ id: "swap.usage", defId: "swap.usage", group: "ram", subKey: "swap", label: "RAM Swap Usage", groupLabel: "RAM", subLabel: "", prefix: "", icon: "nvidia-ram-symbolic", secondaryIcon: "", value: 24.5, displayValue: "25%", popupDisplay: "25%", rawString: "25%", color: "#ffffff", status: "ready", chartKey: "swap", chartMax: 100, hasChart: true, visibleInCompact: true, visibleInPopup: true }
Example 2: Display-Only Metric (Local IP)¶
-
MetricDefinitions.js:
-
Sensor module (
NetworkSensors.qml): -
MetricStore integration (
MetricStore.qml): -
Normalized metric object in
MetricStore.metrics:{ id: "net.ip", defId: "net.ip", group: "net", subKey: "ip", label: "NET Local IP", groupLabel: "NET", subLabel: "", prefix: "", icon: "network-wireless", secondaryIcon: "", value: NaN, displayValue: "192.168.1.10", popupDisplay: "192.168.1.10", rawString: "192.168.1.10", color: "#ffffff", status: "ready", chartKey: "", chartMax: 0, hasChart: false, visibleInCompact: false, visibleInPopup: true }
Architectural Roles¶
- HardwareDiscovery: "What sensors exist?" (topology and IDs)
- Sensor modules: "What does this sensor mean?" (polling, calculations, and domain formatting)
- MetricStore: "How do I normalize and expose this metric?" (contract enforcement, thresholds, chart history)
- ViewHelpers / Views: "How do I present the metric?" (generic layout and presentation)
ViewHelpers.js¶
A .pragma library file with two functions:
buildCompactItems(metricsList, orderedKeys): groups and orders metrics into compact panel items. Items are either{ icon, label, value, color, key }(single value) or{ icon, label, segments, color, key }(multi-value, used for net, disk, multi-fan).buildPopupItems(metricsList, orderedKeys): returns one row per visible popup metric:{ label, value, color, icon, chartKey, chartMax }.iconmay be a two-element array when a secondary icon is present.
Views¶
CompactView (panel)¶
A RowLayout with a Repeater driven by buildCompactItems. Each item renders as:
- Icon (
Kirigami.IconwithisMask: trueto match panel text color) - Label (e.g.
NET ↓:) - Value (e.g.
82.2 KB/s) - Separator (
|between groups)
displayMode controls icon/label/text visibility. layoutType switches between horizontal and vertical delegates.
FullView (popup)¶
A ColumnLayout with a Repeater driven by buildPopupItems. Each row shows a label, a bold value, and an optional sparkline chart drawn from MetricStore.chartHistory.
Tooltip¶
A static "KVitals" title only. Metrics are not duplicated into the tooltip.
Configuration system¶
config/main.xml <- config schema (keys, types, defaults)
config/config.qml <- tab registration
ui/configGeneral.qml <- display mode, layout, font, interval, units
ui/configMetrics.qml <- enable/disable, visibility, order, grouping, overrides
ui/configIcons.qml <- per-metric icon picker
ui/configColors.qml <- font color, warning/critical colors, thresholds
All values flow through MetricConfig.qml. Nothing in the sensor or view layer reads Plasmoid.configuration directly.
Adding new metrics or hardware categories¶
The data pipeline separates hardware polling, metric definitions, configuration, and presentation. How you add telemetry depends on whether you are extending an existing hardware category or introducing an entirely new one.
Workflow A: Adding a sub-metric to an existing group¶
This is the standard workflow for adding new readings to an existing module (such as adding Swap to Memory, Power to Battery, or VRAM to GPU).
For optional sub-metrics (disabled by default until selected by the user in settings), you do not need to modify HardwareDiscovery.qml, sensors/qmldir, main.qml, MetricConfig.qml, or the view components (CompactView.qml, FullView.qml).
-
Register in
MetricDefinitions.js(contents/ui/models/MetricDefinitions.js): Add a definition object underDEFINITIONS["<group>.<subKey>"]: -
Poll and format in the sensor module (
contents/ui/sensors/<Group>Sensors.qml): Subscribe to the required sensor path (viaSensors.SensororSensors.SensorDataModel) and expose reactive value properties (a numeric scalar and/or formatted string): -
Aggregate in
MetricStore.qml(contents/ui/models/MetricStore.qml): In themetricsproperty getter, push the normalized metric into the list using_createMetric:list.push(_createMetric("ram.swap", { value: s.memory.swapPercentage, displayValue: s.memory.swapValue, status: !isNaN(s.memory.swapPercentage) ? "ready" : "loading" }));_createMetricautomatically verifies the Metric Contract, checks visibility viaMetricConfig.isMetricVisible(), resolves threshold colors, and handles sparkline history buffering. -
Add UI toggle in
The configuration page dynamically creates the toggle checkbox and serializes the choice toconfigMetrics.qml(contents/ui/configMetrics.qml): Add the sub-metric entry tometricMeta[group].subs:cfg_<group>SubMetrics. -
(Optional) Enable by default for new installations: If the new sub-metric should be enabled out of the box on fresh installations:
- Add the key to
GROUPS[group].defaultSubMetricsincontents/ui/models/MetricDefinitions.js(which serves as the source of truth forMetricConfig.qmlandconfigMetrics.qml). - Update the static
<default>value for<group>SubMetricsincontents/config/main.xml(required by KDE's KConfig schema).
Views remain sensor-agnostic. ViewHelpers.js groups and routes the metric to CompactView and FullView automatically.
Workflow B: Adding a new hardware category / sensor module¶
Adding an entirely new category (such as NPU or Liquid Cooler) requires creating a dedicated sensor module and registering it across the architecture layers.
HardwareDiscovery.qml is generic and maintains a single SensorTreeModel query cache for the entire widget. You do not need to modify HardwareDiscovery.qml unless you need a new generic topology query method.
- Sensor layer (
contents/ui/sensors/): - Create
contents/ui/sensors/NewSensors.qml. Accept the shareddiscoveryproperty, query required sensors, and expose clean numeric and string properties. - Register the new component in
contents/ui/sensors/qmldir: -
In
contents/ui/main.qml, instantiateNewSensorsinside thesensorLoader.sourceComponentItem and expose an alias: -
Catalog and models layer (
contents/ui/models/): - In
MetricDefinitions.js:- Add group metadata to
GROUPS(id, defaultLabel, defaultIcon, defaultSubMetrics). - Add group ID to
ALL_GROUP_KEYS. - If the hardware relies on dynamic discovery patterns, add regex to
PATTERNS. - Add metric definitions to
DEFINITIONS.
- Add group metadata to
- In
contents/config/main.xml:- Add entries for
<group>Enabled,<group>SubMetrics,<group>Visibility,<group>Label,<group>Icon, and optional thresholds.
- Add entries for
- In
MetricConfig.qml:- Expose typed configuration properties:
readonly property bool newGroupEnabled: Plasmoid.configuration.newGroupEnabled, etc. - Add cases to
isGroupEnabled(),getGroupVisibility(),isSubMetricEnabled(),getGroupLabel(), andgetGroupIcon().
- Expose typed configuration properties:
-
In
MetricStore.qml:- Add a block in
metricspushing_createMetric("<group>.<subKey>", { ... })objects.
- Add a block in
-
Configuration UI layer (
contents/ui/): - In
configMetrics.qml:- Add
cfg_<group>*properties. - Add group ID to
allKeys. - Add category definition to
metricMeta. - Add switch cases to
iconFor(),subMetrics(),isEnabled(),setEnabled(),visibilityFor(),setVisibility(), andtoggleSubMetric().
- Add
- In
configIcons.qml:- Add
cfg_<group>Iconproperty, anIconDialog, a row inFormLayout, and a reset entry in the reset button handler.
- Add
-
In
configColors.qml(if quantitative thresholds apply):- Add threshold properties, sliders in the grid, and reset entries.
-
Views layer (
contents/ui/CompactView.qml,contents/ui/FullView.qml): - No view modifications required.
ViewHelpers.jsprocessesMetricStore.metricsand formats the compact panel items and popup rows automatically.
Project structure¶
kvitals/
├── metadata.json
├── install.sh
├── install-remote.sh
├── CHANGELOG.md
├── ROADMAP.md
├── docs/
│ ├── architecture.md
│ ├── configuration.md
│ ├── installation.md
│ ├── contributing.md
│ ├── troubleshooting.md
│ └── temp-sensor-logic.md
└── contents/
├── config/
│ ├── config.qml
│ └── main.xml
└── ui/
├── main.qml <- widget root, wires sensors to MetricStore
├── CompactView.qml
├── FullView.qml
├── configGeneral.qml
├── configMetrics.qml
├── configIcons.qml
├── configColors.qml
├── models/
│ ├── MetricDefinitions.js <- shared metric catalog
│ ├── MetricConfig.qml <- Plasmoid.configuration adapter
│ ├── MetricStore.qml <- flat metrics list + chart history
│ └── ViewHelpers.js <- grouping/ordering for each view
└── sensors/
├── qmldir
├── CpuSensors.qml
├── MemorySensors.qml
├── TempSensors.qml
├── GpuSensors.qml
├── BatterySensors.qml
├── NetworkSensors.qml
├── DiskSensors.qml
├── FanSensors.qml
├── UptimeSensors.qml
└── Utils.qml