Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"license": "BSD-3-Clause",
"name": "yas",
"repository": "https://github.com/tmck-code/yet-another-statusline",
"version": "0.7.0"
"version": "0.7.1"
}
32 changes: 21 additions & 11 deletions claude/yas/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# Keep in sync with pyproject.toml's [project] version — pyproject isn't
# shipped with the runtime copy under ~/.claude, so the value lives here too.
VERSION = '0.7.0'
VERSION = '0.7.1'

HOME = Path(os.path.expanduser('~'))
CLAUDE_DIR = Path(os.environ.get('CLAUDE_CONFIG_DIR', str(HOME / '.claude')))
Expand Down Expand Up @@ -89,10 +89,14 @@
# the row cannot hold both columns at full size plus the rate/spark leader, so
# build_wide drops it for the compact context line instead of overflowing the
# box. The exact, content-aware minimum is computed per-render by
# Renderer.tokens_cost (its ``min_width`` return) — this constant is the
# realistic-widest floor (the wide layout owns box >= MEDIUM_WIDTH=80, and the
# row first fits around box 84-85 for typical 6-7 digit token magnitudes).
TOKENS_COST_MIN_WIDTH = 85
# Renderer.tokens_cost (its ``min_width`` return) — this constant is a flat
# floor on top of that. Deliberately pinned to MEDIUM_WIDTH (the box width
# where build_wide itself starts) rather than a higher magic number: the old
# value of 85 opened an 80-84 band where the plugin row (gated only by
# MEDIUM_WIDTH) had already appeared but the context/tokens row was still
# degraded to the compact form — an inconsistent shed ladder. Aligning the
# two floors means both upgrade together at the same box width.
TOKENS_COST_MIN_WIDTH = MEDIUM_WIDTH
# Cap, in columns, on each individually-distributed justify "extra" slot in
# the wide top row (path/elapsed/5h/7d/cache breathing room — see
# `build_wide`'s justify block). Below `Renderer.JUSTIFY_PAD_CAP=4`'s sibling
Expand All @@ -112,8 +116,9 @@
TOPROW_JUSTIFY_OUTER_CAP = 8
# Floor for the wide layout's four-segment tokens │ lines │ cost │ rate row.
# This constant gates ONLY the lines segment; TOKENS_COST_MIN_WIDTH must stay
# at 85 because bumping it would regress every 85–103-column terminal into the
# compact context line (losing the cost/rate row entirely, not just the lines).
# at MEDIUM_WIDTH because bumping it would regress every terminal below 103
# into the compact context line (losing the cost/rate row entirely, not just
# the lines).
LINES_SEGMENT_MIN_WIDTH = 103

# Minimum gap between the narrow tasks-header's left cluster (glyph + done/total)
Expand Down Expand Up @@ -388,10 +393,15 @@ class BarChars:
PILL_BOT: '-',
PILL_LEFT: '|',
PILL_RIGHT: '|',
PILL_TL: ' ',
PILL_TR: ' ',
PILL_BL: ' ',
PILL_BR: ' ',
# Corners map to '+' (not ' ') so a pill's start/end column never blanks
# a structural elbow/corner it happens to coincide with -- '+' is exactly
# what BOX_T_DOWN/BOX_T_UP/BOX_ARC_T* already fold to in ascii mode, so
# the pill corner reads as a normal box corner whether or not it lines up
# with a divider underneath.
PILL_TL: '+',
PILL_TR: '+',
PILL_BL: '+',
PILL_BR: '+',
}

# Sparkline density ramp fallbacks (U+2581..U+2588), low->high. Some of these
Expand Down
40 changes: 36 additions & 4 deletions claude/yas/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,8 +1116,12 @@ def _resolve_toprow_shed() -> _TopRowShed:
# total_slack > 0; fall through silently when total_slack == 0 (D3).
total_slack = target_w - path_w
path_extra = elapsed_extra = h5_left = h5_right = h7_left = h7_right = cache_extra = last_extra = 0
# `_has_elapsed` is needed both inside the slack-distribution block below
# (to size the N-way split) and afterward for the baked-in-padding
# rebalance, which must run even when total_slack <= 0 -- so compute it
# once, unconditionally, ahead of the slack gate.
_has_elapsed = elapsed_section_w > 0
if view.cfg.justify and total_slack > 0:
_has_elapsed = elapsed_section_w > 0
_has_cache = cache_section_w > 0
_N = 3 + (1 if _has_elapsed else 0) + (1 if has_7d else 0) + (1 if _has_cache else 0)
_extra_per = total_slack // _N
Expand Down Expand Up @@ -1231,9 +1235,29 @@ def _resolve_toprow_shed() -> _TopRowShed:
else:
line_path = f'{line_path}{" " * path_extra}'
path_w += path_extra
if elapsed_extra:
_e_left = elapsed_extra // 2
_e_right = elapsed_extra - _e_left
if view.cfg.justify and _has_elapsed:
# `elapsed_content` may already carry asymmetric leading padding
# baked in by the renderer's fixed-width `rjust` (elapsed_section
# right-justifies to a constant cell width). Strip that baked-in
# run of leading spaces and fold it back into the slack pool
# before splitting, so the TOTAL whitespace either side of the
# visible digits ends up balanced -- not just the slack added
# here. This runs whenever elapsed is present and justify is on,
# *independent* of `total_slack > 0`: a sibling section (e.g.
# `fit_path` crossing a growth threshold) can consume the entire
# row's slack in the same pass, leaving `total_slack == 0` while
# the elapsed cell's own baked-in asymmetry is still present and
# otherwise never gets corrected (D3's "silent fall-through" only
# applies to *distributed* slack, not this pre-existing bake).
_plain_e = _ANSI_RE.sub('', elapsed_content)
_baked_left = len(_plain_e) - len(_plain_e.lstrip(' '))
if _baked_left:
_b_end = _ansi_byte_offset(elapsed_content, _baked_left)
elapsed_content = elapsed_content[:_b_end].rstrip(' ') + elapsed_content[_b_end:]
_total_pad = _baked_left + elapsed_extra
if _total_pad:
_e_left = _total_pad // 2
_e_right = _total_pad - _e_left
elapsed_content = f'{" " * _e_left}{elapsed_content}{" " * _e_right}'
elapsed_section_w += elapsed_extra

Expand Down Expand Up @@ -1395,8 +1419,16 @@ def _resolve_toprow_shed() -> _TopRowShed:
RowSpec('content', content=f'{middle}{" " * last_extra}', right_pill=right_text),
]
else:
# Measure `middle` directly with `_visible_width` instead of hand-summing
# its components (path_w + vsep_w + elapsed_section_w + helper_w + ...):
# the elapsed vsep is built with `lead=1` (4 visible cols, not the
# generic vsep_w=5), so the summed estimate silently overcounted by 1
# whenever an elapsed cell was present -- consuming pad's last reserved
# column and landing right_text (which carries no trailing space of its
# own) flush against the closing border at widths 79-81 (ascii/justify).
pad = max(1, (width - 3) - (path_w + vsep_w + elapsed_section_w + helper_w + cache_section_w + (1 if cache_section_w else 0) + right_w))
content_full = f'{middle}{" " * pad}{right_text}'

rows += [
RowSpec('top_border', downs=path_row_downs, labels=top_labels),
RowSpec('content', content=content_full),
Expand Down
44 changes: 37 additions & 7 deletions claude/yas/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,11 @@ def path_git(
# middle-ellipsis). show_path=False yields the branch-only rung (glyph +
# arrow + branch) used as a width-degradation step below the path forms.
path_part = f'{self.PWD}{short_pwd}{self.R} ' if show_path else ''
glyph_part = f'{GLYPH_FOLDER} ' if show_icons else ''
# With icons off there is no glyph to reserve the row's usual 2-col
# left margin -- fall back to a single literal space so the path row
# lines up with rows that reserve their margin via a fixed-width
# rjust instead of an icon (e.g. context_line's `fmt_tok(...):>6`).
glyph_part = f'{GLYPH_FOLDER} ' if show_icons else ' '

return (
f'{self.ICON_PATH}{glyph_part}{path_part}'
Expand Down Expand Up @@ -699,9 +703,18 @@ def model_right_section(
else:
glyph_part = f' {lead_glyph} ' if show_icons else ' '
if model_thinking:
right_text = f'{model_clr}{glyph_part}{model_name}{self.R} {model_clr}({model_thinking}){RESET}'
right_text = f'{model_clr}{glyph_part}{model_name}{self.R} {model_clr}({model_thinking}){RESET} '
else:
right_text = f'{model_clr}{glyph_part}{model_name}{self.R}'
right_text = f'{model_clr}{glyph_part}{model_name}{self.R} '
# Trailing space above is deliberate, matching the `pct` pill
# branch's baked-in trailing padding cell above: without it, this
# is the rightmost content in the row and its own budget math
# (build_wide's `pad`) can land on exactly 0 spare columns at
# certain widths (e.g. justify+ascii at 79-81), landing a digit
# flush against the closing border. Baking the space in here
# makes the no-digit-adjacent-to-border invariant hold by
# construction rather than by relying on `pad` always having
# slack left over.

right_w = _visible_width(right_text)

Expand Down Expand Up @@ -780,7 +793,11 @@ def plugins_skills(self, skills_count: int, skills_names: str, plugin_names: str
if plugin_names:
plugins_glyph = f'{c_plugins}{BOLD}{GLYPH_PLUGINS} {self.R}' if show_icons else ''
extras.append(f'{plugins_glyph}{self.SKILLS}{plugin_names}{self.R}')
return f' {self.LABEL}|{self.R} '.join(extras)
line = f' {self.LABEL}|{self.R} '.join(extras)
# With icons off, no glyph reserves the row's usual 2-col left
# margin -- add the same single literal space the path/tokens rows
# fall back to, so this row's leading value lines up with them.
return f' {line}' if (line and not show_icons) else line

def tool_counts_row(self, counts: dict[str, tuple[int, int]], width: int, *, fill: float = 1.0) -> str:
"""Greedy-filled per-tool ``Name main/sub`` counts as a full-width line.
Expand Down Expand Up @@ -1740,7 +1757,13 @@ def tokens_cost(self, sess_in: int, sess_cache: int, sess_out: int, day_in: int,
in_icon = f'{ARROW_IN_ACTIVE} ' if in_active else f'{ARROW_IN_IDLE} ' # both 2 cols
out_icon = f'{ARROW_OUT_ACTIVE} ' if out_active else f'{ARROW_OUT_IDLE} ' # both 2 cols
else:
in_icon = out_icon = '' # show_icons=False: number-only, no arrow glyph
# show_icons=False: number-only, no arrow glyph. The row's left
# margin is reserved by `sess_in`'s own rjust (below) instead of
# an icon-shaped space -- mirrors context_line, which has no
# icon-reservation either and relies solely on its rjust'd
# number for the margin. Reserving *both* here would double up
# and misalign the two rows again.
in_icon = out_icon = ''

# Inter-group gaps in the tokens column and the cost/leader edge pads.
# They start at their minimums (gaps 1 space, edge pads 0) so the
Expand All @@ -1751,15 +1774,22 @@ def tokens_cost(self, sess_in: int, sess_cache: int, sess_out: int, day_in: int,
leader_lpad = ''

if show_day_stats:
# Merged session/day per field; variable width, no fixed rjust (D2).
# Merged session/day per field; variable width, no fixed rjust (D2)
# -- except the row's LEADING number (`sess_in`), which is
# right-justified to `IN_W` (the same fixed width the
# session-only rung below uses) so it shares a stable right
# edge with row 2's context-fill number (also rjust'd, in
# `context_line`) and has headroom to grow (e.g. 25.9K ->
# 123.0K) without shifting every column after it or ending up
# flush against the row's left border.
cache = (f'{self.TOK_DIM}({fmt_tok(sess_cache)}{self.R}'
f'{self.TOK_DAY_DIM}/{fmt_tok(day_cache)}{self.R}'
f'{self.TOK_DIM}){self.R}')

def build_tokens() -> str:
return (
f'{self.LABEL}{self.BOLDY}{in_icon}{self.R}'
f'{self.TOK}{fmt_tok(sess_in)}{self.R}{self.TOK_DAY_DIM}/{fmt_tok(day_in)}{self.R}{gap1}'
f'{self.TOK}{fmt_tok(sess_in).rjust(self.IN_W)}{self.R}{self.TOK_DAY_DIM}/{fmt_tok(day_in)}{self.R}{gap1}'
f'{cache}'
f'{self.LABEL}{gap2}{self.BOLDY}{out_icon}{self.R}'
f'{self.TOK}{fmt_tok(sess_out)}{self.R}{self.TOK_DAY_DIM}/{fmt_tok(day_out)}{self.R}'
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "yet-another-statusline"
version = "0.7.0"
version = "0.7.1"
description = "Claude Code statusline showing info at a glance: tokens, context, model, subagents, burn rate, skills, plugins, OpenSpec specs, task lists, and more"
readme = "README.md"

Expand Down
Loading