SLOPSHOPPER

context-bar

Discover and apply my carefully curated dotfiles for effortless macOS system customization. Follow the step-by-step instructions to personalize your machine…

newbandguardtimer
★ 2v?no licenseupdated 2026-10-10slvstr-dev/dotfiles/dot_claude/skills/context-bar
A shopper browsing a rack in a slop shop
README

dotfiles

My macOS dotfiles, managed with chezmoi.

Setup

Prerequisites

  1. Update macOS: sudo softwareupdate -ia
  2. Install Xcode Command Line Tools: xcode-select --install
  3. Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Install

  1. Install chezmoi: brew install chezmoi (or sh -c "$(curl -fsLS get.chezmoi.io)")
  2. chezmoi init git@github.com:slvstr-dev/dotfiles.git --apply
  3. Answer the setup prompts
  4. Restart shell

Day-to-day

Dotfiles should only be edited in this repo (df) and applied by running chezmoi apply.

Shared

Homebrew

brew:install
brew:update
brew:cleanup
brew:uninstall

GitHub

gh auth login

Raycast

Disable Spotlight hotkey and assign it to Raycast using the Raycast hotkey instructions.

Work

Herdr

herdr:open [cmd]               # workspace from cwd + optional cmd
herdr:worktree <branch> [cmd]  # worktree from repo root for a branch + optional cmd
Source 2 files
hooks/register.tsx 191 lines
1import { atom, read, update } from "claude-code";
2import type { EngineInterface, Register, SessionContextBreakdown } from "claude-code";
3
4import type { Reading, Slice } from "../types";
5
6const MIN_WIDTH = 20;
7const SPLIT = "   ";
8const ACCENT = "#d97757";
9const GREY = "#808080";
10const PALETTE = [
11  "#7aa2f7",
12  "#7dcfff",
13  "#bb9af7",
14  "#9ece6a",
15  "#e0af68",
16  "#f7768e",
17  "#73daca",
18  "#ff9e64",
19  "#c0caf5",
20];
21const KINDS = ["used", "free", "buffer"];
22const BAR = { used: "█", free: "─", buffer: "░" };
23const MARKER = { ...BAR, used: "■" };
24
25const reading = atom({ plugin: "context-bar", key: "reading" } as const, null as Reading | null);
26
27const ignore = () => {};
28
29export const register: Register = (on) => {
30  on("session.start", async ($, e, next) => {
31    const result = await next(e);
32    let tries = 0;
33    const retry = $.clock.every(500, () => {
34      void refresh($)
35        .then((ok) => {
36          if (ok || ++tries >= 20) retry.cancel();
37        })
38        .catch(ignore);
39    });
40    return result;
41  });
42
43  on("tool.call", async ($, e, next) => {
44    const result = await next(e);
45    if (!e.agentId) void refresh($).catch(ignore);
46    return result;
47  });
48
49  on("turn.complete", async ($, e, next) => {
50    const result = await next(e);
51    if (!e.agentId) await refresh($).catch(ignore);
52    return result;
53  });
54
55  on("session.compact", async ($, e, next) => {
56    const result = await next(e);
57    if (!e.agentId && "messages" in result) void refresh($).catch(ignore);
58    return result;
59  });
60
61  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
62    const rest = await next(e);
63    const r = await read($, reading);
64    const width = e.props.bodyColumns - 4;
65    if (!r || e.props.hasSurvey || width < MIN_WIDTH) return rest;
66
67    const { Box, Text } = $.ui.resolve(e);
68    const fill = r.total / (r.compactsAt || r.window);
69    const head = `${tokens(r.total)} of ${tokens(r.window)}${r.compactsAt ? ` · compacts at ${tokens(r.compactsAt)}` : ""} `;
70    return (
71      <Box flexDirection="column">
72        <Box flexDirection="column" borderStyle="round" borderColor="inactive" paddingX={1}>
73          <Box flexDirection="row" justifyContent="space-between">
74            <Text wrap="truncate-end">
75              <Text color={ACCENT}>{"◆ "}</Text>
76              <Text bold>context</Text>
77            </Text>
78            <Text wrap="truncate-start">
79              <Text dimColor>{head}</Text>
80              <Text
81                bold
82                color="black"
83                backgroundColor={fill >= 0.9 ? "red" : fill >= 0.7 ? "yellow" : "green"}
84              >{` ${r.percent}% `}</Text>
85            </Text>
86          </Box>
87          <Text>
88            {bar(r, width).map((run) => (
89              <Text color={run.color}>{BAR[run.kind].repeat(run.size)}</Text>
90            ))}
91          </Text>
92          {legend(r, width).map((line) => (
93            <Text wrap="truncate-end">
94              {line.map((s, i) => (
95                <Text>
96                  {i > 0 && <Text>{SPLIT}</Text>}
97                  <Text color={s.color}>{`${MARKER[s.kind]} `}</Text>
98                  <Text dimColor={s.kind !== "used"}>{`${s.name} `}</Text>
99                  <Text bold={s.kind === "used"}>{tokens(s.tokens)}</Text>
100                  {s.kind === "used" && <Text dimColor>{` ${share(s.tokens, r.window)}`}</Text>}
101                </Text>
102              ))}
103            </Text>
104          ))}
105        </Box>
106        {rest}
107      </Box>
108    );
109  });
110};
111
112async function refresh($: EngineInterface) {
113  const { breakdown } = (await $.session.usage({ breakdown: "summary" })).context;
114  if (!breakdown || !(breakdown.rawMaxTokens > 0)) return false;
115  await update($, reading, () => toReading(breakdown));
116  return true;
117}
118
119function toReading(b: SessionContextBreakdown): Reading {
120  let color = 0;
121  const slices = b.categories
122    .filter((c) => c.kind !== "deferred" && c.tokens > 0)
123    .sort((x, y) => KINDS.indexOf(x.kind) - KINDS.indexOf(y.kind))
124    .map((c) => ({
125      name: c.name.toLowerCase(),
126      tokens: c.tokens,
127      color:
128        c.kind !== "used"
129          ? GREY
130          : c.name.toLowerCase() === "messages"
131            ? ACCENT
132            : PALETTE[color++ % PALETTE.length]!,
133      kind: c.kind as Slice["kind"],
134    }));
135  return {
136    slices,
137    total: b.totalTokens,
138    window: b.rawMaxTokens,
139    percent: b.percentage,
140    compactsAt: b.isAutoCompactEnabled ? b.autoCompactThreshold : undefined,
141  };
142}
143
144function bar(r: Reading, width: number) {
145  const runs = r.slices.map((s) => {
146    const min = s.kind === "used" ? 1 : 0;
147    return { ...s, min, size: Math.max(min, Math.round((s.tokens / r.window) * width)) };
148  });
149  let diff = width - runs.reduce((sum, run) => sum + run.size, 0);
150  for (const run of [...runs].sort((a, b) => a.min - b.min || b.size - a.size)) {
151    const size = Math.max(run.min, run.size + diff);
152    diff -= size - run.size;
153    run.size = size;
154  }
155  return runs.filter((run) => run.size > 0);
156}
157
158function legend(r: Reading, width: number) {
159  const lines: Slice[][] = [];
160  let used = 0;
161  for (const s of r.slices) {
162    const size = label(s, r.window).length;
163    const line = lines.at(-1);
164    if (line && used + SPLIT.length + size <= width) {
165      line.push(s);
166      used += SPLIT.length + size;
167    } else {
168      lines.push([s]);
169      used = size;
170    }
171  }
172  return lines;
173}
174
175function label(s: Slice, window: number) {
176  return `${MARKER[s.kind]} ${s.name} ${tokens(s.tokens)}${s.kind === "used" ? ` ${share(s.tokens, window)}` : ""}`;
177}
178
179function tokens(n: number) {
180  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
181  if (n >= 10_000) return `${Math.round(n / 1000)}k`;
182  if (n >= 1000) return `${+(n / 1000).toFixed(1)}k`;
183  return String(n);
184}
185
186function share(n: number, window: number) {
187  const p = (n / window) * 100;
188  if (p > 0 && p < 0.1) return "<0.1%";
189  return `${p >= 10 ? Math.round(p) : +p.toFixed(1)}%`;
190}
191
types/index.d.ts 21 lines
1export type Slice = {
2  name: string;
3  tokens: number;
4  color: string;
5  kind: "used" | "free" | "buffer";
6};
7
8export type Reading = {
9  slices: Slice[];
10  total: number;
11  window: number;
12  percent: number;
13  compactsAt?: number;
14};
15
16declare module "claude-code" {
17  interface PluginState {
18    "context-bar": { reading: Reading | null };
19  }
20}
21