Add salemsayed/codexbar-meter (#243)

A bar widget and attached panel showing AI provider usage limits reported
by the local CodexBar CLI. The bar keeps a configurable number of provider
meters; the panel lists every provider, quota window, credit balance, and
pace summary, and scrolls when the response is taller than the panel.


Claude-Session: https://claude.ai/code/session_01BCNAY79YEoNxyubdt8Co9A

Co-authored-by: Salem Sayed Abdel Gawad <283208+salemsayed@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Salem Sayed Abdel Gawad
2026-08-05 17:57:32 -04:00
committed by GitHub
co-authored by Salem Sayed Abdel Gawad Claude Opus 5
parent bffb68c86f
commit 1a08a98c4e
6 changed files with 1115 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
# CodexBar Meter
CodexBar Meter is a native Noctalia v5 bar widget and attached panel for usage
limits reported by the local [CodexBar](https://github.com/steipete/CodexBar)
CLI. It discovers the providers enabled in CodexBar, keeps the bar compact,
and exposes every provider and quota window in a scrollable panel.
## Plugin
| Field | Value |
| --- | --- |
| ID | `salemsayed/codexbar-meter` |
| Entries | Bar widget: `bar`; panel entries: `panel-compact`, `panel`, `panel-tall` |
## Requirements
Install these commands on `PATH`:
- `codexbar` 0.47 or newer, configured with at least one provider.
- `timeout` from GNU coreutils.
CodexBar owns provider authentication, network access, and provider discovery.
The plugin does not read or store provider credentials.
## Usage
Add `salemsayed/codexbar-meter:bar` to a Noctalia bar. The bar shows up to two
provider meters by default, followed by a `+N` count when more providers are
enabled in CodexBar. The tooltip includes all returned providers.
- Left-click opens the attached `CodexBar Meter` panel. The widget chooses a
compact, standard, or tall panel tier from the current provider payload;
each tier keeps scrolling as the safety net for unusually large responses.
- Right-click refreshes usage immediately.
- The panel refresh button requests a fresh CodexBar query.
- The panel close button dismisses the panel.
To open the standard panel from a terminal:
```sh
noctalia msg panel-toggle salemsayed/codexbar-meter:panel
```
The adaptive widget also uses these panel entries when opening from the bar:
- `noctalia msg panel-toggle salemsayed/codexbar-meter:panel-compact` — 390 px
- `noctalia msg panel-toggle salemsayed/codexbar-meter:panel` — 560 px
- `noctalia msg panel-toggle salemsayed/codexbar-meter:panel-tall` — 720 px
The panel renders all provider cards and scrolls when the response is taller
than the panel. It understands CodexBar's standard primary, secondary, and
tertiary windows, named `windows` and `extraRateWindows`, credits, pace
summaries, provider status, stale data, and per-provider errors. Unknown
providers receive a readable title, a neutral icon, and a theme-derived color.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `codexbarPath` | `string` | `codexbar` | Command or absolute path used to query CodexBar. |
| `refreshIntervalSec` | `int` | `60` | Background refresh interval in seconds; allowed range is 30–3600. |
| `barProviderLimit` | `int` | `2` | Number of provider meters shown in the bar; allowed range is 1–4. The panel and tooltip always include all providers. |
When no provider flag is supplied, CodexBar's configured enabled-provider list
is used. This lets the plugin work with any current or future CodexBar
provider without changing the Noctalia code.
## IPC
Refresh the widget and panel through the shared plugin state:
```sh
noctalia msg plugin salemsayed/codexbar-meter:bar all refresh
```
## Notes
- The plugin runs `timeout 30s <codexbarPath> usage --format json --json-only`.
- A provider-level error is kept as a card beside healthy providers. If a
refresh returns no usable JSON, the last successful response remains visible
with an explicit warning.
- The plugin is compositor-agnostic: it uses Noctalia's attached layer-shell
panels and theme roles rather than hard-coded light or dark colors, and calls
no compositor IPC of its own. Developed on Niri.
- The plugin writes no files and opens no network connections of its own. The
panel displays the account identity CodexBar reports for a provider, such as
the signed-in email address. CodexBar itself owns provider authentication,
credential storage, and all network access.
+493
View File
@@ -0,0 +1,493 @@
--!nonstrict
-- CodexBar provider usage bar widget.
--
-- CodexBar owns provider discovery, authentication, and quota fetching. This
-- widget only normalizes the shared JSON envelope and keeps the bar bounded:
-- the first configured providers are shown in the capsule, while the tooltip
-- and attached panel expose the complete response.
local codexbarPath = noctalia.getConfig("codexbarPath") or "codexbar"
local refreshIntervalSec = tonumber(noctalia.getConfig("refreshIntervalSec")) or 60
local barProviderLimit = tonumber(noctalia.getConfig("barProviderLimit")) or 2
local providers = {}
local errorMsg = ""
local requestInFlight = false
-- Noctalia v5 panel surfaces have fixed manifest dimensions. Keep the
-- regular `panel` id as the compatibility/default entry, then choose a
-- nearby fixed tier when opening so the surface follows the current data
-- without relying on an unsupported runtime resize API.
local PANEL_IDS = {
compact = "salemsayed/codexbar-meter:panel-compact",
standard = "salemsayed/codexbar-meter:panel",
tall = "salemsayed/codexbar-meter:panel-tall",
}
local PROVIDER_META = {
codex = { label = "Codex", glyph = "brand-openai", color = "primary" },
openai = { label = "OpenAI", glyph = "brand-openai", color = "primary" },
azureopenai = { label = "Azure OpenAI", glyph = "brand-openai", color = "primary" },
claude = { label = "Claude", glyph = "message-chatbot", color = "tertiary" },
gemini = { label = "Gemini", glyph = "brand-google", color = "secondary" },
copilot = { label = "Copilot", glyph = "brand-github", color = "secondary" },
cursor = { label = "Cursor", glyph = "cursor-text", color = "secondary" },
opencode = { label = "OpenCode", glyph = "code", color = "secondary" },
opencodego = { label = "OpenCode Go", glyph = "code", color = "secondary" },
qwencloud = { label = "Qwen Cloud", glyph = "cloud", color = "secondary" },
alibaba = { label = "Alibaba", glyph = "cloud", color = "secondary" },
alibabatokenplan = { label = "Alibaba Token Plan", glyph = "cloud", color = "secondary" },
antigravity = { label = "Antigravity", glyph = "sparkles", color = "tertiary" },
kilo = { label = "Kilo", glyph = "robot", color = "secondary" },
ollama = { label = "Ollama", glyph = "server", color = "secondary" },
openrouter = { label = "OpenRouter", glyph = "route", color = "secondary" },
}
local FALLBACK_COLORS = { "secondary", "tertiary", "primary" }
local function shellQuote(value)
return "'" .. string.gsub(tostring(value), "'", "'\\''") .. "'"
end
local function commandPath()
local path = tostring(codexbarPath)
if string.sub(path, 1, 1) == "~" then
path = noctalia.expandPath(path)
end
return path
end
local function providerId(provider)
return tostring(provider and (provider.provider or provider.id) or "unknown")
end
local function providerAccount(provider)
if type(provider) ~= "table" then return "" end
if provider.account ~= nil and tostring(provider.account) ~= "" then
return tostring(provider.account)
end
local usage = provider.usage
local identity = type(usage) == "table" and usage.identity or nil
if type(identity) == "table" then
if identity.accountEmail ~= nil and tostring(identity.accountEmail) ~= "" then
return tostring(identity.accountEmail)
end
if identity.accountOrganization ~= nil and tostring(identity.accountOrganization) ~= "" then
return tostring(identity.accountOrganization)
end
end
return ""
end
local function titleFromId(value)
local text = string.gsub(tostring(value or "unknown"), "[_%-]+", " ")
text = string.gsub(text, "(%a)([%w]*)", function(first, rest)
return string.upper(first) .. string.lower(rest)
end)
return text
end
local function metaFor(provider, index)
local id = providerId(provider)
local known = PROVIDER_META[string.lower(id)]
if known ~= nil then return known end
local colorIndex = ((index - 1) % #FALLBACK_COLORS) + 1
return {
label = titleFromId(id),
glyph = "chart-donut-3",
color = FALLBACK_COLORS[colorIndex],
}
end
local function providerLabel(provider, index)
local meta = metaFor(provider, index)
local account = providerAccount(provider)
if account ~= "" then
return meta.label .. " · " .. account
end
return meta.label
end
local function providerError(provider)
if type(provider) ~= "table" or provider.error == nil then return "" end
if type(provider.error) == "string" then return provider.error end
if type(provider.error) == "table" then
return tostring(provider.error.message or provider.error.description or provider.error.kind or "Provider unavailable")
end
return "Provider unavailable"
end
local function windowLabel(window, fallback)
if type(window) ~= "table" then return fallback or "Limit" end
local named = window.title or window.name or window.label
if named ~= nil and tostring(named) ~= "" then return tostring(named) end
local minutes = tonumber(window.windowMinutes) or 0
if minutes == 300 then return "5h" end
if minutes == 1440 then return "24h" end
if minutes == 10080 then return "7d" end
if minutes == 43200 then return "30d" end
if minutes >= 1440 then return string.format("%dd", math.floor(minutes / 1440)) end
if minutes >= 60 then return string.format("%dh", math.floor(minutes / 60)) end
if minutes > 0 then return string.format("%dm", minutes) end
return fallback or "limit"
end
local function remainingPercent(window)
if type(window) ~= "table" or window.usedPercent == nil then return nil end
local used = tonumber(window.usedPercent)
if used == nil then return nil end
return math.max(0, math.min(100, 100 - used))
end
local function addWindow(windows, title, window, rank)
if type(window) ~= "table" or tonumber(window.usedPercent) == nil then return end
for _, item in ipairs(windows) do
if item.window == window then return end
end
windows[#windows + 1] = {
title = title or windowLabel(window),
window = window,
rank = rank or 99,
}
end
local function windowsFor(provider)
local result = {}
local usage = type(provider) == "table" and provider.usage or nil
if type(usage) ~= "table" then return result end
addWindow(result, windowLabel(usage.primary, "Session"), usage.primary, 1)
addWindow(result, windowLabel(usage.secondary, "Weekly"), usage.secondary, 2)
addWindow(result, windowLabel(usage.tertiary, "Monthly"), usage.tertiary, 3)
if type(usage.extraRateWindows) == "table" then
for index, extra in ipairs(usage.extraRateWindows) do
if type(extra) == "table" then
addWindow(result, extra.title or extra.name or "Additional window", extra.window or extra, 10 + index)
end
end
end
if type(usage.windows) == "table" then
for index, item in ipairs(usage.windows) do
if type(item) == "table" then
addWindow(result, item.title or item.name or "Usage window", item.window or item, 20 + index)
end
end
end
-- Future providers may add another named rate window. Pick up any table
-- that carries the standard usedPercent field without treating identity,
-- status, or provider-specific metadata as a quota row.
for key, value in pairs(usage) do
if type(value) == "table" and tonumber(value.usedPercent) ~= nil then
addWindow(result, windowLabel(value, titleFromId(key)), value, 40)
end
end
table.sort(result, function(a, b)
if a.rank ~= b.rank then return a.rank < b.rank end
return a.title < b.title
end)
return result
end
local function creditsFor(provider)
if type(provider) ~= "table" then return nil end
if type(provider.credits) == "table" then return provider.credits end
if type(provider.usage) == "table" and type(provider.usage.credits) == "table" then
return provider.usage.credits
end
return nil
end
local function summaryFor(provider)
local windows = windowsFor(provider)
if #windows > 0 then
local window = windows[1].window
return {
window = window,
label = windows[1].title .. " " .. string.format("%d%%", math.floor((remainingPercent(window) or 0) + 0.5)),
remaining = remainingPercent(window),
}
end
local credits = creditsFor(provider)
if credits ~= nil and credits.remaining ~= nil then
local creditPercent = tonumber(credits.remainingPercent)
return {
label = tostring(credits.remaining),
remaining = creditPercent,
credits = true,
}
end
return { label = "—", remaining = nil }
end
local function providerByIndex(index)
return providers[index]
end
local function selectedEntries()
local entries = {}
local healthy = {}
for index, provider in ipairs(providers) do
local entry = { provider = provider, index = index, summary = summaryFor(provider) }
entries[#entries + 1] = entry
if providerError(provider) == "" then healthy[#healthy + 1] = entry end
end
local pool = #healthy > 0 and healthy or entries
local limit = math.max(1, math.min(4, math.floor(barProviderLimit)))
local selected = {}
for index = 1, math.min(limit, #pool) do
selected[#selected + 1] = pool[index]
end
return selected
end
local function percentText(value)
if value == nil then return "—" end
return string.format("%d%%", math.floor(value + 0.5))
end
local function providerSegment(entry)
local provider = entry.provider
local meta = metaFor(provider, entry.index)
local summary = entry.summary
local children = {
ui.glyph({ name = meta.glyph, size = 13, color = meta.color }),
}
if summary.window ~= nil then
children[#children + 1] = ui.progress({
progress = (summary.remaining or 0) / 100,
fill = meta.color,
track = "on_surface/0.16",
radius = 6,
width = 26,
height = 5,
})
end
children[#children + 1] = ui.label({
text = summary.window ~= nil and (windowLabel(summary.window) .. " " .. percentText(summary.remaining)) or summary.label,
fontSize = 11,
fontWeight = "semibold",
color = "on_surface",
maxLines = 1,
})
return ui.row({ key = "provider-" .. tostring(entry.index), align = "center", gap = 5 }, children)
end
local function tooltipFor(provider, index)
local meta = metaFor(provider, index)
local issue = providerError(provider)
if issue ~= "" then
return { key = providerLabel(provider, index), value = "Unavailable · " .. issue }
end
local windows = windowsFor(provider)
local lines = {}
for _, item in ipairs(windows) do
lines[#lines + 1] = item.title .. ": " .. percentText(remainingPercent(item.window)) .. " left"
end
local credits = creditsFor(provider)
if credits ~= nil and credits.remaining ~= nil then
lines[#lines + 1] = "Credits: " .. tostring(credits.remaining)
end
if #lines == 0 then lines[#lines + 1] = "No usage window reported" end
if provider.stale == true then lines[#lines + 1] = "Showing last known data" end
return { key = meta.label, value = table.concat(lines, "\n") }
end
local function hasProviderErrors()
for _, provider in ipairs(providers) do
if providerError(provider) ~= "" then return true end
end
return false
end
local function hasProviderStatus(provider)
local status = type(provider) == "table" and provider.status or nil
return type(status) == "table"
and status.description ~= nil
and tostring(status.indicator or "none") ~= "none"
end
local function hasPaceSummary(provider)
local pace = type(provider) == "table" and provider.pace or nil
if type(pace) ~= "table" then return false end
for _, key in ipairs({ "primary", "secondary", "tertiary" }) do
if type(pace[key]) == "table" and pace[key].summary ~= nil then return true end
end
for _, value in pairs(pace) do
if type(value) == "table" and value.summary ~= nil then return true end
end
return false
end
local function estimatedProviderHeight(provider)
-- Approximate the panel card from semantic content, not provider names.
-- This deliberately errs upward; the scroll view remains the safety net
-- for providers with unusually verbose or future-specific payloads.
local height = 104
height = height + (#windowsFor(provider) * 82)
if providerError(provider) ~= "" then height = height + 44 end
if hasPaceSummary(provider) then height = height + 34 end
if provider.stale == true then height = height + 34 end
local credits = creditsFor(provider)
if credits ~= nil and credits.remaining ~= nil then height = height + 30 end
if hasProviderStatus(provider) then height = height + 34 end
return height
end
local function panelEntryIdForCurrentData()
if #providers == 0 then return PANEL_IDS.compact end
local estimate = 130
for index, provider in ipairs(providers) do
estimate = estimate + estimatedProviderHeight(provider)
if index > 1 then estimate = estimate + 10 end
end
if estimate <= 420 then return PANEL_IDS.compact end
if estimate <= 600 then return PANEL_IDS.standard end
return PANEL_IDS.tall
end
local function render()
local selected = selectedEntries()
local rows = {}
if #selected == 0 then
rows[#rows + 1] = ui.row({ align = "center", gap = 5 }, {
ui.glyph({ name = "chart-donut-3", size = 13, color = "primary" }),
ui.label({ text = "AI —", fontSize = 11, fontWeight = "semibold", color = "on_surface" }),
})
else
for index, entry in ipairs(selected) do
if index > 1 then
rows[#rows + 1] = ui.box({ width = 1, height = 15, fill = "outline/0.35" })
end
rows[#rows + 1] = providerSegment(entry)
end
if #providers > #selected then
rows[#rows + 1] = ui.label({
text = "+" .. tostring(#providers - #selected),
fontSize = 10,
color = "on_surface_variant",
})
end
end
if errorMsg ~= "" or hasProviderErrors() then
rows[#rows + 1] = ui.glyph({ name = "alert-circle", size = 12, color = "error" })
end
barWidget.render(ui.row({ gap = 7, align = "center" }, rows))
local tooltip = {}
for index, provider in ipairs(providers) do
tooltip[#tooltip + 1] = tooltipFor(provider, index)
end
if #tooltip == 0 then
tooltip[#tooltip + 1] = { key = "CodexBar", value = "Waiting for provider data" }
end
if errorMsg ~= "" then
tooltip[#tooltip + 1] = { key = "Status", value = errorMsg }
end
barWidget.setTooltip(tooltip)
end
local function decodeProviders(stdout)
local decoded = noctalia.json.decode(stdout or "")
if type(decoded) ~= "table" then return nil end
if type(decoded.providers) == "table" then return decoded.providers end
if decoded.provider ~= nil or decoded.error ~= nil then return { decoded } end
local result = {}
for _, provider in ipairs(decoded) do
if type(provider) == "table" then result[#result + 1] = provider end
end
return result
end
local function refresh()
if requestInFlight then return end
requestInFlight = true
local command = "timeout 30s " .. shellQuote(commandPath())
.. " usage --format json --json-only"
noctalia.runAsync(command, function(result)
requestInFlight = false
local decoded = result ~= nil and decodeProviders(result.stdout) or nil
-- CodexBar may return a non-zero exit code for a partial provider
-- response. Keep healthy rows and let each errored row explain itself.
if decoded ~= nil and #decoded > 0 then
providers = decoded
errorMsg = ""
noctalia.state.set("providers", providers)
noctalia.state.set("error", "")
noctalia.state.set("lastRefresh", os.time())
render()
return
end
errorMsg = result ~= nil and result.exitCode ~= 0
and "CodexBar could not refresh"
or "CodexBar returned no provider data"
noctalia.state.set("error", errorMsg)
render()
end, 30000)
end
noctalia.state.watch("command", function(value)
if type(value) == "table" and value.action == "refresh" then refresh() end
end)
noctalia.state.watch("providers", function(value)
if type(value) == "table" then
providers = value
render()
end
end)
noctalia.state.watch("error", function(value)
if type(value) == "string" then
errorMsg = value
render()
end
end)
function update()
noctalia.setUpdateInterval(refreshIntervalSec * 1000)
refresh()
end
function onClick()
noctalia.togglePanel(panelEntryIdForCurrentData())
end
function onRightClick()
refresh()
end
function onIpc(event, _payload)
if event == "refresh" then refresh() end
end
local existing = noctalia.state.get("providers")
if type(existing) == "table" then providers = existing end
local existingError = noctalia.state.get("error")
if type(existingError) == "string" then errorMsg = existingError end
noctalia.setUpdateInterval(refreshIntervalSec * 1000)
render()
refresh()
+448
View File
@@ -0,0 +1,448 @@
--!nonstrict
-- CodexBar provider usage panel.
--
-- The panel intentionally renders the provider array instead of naming a
-- fixed pair. CodexBar can add providers and provider-specific quota windows;
-- the standard usedPercent/reset envelope is enough for a useful generic card.
local providers = {}
local errorMsg = ""
local panelOpen = false
local refreshIntervalSec = tonumber(noctalia.getConfig("refreshIntervalSec")) or 60
local PROVIDER_META = {
codex = { label = "Codex", glyph = "brand-openai", color = "primary", source = "OpenAI" },
openai = { label = "OpenAI", glyph = "brand-openai", color = "primary", source = "OpenAI" },
claude = { label = "Claude", glyph = "message-chatbot", color = "tertiary", source = "Anthropic" },
}
local FALLBACK_COLORS = { "secondary", "tertiary", "primary" }
local function providerId(provider)
return tostring(provider and (provider.provider or provider.id) or "unknown")
end
local function titleFromId(value)
local text = string.gsub(tostring(value or "unknown"), "[_%-]+", " ")
text = string.gsub(text, "(%a)([%w]*)", function(first, rest)
return string.upper(first) .. string.lower(rest)
end)
return text
end
local function providerAccount(provider)
if type(provider) ~= "table" then return "" end
if provider.account ~= nil and tostring(provider.account) ~= "" then
return tostring(provider.account)
end
local usage = provider.usage
local identity = type(usage) == "table" and usage.identity or nil
if type(identity) == "table" then
if identity.accountEmail ~= nil and tostring(identity.accountEmail) ~= "" then
return tostring(identity.accountEmail)
end
if identity.accountOrganization ~= nil and tostring(identity.accountOrganization) ~= "" then
return tostring(identity.accountOrganization)
end
end
return ""
end
local function metaFor(provider, index)
local id = providerId(provider)
local known = PROVIDER_META[string.lower(id)]
if known ~= nil then return known end
local colorIndex = ((index - 1) % #FALLBACK_COLORS) + 1
return {
label = titleFromId(id),
glyph = "chart-donut-3",
color = FALLBACK_COLORS[colorIndex],
source = "CodexBar",
}
end
local function providerLabel(provider, index)
local meta = metaFor(provider, index)
return meta.label
end
local function providerError(provider)
if type(provider) ~= "table" or provider.error == nil then return "" end
if type(provider.error) == "string" then return provider.error end
if type(provider.error) == "table" then
return tostring(provider.error.message or provider.error.description or provider.error.kind or "Provider unavailable")
end
return "Provider unavailable"
end
local function parseIso(value)
if type(value) ~= "string" then return nil end
local y, mo, d, h, mi, s = value:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)")
if y == nil then return nil end
local localGuess = os.time({
year = tonumber(y), month = tonumber(mo), day = tonumber(d),
hour = tonumber(h), min = tonumber(mi), sec = tonumber(s),
})
local utcAsLocal = os.time(os.date("!*t", localGuess))
return localGuess + (localGuess - utcAsLocal)
end
local function windowLabel(window, fallback)
if type(window) ~= "table" then return fallback or "Usage window" end
local named = window.title or window.name or window.label
if named ~= nil and tostring(named) ~= "" then return tostring(named) end
local minutes = tonumber(window.windowMinutes) or 0
if minutes == 300 then return "5-hour session" end
if minutes == 1440 then return "24-hour window" end
if minutes == 10080 then return "Weekly window" end
if minutes == 43200 then return "30-day window" end
if minutes >= 1440 then return string.format("%d-day window", math.floor(minutes / 1440)) end
if minutes >= 60 then return string.format("%d-hour window", math.floor(minutes / 60)) end
if minutes > 0 then return string.format("%d-minute window", minutes) end
return fallback or "Usage window"
end
local function remainingPercent(window)
if type(window) ~= "table" or window.usedPercent == nil then return nil end
local used = tonumber(window.usedPercent)
if used == nil then return nil end
return math.max(0, math.min(100, 100 - used))
end
local function percentText(value)
if value == nil then return "—" end
return string.format("%d%%", math.floor(value + 0.5))
end
local function relativeReset(window)
if type(window) ~= "table" then return "Reset unavailable" end
local epoch = parseIso(window.resetsAt)
if epoch == nil then return window.resetDescription or "Reset unavailable" end
local seconds = epoch - os.time()
if seconds <= 0 then return "Resetting now" end
local days = math.floor(seconds / 86400)
local hours = math.floor((seconds % 86400) / 3600)
local minutes = math.floor((seconds % 3600) / 60)
if days > 0 then return string.format("in %dd %dh", days, hours) end
if hours > 0 then return string.format("in %dh %dm", hours, minutes) end
return string.format("in %dm", math.max(1, minutes))
end
local function resetAt(window)
if type(window) ~= "table" then return "—" end
local epoch = parseIso(window.resetsAt)
if epoch ~= nil then return noctalia.formatTime("%a %I:%M %p", epoch) end
return window.resetDescription or "—"
end
local function updatedAge(provider)
local usage = type(provider) == "table" and provider.usage or nil
local credits = type(provider) == "table" and provider.credits or nil
local value = type(usage) == "table" and usage.updatedAt or nil
if value == nil and type(credits) == "table" then value = credits.updatedAt end
if value == nil and type(provider) == "table" then value = provider.updatedAt end
local epoch = parseIso(value)
if epoch == nil then return "not yet" end
local seconds = math.max(0, os.time() - epoch)
if seconds < 10 then return "just now" end
if seconds < 60 then return string.format("%ds ago", seconds) end
if seconds < 3600 then return string.format("%dm ago", math.floor(seconds / 60)) end
return string.format("%dh ago", math.floor(seconds / 3600))
end
local function addWindow(windows, title, window, rank, color)
if type(window) ~= "table" or tonumber(window.usedPercent) == nil then return end
for _, item in ipairs(windows) do
if item.window == window then return end
end
windows[#windows + 1] = {
title = title or windowLabel(window),
window = window,
rank = rank or 99,
color = color,
}
end
local function windowsFor(provider)
local result = {}
local usage = type(provider) == "table" and provider.usage or nil
if type(usage) ~= "table" then return result end
local color = metaFor(provider, 1).color
addWindow(result, windowLabel(usage.primary, "Session"), usage.primary, 1, color)
addWindow(result, windowLabel(usage.secondary, "Weekly"), usage.secondary, 2, color)
addWindow(result, windowLabel(usage.tertiary, "Monthly"), usage.tertiary, 3, color)
if type(usage.extraRateWindows) == "table" then
for index, extra in ipairs(usage.extraRateWindows) do
if type(extra) == "table" then
addWindow(result, extra.title or extra.name or "Additional window", extra.window or extra, 10 + index, "secondary")
end
end
end
if type(usage.windows) == "table" then
for index, item in ipairs(usage.windows) do
if type(item) == "table" then
addWindow(result, item.title or item.name or "Usage window", item.window or item, 20 + index, color)
end
end
end
for key, value in pairs(usage) do
if type(value) == "table" and tonumber(value.usedPercent) ~= nil then
addWindow(result, windowLabel(value, titleFromId(key)), value, 40, color)
end
end
table.sort(result, function(a, b)
if a.rank ~= b.rank then return a.rank < b.rank end
return a.title < b.title
end)
return result
end
local function creditsFor(provider)
if type(provider) ~= "table" then return nil end
if type(provider.credits) == "table" then return provider.credits end
if type(provider.usage) == "table" and type(provider.usage.credits) == "table" then
return provider.usage.credits
end
return nil
end
local function paceFor(provider)
local pace = type(provider) == "table" and provider.pace or nil
if type(pace) ~= "table" then return nil end
for _, key in ipairs({ "primary", "secondary", "tertiary" }) do
if type(pace[key]) == "table" and pace[key].summary ~= nil then return pace[key] end
end
for _, value in pairs(pace) do
if type(value) == "table" and value.summary ~= nil then return value end
end
return nil
end
local function paceColor(pace)
if pace == nil then return "on_surface_variant" end
local stage = string.lower(tostring(pace.stage or ""))
local summary = string.lower(tostring(pace.summary or ""))
if string.find(stage, "risk") or string.find(stage, "exhaust") or string.find(stage, "deficit") or string.find(summary, "deficit") then
return "error"
end
if string.find(stage, "ahead") or string.find(stage, "reserve") or string.find(summary, "reserve") then
return "secondary"
end
return "on_surface_variant"
end
local function windowRow(item)
local window = item.window
local remaining = remainingPercent(window) or 0
local used = tonumber(window.usedPercent) or 0
local color = item.color or "secondary"
return ui.column({ key = item.title, gap = 5 }, {
ui.row({ align = "center", justify = "space_between" }, {
ui.row({ align = "center", gap = 7 }, {
ui.glyph({ name = "chart-donut-3", size = 14, color = color }),
ui.label({ text = item.title, fontSize = 13, fontWeight = "semibold", color = "on_surface", maxLines = 1 }),
}),
ui.label({ text = percentText(remaining) .. " left", fontSize = 13, fontWeight = "bold", color = color }),
}),
ui.progress({
progress = remaining / 100,
fill = color,
track = "on_surface/0.14",
radius = 8,
height = 8,
}),
ui.row({ align = "center", justify = "space_between" }, {
ui.label({ text = string.format("%d%% used", math.floor(used + 0.5)), fontSize = 11, color = "on_surface_variant" }),
ui.label({ text = "Resets " .. relativeReset(window), fontSize = 11, color = "on_surface_variant" }),
}),
})
end
local function providerCard(provider, index)
local meta = metaFor(provider, index)
local windows = windowsFor(provider)
local children = {}
local source = tostring(provider.source or meta.source or "CodexBar")
local account = providerAccount(provider)
if account ~= "" then source = source .. " · " .. account end
children[#children + 1] = ui.row({ align = "center", justify = "space_between" }, {
ui.row({ align = "center", gap = 9 }, {
ui.row({ align = "center", justify = "center", width = 34, height = 34, fill = meta.color .. "/0.16", radius = 10 }, {
ui.glyph({ name = meta.glyph, size = 18, color = meta.color }),
}),
ui.column({ gap = 1 }, {
ui.label({ text = providerLabel(provider, index), fontSize = 15, fontWeight = "semibold", color = "on_surface", maxLines = 1 }),
ui.label({ text = source, fontSize = 11, color = "on_surface_variant", maxLines = 1 }),
}),
}),
ui.label({ text = updatedAge(provider), fontSize = 11, color = "on_surface_variant" }),
})
children[#children + 1] = ui.separator({ spacing = 2, color = "outline", opacity = 0.18 })
local issue = providerError(provider)
if issue ~= "" then
children[#children + 1] = ui.row({ fill = "error/0.10", radius = 9, padding = 10, align = "center", gap = 8 }, {
ui.glyph({ name = "alert-circle", size = 14, color = "error" }),
ui.label({ text = issue, fontSize = 12, color = "error", maxLines = 2 }),
})
end
if #windows == 0 then
children[#children + 1] = ui.row({ fill = "on_surface/0.05", radius = 9, padding = 10, align = "center", gap = 8 }, {
ui.glyph({ name = "info-circle", size = 14, color = "on_surface_variant" }),
ui.label({ text = "No active usage window reported", fontSize = 12, color = "on_surface_variant" }),
})
else
for _, item in ipairs(windows) do children[#children + 1] = windowRow(item) end
end
local pace = paceFor(provider)
if pace ~= nil and pace.summary ~= nil then
local paceAccent = paceColor(pace)
children[#children + 1] = ui.row({ fill = paceAccent .. "/0.10", radius = 8, paddingH = 9, paddingV = 6, align = "center", gap = 7 }, {
ui.glyph({ name = "activity", size = 13, color = paceAccent }),
ui.label({ text = tostring(pace.summary), fontSize = 11, color = paceAccent, maxLines = 2 }),
})
end
local credits = creditsFor(provider)
if credits ~= nil and credits.remaining ~= nil then
children[#children + 1] = ui.row({ align = "center", justify = "space_between" }, {
ui.row({ align = "center", gap = 7 }, {
ui.glyph({ name = "coins", size = 14, color = "on_surface_variant" }),
ui.label({ text = "Credits", fontSize = 12, color = "on_surface_variant" }),
}),
ui.label({ text = tostring(credits.remaining) .. " available", fontSize = 12, color = "on_surface" }),
})
end
local status = provider.status
if type(status) == "table" and status.description ~= nil and tostring(status.indicator or "none") ~= "none" then
children[#children + 1] = ui.row({ fill = "on_surface/0.05", radius = 8, padding = 8, align = "center", gap = 7 }, {
ui.glyph({ name = "info-circle", size = 13, color = "on_surface_variant" }),
ui.label({ text = tostring(status.description), fontSize = 11, color = "on_surface_variant", maxLines = 2 }),
})
end
if provider.stale == true then
children[#children + 1] = ui.row({ fill = "tertiary/0.10", radius = 8, padding = 8, align = "center", gap = 7 }, {
ui.glyph({ name = "clock", size = 13, color = "tertiary" }),
ui.label({ text = "Showing cached provider data", fontSize = 11, color = "tertiary", maxLines = 2 }),
})
end
return ui.column({ key = "provider-card-" .. tostring(index), fill = "surface_variant/0.58", radius = 14, padding = 11, gap = 8 }, children)
end
local function panelStatus()
if errorMsg ~= "" then
return #providers > 0 and "STALE" or "OFFLINE", "alert-circle", "error"
end
for _, provider in ipairs(providers) do
if provider.stale == true then return "STALE", "clock", "tertiary" end
end
if #providers == 0 then return "WAITING", "loader-2", "on_surface_variant" end
return "LIVE", "circle-filled", "primary"
end
local function render()
if not panelOpen then return end
local statusLabel, statusGlyph, statusColor = panelStatus()
local header = ui.row({ align = "center", justify = "space_between" }, {
ui.row({ align = "center", gap = 10 }, {
ui.row({ align = "center", justify = "center", width = 36, height = 36, fill = "primary/0.15", radius = 11 }, {
ui.glyph({ name = "chart-donut-3", size = 19, color = "primary" }),
}),
ui.column({ gap = 1 }, {
ui.label({ text = "CodexBar Meter", fontSize = 17, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = "Enabled provider limits", fontSize = 11, color = "on_surface_variant" }),
}),
}),
ui.row({ align = "center", gap = 2 }, {
ui.button({ glyph = "refresh", variant = "ghost", tooltip = "Refresh now", onClick = "onRefresh" }),
ui.button({ glyph = "x", variant = "ghost", tooltip = "Close", onClick = "onCloseClicked" }),
}),
})
local children = { header }
children[#children + 1] = ui.row({ align = "center", justify = "space_between", fill = statusColor .. "/0.08", radius = 8, paddingH = 9, paddingV = 6 }, {
ui.row({ align = "center", gap = 6 }, {
ui.glyph({ name = statusGlyph, size = 8, color = statusColor }),
ui.label({ text = statusLabel, fontSize = 10, fontWeight = "bold", color = statusColor }),
}),
ui.label({ text = "Updates every " .. tostring(refreshIntervalSec) .. " seconds", fontSize = 11, color = "on_surface_variant" }),
})
children[#children + 1] = ui.separator({ spacing = 2, color = "outline", opacity = 0.20 })
if errorMsg ~= "" and #providers > 0 then
children[#children + 1] = ui.row({ fill = "error/0.10", radius = 8, padding = 8, align = "center", gap = 7 }, {
ui.glyph({ name = "alert-circle", size = 13, color = "error" }),
ui.label({ text = errorMsg .. " · showing last known data", fontSize = 11, color = "error", maxLines = 2 }),
})
end
local content = {}
if #providers == 0 then
content[#content + 1] = ui.column({ align = "center", paddingV = 40, gap = 10 }, {
ui.glyph({ name = errorMsg ~= "" and "alert-circle" or "loader-2", size = 28, color = errorMsg ~= "" and "error" or "primary" }),
ui.label({ text = errorMsg ~= "" and errorMsg or "Waiting for CodexBar…", fontSize = 13, color = errorMsg ~= "" and "error" or "on_surface_variant" }),
})
else
for index, provider in ipairs(providers) do
content[#content + 1] = providerCard(provider, index)
end
end
-- The root column and this scroll child both need flexGrow. This bounds
-- arbitrary provider counts instead of allowing the panel to clip.
children[#children + 1] = ui.scroll({ key = "usage-providers", flexGrow = 1, gap = 10 }, content)
panel.render(ui.column({ flexGrow = 1, padding = 14, gap = 8 }, children))
end
noctalia.state.watch("providers", function(value)
if type(value) == "table" then
providers = value
render()
end
end)
noctalia.state.watch("error", function(value)
if type(value) == "string" then
errorMsg = value
render()
end
end)
function onOpen(_context)
panelOpen = true
local existing = noctalia.state.get("providers")
if type(existing) == "table" then providers = existing end
local existingError = noctalia.state.get("error")
if type(existingError) == "string" then errorMsg = existingError end
render()
end
function onClose()
panelOpen = false
end
function onCloseClicked()
panel.close()
end
function onRefresh()
noctalia.state.set("command", { action = "refresh" })
end
+69
View File
@@ -0,0 +1,69 @@
id = "salemsayed/codexbar-meter"
name = "CodexBar Meter"
version = "1.0.0"
plugin_api = 8
author = "salemsayed"
license = "MIT"
icon = "chart-donut-3"
description = "A native CodexBar-powered usage meter for enabled AI providers."
tags = ["bar", "panel", "ai", "indicator", "utility"]
dependencies = ["codexbar", "timeout"]
[[setting]]
key = "codexbarPath"
type = "string"
label_key = "settings.codexbar_path.label"
description_key = "settings.codexbar_path.description"
default = "codexbar"
[[setting]]
key = "refreshIntervalSec"
type = "int"
label_key = "settings.refresh_interval.label"
description_key = "settings.refresh_interval.description"
default = 60
min = 30
max = 3600
[[setting]]
key = "barProviderLimit"
type = "int"
label_key = "settings.bar_provider_limit.label"
description_key = "settings.bar_provider_limit.description"
default = 2
min = 1
max = 4
[[widget]]
id = "bar"
entry = "bar_widget.luau"
[[panel]]
id = "panel-compact"
entry = "panel.luau"
width = 430
height = 390
placement = "attached"
position = "auto"
open_near_click = true
dismiss_on_outside_click = true
[[panel]]
id = "panel"
entry = "panel.luau"
width = 430
height = 560
placement = "attached"
position = "auto"
open_near_click = true
dismiss_on_outside_click = true
[[panel]]
id = "panel-tall"
entry = "panel.luau"
width = 430
height = 720
placement = "attached"
position = "auto"
open_near_click = true
dismiss_on_outside_click = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"plugin_name": "CodexBar Meter",
"settings": {
"codexbar_path": {
"label": "CodexBar command",
"description": "Command or absolute path used to query enabled CodexBar providers."
},
"refresh_interval": {
"label": "Refresh interval",
"description": "Seconds between background usage refreshes."
},
"bar_provider_limit": {
"label": "Bar provider limit",
"description": "Maximum number of provider meters shown in the bar capsule; the panel and tooltip still include all providers."
}
}
}