@@ -4,8 +4,9 @@ use crate::context::ContextBreakdown;
44use crate :: git:: GitStatus ;
55use 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
1112pub 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