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
4 changes: 2 additions & 2 deletions claude/yas/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def build_medium(
vsep_w = 5
rate_w = _visible_width(rate_text)
target_w = (width - 4) - vsep_w - rate_w - right_w
line_path = r.fit_path(session.short_pwd, git, '', target_w, compact_only=True)
line_path = r.fit_path(session.short_pwd, git, target_w, compact_only=True)
path_w = _visible_width(line_path)

pill: Pill | None = None
Expand Down Expand Up @@ -272,7 +272,7 @@ def build_wide(
elapsed_section_w = _elapsed_sw

target_w = (width - 4) - vsep_w - elapsed_section_w - helper_w - cache_section_w - right_w
line_path = r.fit_path(session.short_pwd, git, '', target_w, compact_only=False)
line_path = r.fit_path(session.short_pwd, git, target_w, compact_only=False)
path_w = _visible_width(line_path)

pill: Pill | None = None
Expand Down
22 changes: 10 additions & 12 deletions claude/yas/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ def border_line(self, content: str, width: int, fill: float = 1.0, bg_lead: str
return self.border.border_line(content, width, fill, bg_lead, bg_trail, pill_flush, right_pill)

def path_git(
self, short_pwd: str, git: GitInfo, elapsed: str = '',
*, show_commit: bool = True, show_dirty: bool = True, show_elapsed: bool = True,
self, short_pwd: str, git: GitInfo,
*, show_commit: bool = True, show_dirty: bool = True,
) -> str:
dirty = ''
if show_dirty:
Expand All @@ -322,14 +322,13 @@ def path_git(
dirty += f'{self.DIRTY}{GLYPH_RENAMED} {git.renamed}{RESET}'
if dirty:
dirty = ' ' + dirty
tail = f' {self.SESSION}[{elapsed}]{self.R}' if (show_elapsed and elapsed and elapsed != '0m') else ''
commit_part = f'{self.LABEL}/{self.R}{self.COMMIT}{git.commit}{self.R}' if show_commit else ''

return (
f'{self.ICON_PATH}{GLYPH_FOLDER} {self.PWD}{short_pwd}{self.R}'
f' {self.LABEL}{self.ARROW}{BOLD}∈{self.R}'
f' {self.BRANCH}{git.branch}{self.R}'
f'{commit_part}{dirty}{tail}'
f'{commit_part}{dirty}'
)

def path_git_compact(self, short_pwd: str, git: GitInfo) -> str:
Expand All @@ -340,7 +339,7 @@ def path_git_compact(self, short_pwd: str, git: GitInfo) -> str:
)

def fit_path(
self, short_pwd: str, git: GitInfo, elapsed: str, target_w: int,
self, short_pwd: str, git: GitInfo, target_w: int,
*, compact_only: bool = False,
) -> str:
def fits(s: str) -> bool:
Expand All @@ -350,10 +349,9 @@ def fits(s: str) -> bool:
for kwargs in (
{},
{'show_commit': False},
{'show_commit': False, 'show_elapsed': False},
{'show_commit': False, 'show_elapsed': False, 'show_dirty': False},
{'show_commit': False, 'show_dirty': False},
):
candidate = self.path_git(short_pwd, git, elapsed, **kwargs)
candidate = self.path_git(short_pwd, git, **kwargs)
if fits(candidate):
return candidate

Expand Down Expand Up @@ -390,16 +388,16 @@ def fill_colour(self, pct: float) -> str:
return self.warn
return self.safe

def elapsed_section(self, elapsed: str) -> tuple[str, int]:
text = f'{self.SESSION}{elapsed}{self.R}'
return text, _visible_width(text)

def cache_section(self, remaining: float, elapsed_pct: int) -> tuple[str, int]:
dur = fmt_dur(remaining)
colour = self.fill_colour(elapsed_pct)
text = f'{GLYPH_CACHE} {colour}{dur}{RESET}'
return text, _visible_width(text)

def elapsed_section(self, elapsed: str) -> tuple[str, int]:
text = f'{self.SESSION}{elapsed}{self.R}'
return text, _visible_width(text)

def risk_zone_color(self, tokens: int) -> str:
if tokens <= 50_000:
return self.safe
Expand Down
38 changes: 12 additions & 26 deletions test/test_install_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,10 @@ def test_wire_only_output_is_valid_json(wire_env):
# on PATH (preflight_full runs before --dry-run takes effect), plus a fake
# plugin root for do_wire to discover.
#
# `ensure_plugin` checks `has("yas@yet-another-statusline")` at the TOP-LEVEL
# of installed_plugins.json (not nested under .plugins). `do_wire` then reads
# .plugins[...].installPath to find the renderer. The fixtures therefore put
# the yas key BOTH at the top level (for ensure_plugin) and under .plugins (for
# do_wire).
# `ensure_plugin` checks `.plugins | has("yas@yet-another-statusline")`.
# `do_wire` scans `.plugins` for any key whose ascii-lower contains "yas" to
# find the renderer installPath. The fixtures seed installed_plugins.json with
# the yas key under `.plugins` to satisfy both checks.
# ---------------------------------------------------------------------------

# Guard: preflight_full requires claude, curl, and jq to be on PATH.
Expand Down Expand Up @@ -166,13 +165,10 @@ def full_dry_env(tmp_path: Path) -> tuple[Path, Path, dict]:
def _seed_installed_plugins(config_dir: Path, plugin_root: Path) -> None:
"""Write installed_plugins.json so both ensure_plugin and do_wire are happy.

ensure_plugin reads: has("yas@yet-another-statusline") at top level.
do_wire reads: .plugins["yas@yet-another-statusline"][].installPath
ensure_plugin reads: .plugins | has("yas@yet-another-statusline")
do_wire reads: .plugins[key with "yas"][].installPath
"""
data = {
# Top-level key: satisfies ensure_plugin's has() check.
'yas@yet-another-statusline': [{'installPath': str(plugin_root)}],
# .plugins key: satisfies do_wire's jq path.
'plugins': {
'yas@yet-another-statusline': [{'installPath': str(plugin_root)}],
},
Expand Down Expand Up @@ -201,30 +197,20 @@ def test_dry_run_marketplace_already_present(full_dry_env):
result = run_install('--full', '--dry-run', env_extra=env)
assert result.returncode == 0, result.stderr
combined = result.stdout.lower()
assert 'already present' in combined or 'skipping' in combined
assert 'would update marketplace' in combined


@requires_full_preflight
def test_dry_run_would_install_when_plugin_absent(full_dry_env):
config_dir, plugin_root, env = full_dry_env
# No installed_plugins.json → has() returns false → "Would install"
# But do_wire still needs to find the plugin root, so we can't omit the
# file entirely from the filesystem — we can leave it absent and accept
# that do_wire will fail after the dry-run install message. However the
# spec says dry-run should print "Would install" and exit 0.
#
# The script: ensure_plugin prints "Would install" (DRY_RUN=1), then
# do_wire tries to find PLUGIN_ROOT from installed_plugins.json which is
# absent → exits 1. So we seed a minimal installed_plugins.json that
# does NOT have the top-level yas key (triggering install path) but DOES
# have the .plugins entry so do_wire can resolve the renderer path.
# ensure_plugin checks `.plugins | has("yas@yet-another-statusline")`.
# do_wire scans `.plugins` for any key where ascii_downcase contains "yas".
# Use a different key name so ensure_plugin sees absent (→ "Would install")
# while do_wire can still resolve the renderer path via the "yas"-containing key.
data = {
# .plugins key satisfies do_wire discovery.
'plugins': {
'yas@yet-another-statusline': [{'installPath': str(plugin_root)}],
'yas-cached@yet-another-statusline': [{'installPath': str(plugin_root)}],
},
# Note: 'yas@yet-another-statusline' is NOT a top-level key here,
# so ensure_plugin sees has() == false → "Would install".
}
(config_dir / 'plugins' / 'installed_plugins.json').write_text(json.dumps(data))
result = run_install('--full', '--dry-run', env_extra=env)
Expand Down
8 changes: 4 additions & 4 deletions test/test_model_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _wide_combo(self, r: Renderer, width: int) -> tuple[str, str, str, int]:
)
helper_w = _visible_width(helper)
target_w = (width - 4) - self._vsep_w - helper_w - right_w
path = r.fit_path('~/deep/project/submodule/src', git, '15m', target_w,
path = r.fit_path('~/deep/project/submodule/src', git, target_w,
compact_only=False)
return path, helper, right, right_w

Expand All @@ -92,7 +92,7 @@ def _medium_combo(self, r: Renderer, width: int) -> tuple[str, str, str, int]:
)
rate_w = _visible_width(_rate)
target_w = (width - 4) - self._vsep_w - rate_w - right_w
path = r.fit_path('~/deep/project/submodule/src', git, '', target_w,
path = r.fit_path('~/deep/project/submodule/src', git, target_w,
compact_only=True)
return path, _rate, right, right_w

Expand All @@ -103,7 +103,7 @@ def test_wide_path_fits_target_at_borderline(self) -> None:
helper, _right, right_w = r.model_right_section('Sonnet 4.6', '', RateLimits())
helper_w = _visible_width(helper)
target_w = (width - 4) - self._vsep_w - helper_w - right_w
path = r.fit_path('~/deep/project/submodule/src', git, '15m', target_w)
path = r.fit_path('~/deep/project/submodule/src', git, target_w)
assert _visible_width(path) <= target_w, f'width={width}: path overflows target_w={target_w}'

def test_medium_path_fits_target_at_borderline(self) -> None:
Expand All @@ -115,7 +115,7 @@ def test_medium_path_fits_target_at_borderline(self) -> None:
)
rate_w = _visible_width(_rate)
target_w = (width - 4) - self._vsep_w - rate_w - right_w
path = r.fit_path('~/deep/project/submodule/src', git, '', target_w,
path = r.fit_path('~/deep/project/submodule/src', git, target_w,
compact_only=True)
assert _visible_width(path) <= target_w, f'width={width}: path overflows target_w={target_w}'

Expand Down
84 changes: 33 additions & 51 deletions test/test_path_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
Renderer = renderer.Renderer


def test_path_git_clean_no_elapsed() -> None:
def test_path_git_clean() -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234')
out = r.path_git('~/proj', git, '')
out = r.path_git('~/proj', git)
stripped = strip_ansi(out)
assert '~/proj' in stripped
assert 'main' in stripped
Expand All @@ -19,22 +19,13 @@ def test_path_git_clean_no_elapsed() -> None:
assert '[' not in stripped


def test_path_git_dirty_with_elapsed() -> None:
def test_path_git_dirty() -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234', modified=3, untracked=1)
out = r.path_git('~/proj', git, '12m')
out = r.path_git('~/proj', git)
stripped = strip_ansi(out)
assert '*3' in stripped # modified
assert '•1' in stripped # untracked
assert '[12m]' in stripped


def test_path_git_zero_elapsed_suppressed() -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234')
out = r.path_git('~/proj', git, '0m')
stripped = strip_ansi(out)
assert '[0m]' not in stripped


def test_path_git_compact_no_commit_no_dirty() -> None:
Expand All @@ -49,44 +40,47 @@ def test_path_git_compact_no_commit_no_dirty() -> None:
assert '*' not in stripped


def test_elapsed_section_shows_clock_time() -> None:
r = Renderer()
text, w = r.elapsed_section('0:12:34')
stripped = strip_ansi(text)
assert '0:12:34' in stripped
assert w == _visible_width(text)


def test_elapsed_section_empty_string_still_renders() -> None:
r = Renderer()
text, w = r.elapsed_section('')
assert w >= 0


# path_git keyword-flag regression (task 4.3)

class TestPathGitFlags:
def test_defaults_byte_identical(self) -> None:
r = Renderer()
git = GitInfo(branch='feat/login', commit='abc1234', modified=2, untracked=1)
explicit = r.path_git('~/proj', git, '5m',
show_commit=True, show_dirty=True, show_elapsed=True)
default = r.path_git('~/proj', git, '5m')
explicit = r.path_git('~/proj', git, show_commit=True, show_dirty=True)
default = r.path_git('~/proj', git)
assert explicit == default

def test_show_commit_false_omits_hash(self) -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234', modified=1)
out = r.path_git('~/proj', git, '3m', show_commit=False)
out = r.path_git('~/proj', git, show_commit=False)
stripped = strip_ansi(out)
assert 'abc1234' not in stripped
assert '/' not in stripped.split('main')[1]
assert '*1' in stripped # modified
assert '[3m]' in stripped

def test_show_elapsed_false_omits_tail(self) -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234')
out = r.path_git('~/proj', git, '5m', show_elapsed=False)
stripped = strip_ansi(out)
assert '[5m]' not in stripped
assert 'abc1234' in stripped

def test_show_dirty_false_omits_markers(self) -> None:
r = Renderer()
git = GitInfo(branch='main', commit='abc1234', modified=3, untracked=2)
out = r.path_git('~/proj', git, '5m', show_dirty=False)
out = r.path_git('~/proj', git, show_dirty=False)
stripped = strip_ansi(out)
assert '●' not in stripped
assert '*' not in stripped
assert 'abc1234' in stripped
assert '[5m]' in stripped


# fit_path ladder (task 4.2)
Expand All @@ -100,38 +94,26 @@ def _git(self, branch: str = 'main', commit: str = 'abc1234',
def test_full_fits_returns_full(self) -> None:
r = Renderer()
git = self._git()
full = r.path_git('~/p', git, '2m')
result = r.fit_path('~/p', git, '2m', _visible_width(full) + 10)
full = r.path_git('~/p', git)
result = r.fit_path('~/p', git, _visible_width(full) + 10)
assert result == full

def test_no_commit_when_full_overflows(self) -> None:
r = Renderer()
git = self._git()
no_commit = r.path_git('~/p', git, '2m', show_commit=False)
no_commit = r.path_git('~/p', git, show_commit=False)
target_w = _visible_width(no_commit)
result = r.fit_path('~/p', git, '2m', target_w)
result = r.fit_path('~/p', git, target_w)
assert strip_ansi(result) == strip_ansi(no_commit)
assert _visible_width(result) <= target_w
assert 'abc1234' not in strip_ansi(result)

def test_no_elapsed_when_no_commit_still_overflows(self) -> None:
r = Renderer()
git = self._git()
no_elapsed = r.path_git('~/p', git, '2m',
show_commit=False, show_elapsed=False)
target_w = _visible_width(no_elapsed)
result = r.fit_path('~/p', git, '2m', target_w)
assert _visible_width(result) <= target_w
assert '[2m]' not in strip_ansi(result)
assert 'abc1234' not in strip_ansi(result)

def test_no_dirty_when_still_overflows(self) -> None:
r = Renderer()
git = self._git()
clean = r.path_git('~/p', git, '2m',
show_commit=False, show_elapsed=False, show_dirty=False)
clean = r.path_git('~/p', git, show_commit=False, show_dirty=False)
target_w = _visible_width(clean)
result = r.fit_path('~/p', git, '2m', target_w)
result = r.fit_path('~/p', git, target_w)
assert _visible_width(result) <= target_w
assert '●' not in strip_ansi(result)

Expand All @@ -140,15 +122,15 @@ def test_compact_when_all_path_git_overflow(self) -> None:
git = self._git()
compact = r.path_git_compact('~/p', git)
target_w = _visible_width(compact)
result = r.fit_path('~/p', git, '2m', target_w)
result = r.fit_path('~/p', git, target_w)
assert strip_ansi(result) == strip_ansi(compact)

def test_ellipsis_pwd_when_compact_overflows(self) -> None:
r = Renderer()
git = self._git(branch='x')
compact = r.path_git_compact('~/very-long-path-name', git)
target_w = _visible_width(compact) - 4
result = r.fit_path('~/very-long-path-name', git, '', target_w)
result = r.fit_path('~/very-long-path-name', git, target_w)
assert _visible_width(result) <= target_w
assert '…' in strip_ansi(result)
assert 'x' in strip_ansi(result)
Expand All @@ -159,7 +141,7 @@ def test_ellipsis_branch_as_last_resort(self) -> None:
pwd = '~/also-very-long-path'
compact = r.path_git_compact(pwd, git)
target_w = max(5, _visible_width(compact) - 20)
result = r.fit_path(pwd, git, '', target_w)
result = r.fit_path(pwd, git, target_w)
assert _visible_width(result) <= target_w + 2 # small tolerance for wide chars

def test_compact_only_skips_path_git_variants(self) -> None:
Expand All @@ -168,12 +150,12 @@ def test_compact_only_skips_path_git_variants(self) -> None:
compact = r.path_git_compact('~/p', git)
# target_w fits compact but not full
target_w = _visible_width(compact)
result = r.fit_path('~/p', git, '2m', target_w, compact_only=True)
result = r.fit_path('~/p', git, target_w, compact_only=True)
assert strip_ansi(result) == strip_ansi(compact)

def test_compact_only_never_returns_full_path_git(self) -> None:
r = Renderer()
git = self._git()
# Very wide target_w — compact_only should still not return full path_git
result = r.fit_path('~/p', git, '2m', 999, compact_only=True)
result = r.fit_path('~/p', git, 999, compact_only=True)
assert 'abc1234' not in strip_ansi(result)
3 changes: 3 additions & 0 deletions test/test_render_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def test_yas_full_width_fills_terminal(tmp_path, monkeypatch, capsys):

monkeypatch.setattr(app, 'terminal_width', lambda: fake_tw)
monkeypatch.setattr(app, 'CLAUDE_DIR', tmp_path / '.claude')
# Isolate from any YAS_* env vars set in the host shell (e.g. YAS_MAX_WIDTH=40).
monkeypatch.delenv('YAS_MAX_WIDTH', raising=False)
monkeypatch.delenv('YAS_FULL_WIDTH', raising=False)

def _first_line_width(env_extra):
for k, v in env_extra.items():
Expand Down