From 2a8fcf2c07ad0395a79eb81e2c95f752b9f7bedb Mon Sep 17 00:00:00 2001 From: cdeust Date: Mon, 3 Aug 2026 23:17:43 +0200 Subject: [PATCH 1/2] fix(statusline): make cost formatting locale-independent Under a comma-decimal locale (LC_NUMERIC=fr_FR.UTF-8) the ledger reported $0. Two independent defects compound: 1. costs.sh had no locale guard at all, so its awk emitted "19,779682" with a comma and wrote that into the per-session cache. Downstream arithmetic then coerced it to 0 -- `costs.sh today` returned 0 and `month` returned "0 0". 2. The render path interpolated the value into the awk PROGRAM TEXT: LC_NUMERIC=C awk "BEGIN{printf \"%.2f\",19,779682}" -> 19.00 The LC_NUMERIC=C guard was on the wrong side: it fixes output formatting, but by then the comma is syntax and printf sees two arguments. Fix: - export LC_ALL=C at the top of costs.sh and statusline-command.sh so every awk/printf emits dot decimals regardless of the user's locale. - fmt_usd (format.sh) and cost_fmt (render.sh) now pass the value via `awk -v` and normalise a stray comma, so a bad value can never become syntax. Verified on a fr_FR.UTF-8 host: `costs.sh today` 0 -> 76.543286; fmt_usd "19,779682" $19.00 -> $19.78; fmt_usd "19.779682" -> $19.78 (unchanged). Existing caches written with comma decimals must be purged once; they regenerate with dot decimals on the next refresh. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JMNwLEZS6SsrWMLwd1cQ6p --- plugins/statusline/assets/costs.sh | 5 +++++ plugins/statusline/assets/statusline-command.sh | 5 +++++ plugins/statusline/assets/statusline-lib/format.sh | 7 +++++-- plugins/statusline/assets/statusline-lib/render.sh | 4 ++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/statusline/assets/costs.sh b/plugins/statusline/assets/costs.sh index ae621d3..cbf30c1 100755 --- a/plugins/statusline/assets/costs.sh +++ b/plugins/statusline/assets/costs.sh @@ -51,6 +51,11 @@ # used to exercise the day/month rollover branches deterministically (e.g. the # month-boundary transition). Unset in production; real `date` is used. +# Numeric formatting must be locale-independent. Under a comma-decimal locale +# (e.g. fr_FR) awk/printf emit "19,78"; interpolated into a downstream awk +# program that value is read as TWO arguments and the amount is truncated. +export LC_ALL=C + set -u COST_LOG="${STATUSLINE_COST_LOG:-${HOME}/.claude/statusline-costs.jsonl}" diff --git a/plugins/statusline/assets/statusline-command.sh b/plugins/statusline/assets/statusline-command.sh index 34784a1..20664cc 100755 --- a/plugins/statusline/assets/statusline-command.sh +++ b/plugins/statusline/assets/statusline-command.sh @@ -63,6 +63,11 @@ # (test harnesses pointing at a working copy). # Load order is dependency order: platform and palette own no dependencies, # everything else builds on them. +# Numeric formatting must be locale-independent. Under a comma-decimal locale +# (e.g. fr_FR) awk/printf emit "19,78"; interpolated into a downstream awk +# program that value is read as TWO arguments and the amount is truncated. +export LC_ALL=C + STATUSLINE_LIB="${STATUSLINE_LIB:-$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/statusline-lib}" for _mod in platform palette fit severity format config gitctx session_state layout render; do if [ -r "${STATUSLINE_LIB}/${_mod}.sh" ]; then diff --git a/plugins/statusline/assets/statusline-lib/format.sh b/plugins/statusline/assets/statusline-lib/format.sh index 7299445..74b4abf 100644 --- a/plugins/statusline/assets/statusline-lib/format.sh +++ b/plugins/statusline/assets/statusline-lib/format.sh @@ -19,8 +19,11 @@ fmt_tokens() { # fmt_usd — a dollar amount, thousands folded to "$1.2k". fmt_usd() { - local v="$1" - LC_NUMERIC=C awk "BEGIN{ if($v>=1000) printf \"\$%.1fk\",$v/1000; else printf \"\$%.2f\",$v }" + # Value passed via -v, never interpolated into the program text: a stray + # comma decimal must not become an argument separator. + local v="${1:-0}" + v="${v/,/.}" + LC_ALL=C awk -v v="$v" 'BEGIN{ v=v+0; if (v>=1000) printf "$%.1fk", v/1000; else printf "$%.2f", v }' } # fmt_dur — compact duration: "Xh Ym" / "Xm Ys" / "Xs" from a seconds count. diff --git a/plugins/statusline/assets/statusline-lib/render.sh b/plugins/statusline/assets/statusline-lib/render.sh index 95e961c..f67c516 100644 --- a/plugins/statusline/assets/statusline-lib/render.sh +++ b/plugins/statusline/assets/statusline-lib/render.sh @@ -128,10 +128,10 @@ render_session() { # "main" so an understated number is never shown as if it were the total. if [ "$RANK" -ge 1 ]; then if [ -n "$cost_session" ]; then - cost_fmt=$(LC_NUMERIC=C awk "BEGIN{printf \"%.2f\",$cost_session}") + cost_fmt=$(LC_ALL=C awk -v v="${cost_session/,/.}" 'BEGIN{printf "%.2f", v+0}') line="${line:+$line ${SEP} }${KEY}session ${SUBTEXT}${DOLLAR}${cost_fmt}${RESET}" elif [ -n "$cost" ]; then - cost_fmt=$(LC_NUMERIC=C awk "BEGIN{printf \"%.2f\",$cost}") + cost_fmt=$(LC_ALL=C awk -v v="${cost/,/.}" 'BEGIN{printf "%.2f", v+0}') line="${line:+$line ${SEP} }${KEY}session main ${SUBTEXT}${DOLLAR}${cost_fmt}${RESET}" fi fi From 2ccdefe4bac832311a2c89c1762d2e2e8249a60b Mon Sep 17 00:00:00 2001 From: cdeust Date: Mon, 3 Aug 2026 23:55:24 +0200 Subject: [PATCH 2/2] fix(statusline): force only the numeric locale, not LC_ALL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit export LC_ALL=C pinned LC_CTYPE as well, and vislen() measures with ${#s}. Under LC_CTYPE=C that counts UTF-8 bytes instead of columns, so a 3-byte glyph read as width 3 and a 10-glyph bar as width 30 — 7 fit/vislen tests failed in CI. Force LC_NUMERIC=C alone, which is the only category the comma-decimal defect needs. LC_ALL is unset first because it outranks LC_NUMERIC when the environment sets it. Verified: tests/statusline/test_fit_and_pace.sh 53/53 under both LC_ALL=fr_FR.UTF-8 and LC_ALL=C; costs.sh today emits a dot decimal under fr_FR; vislen of a 10-glyph bar is 10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JMNwLEZS6SsrWMLwd1cQ6p --- plugins/statusline/assets/costs.sh | 6 +++++- plugins/statusline/assets/statusline-command.sh | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/statusline/assets/costs.sh b/plugins/statusline/assets/costs.sh index cbf30c1..cff374a 100755 --- a/plugins/statusline/assets/costs.sh +++ b/plugins/statusline/assets/costs.sh @@ -54,7 +54,11 @@ # Numeric formatting must be locale-independent. Under a comma-decimal locale # (e.g. fr_FR) awk/printf emit "19,78"; interpolated into a downstream awk # program that value is read as TWO arguments and the amount is truncated. -export LC_ALL=C +# Only the numeric category is forced, matching statusline-command.sh: LC_ALL=C +# would also pin LC_CTYPE, which breaks ${#s} width measurement. LC_ALL must be +# unset first — it outranks LC_NUMERIC when the environment sets it. +unset LC_ALL +export LC_NUMERIC=C set -u diff --git a/plugins/statusline/assets/statusline-command.sh b/plugins/statusline/assets/statusline-command.sh index 20664cc..7d6ba34 100755 --- a/plugins/statusline/assets/statusline-command.sh +++ b/plugins/statusline/assets/statusline-command.sh @@ -66,7 +66,12 @@ # Numeric formatting must be locale-independent. Under a comma-decimal locale # (e.g. fr_FR) awk/printf emit "19,78"; interpolated into a downstream awk # program that value is read as TWO arguments and the amount is truncated. -export LC_ALL=C +# Only the numeric category may be forced: LC_ALL=C would also pin LC_CTYPE, +# and vislen() measures with ${#s}, which then counts UTF-8 bytes instead of +# columns (a 3-byte glyph reads as width 3). LC_ALL must be unset first — +# it outranks LC_NUMERIC when the environment sets it. +unset LC_ALL +export LC_NUMERIC=C STATUSLINE_LIB="${STATUSLINE_LIB:-$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/statusline-lib}" for _mod in platform palette fit severity format config gitctx session_state layout render; do