Files
community-plugins/codexbar-meter/panel.luau
T
1a08a98c4e 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>
2026-08-05 17:57:32 -04:00

449 lines
19 KiB
Luau

--!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