feat: add claude-companion (#33)
* Add lowcache/claude-companion plugin for PR * claude-companion: address review feedback ... * Added tr & timeout to dependency list, and reduced thumbnail description * added missing thumbnail.webp
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
-- EXPERIMENTAL — prototyping always-visible bar-widget animation, since the desktop
|
||||
-- orb only shows on bare desktop. The real pulse.luau bar is UNTOUCHED; this is a
|
||||
-- throwaway comparison surface. The winning behaviour gets folded into pulse.luau.
|
||||
--
|
||||
-- Bar plugin widgets have NO per-frame tick (that's desktop-only) — they re-render on
|
||||
-- a timer via noctalia.setUpdateInterval(ms) + a global update(). The only animatable
|
||||
-- channel is the glyph colour. Rather than alpha (the bar didn't honour 8-digit
|
||||
-- #RRGGBBAA), we breathe BRIGHTNESS: scale the accent RGB toward black (the near-black
|
||||
-- surface) and emit a plain 6-digit "#RRGGBB" so the glyph glows brighter/dimmer while
|
||||
-- keeping its hue. Two waveforms to compare by eye:
|
||||
-- MODE "A" raised-cosine sine breath (smooth glow, like the orb)
|
||||
-- MODE "B" square wave (discrete blink)
|
||||
--
|
||||
-- It reads state from the same claude.pulse rollup pulse.luau publishes (so it needs none
|
||||
-- of the session bookkeeping). Flip MODE + reload to compare.
|
||||
--
|
||||
-- State RGB is hardcoded from the active dark palette (volnix:
|
||||
-- mSecondary/mPrimary/mError) to validate the look fast.
|
||||
|
||||
local MODE = "A" -- "A" sine breath | "B" square blink
|
||||
local INTERVAL_MS = 60 -- ~16 fps re-render (bars can't do 60 fps)
|
||||
local BMIN, BMAX = 0.45, 1.00 -- brightness floor/ceiling the glyph breathes between
|
||||
|
||||
-- glyph + accent RGB (hex, no #) per state — same glyph/role map as the bar dot/orb.
|
||||
local VISUAL = {
|
||||
idle = { glyph = "robot", rgb = "EAFF00", period = 8.0 },
|
||||
turn_start = { glyph = "brain", rgb = "B4FF00", period = 5.0 },
|
||||
text = { glyph = "message-dots", rgb = "B4FF00", period = 4.5 },
|
||||
tool_start = { glyph = "tool", rgb = "EAFF00", period = 5.0 },
|
||||
needs_attention = { glyph = "bell-ringing", rgb = "FF4D1F", period = 3.0 },
|
||||
turn_end = { glyph = "bell", rgb = "B4FF00", period = 5.5 },
|
||||
error = { glyph = "alert-triangle", rgb = "FF4D1F", period = 3.5 },
|
||||
}
|
||||
|
||||
local state = "idle"
|
||||
local phase = 0.0
|
||||
|
||||
local function level_for(v) -- brightness scale in [BMIN, BMAX]
|
||||
local t = phase % v.period
|
||||
if MODE == "B" then
|
||||
return (t < v.period * 0.5) and BMAX or BMIN -- bright for the first half, dim for the second
|
||||
end
|
||||
local s = 0.5 - 0.5 * math.cos((t / v.period) * 2 * math.pi)
|
||||
return BMIN + (BMAX - BMIN) * s
|
||||
end
|
||||
|
||||
-- Scale an "RRGGBB" accent toward black by factor b, returning "#RRGGBB".
|
||||
local function dim(rgb, b)
|
||||
local function ch(i)
|
||||
local x = math.floor((tonumber(rgb:sub(i, i + 1), 16) or 0) * b + 0.5)
|
||||
if x < 0 then x = 0 elseif x > 255 then x = 255 end
|
||||
return x
|
||||
end
|
||||
return string.format("#%02X%02X%02X", ch(1), ch(3), ch(5))
|
||||
end
|
||||
|
||||
local function paint()
|
||||
local v = VISUAL[state] or VISUAL.idle
|
||||
barWidget.setGlyph(v.glyph)
|
||||
barWidget.setGlyphColor(dim(v.rgb, level_for(v)))
|
||||
barWidget.setTooltip(string.format("barpulse [%s] · %s", MODE, state))
|
||||
end
|
||||
|
||||
-- Pull the most-urgent state from the bar pulse's published rollup. Bar widgets don't
|
||||
-- seem to receive noctalia.state.watch callbacks (the desktop orb does; this bar one
|
||||
-- did not), so we POLL noctalia.state.get each tick instead — cheap, and the timer is
|
||||
-- already running. Only adopt a recognised state so a nil/garbage read can't blank it.
|
||||
local function refresh_state()
|
||||
local snap = noctalia.state.get and noctalia.state.get("claude.pulse")
|
||||
if type(snap) == "table" and type(snap.state) == "string" and VISUAL[snap.state] then
|
||||
state = snap.state
|
||||
elseif type(snap) == "string" and VISUAL[snap] then
|
||||
state = snap
|
||||
end
|
||||
end
|
||||
|
||||
-- Timer loop. Re-arm the interval each tick (mirrors the scratchpad pattern), refresh
|
||||
-- the state by polling, and advance the breath by the known step (no dt on bar widgets).
|
||||
function update()
|
||||
noctalia.setUpdateInterval(INTERVAL_MS)
|
||||
refresh_state()
|
||||
phase = phase + INTERVAL_MS / 1000
|
||||
if phase > 1e6 then phase = 0 end
|
||||
paint()
|
||||
end
|
||||
|
||||
refresh_state()
|
||||
noctalia.setUpdateInterval(INTERVAL_MS)
|
||||
paint()
|
||||
Reference in New Issue
Block a user