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

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.
git clone https://github.com/BootlegYouki/dotfiles.git
cd dotfiles
./install.sh
The installer will:
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.
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)
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
| Path | What 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_profile | Starts Hyprland through UWSM on tty1 |
extras/genshin/ holds two Genshin Impact helpers that are not part of the default install:
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../install.sh --extras # as part of a full install
./extras/genshin/install.sh # or on its own, later
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.
systemctl --user status caelestia-romaji.service # romanised / translated lyrics daemon
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)hooks/register.tsx 79 lines1import { 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}
79hooks/format.ts 24 lines1/** 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}
24types/index.d.ts 13 lines1export 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