claude-contextline 1.3.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -37
  2. package/dist/index.js +52 -189
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # claude-contextline
2
2
 
3
- A powerline-style statusline for Claude Code showing context window usage.
4
-
5
- <img width="677" height="38" alt="image" src="https://github.com/user-attachments/assets/070d99fe-290d-4897-88b0-04247bf27866" />
6
-
3
+ A two-line statusline for Claude Code showing context window usage, model, git
4
+ branch, and working directory.
7
5
 
8
6
  ## Usage
9
7
 
@@ -18,49 +16,20 @@ Add to your Claude Code settings (`~/.claude/settings.json`):
18
16
  }
19
17
  ```
20
18
 
21
- ## Features
22
-
23
- - **Directory** - Shows current project/directory name
24
- - **Git** - Shows branch name with dirty indicator (●)
25
- - **Model** - Shows active Claude model
26
- - **Context** - Shows context window usage percentage
27
-
28
19
  ## Display
29
20
 
30
21
  ```
31
- myproject main ● ✱ Opus 4.5 ◫ 21%
22
+ [█████░░░░░] 42% Opus 4.6
23
+ (main) myproject
32
24
  ```
33
25
 
34
- ### Color States
35
-
36
- - **Normal** (<80%): Sky blue text on dark background
37
- - **Warning** (≥80%): White text on orange background
38
- - **Critical** (≥100%): White text on red background
39
-
40
- ## Options
41
-
42
- - `--theme <name>` - Use a named color theme (e.g. `--theme fulcrum` for purple-accented Fulcrum branding)
43
- - `--no-arrows` - Disable powerline arrow separators
44
-
45
- ```json
46
- {
47
- "statusLine": {
48
- "type": "command",
49
- "command": "npx claude-contextline --theme fulcrum"
50
- }
51
- }
52
- ```
26
+ - **Line 1**: Context window battery bar (blue) with usage percentage, model name (red)
27
+ - **Line 2**: Git branch (red), working directory (blue) aligned below the model
53
28
 
54
29
  ## Requirements
55
30
 
56
31
  - Node.js 18+
57
- - A terminal with powerline font support (for arrow glyphs)
58
-
59
- ## Credits
60
-
61
- Styling based on [claude-limitline](https://github.com/tylergraydev/claude-limitline).
62
32
 
63
33
  ## License
64
34
 
65
35
  MIT
66
-
package/dist/index.js CHANGED
@@ -43,9 +43,8 @@ function getEnvironmentInfo(hookData) {
43
43
  return {
44
44
  directory: getDirectoryName(cwd),
45
45
  gitBranch: getGitBranch(cwd),
46
- gitDirty: isGitDirty(cwd),
47
46
  model: getModelName(hookData),
48
- contextPercent: getContextPercent(hookData)
47
+ usedPercentage: getUsedPercentage(hookData)
49
48
  };
50
49
  }
51
50
  function getDirectoryName(cwd) {
@@ -57,220 +56,84 @@ function getGitBranch(cwd) {
57
56
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
58
57
  cwd,
59
58
  encoding: "utf8",
60
- stdio: ["pipe", "pipe", "pipe"]
59
+ stdio: ["pipe", "pipe", "pipe"],
60
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
61
61
  }).trim();
62
62
  if (branch === "HEAD") {
63
- return execSync("git rev-parse --short HEAD", {
64
- cwd,
65
- encoding: "utf8",
66
- stdio: ["pipe", "pipe", "pipe"]
67
- }).trim();
63
+ return null;
68
64
  }
69
65
  return branch;
70
66
  } catch {
71
67
  return null;
72
68
  }
73
69
  }
74
- function isGitDirty(cwd) {
75
- try {
76
- const status = execSync("git status --porcelain", {
77
- cwd,
78
- encoding: "utf8",
79
- stdio: ["pipe", "pipe", "pipe"]
80
- });
81
- return status.trim().length > 0;
82
- } catch {
83
- return false;
84
- }
85
- }
86
70
  function getModelName(hookData) {
87
71
  const displayName = hookData.model?.display_name || "Claude";
88
72
  return displayName.replace(/^Claude\s+/, "");
89
73
  }
90
- function getContextPercent(hookData) {
74
+ function getUsedPercentage(hookData) {
91
75
  const ctx = hookData.context_window;
92
- if (!ctx?.current_usage || !ctx.context_window_size) {
93
- return 0;
76
+ if (!ctx) return null;
77
+ if (ctx.used_percentage != null) {
78
+ return Math.floor(ctx.used_percentage);
94
79
  }
95
- const usage = ctx.current_usage;
96
- const totalTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
97
- return Math.round(totalTokens / ctx.context_window_size * 100);
98
- }
99
-
100
- // src/utils/constants.ts
101
- var SYMBOLS = {
102
- arrow: "\uE0B0",
103
- // Powerline right arrow
104
- branch: "\uE0A0",
105
- // Git branch icon
106
- model: "\u2731",
107
- // Heavy asterisk ✱
108
- context: "\u25EB",
109
- // White square with vertical bisecting line ◫
110
- dirty: "\u25CF"
111
- // Dirty indicator
112
- };
113
- var TEXT_SYMBOLS = {
114
- arrow: "",
115
- branch: "",
116
- model: "",
117
- context: "",
118
- dirty: "*"
119
- };
120
-
121
- // src/themes/index.ts
122
- var darkTheme = {
123
- directory: { bg: "#8b4513", fg: "#ffffff" },
124
- // Brown, white
125
- git: { bg: "#404040", fg: "#ffffff" },
126
- // Dark gray, white
127
- model: { bg: "#2d2d2d", fg: "#ffffff" },
128
- // Very dark gray, white
129
- context: { bg: "#2a2a2a", fg: "#87ceeb" },
130
- // Nearly black, sky blue
131
- warning: { bg: "#d75f00", fg: "#ffffff" },
132
- // Orange, white (80%+)
133
- critical: { bg: "#af0000", fg: "#ffffff" }
134
- // Red, white (100%+)
135
- };
136
- var fulcrumTheme = {
137
- directory: { bg: "#f90013", fg: "#ffffff" },
138
- // Destructive red, white
139
- git: { bg: "#121212", fg: "#0064f4" },
140
- // Secondary, accent blue
141
- model: { bg: "#090909", fg: "#f84331" },
142
- // Card, warning red-orange
143
- context: { bg: "#161616", fg: "#0064f4" },
144
- // Muted, accent blue
145
- warning: { bg: "#f84331", fg: "#ffffff" },
146
- // Warning red-orange, white (80%+)
147
- critical: { bg: "#0064f4", fg: "#ffffff" }
148
- // Accent blue, white (100%+)
149
- };
150
- function getTheme(name) {
151
- if (name === "fulcrum") return fulcrumTheme;
152
- return darkTheme;
153
- }
154
- function hexToAnsi256(hex) {
155
- const r = parseInt(hex.slice(1, 3), 16);
156
- const g = parseInt(hex.slice(3, 5), 16);
157
- const b = parseInt(hex.slice(5, 7), 16);
158
- if (r === g && g === b) {
159
- if (r < 8) return 16;
160
- if (r > 248) return 231;
161
- return Math.round((r - 8) / 247 * 24) + 232;
80
+ if (ctx.current_usage && ctx.context_window_size) {
81
+ const usage = ctx.current_usage;
82
+ const totalTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
83
+ return Math.floor(totalTokens / ctx.context_window_size * 100);
162
84
  }
163
- const ri = Math.round(r / 255 * 5);
164
- const gi = Math.round(g / 255 * 5);
165
- const bi = Math.round(b / 255 * 5);
166
- return 16 + 36 * ri + 6 * gi + bi;
167
- }
168
- var ansi = {
169
- fg: (hex) => `\x1B[38;5;${hexToAnsi256(hex)}m`,
170
- bg: (hex) => `\x1B[48;5;${hexToAnsi256(hex)}m`,
171
- reset: "\x1B[0m"
172
- };
173
- function getContextColors(percent, theme) {
174
- if (percent >= 100) {
175
- return theme.critical;
176
- } else if (percent >= 80) {
177
- return theme.warning;
178
- }
179
- return theme.context;
85
+ return null;
180
86
  }
181
87
 
182
88
  // src/renderer.ts
183
- function detectNerdFontSupport() {
184
- if (process.env.NERD_FONTS === "1") return true;
185
- if (process.env.NERD_FONTS === "0") return false;
186
- const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
187
- const nerdFontTerminals = [
188
- "warp",
189
- "iterm",
190
- "hyper",
191
- "kitty",
192
- "alacritty",
193
- "ghostty"
194
- ];
195
- return nerdFontTerminals.some((t) => termProgram.includes(t));
196
- }
197
- var Renderer = class {
198
- symbols = detectNerdFontSupport() ? SYMBOLS : TEXT_SYMBOLS;
199
- noArrows;
200
- theme;
201
- constructor(options = {}) {
202
- this.noArrows = options.noArrows ?? false;
203
- this.theme = getTheme(options.themeName);
89
+ var ESC = "\x1B";
90
+ var RESET = `${ESC}[0m`;
91
+ var BLUE = `${ESC}[38;5;69m`;
92
+ var RED = `${ESC}[38;5;196m`;
93
+ var GRAY = `${ESC}[38;5;243m`;
94
+ var BAR_WIDTH = 10;
95
+ var FILLED_CHAR = "\u2588";
96
+ var EMPTY_CHAR = "\u2591";
97
+ function render(envInfo) {
98
+ let out = "";
99
+ let modelCol = 0;
100
+ if (envInfo.usedPercentage != null) {
101
+ const pct = Math.max(0, envInfo.usedPercentage);
102
+ const pctStr = String(pct);
103
+ const nFilled = Math.min(
104
+ Math.floor(pct * BAR_WIDTH / 100),
105
+ BAR_WIDTH
106
+ );
107
+ const nEmpty = BAR_WIDTH - nFilled;
108
+ const filled = FILLED_CHAR.repeat(nFilled);
109
+ const empty = EMPTY_CHAR.repeat(nEmpty);
110
+ out += `${BLUE}[${filled}${GRAY}${empty}${BLUE}] ${pctStr}%`;
111
+ modelCol = 16 + pctStr.length;
204
112
  }
205
- /**
206
- * Render the complete statusline
207
- */
208
- render(envInfo) {
209
- const segments = this.buildSegments(envInfo);
210
- if (segments.length === 0) {
211
- return "";
212
- }
213
- return this.renderPowerline(segments);
113
+ if (out.length > 0) {
114
+ out += " ";
214
115
  }
215
- /**
216
- * Build segments based on environment info
217
- */
218
- buildSegments(envInfo) {
219
- const segments = [];
220
- segments.push({
221
- text: ` ${envInfo.directory} `,
222
- colors: this.theme.directory
223
- });
224
- if (envInfo.gitBranch) {
225
- const dirty = envInfo.gitDirty ? ` ${this.symbols.dirty}` : "";
226
- segments.push({
227
- text: ` ${this.symbols.branch} ${envInfo.gitBranch}${dirty} `,
228
- colors: this.theme.git
229
- });
230
- }
231
- segments.push({
232
- text: ` ${this.symbols.model} ${envInfo.model} `,
233
- colors: this.theme.model
234
- });
235
- const contextColors = getContextColors(envInfo.contextPercent, this.theme);
236
- segments.push({
237
- text: ` ${this.symbols.context} ${envInfo.contextPercent}% `,
238
- colors: contextColors
239
- });
240
- return segments;
116
+ out += `${RED}${envInfo.model}`;
117
+ out += "\n";
118
+ if (envInfo.gitBranch) {
119
+ const branchText = `(${envInfo.gitBranch})`;
120
+ out += `${RED}${branchText}`;
121
+ const gap = Math.max(2, modelCol - branchText.length);
122
+ out += " ".repeat(gap);
123
+ } else {
124
+ out += " ".repeat(modelCol);
241
125
  }
242
- /**
243
- * Render segments with powerline arrows
244
- */
245
- renderPowerline(segments) {
246
- let output = "";
247
- for (let i = 0; i < segments.length; i++) {
248
- const seg = segments[i];
249
- const nextColors = i < segments.length - 1 ? segments[i + 1].colors : null;
250
- output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
251
- output += ansi.reset;
252
- if (this.noArrows) {
253
- } else if (nextColors) {
254
- output += ansi.fg(seg.colors.bg) + ansi.bg(nextColors.bg) + this.symbols.arrow;
255
- } else {
256
- output += ansi.fg(seg.colors.bg) + this.symbols.arrow;
257
- }
258
- }
259
- output += ansi.reset;
260
- return output;
261
- }
262
- };
126
+ out += `${BLUE}${envInfo.directory}`;
127
+ out += RESET;
128
+ return out;
129
+ }
263
130
 
264
131
  // src/index.ts
265
132
  async function main() {
266
133
  try {
267
- const noArrows = process.argv.includes("--no-arrows");
268
- const themeIndex = process.argv.indexOf("--theme");
269
- const themeName = themeIndex !== -1 ? process.argv[themeIndex + 1] : void 0;
270
134
  const hookData = await readHookData();
271
135
  const envInfo = getEnvironmentInfo(hookData);
272
- const renderer = new Renderer({ noArrows, themeName });
273
- const output = renderer.render(envInfo);
136
+ const output = render(envInfo);
274
137
  process.stdout.write(output);
275
138
  } catch {
276
139
  process.exit(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-contextline",
3
- "version": "1.3.2",
4
- "description": "Powerline statusline for Claude Code showing context window usage",
3
+ "version": "2.0.1",
4
+ "description": "Two-line statusline for Claude Code showing context window usage",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -25,7 +25,6 @@
25
25
  "claude",
26
26
  "claude-code",
27
27
  "statusline",
28
- "powerline",
29
28
  "context-window",
30
29
  "cli"
31
30
  ],