cctally 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.8.1] - 2026-05-18
9
+
10
+ ### Fixed
11
+ - `cctally five-hour-blocks` and `cctally five-hour-breakdown`: rows annotated with the `⚡` credit marker no longer push the table's right border one cell to the right of non-credit rows. `_boxed_table` was computing column widths via `len()` and padding via `str.ljust` / `str.rjust`, both of which count Unicode codepoints; `⚡` (U+26A1, `unicodedata.east_asian_width == "W"`) is one codepoint but renders two terminal cells, so the credit-prefixed Block Start cell (and the `⚡ CREDIT` divider row in the breakdown's Threshold column) under-padded by one cell and the right border drifted off-column on those rows only. New module-level `_display_width()` helper counts terminal cells (Wide/Fullwidth → 2, combining marks → 0, else 1) and `_boxed_table` now uses it for both width-max and padding. Byte-identical on the common case — any cell with no East Asian Wide / Fullwidth glyph renders unchanged, so existing pytest + cctally-test-all goldens stay green (1124 pytest + 1004 harness scenarios). Regressions: `tests/test_boxed_table_display_width.py` (⚡ in first column, ⚡ in inner column, ASCII no-op invariance).
12
+
8
13
  ## [1.8.0] - 2026-05-18
9
14
 
10
15
  ### Added
package/bin/cctally CHANGED
@@ -80,6 +80,7 @@ import textwrap
80
80
  import threading
81
81
  import time
82
82
  import traceback
83
+ import unicodedata
83
84
  import urllib.error
84
85
  import urllib.request
85
86
  from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
@@ -2691,6 +2692,29 @@ def _supports_unicode_stdout() -> bool:
2691
2692
  return "UTF" in encoding
2692
2693
 
2693
2694
 
2695
+ def _display_width(s: str) -> int:
2696
+ """Terminal cells consumed by ``s``.
2697
+
2698
+ Counts each codepoint by its East Asian Width: ``W`` / ``F`` (Wide
2699
+ / Fullwidth) → 2 cells; combining marks → 0; everything else → 1.
2700
+ Ambiguous (``A``) defaults to 1, matching every non-CJK terminal
2701
+ locale — cctally has no CJK content in cell data, and `→` / `—` /
2702
+ `·` (all `A`) are intentionally rendered narrow.
2703
+
2704
+ Used by `_boxed_table` so cells containing wide glyphs (notably
2705
+ `⚡` U+26A1 on credit-row annotations) pad to the right cell count
2706
+ rather than the right codepoint count. Without this, `len()`-based
2707
+ padding under-pads by one cell per wide glyph and the right border
2708
+ drifts off-column on those rows only.
2709
+ """
2710
+ width = 0
2711
+ for ch in s:
2712
+ if unicodedata.combining(ch):
2713
+ continue
2714
+ width += 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
2715
+ return width
2716
+
2717
+
2694
2718
  def _boxed_table(
2695
2719
  headers: list[str],
2696
2720
  rows: list[list[str]],
@@ -2714,15 +2738,20 @@ def _boxed_table(
2714
2738
 
2715
2739
  widths: list[int] = []
2716
2740
  for idx, header in enumerate(headers):
2717
- max_cell = max((len(r[idx]) for r in sanitized_rows), default=0)
2718
- widths.append(max(len(header), max_cell))
2741
+ max_cell = max((_display_width(r[idx]) for r in sanitized_rows), default=0)
2742
+ widths.append(max(_display_width(header), max_cell))
2719
2743
 
2720
2744
  def _pad(text: str, width: int, align: str) -> str:
2745
+ deficit = width - _display_width(text)
2746
+ if deficit <= 0:
2747
+ return text
2748
+ pad = " " * deficit
2721
2749
  if align == "right":
2722
- return text.rjust(width)
2750
+ return pad + text
2723
2751
  if align == "center":
2724
- return text.center(width)
2725
- return text.ljust(width)
2752
+ left = deficit // 2
2753
+ return (" " * left) + text + (" " * (deficit - left))
2754
+ return text + pad
2726
2755
 
2727
2756
  if _supports_unicode_stdout():
2728
2757
  chars = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
5
5
  "homepage": "https://github.com/omrikais/cctally",
6
6
  "repository": {