cc-context-stats 1.7.0 → 1.8.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.
- package/package.json +9 -1
- package/scripts/context-stats.sh +1 -1
- package/scripts/statusline.js +128 -18
- package/.editorconfig +0 -60
- package/.eslintrc.json +0 -35
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
- package/.github/dependabot.yml +0 -44
- package/.github/workflows/ci.yml +0 -294
- package/.github/workflows/release.yml +0 -151
- package/.pre-commit-config.yaml +0 -74
- package/.prettierrc +0 -33
- package/.shellcheckrc +0 -10
- package/CHANGELOG.md +0 -163
- package/CLAUDE.md +0 -66
- package/CODE_OF_CONDUCT.md +0 -59
- package/CONTRIBUTING.md +0 -240
- package/RELEASE_NOTES.md +0 -19
- package/SECURITY.md +0 -44
- package/TODOS.md +0 -72
- package/assets/logo/favicon.svg +0 -19
- package/assets/logo/logo-black.svg +0 -24
- package/assets/logo/logo-full.svg +0 -40
- package/assets/logo/logo-icon.svg +0 -27
- package/assets/logo/logo-mark.svg +0 -28
- package/assets/logo/logo-white.svg +0 -24
- package/assets/logo/logo-wordmark.svg +0 -6
- package/config/settings-example.json +0 -7
- package/config/settings-node.json +0 -7
- package/config/settings-python.json +0 -7
- package/docs/ARCHITECTURE.md +0 -128
- package/docs/CSV_FORMAT.md +0 -42
- package/docs/DEPLOYMENT.md +0 -71
- package/docs/DEVELOPMENT.md +0 -161
- package/docs/configuration.md +0 -118
- package/docs/context-stats.md +0 -143
- package/docs/installation.md +0 -255
- package/docs/scripts.md +0 -140
- package/docs/troubleshooting.md +0 -278
- package/images/claude-statusline-token-graph.gif +0 -0
- package/images/claude-statusline.png +0 -0
- package/images/context-status-dumbzone.png +0 -0
- package/images/context-status.png +0 -0
- package/images/statusline-detail.png +0 -0
- package/images/token-graph.jpeg +0 -0
- package/images/token-graph.png +0 -0
- package/images/v1.6.1.png +0 -0
- package/install +0 -351
- package/install.sh +0 -298
- package/jest.config.js +0 -11
- package/pyproject.toml +0 -115
- package/requirements-dev.txt +0 -12
- package/scripts/statusline-full.sh +0 -304
- package/scripts/statusline-git.sh +0 -88
- package/scripts/statusline-minimal.sh +0 -67
- package/scripts/statusline.py +0 -485
- package/src/claude_statusline/__init__.py +0 -11
- package/src/claude_statusline/__main__.py +0 -6
- package/src/claude_statusline/cli/__init__.py +0 -1
- package/src/claude_statusline/cli/context_stats.py +0 -512
- package/src/claude_statusline/cli/explain.py +0 -228
- package/src/claude_statusline/cli/statusline.py +0 -169
- package/src/claude_statusline/core/__init__.py +0 -1
- package/src/claude_statusline/core/colors.py +0 -124
- package/src/claude_statusline/core/config.py +0 -148
- package/src/claude_statusline/core/git.py +0 -78
- package/src/claude_statusline/core/state.py +0 -323
- package/src/claude_statusline/formatters/__init__.py +0 -1
- package/src/claude_statusline/formatters/layout.py +0 -67
- package/src/claude_statusline/formatters/time.py +0 -50
- package/src/claude_statusline/formatters/tokens.py +0 -70
- package/src/claude_statusline/graphs/__init__.py +0 -1
- package/src/claude_statusline/graphs/renderer.py +0 -366
- package/src/claude_statusline/graphs/statistics.py +0 -92
- package/src/claude_statusline/ui/__init__.py +0 -1
- package/src/claude_statusline/ui/icons.py +0 -93
- package/src/claude_statusline/ui/waiting.py +0 -62
- package/tests/bash/test_delta_parity.bats +0 -199
- package/tests/bash/test_install.bats +0 -29
- package/tests/bash/test_parity.bats +0 -315
- package/tests/bash/test_statusline_full.bats +0 -139
- package/tests/bash/test_statusline_git.bats +0 -42
- package/tests/bash/test_statusline_minimal.bats +0 -37
- package/tests/fixtures/json/comma_in_path.json +0 -31
- package/tests/fixtures/json/high_usage.json +0 -17
- package/tests/fixtures/json/low_usage.json +0 -17
- package/tests/fixtures/json/medium_usage.json +0 -17
- package/tests/fixtures/json/valid_full.json +0 -30
- package/tests/fixtures/json/valid_minimal.json +0 -9
- package/tests/node/rotation.test.js +0 -89
- package/tests/node/statusline.test.js +0 -240
- package/tests/python/conftest.py +0 -84
- package/tests/python/test_colors.py +0 -105
- package/tests/python/test_config_colors.py +0 -78
- package/tests/python/test_data_pipeline.py +0 -446
- package/tests/python/test_explain.py +0 -177
- package/tests/python/test_icons.py +0 -152
- package/tests/python/test_layout.py +0 -127
- package/tests/python/test_state_rotation_validation.py +0 -232
- package/tests/python/test_statusline.py +0 -215
- package/tests/python/test_waiting.py +0 -127
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""Diagnostic command that dumps the raw JSON context from Claude Code.
|
|
2
|
-
|
|
3
|
-
Usage:
|
|
4
|
-
echo '{"model":...}' | context-stats explain
|
|
5
|
-
echo '{"model":...}' | context-stats explain --no-color
|
|
6
|
-
|
|
7
|
-
Reads the same JSON that Claude Code pipes to the statusline script,
|
|
8
|
-
pretty-prints every field with labels, and shows how cc-context-stats
|
|
9
|
-
interprets them. Useful for debugging blank or missing modules.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import sys
|
|
16
|
-
|
|
17
|
-
from claude_statusline.core.colors import ColorManager
|
|
18
|
-
from claude_statusline.core.config import Config
|
|
19
|
-
from claude_statusline.formatters.tokens import format_tokens
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _pct_color(colors: ColorManager, pct: float) -> str:
|
|
23
|
-
"""Return ANSI color based on free-space percentage."""
|
|
24
|
-
if pct > 50:
|
|
25
|
-
return colors.green
|
|
26
|
-
if pct > 25:
|
|
27
|
-
return colors.yellow
|
|
28
|
-
return colors.red
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _fv(colors: ColorManager, value: object) -> str:
|
|
32
|
-
"""Format a value for display, handling None gracefully."""
|
|
33
|
-
if value is None:
|
|
34
|
-
return f"{colors.dim}(absent){colors.reset}"
|
|
35
|
-
if isinstance(value, float):
|
|
36
|
-
return f"{value:.4f}"
|
|
37
|
-
return str(value)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _render_model(data: dict, colors: ColorManager) -> None:
|
|
41
|
-
model = data.get("model", {})
|
|
42
|
-
print(f"{colors.bold}Model{colors.reset}")
|
|
43
|
-
print(f" display_name: {_fv(colors, model.get('display_name'))}")
|
|
44
|
-
print(f" id: {_fv(colors, model.get('id'))}")
|
|
45
|
-
print(f" api_name: {_fv(colors, model.get('api_name'))}")
|
|
46
|
-
print()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _render_workspace(data: dict, colors: ColorManager) -> None:
|
|
50
|
-
workspace = data.get("workspace", {})
|
|
51
|
-
print(f"{colors.bold}Workspace{colors.reset}")
|
|
52
|
-
print(f" current_dir: {_fv(colors, workspace.get('current_dir'))}")
|
|
53
|
-
print(f" project_dir: {_fv(colors, workspace.get('project_dir'))}")
|
|
54
|
-
print()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _render_context_window(data: dict, colors: ColorManager, config: Config) -> None:
|
|
58
|
-
cw = data.get("context_window", {})
|
|
59
|
-
print(f"{colors.bold}Context Window{colors.reset}")
|
|
60
|
-
total_size = cw.get("context_window_size", 0)
|
|
61
|
-
print(
|
|
62
|
-
f" window_size: "
|
|
63
|
-
f"{format_tokens(total_size, config.token_detail) if total_size else _fv(colors, None)}"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
total_in = cw.get("total_input_tokens")
|
|
67
|
-
total_out = cw.get("total_output_tokens")
|
|
68
|
-
print(
|
|
69
|
-
f" total_input: "
|
|
70
|
-
f"{format_tokens(total_in, config.token_detail) if total_in else _fv(colors, total_in)}"
|
|
71
|
-
)
|
|
72
|
-
print(
|
|
73
|
-
f" total_output: "
|
|
74
|
-
f"{format_tokens(total_out, config.token_detail) if total_out else _fv(colors, total_out)}"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
used_pct = cw.get("used_percentage")
|
|
78
|
-
remaining_pct = cw.get("remaining_percentage")
|
|
79
|
-
print(f" used_pct: {_fv(colors, used_pct)}")
|
|
80
|
-
print(f" remaining_pct: {_fv(colors, remaining_pct)}")
|
|
81
|
-
|
|
82
|
-
cu = cw.get("current_usage")
|
|
83
|
-
if cu:
|
|
84
|
-
_render_current_usage(cu, total_size, colors, config)
|
|
85
|
-
else:
|
|
86
|
-
print(f" current_usage: {colors.dim}(absent — no API call yet this session){colors.reset}")
|
|
87
|
-
print()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _render_current_usage(cu: dict, total_size: int, colors: ColorManager, config: Config) -> None:
|
|
91
|
-
input_tok = cu.get("input_tokens", 0)
|
|
92
|
-
output_tok = cu.get("output_tokens", 0)
|
|
93
|
-
cache_create = cu.get("cache_creation_input_tokens", 0)
|
|
94
|
-
cache_read = cu.get("cache_read_input_tokens", 0)
|
|
95
|
-
used_tokens = input_tok + cache_create + cache_read
|
|
96
|
-
|
|
97
|
-
print(f"\n {colors.bold}Current Usage{colors.reset}")
|
|
98
|
-
print(f" input_tokens: {format_tokens(input_tok, config.token_detail)}")
|
|
99
|
-
print(f" output_tokens: {format_tokens(output_tok, config.token_detail)}")
|
|
100
|
-
print(f" cache_creation_tokens: {format_tokens(cache_create, config.token_detail)}")
|
|
101
|
-
print(f" cache_read_tokens: {format_tokens(cache_read, config.token_detail)}")
|
|
102
|
-
|
|
103
|
-
print(f"\n {colors.bold}Derived Values{colors.reset}")
|
|
104
|
-
print(
|
|
105
|
-
f" context_used (in+cache): "
|
|
106
|
-
f"{colors.cyan}{format_tokens(used_tokens, config.token_detail)}{colors.reset}"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
if total_size > 0:
|
|
110
|
-
free = total_size - used_tokens
|
|
111
|
-
free_pct = (free * 100.0) / total_size
|
|
112
|
-
color = _pct_color(colors, free_pct)
|
|
113
|
-
print(
|
|
114
|
-
f" free_tokens: "
|
|
115
|
-
f"{color}{format_tokens(max(0, free), config.token_detail)} ({free_pct:.1f}%){colors.reset}"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if config.autocompact:
|
|
119
|
-
ac_buffer = int(total_size * 0.225)
|
|
120
|
-
effective_free = max(0, free - ac_buffer)
|
|
121
|
-
eff_pct = (effective_free * 100.0) / total_size
|
|
122
|
-
eff_color = _pct_color(colors, eff_pct)
|
|
123
|
-
print(
|
|
124
|
-
f" autocompact_buffer: "
|
|
125
|
-
f"{colors.dim}{format_tokens(ac_buffer, config.token_detail)}{colors.reset}"
|
|
126
|
-
)
|
|
127
|
-
print(
|
|
128
|
-
f" effective_free (w/ AC): "
|
|
129
|
-
f"{eff_color}{format_tokens(effective_free, config.token_detail)}"
|
|
130
|
-
f" ({eff_pct:.1f}%){colors.reset}"
|
|
131
|
-
)
|
|
132
|
-
else:
|
|
133
|
-
print(f" autocompact: {colors.dim}disabled{colors.reset}")
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _render_cost(data: dict, colors: ColorManager) -> None:
|
|
137
|
-
cost = data.get("cost", {})
|
|
138
|
-
if not cost:
|
|
139
|
-
return
|
|
140
|
-
print(f"{colors.bold}Cost{colors.reset}")
|
|
141
|
-
cost_usd = cost.get("total_cost_usd")
|
|
142
|
-
print(
|
|
143
|
-
f" total_cost_usd: {f'${cost_usd:.4f}' if cost_usd is not None else _fv(colors, None)}"
|
|
144
|
-
)
|
|
145
|
-
print(f" total_duration_ms: {_fv(colors, cost.get('total_duration_ms'))}")
|
|
146
|
-
print(f" lines_added: {_fv(colors, cost.get('total_lines_added'))}")
|
|
147
|
-
print(f" lines_removed: {_fv(colors, cost.get('total_lines_removed'))}")
|
|
148
|
-
print()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _render_session(data: dict, colors: ColorManager) -> None:
|
|
152
|
-
print(f"{colors.bold}Session{colors.reset}")
|
|
153
|
-
print(f" session_id: {_fv(colors, data.get('session_id'))}")
|
|
154
|
-
print(f" version: {_fv(colors, data.get('version'))}")
|
|
155
|
-
print(f" transcript_path: {_fv(colors, data.get('transcript_path'))}")
|
|
156
|
-
print(f" exceeds_200k: {_fv(colors, data.get('exceeds_200k_tokens'))}")
|
|
157
|
-
print()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _render_extensions(data: dict, colors: ColorManager) -> None:
|
|
161
|
-
vim = data.get("vim")
|
|
162
|
-
agent = data.get("agent")
|
|
163
|
-
output_style = data.get("output_style")
|
|
164
|
-
if vim is None and agent is None and output_style is None:
|
|
165
|
-
return
|
|
166
|
-
print(f"{colors.bold}Extensions{colors.reset}")
|
|
167
|
-
if vim is not None:
|
|
168
|
-
vim_mode = vim.get("mode") if isinstance(vim, dict) else vim
|
|
169
|
-
print(f" vim_mode: {_fv(colors, vim_mode)}")
|
|
170
|
-
if agent is not None:
|
|
171
|
-
agent_name = agent.get("name") if isinstance(agent, dict) else agent
|
|
172
|
-
print(f" agent: {_fv(colors, agent_name)}")
|
|
173
|
-
if output_style is not None:
|
|
174
|
-
style_name = output_style.get("name") if isinstance(output_style, dict) else output_style
|
|
175
|
-
print(f" output_style: {_fv(colors, style_name)}")
|
|
176
|
-
print()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _render_config(config: Config, colors: ColorManager) -> None:
|
|
180
|
-
print(
|
|
181
|
-
f"{colors.bold}Active Config{colors.reset} "
|
|
182
|
-
f"{colors.dim}(~/.claude/statusline.conf){colors.reset}"
|
|
183
|
-
)
|
|
184
|
-
for k, v in config.to_dict().items():
|
|
185
|
-
if k == "color_overrides":
|
|
186
|
-
if v:
|
|
187
|
-
print(f" {k}:")
|
|
188
|
-
for slot, ansi_code in v.items():
|
|
189
|
-
print(f" {slot}: {ansi_code}████{colors.reset}")
|
|
190
|
-
continue
|
|
191
|
-
print(f" {k}: {v}")
|
|
192
|
-
print()
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _render_raw_json(data: dict, colors: ColorManager) -> None:
|
|
196
|
-
print(f"{colors.bold}Raw JSON{colors.reset}")
|
|
197
|
-
print(f"{colors.dim}{json.dumps(data, indent=2)}{colors.reset}")
|
|
198
|
-
print()
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def run_explain(data: dict, no_color: bool = False) -> None:
|
|
202
|
-
"""Print a diagnostic dump of the Claude Code session JSON.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
data: Parsed JSON dict from stdin.
|
|
206
|
-
no_color: If True, suppress ANSI color codes.
|
|
207
|
-
"""
|
|
208
|
-
config = Config.load()
|
|
209
|
-
|
|
210
|
-
# Respect --no-color flag and non-TTY output
|
|
211
|
-
color_enabled = not no_color and sys.stdout.isatty()
|
|
212
|
-
colors = ColorManager(enabled=color_enabled, overrides=config.color_overrides)
|
|
213
|
-
|
|
214
|
-
print(f"\n{colors.bold}cc-context-stats explain{colors.reset}")
|
|
215
|
-
print(f"{colors.dim}{'─' * 60}{colors.reset}")
|
|
216
|
-
print(
|
|
217
|
-
f"{colors.dim}Shows how cc-context-stats interprets Claude Code's JSON context."
|
|
218
|
-
f"{colors.reset}\n"
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
_render_model(data, colors)
|
|
222
|
-
_render_workspace(data, colors)
|
|
223
|
-
_render_context_window(data, colors, config)
|
|
224
|
-
_render_cost(data, colors)
|
|
225
|
-
_render_session(data, colors)
|
|
226
|
-
_render_extensions(data, colors)
|
|
227
|
-
_render_config(config, colors)
|
|
228
|
-
_render_raw_json(data, colors)
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""CLI entry point for claude-statusline command.
|
|
3
|
-
|
|
4
|
-
Usage: Copy to ~/.claude/statusline.py and make executable, or install via pip.
|
|
5
|
-
|
|
6
|
-
Configuration:
|
|
7
|
-
Create/edit ~/.claude/statusline.conf and set:
|
|
8
|
-
|
|
9
|
-
autocompact=true (when autocompact is enabled in Claude Code - default)
|
|
10
|
-
autocompact=false (when you disable autocompact via /config in Claude Code)
|
|
11
|
-
|
|
12
|
-
token_detail=true (show exact token count like 64,000 - default)
|
|
13
|
-
token_detail=false (show abbreviated tokens like 64.0k)
|
|
14
|
-
|
|
15
|
-
show_delta=true (show token delta since last refresh like [+2,500] - default)
|
|
16
|
-
show_delta=false (disable delta display - saves file I/O on every refresh)
|
|
17
|
-
|
|
18
|
-
show_session=true (show session_id in status line - default)
|
|
19
|
-
show_session=false (hide session_id from status line)
|
|
20
|
-
|
|
21
|
-
When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import sys
|
|
28
|
-
|
|
29
|
-
from claude_statusline.core.colors import ColorManager
|
|
30
|
-
from claude_statusline.core.config import Config
|
|
31
|
-
from claude_statusline.core.git import get_git_info
|
|
32
|
-
from claude_statusline.core.state import StateEntry, StateFile
|
|
33
|
-
from claude_statusline.formatters.layout import fit_to_width, get_terminal_width
|
|
34
|
-
from claude_statusline.formatters.time import get_current_timestamp
|
|
35
|
-
from claude_statusline.formatters.tokens import calculate_context_usage, format_tokens
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def main() -> None:
|
|
39
|
-
"""Main entry point for claude-statusline CLI."""
|
|
40
|
-
try:
|
|
41
|
-
data = json.load(sys.stdin)
|
|
42
|
-
except json.JSONDecodeError:
|
|
43
|
-
print("[Claude] ~")
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
# Extract data
|
|
47
|
-
cwd = data.get("workspace", {}).get("current_dir", "~")
|
|
48
|
-
project_dir = data.get("workspace", {}).get("project_dir", cwd)
|
|
49
|
-
model = data.get("model", {}).get("display_name", "Claude")
|
|
50
|
-
dir_name = cwd.rsplit("/", 1)[-1] if "/" in cwd else cwd or "~"
|
|
51
|
-
|
|
52
|
-
# Read settings from config file
|
|
53
|
-
config = Config.load()
|
|
54
|
-
|
|
55
|
-
# Build color manager with any user overrides
|
|
56
|
-
colors = ColorManager(enabled=True, overrides=config.color_overrides)
|
|
57
|
-
|
|
58
|
-
# Git info (pass color manager for configurable branch/change colors)
|
|
59
|
-
git_info = get_git_info(project_dir, color_manager=colors)
|
|
60
|
-
|
|
61
|
-
# Extract session_id once for reuse
|
|
62
|
-
session_id = data.get("session_id")
|
|
63
|
-
|
|
64
|
-
# Context window calculation
|
|
65
|
-
context_info = ""
|
|
66
|
-
ac_info = ""
|
|
67
|
-
delta_info = ""
|
|
68
|
-
session_info = ""
|
|
69
|
-
|
|
70
|
-
total_size = data.get("context_window", {}).get("context_window_size", 0)
|
|
71
|
-
current_usage = data.get("context_window", {}).get("current_usage")
|
|
72
|
-
total_input_tokens = data.get("context_window", {}).get("total_input_tokens", 0)
|
|
73
|
-
total_output_tokens = data.get("context_window", {}).get("total_output_tokens", 0)
|
|
74
|
-
cost_usd = data.get("cost", {}).get("total_cost_usd", 0)
|
|
75
|
-
lines_added = data.get("cost", {}).get("total_lines_added", 0)
|
|
76
|
-
lines_removed = data.get("cost", {}).get("total_lines_removed", 0)
|
|
77
|
-
model_id = data.get("model", {}).get("id", "")
|
|
78
|
-
workspace_project_dir = data.get("workspace", {}).get("project_dir", "")
|
|
79
|
-
|
|
80
|
-
if total_size > 0 and current_usage:
|
|
81
|
-
# Get tokens from current_usage (includes cache)
|
|
82
|
-
input_tokens = current_usage.get("input_tokens", 0)
|
|
83
|
-
cache_creation = current_usage.get("cache_creation_input_tokens", 0)
|
|
84
|
-
cache_read = current_usage.get("cache_read_input_tokens", 0)
|
|
85
|
-
|
|
86
|
-
# Total used from current request
|
|
87
|
-
used_tokens = input_tokens + cache_creation + cache_read
|
|
88
|
-
|
|
89
|
-
# Calculate context usage
|
|
90
|
-
free_tokens, free_pct, autocompact_buffer = calculate_context_usage(
|
|
91
|
-
used_tokens,
|
|
92
|
-
total_size,
|
|
93
|
-
config.autocompact,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if config.autocompact:
|
|
97
|
-
buffer_k = autocompact_buffer // 1000
|
|
98
|
-
ac_info = f" {colors.dim}[AC:{buffer_k}k]{colors.reset}"
|
|
99
|
-
else:
|
|
100
|
-
ac_info = f" {colors.dim}[AC:off]{colors.reset}"
|
|
101
|
-
|
|
102
|
-
# Format tokens based on token_detail setting
|
|
103
|
-
free_display = format_tokens(free_tokens, config.token_detail)
|
|
104
|
-
|
|
105
|
-
# Color based on free percentage
|
|
106
|
-
free_pct_int = int(free_pct)
|
|
107
|
-
if free_pct_int > 50:
|
|
108
|
-
ctx_color = colors.green
|
|
109
|
-
elif free_pct_int > 25:
|
|
110
|
-
ctx_color = colors.yellow
|
|
111
|
-
else:
|
|
112
|
-
ctx_color = colors.red
|
|
113
|
-
|
|
114
|
-
context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){colors.reset}"
|
|
115
|
-
|
|
116
|
-
# Calculate and display token delta if enabled
|
|
117
|
-
if config.show_delta:
|
|
118
|
-
state_file = StateFile(session_id)
|
|
119
|
-
prev_entry = state_file.read_last_entry()
|
|
120
|
-
|
|
121
|
-
prev_tokens = prev_entry.current_used_tokens if prev_entry else 0
|
|
122
|
-
has_prev = prev_entry is not None
|
|
123
|
-
|
|
124
|
-
# Calculate delta
|
|
125
|
-
delta = used_tokens - prev_tokens
|
|
126
|
-
|
|
127
|
-
# Only show positive delta (and skip first run when no previous state)
|
|
128
|
-
if has_prev and delta > 0:
|
|
129
|
-
delta_display = format_tokens(delta, config.token_detail)
|
|
130
|
-
delta_info = f" {colors.dim}[+{delta_display}]{colors.reset}"
|
|
131
|
-
|
|
132
|
-
# Build current entry
|
|
133
|
-
cur_input_tokens = current_usage.get("input_tokens", 0)
|
|
134
|
-
cur_output_tokens = current_usage.get("output_tokens", 0)
|
|
135
|
-
|
|
136
|
-
entry = StateEntry(
|
|
137
|
-
timestamp=get_current_timestamp(),
|
|
138
|
-
total_input_tokens=total_input_tokens,
|
|
139
|
-
total_output_tokens=total_output_tokens,
|
|
140
|
-
current_input_tokens=cur_input_tokens,
|
|
141
|
-
current_output_tokens=cur_output_tokens,
|
|
142
|
-
cache_creation=cache_creation,
|
|
143
|
-
cache_read=cache_read,
|
|
144
|
-
cost_usd=cost_usd,
|
|
145
|
-
lines_added=lines_added,
|
|
146
|
-
lines_removed=lines_removed,
|
|
147
|
-
session_id=session_id or "",
|
|
148
|
-
model_id=model_id,
|
|
149
|
-
workspace_project_dir=workspace_project_dir,
|
|
150
|
-
context_window_size=total_size,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
# Only append if context usage changed (avoid duplicates from multiple refreshes)
|
|
154
|
-
if not has_prev or used_tokens != prev_tokens:
|
|
155
|
-
state_file.append_entry(entry)
|
|
156
|
-
|
|
157
|
-
# Display session_id if enabled
|
|
158
|
-
if config.show_session and session_id:
|
|
159
|
-
session_info = f" {colors.dim}{session_id}{colors.reset}"
|
|
160
|
-
|
|
161
|
-
# Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [session_id]
|
|
162
|
-
base = f"{colors.dim}[{model}]{colors.reset} {colors.blue}{dir_name}{colors.reset}"
|
|
163
|
-
max_width = get_terminal_width()
|
|
164
|
-
parts = [base, git_info, context_info, delta_info, ac_info, session_info]
|
|
165
|
-
print(fit_to_width(parts, max_width))
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if __name__ == "__main__":
|
|
169
|
-
main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Core functionality for claude-statusline."""
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"""ANSI color constants and utilities."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
# ANSI color codes (defaults)
|
|
8
|
-
BLUE = "\033[0;34m"
|
|
9
|
-
MAGENTA = "\033[0;35m"
|
|
10
|
-
CYAN = "\033[0;36m"
|
|
11
|
-
GREEN = "\033[0;32m"
|
|
12
|
-
YELLOW = "\033[0;33m"
|
|
13
|
-
RED = "\033[0;31m"
|
|
14
|
-
BOLD = "\033[1m"
|
|
15
|
-
DIM = "\033[2m"
|
|
16
|
-
RESET = "\033[0m"
|
|
17
|
-
|
|
18
|
-
# Mapping from color names to ANSI codes
|
|
19
|
-
COLOR_NAMES: dict[str, str] = {
|
|
20
|
-
"black": "\033[0;30m",
|
|
21
|
-
"red": "\033[0;31m",
|
|
22
|
-
"green": "\033[0;32m",
|
|
23
|
-
"yellow": "\033[0;33m",
|
|
24
|
-
"blue": "\033[0;34m",
|
|
25
|
-
"magenta": "\033[0;35m",
|
|
26
|
-
"cyan": "\033[0;36m",
|
|
27
|
-
"white": "\033[0;37m",
|
|
28
|
-
"bright_black": "\033[0;90m",
|
|
29
|
-
"bright_red": "\033[0;91m",
|
|
30
|
-
"bright_green": "\033[0;92m",
|
|
31
|
-
"bright_yellow": "\033[0;93m",
|
|
32
|
-
"bright_blue": "\033[0;94m",
|
|
33
|
-
"bright_magenta": "\033[0;95m",
|
|
34
|
-
"bright_cyan": "\033[0;96m",
|
|
35
|
-
"bright_white": "\033[0;97m",
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
_HEX_RE = re.compile(r"^#([0-9a-fA-F]{6})$")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def parse_color(value: str) -> str | None:
|
|
42
|
-
"""Parse a color value into an ANSI escape code.
|
|
43
|
-
|
|
44
|
-
Accepts:
|
|
45
|
-
- Named colors: "red", "green", "bright_cyan", etc.
|
|
46
|
-
- Hex colors: "#ff5733" (converted to 24-bit ANSI)
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
ANSI escape code string, or None if the value is not recognized.
|
|
50
|
-
"""
|
|
51
|
-
value = value.strip().lower()
|
|
52
|
-
if not value:
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
# Named color
|
|
56
|
-
if value in COLOR_NAMES:
|
|
57
|
-
return COLOR_NAMES[value]
|
|
58
|
-
|
|
59
|
-
# Hex color (#rrggbb)
|
|
60
|
-
m = _HEX_RE.match(value)
|
|
61
|
-
if m:
|
|
62
|
-
hex_str = m.group(1)
|
|
63
|
-
r = int(hex_str[0:2], 16)
|
|
64
|
-
g = int(hex_str[2:4], 16)
|
|
65
|
-
b = int(hex_str[4:6], 16)
|
|
66
|
-
return f"\033[38;2;{r};{g};{b}m"
|
|
67
|
-
|
|
68
|
-
return None
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class ColorManager:
|
|
72
|
-
"""Manage color output based on terminal capabilities.
|
|
73
|
-
|
|
74
|
-
Supports custom color overrides via a dict of {slot_name: ansi_code}.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
def __init__(
|
|
78
|
-
self,
|
|
79
|
-
enabled: bool = True,
|
|
80
|
-
overrides: dict[str, str] | None = None,
|
|
81
|
-
) -> None:
|
|
82
|
-
self.enabled = enabled
|
|
83
|
-
self._overrides = overrides or {}
|
|
84
|
-
|
|
85
|
-
def _get(self, slot: str, default: str) -> str:
|
|
86
|
-
if not self.enabled:
|
|
87
|
-
return ""
|
|
88
|
-
return self._overrides.get(slot, default)
|
|
89
|
-
|
|
90
|
-
@property
|
|
91
|
-
def blue(self) -> str:
|
|
92
|
-
return self._get("blue", BLUE)
|
|
93
|
-
|
|
94
|
-
@property
|
|
95
|
-
def magenta(self) -> str:
|
|
96
|
-
return self._get("magenta", MAGENTA)
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def cyan(self) -> str:
|
|
100
|
-
return self._get("cyan", CYAN)
|
|
101
|
-
|
|
102
|
-
@property
|
|
103
|
-
def green(self) -> str:
|
|
104
|
-
return self._get("green", GREEN)
|
|
105
|
-
|
|
106
|
-
@property
|
|
107
|
-
def yellow(self) -> str:
|
|
108
|
-
return self._get("yellow", YELLOW)
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def red(self) -> str:
|
|
112
|
-
return self._get("red", RED)
|
|
113
|
-
|
|
114
|
-
@property
|
|
115
|
-
def bold(self) -> str:
|
|
116
|
-
return BOLD if self.enabled else ""
|
|
117
|
-
|
|
118
|
-
@property
|
|
119
|
-
def dim(self) -> str:
|
|
120
|
-
return DIM if self.enabled else ""
|
|
121
|
-
|
|
122
|
-
@property
|
|
123
|
-
def reset(self) -> str:
|
|
124
|
-
return RESET if self.enabled else ""
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Configuration management for statusline."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from dataclasses import dataclass, field
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from claude_statusline.core.colors import parse_color
|
|
11
|
-
|
|
12
|
-
# Color config keys and which ColorManager slot they map to
|
|
13
|
-
_COLOR_KEYS: dict[str, str] = {
|
|
14
|
-
"color_green": "green",
|
|
15
|
-
"color_yellow": "yellow",
|
|
16
|
-
"color_red": "red",
|
|
17
|
-
"color_blue": "blue",
|
|
18
|
-
"color_magenta": "magenta",
|
|
19
|
-
"color_cyan": "cyan",
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class Config:
|
|
25
|
-
"""Configuration settings for the statusline."""
|
|
26
|
-
|
|
27
|
-
autocompact: bool = True
|
|
28
|
-
token_detail: bool = True
|
|
29
|
-
show_delta: bool = True
|
|
30
|
-
show_session: bool = True
|
|
31
|
-
show_io_tokens: bool = True
|
|
32
|
-
reduced_motion: bool = False
|
|
33
|
-
|
|
34
|
-
# Custom color overrides (slot_name -> ANSI code)
|
|
35
|
-
color_overrides: dict[str, str] = field(default_factory=dict)
|
|
36
|
-
|
|
37
|
-
_config_path: Path = field(default_factory=lambda: Path.home() / ".claude" / "statusline.conf")
|
|
38
|
-
|
|
39
|
-
@classmethod
|
|
40
|
-
def load(cls, config_path: str | Path | None = None) -> Config:
|
|
41
|
-
"""Load configuration from file.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
config_path: Path to config file. Defaults to ~/.claude/statusline.conf
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
Config instance with loaded settings
|
|
48
|
-
"""
|
|
49
|
-
config = cls()
|
|
50
|
-
if config_path:
|
|
51
|
-
config._config_path = Path(config_path).expanduser()
|
|
52
|
-
|
|
53
|
-
if not config._config_path.exists():
|
|
54
|
-
config._create_default()
|
|
55
|
-
return config
|
|
56
|
-
|
|
57
|
-
config._read_config()
|
|
58
|
-
return config
|
|
59
|
-
|
|
60
|
-
def _create_default(self) -> None:
|
|
61
|
-
"""Create default config file if it doesn't exist."""
|
|
62
|
-
try:
|
|
63
|
-
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
-
self._config_path.write_text(
|
|
65
|
-
"""# Autocompact setting - sync with Claude Code's /config
|
|
66
|
-
autocompact=true
|
|
67
|
-
|
|
68
|
-
# Token display format
|
|
69
|
-
token_detail=true
|
|
70
|
-
|
|
71
|
-
# Show token delta since last refresh (adds file I/O on every refresh)
|
|
72
|
-
# Disable if you don't need it to reduce overhead
|
|
73
|
-
show_delta=true
|
|
74
|
-
|
|
75
|
-
# Show session_id in status line
|
|
76
|
-
show_session=true
|
|
77
|
-
|
|
78
|
-
# Disable rotating text animations
|
|
79
|
-
reduced_motion=false
|
|
80
|
-
|
|
81
|
-
# Custom colors - use named colors or hex (#rrggbb)
|
|
82
|
-
# Available color slots: color_green, color_yellow, color_red,
|
|
83
|
-
# color_blue, color_magenta, color_cyan
|
|
84
|
-
# Named colors: black, red, green, yellow, blue, magenta, cyan, white,
|
|
85
|
-
# bright_black, bright_red, bright_green, bright_yellow,
|
|
86
|
-
# bright_blue, bright_magenta, bright_cyan, bright_white
|
|
87
|
-
# Examples:
|
|
88
|
-
# color_green=#7dcfff
|
|
89
|
-
# color_yellow=bright_yellow
|
|
90
|
-
# color_red=#f7768e
|
|
91
|
-
"""
|
|
92
|
-
)
|
|
93
|
-
except OSError as e:
|
|
94
|
-
sys.stderr.write(
|
|
95
|
-
f"[statusline] warning: failed to create config {self._config_path}: {e}\n"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
def _read_config(self) -> None:
|
|
99
|
-
"""Read settings from config file."""
|
|
100
|
-
try:
|
|
101
|
-
content = self._config_path.read_text()
|
|
102
|
-
for line in content.splitlines():
|
|
103
|
-
line = line.strip()
|
|
104
|
-
if line.startswith("#") or "=" not in line:
|
|
105
|
-
continue
|
|
106
|
-
key, value = line.split("=", 1)
|
|
107
|
-
key = key.strip()
|
|
108
|
-
raw_value = value.strip()
|
|
109
|
-
value_lower = raw_value.lower()
|
|
110
|
-
|
|
111
|
-
if key == "autocompact":
|
|
112
|
-
self.autocompact = value_lower != "false"
|
|
113
|
-
elif key == "token_detail":
|
|
114
|
-
self.token_detail = value_lower != "false"
|
|
115
|
-
elif key == "show_delta":
|
|
116
|
-
self.show_delta = value_lower != "false"
|
|
117
|
-
elif key == "show_session":
|
|
118
|
-
self.show_session = value_lower != "false"
|
|
119
|
-
elif key == "show_io_tokens":
|
|
120
|
-
self.show_io_tokens = value_lower != "false"
|
|
121
|
-
elif key == "reduced_motion":
|
|
122
|
-
self.reduced_motion = value_lower != "false"
|
|
123
|
-
elif key in _COLOR_KEYS:
|
|
124
|
-
slot = _COLOR_KEYS[key]
|
|
125
|
-
ansi = parse_color(raw_value)
|
|
126
|
-
if ansi:
|
|
127
|
-
self.color_overrides[slot] = ansi
|
|
128
|
-
else:
|
|
129
|
-
sys.stderr.write(
|
|
130
|
-
f"[statusline] warning: unrecognized color value "
|
|
131
|
-
f"'{raw_value}' for {key}\n"
|
|
132
|
-
)
|
|
133
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
134
|
-
sys.stderr.write(
|
|
135
|
-
f"[statusline] warning: failed to read config {self._config_path}: {e}\n"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
def to_dict(self) -> dict[str, Any]:
|
|
139
|
-
"""Convert config to dictionary."""
|
|
140
|
-
return {
|
|
141
|
-
"autocompact": self.autocompact,
|
|
142
|
-
"token_detail": self.token_detail,
|
|
143
|
-
"show_delta": self.show_delta,
|
|
144
|
-
"show_session": self.show_session,
|
|
145
|
-
"show_io_tokens": self.show_io_tokens,
|
|
146
|
-
"reduced_motion": self.reduced_motion,
|
|
147
|
-
"color_overrides": dict(self.color_overrides),
|
|
148
|
-
}
|