Add Cat plugin (#59)

An animated cat bar widget whose pace reflects CPU usage: sleeps
idle, walks under light load, runs under heavy load. Color follows
the theme by default or a pinned custom color. Cat artwork traced
from the MIT-licensed CatWalk plasmoid into a custom icon font.
This commit is contained in:
Robert Ward
2026-07-20 21:01:42 -04:00
committed by GitHub
parent c72063a2fe
commit 813efca594
6 changed files with 308 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
# Cat
An animated cat that lives in your bar. It sleeps when your CPU is idle,
walks as load picks up, and breaks into a full sprint under heavy load —
colored to match your theme, or any color you pick.
## Plugin
| Field | Value |
| --- | --- |
| ID | `dotnetrob/cat` |
| Entries | Bar widget: `cat` |
## Usage
Add the "Cat" widget to any bar from Settings → Bar → Add Widget. Click the
widget to show a notification with the current CPU percentage.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `cat_size` | `int` | `24` | Sprite size in the bar, in pixels (12–48). |
| `show_cpu_percent` | `bool` | `false` | Display the CPU percentage next to the cat. |
| `walk_threshold` | `int` | `15` | CPU percentage at which the cat wakes up and starts walking. |
| `run_threshold` | `int` | `60` | CPU percentage at which the cat breaks into a run. |
| `poll_interval` | `int` | `2` | How often to sample CPU usage, in seconds. |
| `cat_color_mode` | `select` | `theme` | `theme` colors the cat with the palette's `secondary` role and tracks theme changes; `custom` uses the color below. |
| `cat_color` | `color` | `#E8A24C` | Used when `cat_color_mode` is `custom`. |
## Notes
Every `poll_interval` seconds the widget reads `/proc/stat` to compute CPU
usage — no other files are read or written, nothing is downloaded, and no
processes are spawned. The cat's shape comes from a small custom icon font
(`fonts/catwalk2.otf`) traced from the MIT-licensed
[CatWalk](https://store.kde.org/p/2055225) plasmoid by Driglu4it, which lets
it be recolored like normal bar text instead of a fixed-color image.
+159
View File
@@ -0,0 +1,159 @@
--!nonstrict
-- Cat: an animated running cat bar widget. Pace reflects CPU usage, sampled
-- from /proc/stat, with separately configurable walk/run thresholds. Color
-- follows the theme's secondary role by default, or a pinned custom color.
--
-- The cat shape is a custom pictographic font (fonts/catwalk2.otf, traced
-- from the MIT-licensed CatWalk plasmoid by Driglu4it) so it can be colored
-- like any other bar text: "a" = idle/asleep, "b".."f" = the 5-frame run
-- cycle. Each glyph's advance width is trimmed to the shared ink extent
-- (non-negative left bearing, no dead trailing space) so bar layout doesn't
-- overlap the previous widget or leave a large gap before the next one.
--
-- Font registration is process-global and keyed by file path/family, so this
-- filename must change (not just its contents) whenever the glyph set changes
-- during development, or the shell keeps rendering whatever it cached the
-- first time it saw this path.
local catFont = noctalia.loadFont("fonts/catwalk2.otf")
local GLYPH_IDLE = "a"
local RUN_GLYPHS = { "b", "c", "d", "e", "f" }
local TICK_MS = 80
local WALK_MAX_MS = 380
local WALK_MIN_MS = 180
local RUN_MAX_MS = 160
local RUN_MIN_MS = 55
local catSize = noctalia.getConfig("cat_size")
local showCpuPercent = noctalia.getConfig("show_cpu_percent")
local walkThreshold = noctalia.getConfig("walk_threshold")
local runThreshold = noctalia.getConfig("run_threshold")
local pollIntervalMs = noctalia.getConfig("poll_interval") * 1000
local colorMode = noctalia.getConfig("cat_color_mode")
local customColor = noctalia.getConfig("cat_color")
local frameIndex = 1
local frameElapsed = 0
local frameDurationMs = WALK_MAX_MS
local sampleElapsed = pollIntervalMs -- sample immediately on first tick
local cpuPercent = 0
local prevTotal, prevIdle = nil, nil
local function sampleCpu()
local stat = noctalia.readFile("/proc/stat")
if not stat then
return
end
local line = stat:match("^cpu%s+(.-)\n") or stat:match("^cpu%s+(.-)$")
if not line then
return
end
local fields = {}
for n in line:gmatch("%d+") do
table.insert(fields, tonumber(n))
end
local idle = (fields[4] or 0) + (fields[5] or 0)
local total = 0
for _, v in fields do
total += v
end
if prevTotal then
local totalDelta = total - prevTotal
local idleDelta = idle - prevIdle
if totalDelta > 0 then
cpuPercent = math.clamp((totalDelta - idleDelta) / totalDelta * 100, 0, 100)
end
end
prevTotal = total
prevIdle = idle
end
-- Returns ("idle" | "walk" | "run", frame-duration-ms-for-that-pace).
local function paceFor(cpu)
local runFloor = math.max(runThreshold, walkThreshold + 1)
if cpu < walkThreshold then
return "idle", WALK_MAX_MS
elseif cpu < runFloor then
local range = math.max(runFloor - walkThreshold, 1)
local t = math.clamp((cpu - walkThreshold) / range, 0, 1)
return "walk", WALK_MAX_MS - t * (WALK_MAX_MS - WALK_MIN_MS)
else
local range = math.max(100 - runFloor, 1)
local t = math.clamp((cpu - runFloor) / range, 0, 1)
return "run", RUN_MAX_MS - t * (RUN_MAX_MS - RUN_MIN_MS)
end
end
local function resolveColor()
if colorMode == "custom" and customColor and customColor ~= "" then
return customColor
end
return "secondary"
end
local function render(pace)
local glyph = pace == "idle" and GLYPH_IDLE or RUN_GLYPHS[frameIndex]
local color = resolveColor()
local children = {
ui.label({ text = glyph, fontFamily = catFont, baseline = "inkCentered", fontSize = catSize, color = color }),
}
if showCpuPercent then
table.insert(children, ui.label({ text = `{math.floor(cpuPercent)}%`, fontSize = 11, color = color }))
end
local container = barWidget.isVertical() and ui.column or ui.row
barWidget.render(container({ gap = 4, align = "center" }, children))
barWidget.setTooltip(`Cat — CPU {math.floor(cpuPercent)}% ({pace})`)
end
noctalia.setUpdateInterval(TICK_MS)
function update()
sampleElapsed += TICK_MS
if sampleElapsed >= pollIntervalMs then
sampleElapsed = 0
sampleCpu()
end
local pace, duration = paceFor(cpuPercent)
frameDurationMs = duration
if pace == "idle" then
frameElapsed = 0
frameIndex = 1
else
frameElapsed += TICK_MS
if frameElapsed >= frameDurationMs then
frameElapsed = 0
frameIndex = frameIndex % #RUN_GLYPHS + 1
end
end
render(pace)
end
function onClick()
noctalia.notify(noctalia.tr("title"), `CPU {math.floor(cpuPercent)}%`)
end
function onConfigChanged()
catSize = noctalia.getConfig("cat_size")
showCpuPercent = noctalia.getConfig("show_cpu_percent")
walkThreshold = noctalia.getConfig("walk_threshold")
runThreshold = noctalia.getConfig("run_threshold")
pollIntervalMs = noctalia.getConfig("poll_interval") * 1000
colorMode = noctalia.getConfig("cat_color_mode")
customColor = noctalia.getConfig("cat_color")
render((paceFor(cpuPercent)))
end
Binary file not shown.
+76
View File
@@ -0,0 +1,76 @@
id = "dotnetrob/cat"
name = "Cat"
version = "1.1.2"
plugin_api = 3
author = "DotNetRob"
license = "MIT"
dependencies = []
icon = "paw"
description = "An animated running cat bar widget whose speed reflects CPU usage."
tags = ["bar", "animation", "system", "fun"]
[[widget]]
id = "cat"
entry = "cat.luau"
[[widget.setting]]
key = "cat_size"
type = "int"
label_key = "settings.cat_size.label"
description_key = "settings.cat_size.description"
default = 24
min = 12
max = 48
[[widget.setting]]
key = "show_cpu_percent"
type = "bool"
label_key = "settings.show_cpu_percent.label"
description_key = "settings.show_cpu_percent.description"
default = false
[[widget.setting]]
key = "walk_threshold"
type = "int"
label_key = "settings.walk_threshold.label"
description_key = "settings.walk_threshold.description"
default = 15
min = 0
max = 100
[[widget.setting]]
key = "run_threshold"
type = "int"
label_key = "settings.run_threshold.label"
description_key = "settings.run_threshold.description"
default = 60
min = 0
max = 100
[[widget.setting]]
key = "poll_interval"
type = "int"
label_key = "settings.poll_interval.label"
description_key = "settings.poll_interval.description"
default = 2
min = 1
max = 10
[[widget.setting]]
key = "cat_color_mode"
type = "select"
label_key = "settings.cat_color_mode.label"
description_key = "settings.cat_color_mode.description"
default = "theme"
options = [
{ value = "theme", label_key = "settings.cat_color_mode.option_theme" },
{ value = "custom", label_key = "settings.cat_color_mode.option_custom" },
]
[[widget.setting]]
key = "cat_color"
type = "color"
label_key = "settings.cat_color.label"
description_key = "settings.cat_color.description"
default = "#E8A24C"
visible_when = { key = "cat_color_mode", values = ["custom"] }
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+35
View File
@@ -0,0 +1,35 @@
{
"title": "Cat",
"settings": {
"cat_size": {
"label": "Cat Size",
"description": "Sprite size in the bar, in pixels."
},
"show_cpu_percent": {
"label": "Show CPU Percentage",
"description": "Display the CPU percentage next to the cat."
},
"walk_threshold": {
"label": "Walk Threshold",
"description": "CPU percentage at which the cat wakes up and starts walking."
},
"run_threshold": {
"label": "Run Threshold",
"description": "CPU percentage at which the cat breaks into a run."
},
"poll_interval": {
"label": "CPU Poll Interval",
"description": "How often to sample CPU usage, in seconds."
},
"cat_color_mode": {
"label": "Cat Color",
"description": "Use the current theme color, or pick a custom color below.",
"option_theme": "Match Theme",
"option_custom": "Custom"
},
"cat_color": {
"label": "Custom Color",
"description": "Used when Cat Color is set to Custom."
}
}
}