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:
co-authored by
Salem Sayed Abdel Gawad
Claude Opus 5
parent
bffb68c86f
commit
1a08a98c4e
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user