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:
+159
@@ -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
|
||||
Reference in New Issue
Block a user