A powerline-style line above the prompt with plan usage, context, cost, model and git branch

Dotbot manages links, vim-plug manages Neovim plugins, and Git submodules provide Dotbot, Oh My Zsh, TPM, and the custom Zsh plugins.
Supported platforms:
Linux installs the checksum-verified Neovim 0.12.5 release under ~/.local/lib; macOS uses Homebrew's current stable Neovim.
Clone over HTTPS so a new machine does not require SSH credentials before bootstrap:
git clone https://github.com/jeffsaracco/dotfiles.git ~/dotfiles
cd ~/dotfiles
./install
With no arguments, install applies the safe CLI base. Profiles are additive:
./install desktop
./install services
./install desktop services
desktop adds Alacritty, AeroSpace, a Nerd Font, and their configuration.services installs database, cache, and web-server packages without explicitly enabling or starting them.On Ubuntu or Debian, authorize sudo before running the installer:
sudo -v
./install
On macOS, install Xcode Command Line Tools first:
xcode-select --install
The bootstrap explicitly updates Homebrew before evaluating the Brewfiles so an existing installation recognizes the host macOS release.
The bootstrap does not clean $HOME, force-overwrite files, or replace an existing non-dotfiles configuration. If a managed destination already exists, Dotbot stops so it can be reviewed and moved manually.
The one automatic migration is a legacy ~/.oh-my-zsh checkout created by an older version of this repository. It is moved to a timestamped backup before the pinned Oh My Zsh submodule is linked.
The installer does not change the login shell. To opt in after installation:
chsh -s "$(command -v zsh)"
The installer creates ~/.gitconfig.local and ~/.zshrc.local with mode 600 when they are absent. It never overwrites existing values. The Git file is seeded with this repository owner's identity and the macOS credential helper. Put signing settings, private environment variables, and machine-specific paths in the local files.
Examples are tracked at:
home/git/gitconfig.local.examplehome/zsh/zshrc.local.exampleStart only services needed on a particular machine. For example:
brew services start redis
sudo systemctl enable --now redis-server
Inside tmux, install or update TPM plugins with <prefix> + I.
hooks/register.tsx 141 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Limit, Meter } from '../types'
5
6const meter = atom({ plugin: 'session-meter', key: 'meter' } as const, null)
7const isHidden = atom({ plugin: 'session-meter', key: 'isHidden' } as const, false)
8
9const LIMIT_LABELS: Record<string, string> = {
10 five_hour: '5h',
11 seven_day: '7d',
12 spend_limit: 'spend',
13}
14
15export function formatReset(resetsAt: string | undefined, now: number): string {
16 if (!resetsAt) return ''
17 const minutes = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60_000))
18 if (Number.isNaN(minutes)) return ''
19 if (minutes < 60) return ` ↻${minutes}m`
20 const hours = Math.floor(minutes / 60)
21 if (hours < 24) return ` ↻${hours}h${minutes % 60 ? `${minutes % 60}m` : ''}`
22 return ` ↻${Math.round(hours / 24)}d`
23}
24
25export function formatTokens(n: number): string {
26 return n >= 1_000_000 ? `${n / 1_000_000}M` : `${Math.round(n / 1000)}k`
27}
28
29// Powerline glyphs: they need a patched font (Nerd Font or Powerline), as
30// the tmux and vim status lines do.
31const ARROW = '\ue0b0'
32const BRANCH = '\ue0a0'
33
34type Segment = { key: string; text: string; bg: string; fg: string; bold?: boolean }
35
36const MODEL = { bg: '#d97757', fg: '#1c1c1c' }
37const BRANCH_SEG = { bg: '#3a3a3a', fg: '#d0d0d0' }
38const COST = { bg: '#303030', fg: '#bcbcbc' }
39
40export function levelColors(percent: number, calm: { bg: string; fg: string }) {
41 if (percent >= 90) return { bg: '#d70000', fg: '#ffffff' }
42 if (percent >= 75) return { bg: '#d7af00', fg: '#1c1c1c' }
43 return calm
44}
45
46async function refresh($: EngineInterface): Promise<void> {
47 const [usage, model, cwd] = await Promise.all([
48 $.session.usage(),
49 $.session.model(),
50 $.session.cwd(),
51 ])
52 let branch: string | undefined
53 try {
54 const git = await $.process.run(['git', 'branch', '--show-current'], { cwd })
55 branch = git.exitCode === 0 ? git.stdout.trim() || undefined : undefined
56 } catch {
57 // Not a repo, or no git: leave the branch off.
58 }
59 const next: Meter = {
60 limits: usage.rateLimits.map(({ kind, percentUsed, resetsAt }): Limit => ({
61 kind,
62 percentUsed,
63 resetsAt,
64 })),
65 contextPercent: usage.context.percent,
66 contextWindow: usage.context.window,
67 usd: usage.cost?.usd,
68 model: model.replace(/^claude-/, ''),
69 branch,
70 }
71 await update($, meter, () => next)
72}
73
74export const register: Register = on => {
75 on('session.start', async ($, e, next) => {
76 await $.command.register({
77 name: 'meter',
78 description: 'Show or hide the usage line above the prompt',
79 })
80 await refresh($)
81
82 return next(e)
83 })
84
85 on('command.run', { command: 'meter' }, async $ => {
86 const hidden = await update($, isHidden, was => !was)
87
88 return { text: hidden ? 'Usage line hidden.' : 'Usage line shown.' }
89 })
90
91 on('session.measure', async ($, e, next) => {
92 await refresh($)
93
94 return next(e)
95 })
96
97 on('turn.complete', async ($, e, next) => {
98 await refresh($)
99
100 return next(e)
101 })
102
103 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
104 const m = await read($, meter)
105 if (e.props.hasSurvey || m === null || (await read($, isHidden))) {
106 return next(e)
107 }
108
109 const { Box, Text } = $.ui.resolve(e)
110 const now = await $.clock.now()
111 const segments: Segment[] = [
112 { key: 'model', text: m.model, ...MODEL, bold: true },
113 ...(m.branch ? [{ key: 'branch', text: `${BRANCH} ${m.branch}`, ...BRANCH_SEG }] : []),
114 ...m.limits.map(limit => ({
115 key: limit.kind,
116 text: `${LIMIT_LABELS[limit.kind] ?? limit.kind} ${limit.percentUsed}%${formatReset(limit.resetsAt, now)}`,
117 ...levelColors(limit.percentUsed, { bg: '#87af5f', fg: '#1c1c1c' }),
118 })),
119 {
120 key: 'ctx',
121 text: `ctx ${m.contextPercent ?? 0}% of ${formatTokens(m.contextWindow)}`,
122 ...levelColors(m.contextPercent ?? 0, { bg: '#005f87', fg: '#ffffff' }),
123 },
124 ...(m.usd !== undefined ? [{ key: 'cost', text: `$${m.usd.toFixed(2)}`, ...COST }] : []),
125 ]
126
127 return (
128 <Box>
129 {segments.flatMap((seg, i) => [
130 <Text key={seg.key} backgroundColor={seg.bg} color={seg.fg} bold={seg.bold}>
131 {` ${seg.text} `}
132 </Text>,
133 <Text key={`${seg.key}-arrow`} color={seg.bg} backgroundColor={segments[i + 1]?.bg}>
134 {ARROW}
135 </Text>,
136 ])}
137 </Box>
138 )
139 })
140}
141types/index.d.ts 17 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Meter = {
4 limits: Limit[]
5 contextPercent?: number
6 contextWindow: number
7 usd?: number
8 model: string
9 branch?: string
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'session-meter': { meter: Meter | null; isHidden: boolean }
15 }
16}
17