Skip to content

Commit 963ce8e

Browse files
Demwunzclaude
andcommitted
feat: two-texture bar and percentage-based fill
- Overhead segments (base/skills/plugins/mcp) use ▓, conversation uses █ for at-a-glance visual distinction - Fix fill ratio to use authoritative used_percentage instead of raw token math (total_tokens/context_window) - Update legend to reflect new textures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f30ae9f commit 963ce8e

2 files changed

Lines changed: 57 additions & 38 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cc-statusline"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
description = "Lightweight statusline for Claude Code showing context usage and costs"
66
license = "MIT"

src/render.rs

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use crate::context::ContextBreakdown;
44
use crate::git::GitStatus;
55
use crate::workspace::shorten_path;
66

7-
const FILLED_CHAR: char = '█';
8-
const EMPTY_CHAR: char = '░';
7+
const OVERHEAD_CHAR: char = '▓'; // U+2593 — base/skills/plugins/mcp
8+
const CONVERSATION_CHAR: char = '█'; // U+2588 — conversation
9+
const EMPTY_CHAR: char = '░'; // U+2591 — available
910

1011
/// Render the statusline to a string
1112
pub fn render(
@@ -166,41 +167,59 @@ fn render_bar(breakdown: &ContextBreakdown, config: &Config) -> String {
166167
let mut bar = String::new();
167168
let mut chars_used = 0;
168169

169-
// Calculate the total filled portion of the bar (capped at 100%)
170-
let total_tokens = breakdown.total();
171-
let fill_ratio = (total_tokens as f64 / context_window as f64).min(1.0);
170+
// Use authoritative percentage for fill ratio (fixes token-based mismatch)
171+
let fill_ratio = (breakdown.percentage() as f64 / 100.0).min(1.0);
172172
let total_filled_chars = (fill_ratio * width as f64).round() as usize;
173173

174-
// Render each segment proportionally within the filled area
175-
// Segments are scaled relative to total tokens used (not context window)
176-
for (tokens, segment_type) in breakdown.segments() {
177-
if tokens == 0 || chars_used >= total_filled_chars {
178-
continue;
174+
// Collect non-zero segments
175+
let total_tokens = breakdown.total();
176+
let segments: Vec<(u64, &str)> = breakdown
177+
.segments()
178+
.into_iter()
179+
.filter(|(tokens, _)| *tokens > 0)
180+
.collect();
181+
182+
if !segments.is_empty() && total_filled_chars > 0 && total_tokens > 0 {
183+
// Allocate chars proportionally (min 1 per segment, last gets remainder)
184+
let mut allocs: Vec<usize> = Vec::with_capacity(segments.len());
185+
let mut allocated = 0;
186+
for (i, (tokens, _)) in segments.iter().enumerate() {
187+
if i == segments.len() - 1 {
188+
allocs.push(total_filled_chars - allocated);
189+
} else {
190+
let frac = *tokens as f64 / total_tokens as f64;
191+
let chars = ((frac * total_filled_chars as f64).round() as usize).max(1);
192+
let chars = chars.min(total_filled_chars - allocated - (segments.len() - 1 - i));
193+
allocs.push(chars);
194+
allocated += chars;
195+
}
179196
}
180197

181-
// Calculate this segment's proportion of the total used tokens
182-
let segment_fraction = tokens as f64 / total_tokens as f64;
183-
let segment_chars = ((segment_fraction * total_filled_chars as f64).round() as usize).max(1);
184-
let chars_to_draw = segment_chars.min(total_filled_chars - chars_used);
185-
186-
if chars_to_draw == 0 {
187-
continue;
198+
// Render segments
199+
for ((_, segment_type), seg_chars) in
200+
segments.iter().zip(allocs.iter())
201+
{
202+
let color = match *segment_type {
203+
"base" => colors::color_code(&config.colors.base),
204+
"skills" => colors::color_code(&config.colors.skills),
205+
"plugins" => colors::color_code(&config.colors.plugins),
206+
"mcp" => colors::color_code(&config.colors.mcp),
207+
"conversation" => colors::color_code(&config.colors.conversation),
208+
_ => colors::color_code(&config.colors.empty),
209+
};
210+
211+
// Two-texture fill: overhead segments use ▓, conversation uses █
212+
let fill_char = if *segment_type == "conversation" {
213+
CONVERSATION_CHAR
214+
} else {
215+
OVERHEAD_CHAR
216+
};
217+
218+
bar.push_str(color);
219+
bar.push_str(&fill_char.to_string().repeat(*seg_chars));
220+
bar.push_str(RESET);
221+
chars_used += seg_chars;
188222
}
189-
190-
let color = match segment_type {
191-
"base" => colors::color_code(&config.colors.base),
192-
"skills" => colors::color_code(&config.colors.skills),
193-
"plugins" => colors::color_code(&config.colors.plugins),
194-
"mcp" => colors::color_code(&config.colors.mcp),
195-
"conversation" => colors::color_code(&config.colors.conversation),
196-
_ => colors::color_code(&config.colors.empty),
197-
};
198-
199-
bar.push_str(color);
200-
bar.push_str(&FILLED_CHAR.to_string().repeat(chars_to_draw));
201-
bar.push_str(RESET);
202-
203-
chars_used += chars_to_draw;
204223
}
205224

206225
// Fill remaining with empty
@@ -285,31 +304,31 @@ pub fn print_legend(config: &Config) {
285304
println!(
286305
" {}{}{} blue = base system (~5k tokens)",
287306
colors::color_code(&config.colors.base),
288-
FILLED_CHAR.to_string().repeat(2),
307+
OVERHEAD_CHAR.to_string().repeat(2),
289308
RESET
290309
);
291310
println!(
292311
" {}{}{} cyan = skills",
293312
colors::color_code(&config.colors.skills),
294-
FILLED_CHAR.to_string().repeat(2),
313+
OVERHEAD_CHAR.to_string().repeat(2),
295314
RESET
296315
);
297316
println!(
298317
" {}{}{} magenta = plugins (enabled)",
299318
colors::color_code(&config.colors.plugins),
300-
FILLED_CHAR.to_string().repeat(2),
319+
OVERHEAD_CHAR.to_string().repeat(2),
301320
RESET
302321
);
303322
println!(
304323
" {}{}{} yellow = MCP servers",
305324
colors::color_code(&config.colors.mcp),
306-
FILLED_CHAR.to_string().repeat(2),
325+
OVERHEAD_CHAR.to_string().repeat(2),
307326
RESET
308327
);
309328
println!(
310329
" {}{}{} green = conversation",
311330
colors::color_code(&config.colors.conversation),
312-
FILLED_CHAR.to_string().repeat(2),
331+
CONVERSATION_CHAR.to_string().repeat(2),
313332
RESET
314333
);
315334
println!(

0 commit comments

Comments
 (0)