SLOPSHOPPER

usage-band

A band above the prompt showing context window fill, 5-hour usage and 7-day usage, each with its reset countdown.

newbandtimer
v0.1.0no licenseupdated 2026-10-09BootlegYouki/dotfiles/.claude/my-mods/usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ctx ███░░░ 49% │ 5h ██░░░░ 31% — │ 7d — ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
ctx ███░░░ 49% │ 5h ██░░░░ 31% — │ 7d —
README

❄️ Caelestia Desktop Dotfiles

My custom Caelestia desktop for CachyOS / Arch: a forked Quickshell shell, Hyprland (Lua config), the SDDM login theme, and the scripts and theming around them.


🚀 Fresh install (CachyOS / Arch minimal, CLI only)

git clone https://github.com/BootlegYouki/dotfiles.git
cd dotfiles
./install.sh

The installer will:

  1. Refresh mirrors and install build essentials and an AUR helper.
  2. Detect your GPU (NVIDIA / AMD / Intel) and install the right drivers.
  3. Install Hyprland, UWSM, PipeWire, Ghostty, fonts and the utilities the shell needs.
  4. Install SDDM with the Caelestia theme.
  5. Install Quickshell and the Caelestia shell, then deploy this repo's configs and scripts.
  6. Set Fish (with Starship) as the login shell and enable the user services.

The shell's default apps (shell.json → general.apps: VLC, Nautilus, pwvucontrol, Ghostty) are installed too. Paths in the configs use $HOME/%h, so any username works.

Reboot afterwards to land on the Caelestia login screen.

🔄 Updating an existing system

Open the Sync card in the shell's Utilities panel (hover the bottom edge of the screen): the sync button checks for news, the download button pulls changes from your other desktop into your live config, and the upload button shares yours. The same from a terminal:

dotfiles-sync --pull-only    # download only: copy incoming changes into place, share nothing
dotfiles-sync                # pull and push (what the upload button runs)

Caelestia package updates

The fork in ~/.config/quickshell/caelestia is yours alone: a caelestia-shell update never touches it, and nothing in this repo is copied into /etc/xdg. bin/caelestia-merge-upstream (run by caelestia-shell-start when the shell starts) three-way merges each new upstream release into the fork, keeps your file on a real conflict (upstream's version is saved under its state/pending/), backs up first and rolls back if the merged shell fails to load.

caelestia-merge-upstream --status     # versions and what's pending
caelestia-merge-upstream --dry-run    # preview a merge
caelestia-merge-upstream --rollback   # restore the newest backup

📁 What's in here

PathWhat it is
.config/quickshell/caelestia/The shell (a fork): bar, panels, launcher, lock screen, the Settings app (modules/nexus), including the Display page
.config/caelestia/Caelestia config (shell.json, per-monitor config, templates) and the generated hypr-monitors.lua / .json
.config/hypr/Hyprland configuration (Lua) and keybinds
.config/{fish,ghostty,starship.toml,fastfetch,fuzzel,btop,cava,uwsm,spicetify}Terminal, prompt and desktop theming
.config/systemd/user/User services for this setup (romaji lyrics daemon, shell auto-reload, nightly sync)
bin/, custom_scripts/Scripts the setup uses (wallpaper shift, night light, autologin toggle, romaji daemon, SDDM theme sync)
system/SDDM theme and config, pacman hook for Spicetify
.bash_profileStarts Hyprland through UWSM on tty1

Optional extras

extras/genshin/ holds two Genshin Impact helpers that are not part of the default install:

  • Rapid-F macro (genshin-f-macro): press Ctrl+F in Genshin to toggle it; while on and the game is focused it presses F about 10 times a second (handy for skipping dialogue). The shell's macro indicator follows its state. It runs as root, so it is installed to /usr/local/lib/genshin-macro/ (root-owned) rather than your home directory.
  • Clipboard bridge: copies the Wayland clipboard into gamescope's Xwayland so Ctrl+V works in the game.
./install.sh --extras                 # as part of a full install
./extras/genshin/install.sh           # or on its own, later

Display settings

The shell's Settings → Display page edits monitors live (resolution, scale, orientation, arrangement by dragging, Duplicate / Extend / Show only, main display) with a keep-or-revert prompt. Saved layouts go to ~/.config/caelestia/hypr-monitors.lua, which hypr-user.lua loads.


⚡ Services

systemctl --user status caelestia-romaji.service    # romanised / translated lyrics daemon

Keeping machines in sync

bin/dotfiles-sync runs every 15 minutes (systemd user timer) on each machine, but the timer only fetches: it tells you when the other machine has pushed something and asks before sharing yours. You pull incoming changes yourself (card or --pull-only). It compares each managed file with the last synced version: your edits are committed and pushed, edits from the other machine are copied into your live config when you pull, and a file changed on both sides is reported as a conflict and left alone. Deletions are not propagated. Per-machine files (monitor layout, generated colour scheme, fish_variables) are listed in bin/dotfiles-sync and sync/exclude.txt.

dotfiles-sync --dry-run      # see what it would do
journalctl --user -u dotfiles-sync.service   # or ~/.local/state/dotfiles-sync/sync.log
tests/run.sh                 # tests, including tests/sync.sh (two simulated machines)
Source 3 files
hooks/register.tsx 79 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5import { barCells, countdown, tone } from './format'
6
7const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null)
8const now = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
9
10const TICK_MS = 30_000
11
12export const register: Register = on => {
13  on('session.start', async ($, e, next) => {
14    try {
15      const u = await $.session.usage()
16      await update($, usage, () => ({ context: u.context, rateLimits: u.rateLimits }))
17    } catch {
18      // no reading yet; the first session.measure fills it in
19    }
20    const started = await $.clock.now()
21    await update($, now, () => started)
22    $.clock.every(TICK_MS, async () => {
23      const t = await $.clock.now()
24      await update($, now, () => t)
25    })
26
27    return next(e)
28  })
29
30  on('session.measure', async ($, e, next) => {
31    const snapshot: Snapshot = { context: e.context, rateLimits: e.rateLimits }
32    const t = await $.clock.now()
33    await update($, usage, () => snapshot)
34    await update($, now, () => t)
35
36    return next(e)
37  })
38
39  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
40    const snapshot = await read($, usage)
41    if (e.props.hasSurvey || snapshot === null) return next(e)
42
43    const t = (await read($, now)) || (await $.clock.now())
44    const { Box, Text } = $.ui.resolve(e)
45    const { context, rateLimits } = snapshot
46    const five = rateLimits.find(l => l.kind === 'five_hour')
47    const seven = rateLimits.find(l => l.kind === 'seven_day')
48
49    // Flat Text pieces in one Box row: no fragments (the engine draws them as boxes).
50    const dim = (text: string) => <Text dimColor>{text}</Text>
51    const meter = (percent: number) => {
52      const { filled, empty } = barCells(percent)
53      return [
54        <Text color={tone(percent)}>
55          {'█'.repeat(filled)}
56          {'░'.repeat(empty)} {Math.round(percent)}%
57        </Text>,
58      ]
59    }
60    const limit = (label: string, l: Limit | undefined) =>
61      l
62        ? [dim(`${label} `), ...meter(l.percentUsed), dim(` ${countdown(l.resetsAt, t)}`)]
63        : [dim(`${label} —`)]
64
65    return (
66      <Box>
67        {[
68          dim('ctx '),
69          ...(context.percent === undefined ? [dim('—')] : meter(context.percent)),
70          dim('  │  '),
71          ...limit('5h', five),
72          dim('  │  '),
73          ...limit('7d', seven),
74        ]}
75      </Box>
76    )
77  })
78}
79
hooks/format.ts 24 lines
1/** Time left until `iso`, as "2h 14m", "3d 4h", "<1m", or "now" once past. */
2export const countdown = (iso: string | undefined, now: number): string => {
3  if (!iso) return '—'
4  const ms = Date.parse(iso) - now
5  if (Number.isNaN(ms)) return '—'
6  if (ms <= 0) return 'now'
7  const mins = Math.floor(ms / 60_000)
8  if (mins < 1) return '<1m'
9  const days = Math.floor(mins / 1440)
10  const hours = Math.floor((mins % 1440) / 60)
11  if (days > 0) return `${days}d ${hours}h`
12  if (hours > 0) return `${hours}h ${mins % 60}m`
13  return `${mins}m`
14}
15
16export const tone = (percent: number): 'success' | 'warning' | 'error' =>
17  percent >= 85 ? 'error' : percent >= 60 ? 'warning' : 'success'
18
19/** Cells of a half-height bar: how many are filled and how many are empty. */
20export const barCells = (percent: number, width = 6): { filled: number; empty: number } => {
21  const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)))
22  return { filled, empty: width - filled }
23}
24
types/index.d.ts 13 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Snapshot = {
4  context: { tokens?: number; window: number; percent?: number }
5  rateLimits: Limit[]
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'usage-band': { usage: Snapshot | null; now: number }
11  }
12}
13