Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions system-monitor/LICENSE

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions system-monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# System Monitor

System Monitor combines seven host metrics into one compact Noctalia bar capsule: CPU usage, CPU temperature, RAM, swap, disk usage, download rate, and upload rate. Subtle dividers separate processor, memory, storage, and network groups.

## Plugin

| Field | Value |
| --- | --- |
| ID | `tmelik/system-monitor` |
| Entries | Bar widget: `summary` |
| License | GPL-3.0-only |

## Requirements

- No external programs or libraries.
- Noctalia v5 with plugin API 16 or newer.
- The Noctalia system monitor service must be enabled.

## Usage

1. Open Noctalia Settings.
2. Go to **Bar**, add a plugin widget, and select **System Monitor**.
3. Place the widget in the desired section of the bar.

Left-click the capsule to open the **System** tab in Control Center. Hover it to see the full metric names and values.

Middle-click the capsule to open its settings. Each metric can be shown or hidden independently. You can also toggle category separators and the detailed tooltip, choose compact or explicit network units, and change the monitored disk path. The default path is the root filesystem (`/`).

Unavailable sensors are displayed as an em dash instead of a misleading zero. Network rates are totals across active non-loopback interfaces. RAM uses a distinct server-module glyph so it is easy to tell apart from CPU at a glance.

## Settings

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `show_cpu_usage` | `bool` | `true` | Show aggregate CPU usage. |
| `show_cpu_temperature` | `bool` | `true` | Show CPU temperature when a sensor is available. |
| `show_ram` | `bool` | `true` | Show RAM utilization. |
| `show_swap` | `bool` | `true` | Show swap utilization. |
| `show_disk` | `bool` | `true` | Show utilization for `disk_path`. |
| `show_download` | `bool` | `true` | Show aggregate receive rate. |
| `show_upload` | `bool` | `true` | Show aggregate transmit rate. |
| `show_separators` | `bool` | `true` | Separate non-empty processor, memory, storage, and network groups. |
| `show_tooltip` | `bool` | `true` | Show expanded metric names and values on hover. |
| `compact_network` | `bool` | `true` | Use `M/s`-style labels; disable for `MB/s`-style labels. |
| `disk_path` | `string` | `/` | Select the filesystem whose utilization is displayed. |

## Notes

- The plugin only reads snapshots exposed by Noctalia through `systemStats()` and `diskStats()`.
- It does not access the network, execute processes, read arbitrary files, or write data.
- Values refresh according to Noctalia's system-monitor sampling intervals; the widget redraws once per second.
- If every metric is disabled, the capsule remains accessible and prompts the user to enable a metric.
85 changes: 85 additions & 0 deletions system-monitor/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
id = "tmelik/system-monitor"
name = "System Monitor"
version = "1.2.0"
plugin_api = 16
author = "TMelik"
license = "GPL-3.0-only"
dependencies = []
tags = ["bar", "indicator", "system"]
icon = "cpu"
description = "A compact bar widget combining CPU, temperature, memory, disk, and network activity."

[[widget]]
id = "summary"
entry = "widget.luau"

[widget.actions]
left = "panel-toggle control-center system"

[[widget.setting]]
key = "show_cpu_usage"
type = "bool"
label_key = "settings.show_cpu_usage.label"
default = true

[[widget.setting]]
key = "show_cpu_temperature"
type = "bool"
label_key = "settings.show_cpu_temperature.label"
default = true

[[widget.setting]]
key = "show_ram"
type = "bool"
label_key = "settings.show_ram.label"
default = true

[[widget.setting]]
key = "show_swap"
type = "bool"
label_key = "settings.show_swap.label"
default = true

[[widget.setting]]
key = "show_disk"
type = "bool"
label_key = "settings.show_disk.label"
default = true

[[widget.setting]]
key = "show_download"
type = "bool"
label_key = "settings.show_download.label"
default = true

[[widget.setting]]
key = "show_upload"
type = "bool"
label_key = "settings.show_upload.label"
default = true

[[widget.setting]]
key = "show_separators"
type = "bool"
label_key = "settings.show_separators.label"
default = true

[[widget.setting]]
key = "show_tooltip"
type = "bool"
label_key = "settings.show_tooltip.label"
default = true

[[widget.setting]]
key = "compact_network"
type = "bool"
label_key = "settings.compact_network.label"
description_key = "settings.compact_network.description"
default = true

[[widget.setting]]
key = "disk_path"
type = "string"
label_key = "settings.disk_path.label"
description_key = "settings.disk_path.description"
default = "/"
Binary file added system-monitor/thumbnail.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions system-monitor/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"settings": {
"show_cpu_usage": { "label": "CPU usage" },
"show_cpu_temperature": { "label": "CPU temperature" },
"show_ram": { "label": "RAM usage" },
"show_swap": { "label": "Swap usage" },
"show_disk": { "label": "Disk usage" },
"show_download": { "label": "Download rate" },
"show_upload": { "label": "Upload rate" },
"show_separators": { "label": "Category separators" },
"show_tooltip": { "label": "Detailed tooltip" },
"compact_network": {
"label": "Compact network units",
"description": "Use short labels such as M/s instead of MB/s."
},
"disk_path": {
"label": "Disk path",
"description": "Filesystem path whose disk usage should be displayed."
}
}
}
199 changes: 199 additions & 0 deletions system-monitor/widget.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
--!nonstrict

local function formatPercent(value)
if type(value) ~= "number" then
return "—"
end

return string.format("%.0f%%", value)
end

local function formatTemperature(value)
if type(value) ~= "number" then
return "—"
end

return string.format("%.0f°C", value)
end

local function formatRate(value, compact)
if type(value) ~= "number" then
return "—"
end

if value >= 1000000000 then
return string.format(compact and "%.1fG/s" or "%.1f GB/s", value / 1000000000)
elseif value >= 1000000 then
return string.format(compact and "%.1fM/s" or "%.1f MB/s", value / 1000000)
elseif value >= 1000 then
return string.format(compact and "%.1fk/s" or "%.1f kB/s", value / 1000)
end

return string.format(compact and "%.0fB/s" or "%.0f B/s", value)
end

local function configBool(key, fallback)
local value = noctalia.getConfig(key)
if type(value) == "boolean" then
return value
end

return fallback
end

local function configString(key, fallback)
local value = noctalia.getConfig(key)
if type(value) == "string" and value ~= "" then
return value
end

return fallback
end

local function swapPercent(swap)
if type(swap) ~= "table"
or type(swap.usedMb) ~= "number"
or type(swap.totalMb) ~= "number"
or swap.totalMb <= 0 then
return nil
end

return swap.usedMb / swap.totalMb * 100
end

local function appendMetric(children, glyph, text)
table.insert(children, ui.glyph({ name = glyph, size = 14 }))
table.insert(children, ui.label({ text = text, fontWeight = "medium" }))
end

local function appendDivider(children)
local mark = barWidget.isVertical() and "─" or "│"
table.insert(children, ui.label({ text = mark, color = "outline", fontWeight = "light" }))
end

local function appendGroup(children, group, showSeparators)
if #group == 0 then
return
end

if #children > 0 and showSeparators then
appendDivider(children)
end

for _, child in ipairs(group) do
table.insert(children, child)
end
end

local function metric(glyph, text)
return {
ui.glyph({ name = glyph, size = 14 }),
ui.label({ text = text, fontWeight = "medium" }),
}
end

local function appendMetricToGroup(group, glyph, text)
for _, child in ipairs(metric(glyph, text)) do
table.insert(group, child)
end
end

local function renderUnavailable(showTooltip)
barWidget.render(ui.row({ gap = 4, align = "center" }, {
ui.glyph({ name = "cpu", size = 14 }),
ui.label({ text = "System data unavailable", fontWeight = "medium" }),
}))
if showTooltip then
barWidget.setTooltip("System monitoring is disabled or no sample is available yet.")
else
barWidget.clearTooltip()
end
end

function update()
noctalia.setUpdateInterval(1000)

local showCpuUsage = configBool("show_cpu_usage", true)
local showCpuTemperature = configBool("show_cpu_temperature", true)
local showRam = configBool("show_ram", true)
local showSwap = configBool("show_swap", true)
local showDisk = configBool("show_disk", true)
local showDownload = configBool("show_download", true)
local showUpload = configBool("show_upload", true)
local showSeparators = configBool("show_separators", true)
local showTooltip = configBool("show_tooltip", true)
local compactNetwork = configBool("compact_network", true)
local diskPath = configString("disk_path", "/")

local stats = noctalia.systemStats()
if stats == nil then
renderUnavailable(showTooltip)
return
end

local disk = showDisk and noctalia.diskStats(diskPath) or nil
local cpuUsage = stats.cpu and stats.cpu.usagePercent or nil
local cpuTemp = stats.cpu and stats.cpu.tempC or nil
local ramUsage = stats.ram and stats.ram.usagePercent or nil
local swapUsage = swapPercent(stats.swap)
local diskUsage = disk and disk.usagePercent or nil
local rx = stats.net and stats.net.rxBytesPerSec or nil
local tx = stats.net and stats.net.txBytesPerSec or nil

local cpuText = formatPercent(cpuUsage)
local tempText = formatTemperature(cpuTemp)
local ramText = formatPercent(ramUsage)
local swapText = formatPercent(swapUsage)
local diskText = formatPercent(diskUsage)
local rxText = formatRate(rx, compactNetwork)
local txText = formatRate(tx, compactNetwork)

local children = {}
local processor = {}
local memory = {}
local storage = {}
local network = {}

if showCpuUsage then appendMetricToGroup(processor, "cpu", cpuText) end
if showCpuTemperature then appendMetricToGroup(processor, "temperature", tempText) end
if showRam then appendMetricToGroup(memory, "server-2", ramText) end
if showSwap then appendMetricToGroup(memory, "arrows-exchange", swapText) end
if showDisk then appendMetricToGroup(storage, "database", diskText) end
if showDownload then appendMetricToGroup(network, "arrow-down", rxText) end
if showUpload then appendMetricToGroup(network, "arrow-up", txText) end

appendGroup(children, processor, showSeparators)
appendGroup(children, memory, showSeparators)
appendGroup(children, storage, showSeparators)
appendGroup(children, network, showSeparators)

if #children == 0 then
barWidget.render(ui.row({ gap = 4, align = "center" }, {
ui.glyph({ name = "adjustments", size = 14 }),
ui.label({ text = "No metrics", fontWeight = "medium" }),
}))
if showTooltip then
barWidget.setTooltip("Enable at least one metric in the widget settings.")
else
barWidget.clearTooltip()
end
return
end

local container = barWidget.isVertical() and ui.column or ui.row
barWidget.render(container({ gap = 4, align = "center" }, children))

if showTooltip then
local lines = {}
if showCpuUsage then table.insert(lines, "CPU: " .. cpuText) end
if showCpuTemperature then table.insert(lines, "Temperature: " .. tempText) end
if showRam then table.insert(lines, "RAM: " .. ramText) end
if showSwap then table.insert(lines, "Swap: " .. swapText) end
if showDisk then table.insert(lines, "Disk " .. diskPath .. ": " .. diskText) end
if showDownload then table.insert(lines, "Download: " .. rxText) end
if showUpload then table.insert(lines, "Upload: " .. txText) end
barWidget.setTooltip(table.concat(lines, "\n"))
else
barWidget.clearTooltip()
end
end
Loading