Add side panels with widget cards and Claude Code usage stats to quick settings

This commit is contained in:
2026-07-27 11:39:54 -04:00
parent 66a8482254
commit fb57093a75
7 changed files with 238 additions and 16 deletions
+1
View File
@@ -38,6 +38,7 @@ The installer automatically exposes scripts from the `scripts/` directory as glo
- **`blob_glass [on|off|toggle]`**: A quick toggle to enable or disable window transparency on the fly.
- **`blob_boot [path]`**: Safely updates your Plymouth boot splash image (defaults to `branding/boot_flash.png`) and rebuilds the `initramfs` (GRUB compatible via `mkinitcpio`).
- **`blob_wifi`**: A streamlined script to connect to the GMU Eduroam Wi-Fi network using `iwd` and `systemd-resolved` (replaces NetworkManager).
- **`blob_claude_key <set|show|clear>`**: Stores your Claude Console Admin API key (`sk-ant-admin01-...`), used by the Claude Code Usage card in the quick settings panel to pull daily token and cost stats.
## Installation
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
KEY_FILE="$HOME/.config/ags/secrets/anthropic_admin_key"
if [ ! -f "$KEY_FILE" ]; then
echo '{"available":false,"tokens":0,"costUsd":0}'
exit 0
fi
ADMIN_KEY=$(cat "$KEY_FILE")
TODAY_START=$(date -u '+%Y-%m-%dT00:00:00Z')
TODAY_END=$(date -u -d tomorrow '+%Y-%m-%dT00:00:00Z')
USAGE_JSON=$(curl -fsS --max-time 6 \
-H "anthropic-version: 2023-06-01" \
-H "x-api-key: $ADMIN_KEY" \
"https://api.anthropic.com/v1/organizations/usage_report/messages?starting_at=${TODAY_START}&ending_at=${TODAY_END}&bucket_width=1d")
COST_JSON=$(curl -fsS --max-time 6 \
-H "anthropic-version: 2023-06-01" \
-H "x-api-key: $ADMIN_KEY" \
"https://api.anthropic.com/v1/organizations/cost_report?starting_at=${TODAY_START}&ending_at=${TODAY_END}&bucket_width=1d")
if [ -z "$USAGE_JSON" ] || [ -z "$COST_JSON" ]; then
echo '{"available":false,"tokens":0,"costUsd":0}'
exit 0
fi
TOKENS=$(echo "$USAGE_JSON" | jq '[(.data // [])[].results[]? | (.uncached_input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation.ephemeral_1h_input_tokens // 0) + (.cache_creation.ephemeral_5m_input_tokens // 0) + (.output_tokens // 0)] | add // 0')
COST_CENTS=$(echo "$COST_JSON" | jq '[(.data // [])[].results[]?.amount | tonumber] | add // 0')
jq -n --argjson tokens "$TOKENS" --argjson costCents "$COST_CENTS" \
'{available: true, tokens: $tokens, costUsd: ($costCents / 100)}'
+42 -9
View File
@@ -349,6 +349,48 @@
font-size: 13px;
}
.qs-divider {
min-width: 1px;
background-color: alpha(@foreground, 0.15);
}
.qs-side-panel {
min-width: 160px;
}
.side-card {
background-color: alpha(@color0, 0.6);
border-radius: 10px;
padding: 10px;
}
.side-card-icon {
color: @accent;
font-size: 14px;
}
.side-card-title {
font-size: 12px;
font-weight: bold;
color: @foreground;
}
.side-card-label {
font-size: 12px;
opacity: 0.8;
}
.side-card-value {
font-size: 13px;
font-weight: bold;
color: @foreground;
}
.side-card-empty {
font-size: 12px;
opacity: 0.6;
}
.control-center {
min-width: 360px;
}
@@ -357,15 +399,6 @@
margin-bottom: 2px;
}
.cc-uptime {
font-size: 13px;
color: @foreground;
}
.cc-uptime-icon {
color: @accent;
}
.cc-clock {
font-size: 22px;
font-weight: bold;
+56
View File
@@ -0,0 +1,56 @@
import { Gtk } from "ags/gtk3"
import { createPoll } from "ags/time"
import { shell } from "../lib/utils"
type ClaudeUsage = {
available: boolean
tokens: number
costUsd: number
}
const usage = createPoll<ClaudeUsage>(
{ available: false, tokens: 0, costUsd: 0 },
60000,
shell("bash $HOME/.config/ags/lib/claude-usage.sh"),
(stdout) => {
try {
return JSON.parse(stdout.trim())
} catch {
return { available: false, tokens: 0, costUsd: 0 }
}
},
)
function formatTokens(tokens: number) {
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(2)}M`
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
return `${tokens}`
}
export function ClaudeUsageCard() {
return (
<box class="side-card" vertical spacing={6}>
<box spacing={8}>
<label class="side-card-icon" label={""} />
<label class="side-card-title" label="Claude Code" xalign={0} halign={Gtk.Align.START} hexpand />
</box>
<label
class="side-card-empty"
label="No admin key set"
xalign={0}
halign={Gtk.Align.START}
visible={usage.as((u) => !u.available)}
/>
<box vertical spacing={4} visible={usage.as((u) => u.available)}>
<box spacing={8}>
<label class="side-card-label" label="Tokens today" xalign={0} halign={Gtk.Align.START} hexpand />
<label class="side-card-value" label={usage.as((u) => formatTokens(u.tokens))} />
</box>
<box spacing={8}>
<label class="side-card-label" label="Cost today" xalign={0} halign={Gtk.Align.START} hexpand />
<label class="side-card-value" label={usage.as((u) => `$${u.costUsd.toFixed(2)}`)} />
</box>
</box>
</box>
)
}
+63 -7
View File
@@ -3,6 +3,7 @@ import { Astal, Gtk } from "ags/gtk3"
import { createState } from "ags"
import { createPoll, interval } from "ags/time"
import { sh, shell } from "../lib/utils"
import { ClaudeUsageCard } from "./ClaudeUsage"
export const QUICKSETTINGS_WINDOW = "quick-settings"
@@ -306,6 +307,55 @@ function CalendarSection() {
)
}
const uptime = createPoll("", 60000, shell("uptime -p"), (stdout) =>
stdout.trim().replace(/^up /, ""),
)
function UptimeCard() {
return (
<box class="side-card" vertical spacing={6}>
<box spacing={8}>
<label class="side-card-icon" label={""} />
<label class="side-card-title" label="Uptime" xalign={0} halign={Gtk.Align.START} hexpand />
</box>
<label class="side-card-value" label={uptime} xalign={0} halign={Gtk.Align.START} wrap />
</box>
)
}
const weather = createPoll("Loading...", 600000, shell("omarchy-weather-status"), (stdout) =>
stdout.trim(),
)
function WeatherCard() {
return (
<box class="side-card" vertical spacing={6}>
<box spacing={8}>
<label class="side-card-icon" label={""} />
<label class="side-card-title" label="Weather" xalign={0} halign={Gtk.Align.START} hexpand />
</box>
<label class="side-card-value" label={weather} xalign={0} halign={Gtk.Align.START} wrap />
</box>
)
}
function LeftPanel() {
return (
<box class="qs-side-panel" vertical spacing={10}>
<UptimeCard />
<WeatherCard />
</box>
)
}
function RightPanel() {
return (
<box class="qs-side-panel" vertical spacing={10}>
<ClaudeUsageCard />
</box>
)
}
export default function QuickSettings() {
const { TOP } = Astal.WindowAnchor
@@ -321,13 +371,19 @@ export default function QuickSettings() {
visible={false}
application={app}
>
<box class="panel quick-settings control-center" vertical spacing={12}>
<Header />
<CalendarSection />
<Toggles />
<VolumeSlider />
<BrightnessSlider />
<MediaPlayer />
<box class="panel quick-settings-shell" spacing={12}>
<LeftPanel />
<box class="qs-divider" vexpand />
<box class="quick-settings control-center" vertical spacing={12}>
<Header />
<CalendarSection />
<Toggles />
<VolumeSlider />
<BrightnessSlider />
<MediaPlayer />
</box>
<box class="qs-divider" vexpand />
<RightPanel />
</box>
</window>
)
+2
View File
@@ -139,6 +139,8 @@ check_file "$SCRIPT_DIR/omarchy/hooks/theme-set" "$HOME_DIR/.config/omarchy/hook
check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts" || check_status=1
check_file "$SCRIPT_DIR/ags/style.css" "$HOME_DIR/.config/ags/style.css" "ags/style.css" || check_status=1
check_file "$SCRIPT_DIR/ags/lib/utils.ts" "$HOME_DIR/.config/ags/lib/utils.ts" "ags/lib/utils.ts" || check_status=1
check_file "$SCRIPT_DIR/ags/lib/claude-usage.sh" "$HOME_DIR/.config/ags/lib/claude-usage.sh" "ags/lib/claude-usage.sh" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/ClaudeUsage.tsx" "$HOME_DIR/.config/ags/widget/ClaudeUsage.tsx" "ags/widget/ClaudeUsage.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/Media.tsx" "$HOME_DIR/.config/ags/widget/Media.tsx" "ags/widget/Media.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/Notifications.tsx" "$HOME_DIR/.config/ags/widget/Notifications.tsx" "ags/widget/Notifications.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/QuickSettings.tsx" "$HOME_DIR/.config/ags/widget/QuickSettings.tsx" "ags/widget/QuickSettings.tsx" || check_status=1
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
KEY_FILE="$HOME/.config/ags/secrets/anthropic_admin_key"
usage() {
echo "Usage: blob_claude_key <set|show|clear>"
echo " set <key> Store your Anthropic Admin API key (sk-ant-admin01-...)"
echo " show Print the stored key, masked"
echo " clear Remove the stored key"
exit 1
}
case "$1" in
set)
key="$2"
if [ -z "$key" ]; then
echo "Usage: blob_claude_key set <key>"
exit 1
fi
mkdir -p "$(dirname "$KEY_FILE")"
printf '%s' "$key" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
echo "Admin key saved to $KEY_FILE"
;;
show)
if [ -f "$KEY_FILE" ]; then
key=$(cat "$KEY_FILE")
echo "${key:0:14}...${key: -4}"
else
echo "No admin key stored"
fi
;;
clear)
rm -f "$KEY_FILE"
echo "Admin key removed"
;;
*)
usage
;;
esac