diff --git a/claude/statusline.isaacaudet.sh b/claude/statusline.isaacaudet.sh index 0be5212..7d329fc 100755 --- a/claude/statusline.isaacaudet.sh +++ b/claude/statusline.isaacaudet.sh @@ -11,8 +11,9 @@ SHOW_TOKENS=true # token usage bar SHOW_THINKING=true # extended thinking indicator SHOW_RATE_LIMITS=true # 5h / 7d rate limit bars SHOW_MODEL_LIMIT=true # per-model weekly limit bar (e.g. Fable), when the API reports one -WRAP_NARROW=true # wrap onto a second line rather than dropping segments or being clipped -WRAP_MIN_WIDTH=100 # below this, keep wide-tier line-one content (it can wrap) +RL_MIN_WIDTH=35 # narrowest usable width the 5h + 7d bars fit on their own + # line: two of "5h ██████ 100%" plus a separator, +4 +COMPACT_WIDTH=100 # below this, shorten line one to what it has room for BRANCH_MAX_LEN=28 # truncate branch names longer than this CWD_MAX_LEN=20 # truncate the cwd basename longer than this GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large repos) @@ -338,8 +339,8 @@ format_reset_time() { model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"') cwd=$(echo "$input" | jq -r '.cwd // empty') cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty') -# Formatted here rather than where it is rendered: the wrap-mode branch budget -# needs its width, and it is not fixed ("$4.61" vs "$1234.56"). +# Formatted here rather than where it is rendered: the compact-tier branch +# budget needs its width, and it is not fixed ("$4.61" vs "$1234.56"). cost_fmt="" [ -n "$cost_usd" ] && cost_fmt=$(printf '%.2f' "$cost_usd" 2>/dev/null) @@ -370,8 +371,8 @@ thinking_on=true [ "$sl_thinking" = "false" ] && thinking_on=false # The effort level replaces the static "thinking" label. Whitelisted rather -# than printed as-is: an unrecognised value would escape the width budget that -# the wrap branch computes from this string. +# than printed as-is: an unrecognised value would escape the width budget the +# compact tier computes from this string. case "$effort" in low|medium|high|xhigh|max) thinking_label="$effort" ;; *) thinking_label="thinking" ;; @@ -379,36 +380,28 @@ esac # ===== Adaptive width tiers ===== # -# Tiers now choose only how much of LINE ONE to show. What the rate-limit -# group contains, and whether it wraps, is decided by measurement at the end of -# the script, not here -- tiers cannot see how long the branch, cwd or model -# name is, which is how content used to end up clipped. +# The rate-limit group always gets line two to itself, so tiers choose only how +# much of LINE ONE to show. What that group contains is decided by measurement +# at the end of the script, not here -- tiers cannot see how long the branch, +# cwd or model name is, which is how content used to end up clipped. # # full (≥150): CWD, ahead/behind, "◆ high" effort, cost -# wide (100–149): ahead/behind, "◆ high" effort, cost -# split (76–99): short model, ahead/behind, "◆" symbol -# (reachable only with WRAP_NARROW=false; otherwise the wrap -# band below overrides this range to wide) -# narrow (<76): short model + branch + token only; no rate limits at all +# wide (68–149): the same without the CWD, and below COMPACT_WIDTH with a +# short model name and a branch budgeted to what is left +# narrow (<68): short model + branch + token bar only -- the usage group is +# unaffected, it has its own line at every width # if [ "$USABLE_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full" -elif [ "$USABLE_WIDTH" -ge 100 ] 2>/dev/null; then width_tier="wide" -elif [ "$USABLE_WIDTH" -ge 76 ] 2>/dev/null; then width_tier="split" +elif [ "$USABLE_WIDTH" -ge 68 ] 2>/dev/null; then width_tier="wide" else width_tier="narrow" fi -# Two-line mode. Between WRAP_FLOOR and WRAP_MIN_WIDTH there isn't room for -# everything on one line, but there IS room across two — so instead of dropping -# the rate-limit group we break before it and render the wide-tier content. -# Below WRAP_FLOOR the narrow tier applies instead, which drops the rate-limit -# group entirely -- so there is nothing to wrap and no second line to put it on. -WRAP_FLOOR=68 -wrap_mode=false -if $WRAP_NARROW \ - && [ "$USABLE_WIDTH" -lt "$WRAP_MIN_WIDTH" ] 2>/dev/null \ - && [ "$USABLE_WIDTH" -ge "$WRAP_FLOOR" ] 2>/dev/null; then - wrap_mode=true - width_tier="wide" +# Compact: the wide tier with a short model name and the branch cut to whatever +# line one has left. Between 68 and COMPACT_WIDTH columns the wide-tier +# segments do fit, but only once those two are budgeted rather than assumed. +compact=false +if [ "$width_tier" = "wide" ] && [ "$USABLE_WIDTH" -lt "$COMPACT_WIDTH" ] 2>/dev/null; then + compact=true fi # Shorten model name for tight spaces @@ -428,8 +421,9 @@ out="" rl_bare="" rl_lean="" rl_pace="" -rl_mid="" +rl_time="" rl_rich="" +pace_len="" # Model — color by family model_color="$blue" @@ -439,10 +433,9 @@ case "$model_name" in esac display_model="$model_name" -# split/narrow: shorten so the 5h + 7d bars still fit on one line. -# wrap mode too: it renders wide-tier content at a split-tier width, and a long -# display name ("Opus 5 (1M context)" is 19 cols) would overflow line one. -if [ "$width_tier" = "split" -o "$width_tier" = "narrow" ] || $wrap_mode; then +# A long display name ("Opus 5 (1M context)" is 19 cols) would overflow line +# one at these widths, so it gives up everything but the family. +if [ "$width_tier" = "narrow" ] || $compact; then display_model=$(short_model "$model_name") fi out+="${model_color}$(esc_data "$display_model")${reset}" @@ -457,7 +450,6 @@ fi bar_w="$TOKEN_BAR_WIDTH" [ "$width_tier" = "wide" ] && bar_w=6 -[ "$width_tier" = "split" ] && bar_w=5 [ "$width_tier" = "narrow" ] && bar_w=4 # Git branch + dirty + ahead/behind @@ -469,15 +461,14 @@ if $SHOW_GIT && [ -n "$cwd" ]; then # Progressively tighten branch truncation local_max="$BRANCH_MAX_LEN" [ "$width_tier" = "wide" ] && local_max=24 - [ "$width_tier" = "split" ] && local_max=18 [ "$width_tier" = "narrow" ] && local_max=12 - # In wrap mode line one is + # In compact mode line one is # model │ ⎇ branch ✔ ↑1 │ used/total pct% │ ◇ high │ $cost # and the branch gets whatever the rest of it leaves. Everything after # the branch is appended below, so its width is added up here rather # than assumed: a flat 56 columns was three short of a four-digit cost, # which pushed line one past the edge to be clipped by the renderer. - if $wrap_mode; then + if $compact; then tail_len=$(( 5 + 2 )) # "│ ⎇ " and the dirty mark [ "${g_ahead:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_ahead} )) [ "${g_behind:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_behind} )) @@ -514,18 +505,12 @@ if $SHOW_TOKENS; then fi # Thinking and effort. The diamond is thinking state, the label is the effort -# level ("thinking" only when the payload reports no level): -# full/wide → "◆ high" / "◇ high" (label) -# split → "◆" / "◇" (symbol only, saves the label's columns) -# narrow → hidden +# level ("thinking" only when the payload reports no level). Hidden at the +# narrow tier, where line one has no columns to spare. if $SHOW_THINKING && [ "$width_tier" != "narrow" ]; then out+="${sep}" - if $thinking_on; then - if [ "$width_tier" = "split" ]; then out+="${amber}◆${reset}" - else out+="${amber}◆ ${thinking_label}${reset}"; fi - else - if [ "$width_tier" = "split" ]; then out+="${dim}◇${reset}" - else out+="${dim}◇ ${thinking_label}${reset}"; fi + if $thinking_on; then out+="${amber}◆ ${thinking_label}${reset}" + else out+="${dim}◇ ${thinking_label}${reset}" fi fi @@ -535,9 +520,10 @@ if [ -n "$cost_fmt" ] && [ "$width_tier" = "wide" -o "$width_tier" = "full" ]; t fi # ===== Rate limits (API, cached USAGE_CACHE_SECS) ===== -# Built at every tier except narrow. Which variant is actually emitted, -# and on how many lines, is decided by the measured ladder at the end. -if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then +# Always rendered on a line of its own; which variant is emitted is decided by +# the measured ladder at the end. Skipped below RL_MIN_WIDTH, where not even +# the two bars fit on that line -- and skipping it also means no API call. +if $SHOW_RATE_LIMITS && [ "$USABLE_WIDTH" -ge "$RL_MIN_WIDTH" ] 2>/dev/null; then api_cache="$CACHE_DIR/statusline-usage-cache.json" fail_marker="$CACHE_DIR/statusline-usage-fail" needs_refresh=true @@ -614,7 +600,6 @@ if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then bar_width=6 - [ "$width_tier" = "split" ] && bar_width=4 # One jq pass for the whole payload. Parsing it field by field meant # ~10 jq processes per render on the same JSON; the status line runs on @@ -754,14 +739,16 @@ EOF fi fi - # Four variants of the rate-limit group, richest first: - # rl_rich bars + reset times + pace + extra usage - # rl_mid bars + pace + extra usage + # Five nested variants of the rate-limit group, each rung giving up the + # least useful thing left: + # rl_rich bars + pace + reset times + extra usage + # rl_time bars + pace + reset times # rl_pace bars + pace - # rl_lean bars only - # The ladder at the end emits the richest one that fits the space it - # has. Keeping a lean variant matters: extra-usage credits add ~30 - # columns, which can overflow line two on its own. + # rl_lean bars, per-model included + # rl_bare the 5h and 7d bars alone + # The ladder at the end emits the richest one that fits line two. + # Keeping the leaner rungs matters: extra-usage credits add ~30 columns + # and the two timestamps ~26, either of which can overflow the line. seg_5h="${dim}5h${reset} $(build_bar "${five_hour_pct:-0}" "$bar_width") ${cyan}${five_hour_pct:-0}%${reset}" seg_7d="${sep}${dim}7d${reset} $(build_bar "${seven_day_pct:-0}" "$bar_width") ${cyan}${seven_day_pct:-0}%${reset}" @@ -780,8 +767,7 @@ EOF fi # rl_bare drops the per-model bar too: the last thing worth giving up, - # and the only way to fit at all when wrapping is disabled and the - # terminal is narrow. + # and all that fits on a terminal barely wider than the two bars. # # rl_pace is its own rung so pace is given up BEFORE the per-model bar # it annotates: a bar with no pace still says something, a pace with no @@ -790,59 +776,55 @@ EOF rl_bare="${seg_5h}${seg_7d}" rl_lean="${rl_bare}${seg_scoped}" rl_pace="${rl_lean}${seg_pace}" - rl_mid="${rl_pace}${seg_extra}" - rl_rich="$rl_mid" - # Formatting the two reset timestamps costs ~10 subprocesses (date has - # no portable one-shot form here), so only pay for it when there is a - # chance they will be shown: appended to line one, or alone on line two. - # RESET_COST is the combined width of " reset 4:40p.m." and - # " reset aug 28, 3:00a.m.". - line1_len=$(vis_len "$out") - mid_len=$(vis_len "$rl_mid") - RESET_COST=30 - if [ $(( line1_len + 3 + mid_len + RESET_COST )) -le "$USABLE_WIDTH" ] \ - || { $WRAP_NARROW && [ $(( mid_len + RESET_COST )) -le "$USABLE_WIDTH" ]; }; then + # rl_time interleaves rather than appends: the 5h timestamp sits with + # its own bar, and the 7d one closes the weekly group it shares with + # the per-model bar and the pace. + # + # Formatting the pair costs ~10 subprocesses (date has no portable + # one-shot form here) on a line that redraws per keystroke, so it is + # skipped when line two provably cannot show them. RESET_COST + # deliberately UNDER-estimates their combined width -- " ↺ 1:00am" and + # " ↺ may 4, 1:00am" at their shortest -- so the ladder below, which + # measures the real string, stays the thing that decides. + RESET_COST=25 + pace_len=$(vis_len "$rl_pace") + rl_time="$rl_pace" + if [ $(( pace_len + RESET_COST )) -le "$USABLE_WIDTH" ]; then five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time") seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime") - r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}" + r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}" r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}↺ ${seven_day_reset}${reset}" - rl_rich="${seg_5h}${r5}${seg_7d}${seg_scoped}${seg_pace}${r7}${seg_extra}" + rl_time="${seg_5h}${r5}${seg_7d}${seg_scoped}${seg_pace}${r7}" fi + rl_rich="${rl_time}${seg_extra}" fi fi -# Attach the rate-limit group, emitting the richest layout that fits. +# Attach the rate-limit group on a line of its own, emitting the richest +# variant that fits it. # -# A second line is worth it for the per-model bar and nothing else: rl_bare is -# the only variant that drops that bar, and it is the whole reason the group is -# worth showing on an account that has one. Reset times and extra-usage credits -# are given up instead of wrapped for, so content is NOT monotone in width -- at -# one column narrower, rung 3 stops fitting and the wrap that follows has room -# for the timestamps as well. Only wrapping being switched off makes rl_bare the -# answer. -# 1-4. one line: with resets / with extra usage / with pace / bars + per-model -# 5-8. two lines: same order, line two having room the single line lacked -# 9. one line, bars only -- WRAP_NARROW=false, the least-bad single line -# Every branch is fit-checked, so no layout is chosen that would be clipped by -# the renderer, whatever the branch name, cwd, model name or extra-usage width. +# Line two is never shared with line one's segments, however wide the terminal +# is. Squeezing the group onto line one is what used to cost the reset times +# and the pace figure on a laptop-width window, and it made the layout jump +# between one and two lines as the branch name or the cost changed width. +# A full line to itself is worth more than the columns it wastes. +# +# The extra-usage credits ride on the richest rung alone, so an account with +# overage enabled sees them only on a terminal wide enough for the whole group +# -- around 110 columns. Below that the reset times win the space: they are the +# number you act on, the credits the one you read afterwards. +# +# Every rung is fit-checked, the last one included, so nothing is chosen that +# the renderer would clip, whatever the model name, the extra-usage width or +# the percentages the API reports. If not even the two bars fit, the group is +# dropped rather than overflowed. if [ -n "$rl_bare" ]; then - rich_len=$(vis_len "$rl_rich") - pace_len=$(vis_len "$rl_pace") - lean_len=$(vis_len "$rl_lean") - if [ $(( line1_len + 3 + rich_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_rich}" - elif [ $(( line1_len + 3 + mid_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_mid}" - elif [ $(( line1_len + 3 + pace_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_pace}" - elif [ $(( line1_len + 3 + lean_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_lean}" - elif $WRAP_NARROW; then - if [ "$rich_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich" - elif [ "$mid_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_mid" - elif [ "$pace_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_pace" - elif [ "$lean_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean" - else out+=$'\n'"$rl_bare" - fi - else - out+="${sep}${rl_bare}" + if [ "$(vis_len "$rl_rich")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich" + elif [ "$(vis_len "$rl_time")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_time" + elif [ "$pace_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_pace" + elif [ "$(vis_len "$rl_lean")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean" + elif [ "$(vis_len "$rl_bare")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_bare" fi fi diff --git a/claude/tests/statusline.isaacaudet.cache_test.sh b/claude/tests/statusline.isaacaudet.cache_test.sh index 837bb80..4c9299c 100755 --- a/claude/tests/statusline.isaacaudet.cache_test.sh +++ b/claude/tests/statusline.isaacaudet.cache_test.sh @@ -124,13 +124,13 @@ out=$(render 245) || bad "30min old: refetched, not held for the hour" "$out" # ------------------------------------------------- bars shown => bars refreshed -# The refresh is gated on the same tier check as the bars themselves, so the -# two must agree at every width: wrapped onto line two included, and 68 being -# WRAP_FLOOR, the narrowest width that still shows them. +# The refresh is gated on the same width check as the bars themselves, so the +# two must agree at every width, 35 being RL_MIN_WIDTH -- the narrowest usable +# width the bars still fit on their own line. echo echo "Every width that shows the bars refreshes them:" -for u in 245 115 85 68; do +for u in 245 115 85 68 50 35; do cached_response_aged 360 out=$(render "$u") if case "$out" in *"5h"*) true ;; *) false ;; esac; then @@ -141,14 +141,14 @@ for u in 245 115 85 68; do fi done -# One column below the wrap floor the group is dropped entirely — nothing is +# One column below RL_MIN_WIDTH the group is dropped entirely — nothing is # shown, so nothing should be paid for either. cached_response_aged 360 -out=$(render 67) -case "$out" in *"5h"*) bad "usable 67: no bars at the narrow tier" "$out" ;; - *) ok "usable 67: no bars at the narrow tier" ;; esac -[ "$(calls)" = "0" ] && ok "usable 67: no API call when no bars are shown" \ - || bad "usable 67: no API call when no bars are shown" "$out" +out=$(render 34) +case "$out" in *"5h"*) bad "usable 34: no bars below the group's floor" "$out" ;; + *) ok "usable 34: no bars below the group's floor" ;; esac +[ "$(calls)" = "0" ] && ok "usable 34: no API call when no bars are shown" \ + || bad "usable 34: no API call when no bars are shown" "$out" # --------------------------------------------------------- offline retry backoff # With no network, curl can burn --max-time 10 before giving up. A failed fetch diff --git a/claude/tests/statusline.isaacaudet.overflow_test.py b/claude/tests/statusline.isaacaudet.overflow_test.py index 9bd02ac..790c55f 100755 --- a/claude/tests/statusline.isaacaudet.overflow_test.py +++ b/claude/tests/statusline.isaacaudet.overflow_test.py @@ -20,7 +20,12 @@ SCRIPT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__)) BRANCHES = ["main", "feature/some-longer-branch-name", "feature/an-extremely-long-branch-name-that-keeps-going-and-going"] MODELS = ["Opus 5", "Opus 5 (1M context)"] -WIDTHS = range(64, 245, 10) +# Starts at the narrowest terminal line ONE fits on with the longest branch +# below (measured: 44 usable columns, i.e. 49 with padding 2) -- the pre-existing +# narrow-tier floor, which width_test.sh marks XFAIL. Everything above it is +# fair game, and that now includes the band from RL_MIN_WIDTH up, where the +# usage group is shown but has few columns to spare. +WIDTHS = range(49, 245, 10) # The cwd basename is rendered at the widest layouts and is capped by # CWD_MAX_LEN; vary it, since a long project directory was one of the ways # line one used to overflow. diff --git a/claude/tests/statusline.isaacaudet.pace_test.sh b/claude/tests/statusline.isaacaudet.pace_test.sh index 9b121ff..5e9e2f0 100755 --- a/claude/tests/statusline.isaacaudet.pace_test.sh +++ b/claude/tests/statusline.isaacaudet.pace_test.sh @@ -214,8 +214,8 @@ import sys,re,unicodedata for ln in sys.stdin.read().rstrip('\n').split('\n'): p=re.sub(r'\033\[[0-9;]*m','',ln) print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in p))"; } -over=0; toomany=0; orphan=0; dropped=0; shown=0 -for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68; do +over=0; toomany=0; orphan=0; dropped=0; shown=0; missing_wide=0 +for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68 60 55 50 45 40; do o=$(render "$MON" 12345 "$u") ws=$(printf '%s' "$o" | widths) while read -r c; do [ "$c" -gt "$u" ] && over=1; done <<< "$ws" @@ -224,13 +224,18 @@ for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 # be the last thing standing. case "$o" in *"%/d"*) shown=1; case "$o" in *Fable*) ;; *) orphan=1 ;; esac ;; - *) case "$o" in *Fable*) dropped=1 ;; esac ;; + *) case "$o" in *Fable*) dropped=1 ;; esac + [ "$u" -ge 68 ] && missing_wide=1 ;; esac done [ "$over" = 0 ] && ok "no line exceeds the usable width at any width" || bad "no line exceeds the usable width at any width" [ "$toomany" = 0 ] && ok "never more than two lines" || bad "never more than two lines" [ "$orphan" = 0 ] && ok "pace never outlives the per-model bar" || bad "pace never outlives the per-model bar" [ "$shown" = 1 ] && ok "pace is actually rendered somewhere in the sweep" || bad "pace is actually rendered somewhere in the sweep" +# Line two is the group's own, so pace survives every width that fits a normal +# status line -- it is only given up on a genuinely tiny terminal. +[ "$missing_wide" = 0 ] && ok "pace is kept at every width from 68 columns up" \ + || bad "pace is kept at every width from 68 columns up" [ "$dropped" = 1 ] && ok "the pace rung is reachable: some width keeps the bar but drops pace" \ || bad "the pace rung is reachable: some width keeps the bar but drops pace" @@ -238,7 +243,7 @@ done # readout, pace is the number this segment exists for, so the credits go first. fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" "$EXTRA_ON" pace_alone=0; credits_alone=0 -for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68; do +for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68 60 55 50 45 40; do o=$(render "$MON" 12345 "$u") case "$o" in *"%/d"*) case "$o" in *extra*) ;; *) pace_alone=1 ;; esac ;; diff --git a/claude/tests/statusline.isaacaudet.payload_test.sh b/claude/tests/statusline.isaacaudet.payload_test.sh index 7b845db..7b5e0c3 100755 --- a/claude/tests/statusline.isaacaudet.payload_test.sh +++ b/claude/tests/statusline.isaacaudet.payload_test.sh @@ -68,13 +68,25 @@ out=$(render 250) case "$out" in *"Fable"*"10%"*) ok "wide: renders 'Fable' with its percentage" ;; *) bad "wide: renders 'Fable' with its percentage" "$out" ;; esac -# Wrapped: assert Fable is on line TWO specifically, not merely present. -out=$(render 90) -[ "$(count "$out")" = "2" ] && ok "narrow: wraps to two lines" || bad "narrow: wraps to two lines" "$out" -case "$(nth "$out" 2)" in *"Fable"*"10%"*) ok "narrow: Fable is on line two" ;; - *) bad "narrow: Fable is on line two" "$out" ;; esac -case "$(nth "$out" 1)" in *"Fable"*) bad "narrow: Fable not duplicated on line one" "$out" ;; - *) ok "narrow: Fable not duplicated on line one" ;; esac +# The usage group always gets a line of its own, however wide the terminal is. +for w in 250 116 90; do + out=$(render "$w") + [ "$(count "$out")" = "2" ] && ok "width $w: usage is on its own line" \ + || bad "width $w: usage is on its own line" "$out" + case "$(nth "$out" 2)" in *"Fable"*"10%"*) ok "width $w: Fable is on line two" ;; + *) bad "width $w: Fable is on line two" "$out" ;; esac + case "$(nth "$out" 1)" in *"Fable"*) bad "width $w: Fable not duplicated on line one" "$out" ;; + *) ok "width $w: Fable not duplicated on line one" ;; esac +done + +# A line of its own is what buys room for the reset times: both the 5-hour +# clock time and the 7-day date survive down to a laptop-sized terminal, where +# they used to be the first thing given up. +for w in 250 116 100; do + out=$(render "$w") + case "$(nth "$out" 2)" in *"↺"*"↺"*) ok "width $w: both reset times are shown" ;; + *) bad "width $w: both reset times are shown" "$out" ;; esac +done # The per-model bar is the reason the group exists on a Fable-capable account, # so it must survive by wrapping and never be dropped to squeeze the group onto @@ -105,7 +117,7 @@ for desc in "no limits key:{$BASE,\"extra_usage\":{\"is_enabled\":false}}" \ name="${desc%%:*}"; json="${desc#*:}" printf '%s' "$json" | fixture out=$(render 250) - if [ "$(count "$out")" = "1" ] && case "$out" in *"5h"*"7d"*) true;; *) false;; esac \ + if [ "$(count "$out")" = "2" ] && case "$(nth "$out" 2)" in *"5h"*"7d"*) true;; *) false;; esac \ && case "$out" in *error*|*null*|*"jq:"*|*"line "*) false;; *) true;; esac; then ok "$name" else bad "$name" "$out"; fi @@ -139,33 +151,17 @@ fixture < "$NOWRAP" -fixture <&1 | sed $'s/\033\[[0-9;]*m//g') - u=$(( w - 5 )); c=$(vis "$out") - [ "$(count "$out")" = "1" ] && [ "$c" -le "$u" ] \ - && ok "width $w: one line, $c <= usable $u" \ - || bad "width $w: one line within usable $u (got $(count "$out") line/s, $c cols)" "$out" -done -rm -f "$NOWRAP" - echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ] diff --git a/claude/tests/statusline.isaacaudet.thinking_test.sh b/claude/tests/statusline.isaacaudet.thinking_test.sh index 203edf7..844b171 100755 --- a/claude/tests/statusline.isaacaudet.thinking_test.sh +++ b/claude/tests/statusline.isaacaudet.thinking_test.sh @@ -82,8 +82,10 @@ echo "Fallbacks and tiers:" has true "" "◆ thinking" 200 "absent effort falls back to the old label" has true garbage "◆ thinking" 200 "an unrecognised level falls back rather than widening the line" has true high "◆ high" 120 "the label survives the wide tier" -# The split tier (76-99) is unreachable while WRAP_NARROW=true -- that band is -# overridden to wide and wrapped -- so only narrow is asserted here. +# Below 68 columns the narrow tier drops the segment entirely; between there +# and 100 the compact tier keeps the label, since the usage group has moved off +# line one and left room for it. +has true high "◆ high" 85 "the label survives the compact tier" lacks true high "◆" 60 "narrow tier hides the segment" echo diff --git a/claude/tests/statusline.isaacaudet.width_test.sh b/claude/tests/statusline.isaacaudet.width_test.sh index a9e1869..ac74b48 100755 --- a/claude/tests/statusline.isaacaudet.width_test.sh +++ b/claude/tests/statusline.isaacaudet.width_test.sh @@ -54,10 +54,13 @@ check() { # usable, branch, model, cost, [xfail-reason] [ "$over" = "1" ] && why="line exceeds usable width" # INVARIANT 2: at most two lines [ "$n" -gt 2 ] && why="${why:-more than two lines}" - # INVARIANT 3: only wrap when it was actually necessary - if [ -z "$why" ] && [ "$n" = "2" ]; then - local l1 l2; l1=$(printf '%s\n' "$ws" | sed -n 1p); l2=$(printf '%s\n' "$ws" | sed -n 2p) - [ $(( l1 + 3 + l2 )) -le "$u" ] && why="wrapped unnecessarily (would have fit on one line)" + # INVARIANT 3: the usage group always gets a line of its own, so that the + # reset times and the pace figure have room whatever line one holds. Below + # RL_MIN_WIDTH there is no group to wrap, so a single line is expected. + if [ -z "$why" ]; then + if [ "$u" -ge 35 ] && [ "$n" != "2" ]; then why="usage not on its own line" + elif [ "$u" -lt 35 ] && [ "$n" != "1" ]; then why="wrapped with no usage group" + fi fi local label="usable=$u branch=${#branch}ch model='${model:0:12}' lines=$n max=$mx" @@ -75,7 +78,7 @@ M1="Opus 5" M2="Opus 5 (1M context)" echo "Sweep of usable widths, short branch:" -for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 40; do check "$u" "$SHORT" "$M1" 0.50; done +for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 50 40; do check "$u" "$SHORT" "$M1" 0.50; done # Narrow tier has a ~35-col floor; nothing can fit below that. Verified byte-identical # on the pre-change script, so not a regression. check 25 "$SHORT" "$M1" 0.50 "narrow tier floor ~35 cols" @@ -86,7 +89,7 @@ for u in 200 150 116 100 85 68; do check "$u" "$LONG" "$M2" 4.61; done check 40 "$LONG" "$M2" 4.61 "narrow tier floor ~43 cols with a long branch" echo "Large cost figure:" for u in 116 85 68; do check "$u" "$SHORT" "$M1" 1234.56; done -# ... and with a long branch, which is what makes the wrap-mode branch budget +# ... and with a long branch, which is what makes the compact-tier branch budget # bite: a four-digit cost is three columns wider than the budget assumed. echo "Large cost figure with a long branch:" for u in 68 69 70 75 85; do check "$u" "$LONG" "$M1" 1234.56; done