add(deepseek_usage): DeepSeek API credit & balance monitor (#220)

* add(deepseek_usage): add DeepSeek usage balance monitor plugin

* add(deepseek_usage): add thumbnail.webp

* chore(deepseek_usage): update plugin id to coder/deepseek_usage

* fix(deepseek_usage): update 960x540 thumbnail, README IPC docs, and plugin.toml schema

* fix(deepseek_usage): fix panel ID, normalize ui.graph values 0-1, declare xdg-open dependency

* fix(deepseek_usage): replace thumbnail with official Noctalia generator asset
This commit is contained in:
Priyanshu Gupta
2026-08-03 16:33:41 -04:00
committed by GitHub
parent 97d2d5d8b6
commit 8aa75342f3
6 changed files with 591 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# DeepSeek Usage Plugin for Noctalia
A balance and API credit monitor for [Noctalia shell](https://noctalia.dev).
## Plugin
Plugin ID: `coder/deepseek_usage`
Widget(s):
- `bar` — bar widget entry.
Panel(s):
- `panel` — panel entry.
## Requirements
- `xdg-open` — used by the top-up button to open platform.deepseek.com/top_up in the default browser.
## External dependencies
- `xdg-open`
## Usage
Widget
- Add the `bar` widget to your bar to display usage.
Panel
- Toggle the plugin panel with the following IPC command:
`noctalia msg panel-toggle coder/deepseek_usage:panel`
## Features
- **Bar Widget**: Shows live balance directly on your Noctalia bar.
- **Low Balance Warning**: Visual indicator and desktop notifications when credits drop below your set threshold.
- **Interactive Panel**: Displays account wallet summary, local trend graph, and quick top-up action.
- **One-Click Top-Up**: "Add Credits" button opens `platform.deepseek.com/top_up` directly in your browser.
## Settings
Configure the update interval and API key in Noctalia plugin settings.
- **DeepSeek API Key**: Create an API key at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) and enter it in plugin settings.
- **Refresh Interval**: Default is 15 minutes.
- **Low Balance Warning**: Default threshold is 2.00.
+213
View File
@@ -0,0 +1,213 @@
--!nonstrict
-- DeepSeek Usage Bar Widget & Background Service Entry
local DEFAULT_REFRESH_MINUTES = 15
local HISTORY_FILE = "balance_history.json"
local MAX_HISTORY_POINTS = 48
local CURRENCY_SYMBOLS = {
USD = "$",
CNY = "¥",
EUR = "€",
GBP = "£",
JPY = "¥",
}
local function formatBalance(balance: number?, currency: string?): string
if balance == nil then
return "--.--"
end
local curr = currency or "USD"
local symbol = CURRENCY_SYMBOLS[curr]
if symbol then
return string.format("%s%.2f", symbol, balance)
else
return string.format("%.2f %s", balance, curr)
end
end
local state = {
balance = nil :: number?,
currency = "USD",
lastUpdated = nil :: string?,
errorMsg = nil :: string?,
isFetching = false,
}
-- Load accumulated balance history from disk
local function loadHistory(): { { time: number, balance: number } }
local dir = noctalia.pluginDataDir()
if not dir then return {} end
local path = dir .. "/" .. HISTORY_FILE
local content = noctalia.readFile(path)
if not content then return {} end
local decoded = noctalia.json.decode(content)
if type(decoded) == "table" then
return decoded
end
return {}
end
-- Save balance sample to persistent history
local function recordBalanceSample(val: number)
local dir = noctalia.pluginDataDir()
if not dir then return end
local history = loadHistory()
local now = os.time()
table.insert(history, { time = now, balance = val })
-- Keep only the latest N samples
while #history > MAX_HISTORY_POINTS do
table.remove(history, 1)
end
local encoded = noctalia.json.encode(history, false)
if encoded then
noctalia.writeFile(dir .. "/" .. HISTORY_FILE, encoded)
end
-- Share history state with panel
noctalia.state.set("deepseek.history", history)
end
local function renderWidget()
local apiKey = noctalia.getConfig("api_key") or ""
if apiKey == "" then
barWidget.setGlyph("wallet")
barWidget.setText(noctalia.tr("widget.no_key"))
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.status"), value = noctalia.tr("status.unconfigured") },
{ key = noctalia.tr("tooltip.action"), value = noctalia.tr("tooltip.click_to_configure") },
})
return
end
if state.isFetching and state.balance == nil then
barWidget.setGlyph("refresh")
barWidget.setText(noctalia.tr("widget.loading"))
return
end
if state.errorMsg and state.balance == nil then
barWidget.setGlyph("alert-circle")
barWidget.setText(noctalia.tr("widget.error"))
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.status"), value = state.errorMsg },
})
return
end
local threshold = tonumber(noctalia.getConfig("low_balance_threshold")) or 2.0
local balanceStr = formatBalance(state.balance, state.currency)
barWidget.setGlyph("wallet")
barWidget.setText(balanceStr)
if state.balance ~= nil and state.balance < threshold then
barWidget.setColor("error")
else
barWidget.setColor("primary")
end
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.balance"), value = balanceStr .. " " .. state.currency },
{ key = noctalia.tr("tooltip.last_updated"), value = state.lastUpdated or noctalia.tr("status.never") },
{ key = noctalia.tr("tooltip.status"), value = state.errorMsg or noctalia.tr("status.ok") },
})
end
local function fetchSummary()
local apiKey = noctalia.getConfig("api_key") or ""
if apiKey == "" then
state.errorMsg = noctalia.tr("status.unconfigured")
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
state.isFetching = true
renderWidget()
noctalia.http({
url = "https://api.deepseek.com/user/balance",
method = "GET",
headers = {
"Authorization: Bearer " .. apiKey,
"Accept: application/json",
},
}, function(res)
state.isFetching = false
if not res.ok then
state.errorMsg = string.format("HTTP Transport Error (%d)", res.status)
noctalia.notifyError("DeepSeek Usage", state.errorMsg)
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
if res.status ~= 200 then
state.errorMsg = string.format("API Error (%d)", res.status)
if res.status == 401 then
state.errorMsg = noctalia.tr("error.invalid_key")
end
noctalia.notifyError("DeepSeek Usage", state.errorMsg)
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
local parsed, err = noctalia.json.decode(res.body)
if not parsed or type(parsed) ~= "table" then
state.errorMsg = noctalia.tr("error.parse_failed")
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
local balanceInfos = parsed.balance_infos
if balanceInfos and #balanceInfos > 0 then
local primaryWallet = balanceInfos[1]
state.currency = primaryWallet.currency or "USD"
state.balance = tonumber(primaryWallet.total_balance) or 0.0
state.errorMsg = nil
state.lastUpdated = noctalia.formatTime("%H:%M:%S", os.time())
recordBalanceSample(state.balance)
-- Check low balance notification
local threshold = tonumber(noctalia.getConfig("low_balance_threshold")) or 2.0
if state.balance < threshold then
noctalia.notify("DeepSeek Balance Warning", string.format("Your DeepSeek balance is low: %s", formatBalance(state.balance, state.currency)))
end
else
state.errorMsg = noctalia.tr("error.no_wallet")
end
noctalia.state.set("deepseek.state", state)
renderWidget()
end)
end
function update()
local refreshMinutes = tonumber(noctalia.getConfig("refresh_minutes")) or DEFAULT_REFRESH_MINUTES
noctalia.setUpdateInterval(refreshMinutes * 60 * 1000)
fetchSummary()
end
function onClick()
noctalia.togglePanel("coder/deepseek_usage:panel")
end
noctalia.state.watch("deepseek.refresh_requested", function(req)
if req then
noctalia.state.set("deepseek.refresh_requested", false)
fetchSummary()
end
end)
-- Initial setup
renderWidget()
+235
View File
@@ -0,0 +1,235 @@
--!nonstrict
-- DeepSeek Usage Panel Entry — Graphical Balance & Top-Up Dashboard
local CURRENCY_SYMBOLS = {
USD = "$",
CNY = "¥",
EUR = "€",
GBP = "£",
JPY = "¥",
}
local function formatBalance(balance: number?, currency: string?): string
if balance == nil then
return "--.--"
end
local curr = currency or "USD"
local symbol = CURRENCY_SYMBOLS[curr]
if symbol then
return string.format("%s%.2f", symbol, balance)
else
return string.format("%.2f %s", balance, curr)
end
end
local state = {
balance = nil :: number?,
currency = "USD",
lastUpdated = nil :: string?,
errorMsg = nil :: string?,
isFetching = false,
}
local history = {}
function openTopUpPage()
noctalia.runAsync("xdg-open https://platform.deepseek.com/top_up")
end
function triggerRefresh()
noctalia.state.set("deepseek.refresh_requested", true)
end
function onClosePanel()
panel.close()
end
function onOpenSettings()
noctalia.openSettings()
end
local function buildGraphValues(): ({ number }, number, number)
if #history == 0 then
return { 0 }, 0, 0
end
local minVal = history[1].balance
local maxVal = history[1].balance
for _, pt in ipairs(history) do
if pt.balance < minVal then minVal = pt.balance end
if pt.balance > maxVal then maxVal = pt.balance end
end
local range = maxVal - minVal
local values = {}
for _, pt in ipairs(history) do
table.insert(values, range > 0 and (pt.balance - minVal) / range or 0.5)
end
return values, minVal, maxVal
end
local function renderGraphSection(values: { number }, minVal: number, maxVal: number)
-- graph props unverified — adjust after hot-reload test
local graphOk, graphNode = pcall(function()
return ui.graph({
values = values,
height = 64,
color = "primary",
})
end)
local graphElement
if graphOk and type(graphNode) == "table" then
graphElement = graphNode
else
graphElement = ui.label({
text = string.format("History (%d data points)", #history),
fontSize = 11,
color = "on_surface_variant",
})
end
return ui.column({ gap = 6 }, {
ui.row({ justify = "space_between", align = "center" }, {
ui.label({ text = noctalia.tr("panel.history_title"), fontSize = 12, fontWeight = "medium", color = "on_surface_variant" }),
ui.label({ text = string.format("%d samples", #history), fontSize = 10, color = "on_surface_variant" }),
}),
ui.column({
fill = "surface_variant/0.2",
radius = 8,
padding = 8,
gap = 4,
}, {
graphElement,
ui.row({ justify = "space_between" }, {
ui.label({ text = string.format("Min: %s", formatBalance(minVal, state.currency)), fontSize = 10, color = "on_surface_variant" }),
ui.label({ text = string.format("Max: %s", formatBalance(maxVal, state.currency)), fontSize = 10, color = "on_surface_variant" }),
}),
}),
})
end
local function render()
local apiKey = noctalia.getConfig("api_key") or ""
local isUnconfigured = apiKey == ""
local values, minVal, maxVal = buildGraphValues()
local balanceDisplay = formatBalance(state.balance, state.currency)
-- Header Row
local headerRow = ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "wallet", size = 18, color = "primary" }),
ui.label({ text = noctalia.tr("panel.title"), fontSize = 16, fontWeight = "bold", color = "on_surface" }),
}),
ui.row({ gap = 4, align = "center" }, {
ui.button({ glyph = "refresh", variant = "ghost", onClick = "triggerRefresh", tooltip = noctalia.tr("panel.refresh_tooltip") }),
ui.button({ glyph = "close", variant = "ghost", onClick = "onClosePanel" }),
}),
})
-- Scrollable Body Items
local bodyItems = {}
-- Unconfigured State Banner
if isUnconfigured then
table.insert(bodyItems, ui.column({
fill = "surface_variant/0.5",
radius = 8,
padding = 12,
gap = 8,
align = "stretch",
}, {
ui.label({ text = noctalia.tr("panel.unconfigured_title"), fontWeight = "bold", color = "error" }),
ui.label({ text = noctalia.tr("panel.unconfigured_desc"), fontSize = 12, color = "on_surface_variant" }),
ui.button({
text = noctalia.tr("panel.open_settings"),
variant = "primary",
onClick = "onOpenSettings",
}),
}))
end
-- Error Banner
if not isUnconfigured and state.errorMsg ~= nil then
table.insert(bodyItems, ui.row({
fill = "error/0.15",
radius = 8,
padding = 10,
gap = 8,
align = "center",
}, {
ui.glyph({ name = "alert-circle", color = "error", size = 16 }),
ui.label({ text = state.errorMsg, color = "error", fontSize = 12, flexGrow = 1 }),
}))
end
-- Hero Balance Card
if not isUnconfigured then
table.insert(bodyItems, ui.column({
fill = "surface_variant/0.35",
radius = 12,
padding = 16,
align = "center",
gap = 6,
}, {
ui.label({ text = noctalia.tr("panel.current_balance"), fontSize = 12, color = "on_surface_variant" }),
ui.label({ text = balanceDisplay .. " " .. state.currency, fontSize = 28, fontWeight = "bold", color = "primary" }),
ui.label({ text = noctalia.tr("panel.wallet_info"), fontSize = 11, color = "on_surface_variant" }),
ui.spacer({ height = 4 }),
ui.button({
text = " " .. noctalia.tr("panel.add_credits") .. " ",
glyph = "external-link",
variant = "primary",
onClick = "openTopUpPage",
tooltip = noctalia.tr("panel.add_credits_tooltip"),
}),
}))
-- Graph / Trend Section
table.insert(bodyItems, renderGraphSection(values, minVal, maxVal))
end
-- Footer Status Info
local footerRow = ui.row({ justify = "space_between", align = "center" }, {
ui.label({
text = state.lastUpdated and (noctalia.tr("panel.updated_at") .. ": " .. state.lastUpdated) or "",
fontSize = 10,
color = "on_surface_variant",
}),
ui.button({
text = noctalia.tr("panel.settings_btn"),
variant = "ghost",
onClick = "onOpenSettings",
}),
})
panel.render(ui.column({ flexGrow = 1, gap = 10, padding = 12 }, {
headerRow,
ui.separator({}),
ui.scroll({ flexGrow = 1, gap = 10 }, bodyItems),
footerRow,
}))
end
function onOpen(_context)
state = noctalia.state.get("deepseek.state") or state
history = noctalia.state.get("deepseek.history") or {}
render()
end
noctalia.state.watch("deepseek.state", function(newState)
if type(newState) == "table" then
state = newState
render()
end
end)
noctalia.state.watch("deepseek.history", function(newHistory)
if type(newHistory) == "table" then
history = newHistory
render()
end
end)
+47
View File
@@ -0,0 +1,47 @@
id = "coder/deepseek_usage"
name = "DeepSeek Usage"
version = "1.0.0"
plugin_api = 19
author = "coder"
license = "MIT"
dependencies = ["xdg-open"]
tags = ["utility", "productivity", "bar", "panel"]
icon = "wallet"
description = "DeepSeek API credit and balance monitor for Noctalia shell"
[[widget]]
id = "bar"
entry = "deepseek_usage.luau"
[[panel]]
id = "panel"
entry = "panel.luau"
width = 360
height = 480
placement = "attached"
position = "auto"
[[setting]]
key = "api_key"
type = "string"
label_key = "settings.api_key.label"
description_key = "settings.api_key.description"
default = ""
[[setting]]
key = "refresh_minutes"
type = "int"
label_key = "settings.refresh_minutes.label"
description_key = "settings.refresh_minutes.description"
default = 15
min = 1
max = 1440
[[setting]]
key = "low_balance_threshold"
type = "double"
label_key = "settings.low_balance_threshold.label"
description_key = "settings.low_balance_threshold.description"
default = 2.0
min = 0.0
max = 100.0
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+53
View File
@@ -0,0 +1,53 @@
{
"title": "DeepSeek Usage",
"widget": {
"no_key": "Set Key",
"loading": "Checking...",
"error": "Error"
},
"tooltip": {
"balance": "Current Balance",
"last_updated": "Last Checked",
"status": "API Status",
"action": "Action",
"click_to_configure": "Click to open settings and set API Key"
},
"status": {
"ok": "Active",
"unconfigured": "API Key Not Set",
"never": "Never"
},
"error": {
"invalid_key": "Invalid or expired API Key (401)",
"parse_failed": "Failed to parse API response",
"no_wallet": "No active wallet found in user summary"
},
"panel": {
"title": "DeepSeek Balance",
"refresh_tooltip": "Refresh balance now",
"unconfigured_title": "API Key Required",
"unconfigured_desc": "Please configure your DeepSeek API Key in the plugin settings.",
"open_settings": "Open Settings",
"current_balance": "Available Balance",
"wallet_info": "Normal Wallet",
"add_credits": "Add Credits",
"add_credits_tooltip": "Open DeepSeek platform top-up page in browser",
"history_title": "Balance History (24h Trend)",
"updated_at": "Updated",
"settings_btn": "Settings"
},
"settings": {
"api_key": {
"label": "DeepSeek API Key",
"description": "API Key from platform.deepseek.com/api_keys"
},
"refresh_minutes": {
"label": "Refresh Interval (Minutes)",
"description": "How often to poll DeepSeek API for balance updates"
},
"low_balance_threshold": {
"label": "Low Balance Warning Threshold",
"description": "Send a desktop notification when balance drops below this amount"
}
}
}