diff --git a/cat/README.md b/cat/README.md new file mode 100644 index 0000000..81659c8 --- /dev/null +++ b/cat/README.md @@ -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. diff --git a/cat/cat.luau b/cat/cat.luau new file mode 100644 index 0000000..72e904e --- /dev/null +++ b/cat/cat.luau @@ -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 diff --git a/cat/fonts/catwalk2.otf b/cat/fonts/catwalk2.otf new file mode 100644 index 0000000..006a4ab Binary files /dev/null and b/cat/fonts/catwalk2.otf differ diff --git a/cat/plugin.toml b/cat/plugin.toml new file mode 100644 index 0000000..59dd8cc --- /dev/null +++ b/cat/plugin.toml @@ -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"] } diff --git a/cat/thumbnail.webp b/cat/thumbnail.webp new file mode 100644 index 0000000..fcf8a0a Binary files /dev/null and b/cat/thumbnail.webp differ diff --git a/cat/translations/en.json b/cat/translations/en.json new file mode 100644 index 0000000..a208e6f --- /dev/null +++ b/cat/translations/en.json @@ -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." + } + } +}