Files
community-plugins/pulsar-mouse/bar.luau
T
13bde174ad pulsar-mouse: make the per-state widget colors configurable (#336)
* pulsar-mouse: make the per-state widget colors configurable

Both widgets hardcoded their state colors: error for a fault, secondary
while charging, error at the mouse's low-power threshold. The desktop
widget's one `color` setting only covered the normal state, and the bar
widget had no color setting at all.

Expose all four states (normal/charging/low/error) as `type = "color"`
settings on each widget, in the same shape battery-widget uses. Defaults
reproduce exactly what was hardcoded, so an existing install renders
identically until someone changes one.

The new settings are `advanced`; the desktop widget's pre-existing
`color` stays where it was so it does not disappear behind the toggle
for anyone already using it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* pulsar-mouse: rename the bar's normal-state key to normal_color

A bar widget's plugin settings share one TOML table with Noctalia's own
per-widget presentation settings, where `color` already exists ("Color
role for this widget's icon and label"). A plugin declaring `color`
there does not shadow it, it aliases it - one `color = "primary"` under
[widget.<id>] drove both pickers at once, so setting either silently
moved the other.

Renamed to `normal_color`, which also makes the retest unambiguous: the
glyph follows the plugin setting while Presentation's own Color picker
stays on Default.

The desktop widget keeps plain `color` - no clash there, and renaming it
would break existing configs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:28:30 -04:00

211 lines
7.3 KiB
Luau

--!nonstrict
-- Pulsar Mouse battery/charging status - [[widget]] (bar).
--
-- For anyone who only has the `pulsar-mouse` CLI installed, not the GUI/
-- tray - see desktop.luau's header for the read strategy (same here): reads
-- pulsar-mouse-gui's ~/.cache/pulsar-mouse/battery.json when it's fresh, or
-- falls back to a direct `pulsar-mouse --battery-json` call otherwise. A
-- CLI-only setup just always takes the fallback path, no special-casing
-- needed - the file will never exist, so every tick reads live from the
-- mouse instead.
--
-- Stays visible for a wired mouse (no battery to report - --battery-json
-- returns {"wireless": false} for those rather than an error, a normal, not
-- a failure, state) - shows a plain neutral glyph instead of vanishing, so
-- it's still clickable to reach the controls panel (DPI/lighting work on any
-- mouse, only the panel's Power tab is wireless-only - see panel.luau).
--
-- Icon-only (no percentage label): battery level and charging state are
-- already in the hover tooltip, so a bar-row number would just be the same
-- fact twice. Just the Tabler "mouse-filled" glyph, tinted per state by
-- statusColor(), swapping to "mouse-off" for a genuine error/no-mouse state.
-- Each of those state colors is a setting (normal/charging/low/error),
-- defaulting to what they were previously hardcoded to.
--
-- Signal quality has no synchronous getter (see gui.py's own comment on
-- this) - it only arrives via an async hidraw event the GUI listens for, so
-- it's only ever available via the cached state file (when the GUI wrote
-- one after seeing an event, i.e. pulsar-mouse-gui/tray is running), never
-- from the CLI fallback read - the tooltip's signal line just doesn't
-- appear when only that path is available.
--
-- Click opens the "controls" panel (panel.luau) for quick DPI/polling rate
-- changes - that one always talks to the CLI directly, since neither of
-- those live in battery.json and writes can't be cached anyway.
--
-- barWidget.render(tree) declarative UI tree
-- barWidget.isVertical() branch on bar orientation
-- barWidget.setTooltip(text) hover detail
-- barWidget.setVisible(bool) hover/click surface toggle - always kept true now
-- noctalia.readFile(path) cheap path - reads the GUI's last reading
-- noctalia.runAsync(cmd, cb) fallback path - direct CLI read
-- noctalia.json.decode(str) both paths return JSON on stdout/in the file
-- noctalia.togglePanel(id) open/close the controls panel on click
local STATE_PATH = "~/.cache/pulsar-mouse/battery.json"
local STALE_AFTER = 180 -- seconds - 3x the GUI's own 60s poll interval
local LOW_POWER_DEFAULT = 15 -- used when the driver has no low-power-threshold
-- getter (e.g. nordic.py) or it hasn't been read yet
local wireless = true -- assume true until a read says otherwise, so the
-- widget doesn't flash hidden then visible on load
local percent = nil
local charging = false
local signalPercent = nil
local lowPowerThreshold = nil
local errorText = nil
local checkingCli = false
local isVertical = false
local glyphSize = noctalia.getConfig("glyph_size")
-- Per-state colors, defaulted in plugin.toml to the values these used to be
-- hardcoded to. Read once here rather than per-render, same as glyph_size -
-- a settings change reloads the widget anyway.
--
-- `normal_color`, not `color` - see plugin.toml for why that name is
-- unusable on a bar widget.
local normalColor = noctalia.getConfig("normal_color")
local chargingColor = noctalia.getConfig("charging_color")
local warningColor = noctalia.getConfig("warning_color")
local errorColor = noctalia.getConfig("error_color")
local function statusGlyph(color)
local name = (errorText ~= nil or percent == nil) and "mouse-off" or "mouse-filled"
return ui.glyph({ name = name, size = glyphSize, color = color })
end
local function statusColor()
if errorText ~= nil then
return errorColor
end
if charging then
return chargingColor
end
if percent ~= nil and percent <= (lowPowerThreshold or LOW_POWER_DEFAULT) then
return warningColor
end
return normalColor
end
local function tooltipText()
if errorText ~= nil then
return errorText
end
if percent == nil then
return noctalia.tr("ui.no-mouse")
end
local text = tostring(percent) .. "%"
if charging then
text = text .. " " .. noctalia.tr("ui.charging")
end
if signalPercent ~= nil then
text = text .. " · " .. noctalia.tr("ui.signal-label") .. " " .. tostring(signalPercent) .. "%"
end
return text
end
local function render()
barWidget.setVisible(true)
local glyph
if not wireless then
-- No battery to show, but still clickable - a wired mouse still has
-- DPI/lighting controls in the panel, just not the Power tab. Takes the
-- normal-state color: there's nothing wrong here, it's just permanently
-- the only state this mouse has.
glyph = ui.glyph({ name = "mouse-filled", size = glyphSize, color = normalColor })
barWidget.setTooltip(noctalia.tr("ui.wired"))
else
glyph = statusGlyph(statusColor())
barWidget.setTooltip(tooltipText())
end
local container = isVertical and ui.column or ui.row
barWidget.render(container({ align = "center" }, { glyph }))
end
local function readViaCli()
if checkingCli then
return
end
if not noctalia.commandExists("pulsar-mouse") then
errorText = noctalia.tr("ui.not-installed")
render()
return
end
checkingCli = true
noctalia.runAsync("pulsar-mouse --battery-json", function(result)
checkingCli = false
if type(result) ~= "table" or result.timedOut or result.exitCode ~= 0 then
errorText = noctalia.tr("ui.no-mouse")
render()
return
end
local decoded = noctalia.json.decode(result.stdout or "")
if type(decoded) ~= "table" then
errorText = noctalia.tr("ui.no-mouse")
render()
return
end
if decoded.wireless == false then
wireless = false
errorText = nil
render()
return
end
if decoded.battery_percent == nil then
errorText = noctalia.tr("ui.no-mouse")
render()
return
end
wireless = true
errorText = nil
percent = decoded.battery_percent
charging = decoded.power_connected == true
-- Not available via this path - see header comment.
signalPercent = nil
-- Unlike signal, this has a synchronous getter, so it IS available here
-- (nil on a driver without one, e.g. nordic.py - falls back to LOW_POWER_DEFAULT).
lowPowerThreshold = decoded.low_power_threshold
render()
end, 10000)
end
local function readState()
local contents = noctalia.readFile(STATE_PATH)
if type(contents) ~= "string" or contents == "" then
readViaCli()
return
end
local decoded = noctalia.json.decode(contents)
if type(decoded) ~= "table" or decoded.battery_percent == nil then
readViaCli()
return
end
local age = os.time() - (decoded.updated_at or 0)
if age > STALE_AFTER then
readViaCli()
return
end
wireless = true
errorText = nil
percent = decoded.battery_percent
charging = decoded.power_connected == true
signalPercent = decoded.signal_percent
lowPowerThreshold = decoded.low_power_threshold
render()
end
function update()
isVertical = barWidget.isVertical()
noctalia.setUpdateInterval(30000)
readState()
end
function onClick()
noctalia.togglePanel("harveywuk/pulsar-mouse:controls")
end