Add side panels with widget cards and Claude Code usage stats to quick settings
This commit is contained in:
@@ -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_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_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_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
|
## Installation
|
||||||
|
|
||||||
|
|||||||
Executable
+34
@@ -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
@@ -349,6 +349,48 @@
|
|||||||
font-size: 13px;
|
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 {
|
.control-center {
|
||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
}
|
}
|
||||||
@@ -357,15 +399,6 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cc-uptime {
|
|
||||||
font-size: 13px;
|
|
||||||
color: @foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cc-uptime-icon {
|
|
||||||
color: @accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cc-clock {
|
.cc-clock {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Astal, Gtk } from "ags/gtk3"
|
|||||||
import { createState } from "ags"
|
import { createState } from "ags"
|
||||||
import { createPoll, interval } from "ags/time"
|
import { createPoll, interval } from "ags/time"
|
||||||
import { sh, shell } from "../lib/utils"
|
import { sh, shell } from "../lib/utils"
|
||||||
|
import { ClaudeUsageCard } from "./ClaudeUsage"
|
||||||
|
|
||||||
export const QUICKSETTINGS_WINDOW = "quick-settings"
|
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() {
|
export default function QuickSettings() {
|
||||||
const { TOP } = Astal.WindowAnchor
|
const { TOP } = Astal.WindowAnchor
|
||||||
|
|
||||||
@@ -321,7 +371,10 @@ export default function QuickSettings() {
|
|||||||
visible={false}
|
visible={false}
|
||||||
application={app}
|
application={app}
|
||||||
>
|
>
|
||||||
<box class="panel quick-settings control-center" vertical spacing={12}>
|
<box class="panel quick-settings-shell" spacing={12}>
|
||||||
|
<LeftPanel />
|
||||||
|
<box class="qs-divider" vexpand />
|
||||||
|
<box class="quick-settings control-center" vertical spacing={12}>
|
||||||
<Header />
|
<Header />
|
||||||
<CalendarSection />
|
<CalendarSection />
|
||||||
<Toggles />
|
<Toggles />
|
||||||
@@ -329,6 +382,9 @@ export default function QuickSettings() {
|
|||||||
<BrightnessSlider />
|
<BrightnessSlider />
|
||||||
<MediaPlayer />
|
<MediaPlayer />
|
||||||
</box>
|
</box>
|
||||||
|
<box class="qs-divider" vexpand />
|
||||||
|
<RightPanel />
|
||||||
|
</box>
|
||||||
</window>
|
</window>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/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/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/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/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/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
|
check_file "$SCRIPT_DIR/ags/widget/QuickSettings.tsx" "$HOME_DIR/.config/ags/widget/QuickSettings.tsx" "ags/widget/QuickSettings.tsx" || check_status=1
|
||||||
|
|||||||
Executable
+40
@@ -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
|
||||||
Reference in New Issue
Block a user