wcwidth measures the display width of Unicode characters in a terminal. pip install wcwidth. wcwidth: from wcwidth import wcwidth, wcswidth. Single char: wcwidth("A") → 1. wcwidth("中") → 2. wcwidth("\u0300") → 0 (combining accent). wcwidth("\x00") → 0. Wide chars: all CJK unified ideographs, katakana, fullwidth forms → 2. Zero-width: combining marks, control characters → 0 or -1. String width: wcswidth("Hello") → 5. wcswidth("Hello 世界") → 9. Returns -1 if string contains non-printable control characters. Safe: wcswidth("text") or len("text") for fallback. Padding: text.ljust(width) is wrong for CJK — use display_ljust(text, width) helper. Truncate: loop chars summing wcwidth until budget exhausted rather than slicing by index. Table: calculate column widths using wcswidth not len. Progress bar: wcswidth(label) to account for wide emoji and CJK labels. East Asian Width: full/wide characters (F/W) → 2 columns. Half/narrow/neutral/ambiguous → 1 column. Emoji: most emoji are wide (2 columns) but presentation modifiers affect display. wcwidth("👋") → 2. wcwidth("❤") → 1 (narrow heart). Claude Code generates wcwidth-aware table formatters, terminal progress bars, and Unicode column alignment utilities.
CLAUDE.md for wcwidth
## wcwidth Stack
- Version: wcwidth >= 0.2 | pip install wcwidth
- Char width: wcwidth("A") → 1 | wcwidth("中") → 2 | wcwidth("\u0300") → 0
- String width: wcswidth("Hello 世界") → 9 | returns -1 for non-printable chars
- Pad: never use str.ljust(n) for Unicode — use display_pad(text, width) helpers
- Truncate: sum wcwidth per character until budget exhausted, not s[:n]
- Tables: compute column widths with wcswidth, not len(); pad with display_ljust()
- Emoji: wcwidth("👋") → 2 (wide) | wcwidth("❤") → 1 (narrow)
wcwidth Unicode Display Width Pipeline
# app/display_width.py — wcwidth terminal alignment, padding, and table formatting
from __future__ import annotations
from wcwidth import wcwidth, wcswidth
# ─────────────────────────────────────────────────────────────────────────────
# 1. Character and string width
# ─────────────────────────────────────────────────────────────────────────────
def char_width(ch: str) -> int:
"""
Return the display width of a single character.
0: combining/zero-width characters (e.g. U+0300 combining grave)
1: standard ASCII and most Latin characters
2: CJK ideographs, fullwidth forms, most emoji
-1: non-printable control characters
"""
return wcwidth(ch)
def string_width(text: str) -> int:
"""
Return the total display width of a string in a terminal.
Each wide (CJK/emoji) character counts as 2; zero-width counts as 0.
Returns -1 if the string contains non-printable control characters.
"Hello 世界" → 9 (5 + 1 space + 2 + 2)
"""
return wcswidth(text)
def safe_string_width(text: str) -> int:
"""
Return display width; fall back to len(text) if wcswidth returns -1.
Useful when text may contain escape sequences (e.g. ANSI color codes).
"""
w = wcswidth(text)
return w if w >= 0 else len(text)
def has_wide_chars(text: str) -> bool:
"""Return True if the string contains any wide (2-column) characters."""
return any(wcwidth(ch) == 2 for ch in text)
def has_zero_width(text: str) -> bool:
"""Return True if the string contains zero-width combining characters."""
return any(wcwidth(ch) == 0 for ch in text)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Unicode-safe padding and alignment
# ─────────────────────────────────────────────────────────────────────────────
def display_ljust(text: str, width: int, fillchar: str = " ") -> str:
"""
Left-justify text in a field of `width` display columns.
Correctly handles wide CJK characters that occupy 2 columns.
"Hello 世界" display-width=9 → padded to 15 with 6 spaces → 15 columns wide.
"""
current = safe_string_width(text)
padding = max(0, width - current)
return text + fillchar * padding
def display_rjust(text: str, width: int, fillchar: str = " ") -> str:
"""Right-justify text in a field of `width` display columns."""
current = safe_string_width(text)
padding = max(0, width - current)
return fillchar * padding + text
def display_center(text: str, width: int, fillchar: str = " ") -> str:
"""Center text in a field of `width` display columns."""
current = safe_string_width(text)
padding = max(0, width - current)
left = padding // 2
right = padding - left
return fillchar * left + text + fillchar * right
def display_pad(text: str, width: int, align: str = "left", fillchar: str = " ") -> str:
"""
Pad text to `width` display columns.
align: "left" (default), "right", "center"
"""
if align == "right":
return display_rjust(text, width, fillchar)
if align == "center":
return display_center(text, width, fillchar)
return display_ljust(text, width, fillchar)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Unicode-safe truncation
# ─────────────────────────────────────────────────────────────────────────────
def display_truncate(text: str, max_width: int, ellipsis: str = "…") -> str:
"""
Truncate text to at most `max_width` display columns.
Adds `ellipsis` if truncation occurs.
Never cuts in the middle of a wide character.
"Hello 世界 World" truncated to 10 → "Hello 世…" (not "Hello 世�")
"""
ellipsis_width = safe_string_width(ellipsis)
budget = max_width - ellipsis_width
width_so_far = 0
cut_point = 0
for i, ch in enumerate(text):
cw = wcwidth(ch)
if cw < 0:
cw = 1 # treat non-printable as 1 for safety
if width_so_far + cw > budget:
return text[:cut_point] + ellipsis
width_so_far += cw
cut_point = i + 1
# Fits without truncation
return text
def display_slice(text: str, start_col: int, end_col: int) -> str:
"""
Extract a substring that spans columns [start_col, end_col).
Handles wide characters: a wide char is included only if it fits entirely.
"""
result = []
col = 0
for ch in text:
cw = max(0, wcwidth(ch))
if col >= end_col:
break
if col + cw > end_col:
# Wide char straddles the boundary — pad with space
if col >= start_col:
result.append(" " * (end_col - col))
break
if col + cw > start_col:
result.append(ch)
col += cw
return "".join(result)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Table formatting
# ─────────────────────────────────────────────────────────────────────────────
def format_table(
rows: list[list[str]],
headers: list[str] | None = None,
padding: int = 1,
separator: str = "│",
header_separator: str = "─",
) -> str:
"""
Format a 2D list of strings as a fixed-width terminal table.
Uses display widths (wcswidth) instead of len() for CJK-safe alignment.
format_table(
headers=["Name", "City"],
rows=[["Alice", "München"], ["Bob", "北京"], ["Charlie", "New York"]]
)
"""
all_rows = ([headers] if headers else []) + rows
num_cols = max(len(r) for r in all_rows)
# Compute column widths as maximum display width per column
col_widths = [0] * num_cols
for row in all_rows:
for j, cell in enumerate(row):
col_widths[j] = max(col_widths[j], safe_string_width(str(cell)))
pad = " " * padding
def format_row(row: list[str]) -> str:
cells = []
for j in range(num_cols):
cell = str(row[j]) if j < len(row) else ""
cells.append(pad + display_ljust(cell, col_widths[j]) + pad)
return separator + separator.join(cells) + separator
lines = []
if headers:
lines.append(format_row(headers))
# Separator line
divider_cells = [header_separator * (col_widths[j] + 2 * padding) for j in range(num_cols)]
lines.append("├" + "┼".join(divider_cells) + "┤")
for row in rows:
lines.append(format_row(row))
return "\n".join(lines)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Progress bar label helper
# ─────────────────────────────────────────────────────────────────────────────
def progress_label(label: str, max_cols: int = 20) -> str:
"""
Fit a label into exactly max_cols display columns.
Wide CJK/emoji characters are accounted for so the bar stays aligned.
"""
w = safe_string_width(label)
if w >= max_cols:
return display_truncate(label, max_cols)
return display_ljust(label, max_cols)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Character widths ===")
samples = [
("ASCII A", "A"),
("Space", " "),
("CJK 中", "中"),
("CJK 北", "北"),
("Emoji 👋", "👋"),
("Narrow heart ❤", "❤"),
("Combining ̀", "\u0300"),
("Fullwidth A", "\uff21"),
("Katakana ア", "\u30a2"),
]
for label, ch in samples:
print(f" {label:20} wcwidth={wcwidth(ch):2} char={ch!r}")
print("\n=== String display widths ===")
strings = [
"Hello",
"Hello 世界",
"北京 Shanghai",
"café",
"👋 Hello",
"ASCII only",
]
for s in strings:
print(f" len={len(s):3} wcswidth={wcswidth(s):3} {s!r}")
print("\n=== Padding (width=15) ===")
texts = ["Hello", "Hello 世界", "北京", "café", "👋 Hi"]
for t in texts:
padded = display_ljust(t, 15)
print(f" |{padded}| (input={safe_string_width(t)} cols)")
print("\n=== Truncation (max_width=10) ===")
for t in texts:
print(f" {t!r:20} → {display_truncate(t, 10)!r}")
print("\n=== Table ===")
table = format_table(
headers=["Name", "City", "Score"],
rows=[
["Alice", "München", "98"],
["Bob", "北京", "87"],
["Charlie", "New York", "92"],
["飞鸿", "上海", "76"],
],
)
print(table)
For the len() alternative — len(text) counts code points, not display columns; “Hello 世界” has len of 9 but displays as 12 columns wide because each CJK character occupies 2 columns in a monospace terminal; using len for column alignment in a table with CJK content causes misaligned rows, which is why text coming from APIs, CSV imports, or user input with Japanese/Chinese/Korean text needs wcswidth instead. For the unicodedata.east_asian_width() alternative — unicodedata.east_asian_width(ch) returns the East Asian Width category string (“W”, “F”, “H”, “Na”, “A”, “N”) which you’d then have to map to 1/2 yourself and handle ambiguous characters; wcwidth wraps this mapping and handles the standard correctly, making it a simpler one-call API for terminal column math. The Claude Skills 360 bundle includes wcwidth skill sets covering wcwidth() single-character width, wcswidth() string display width, safe_string_width() with fallback, has_wide_chars()/has_zero_width() detection helpers, display_ljust()/display_rjust()/display_center() padding functions, display_pad() multi-alignment, display_truncate() safe truncation at column boundary, display_slice() column range extraction, format_table() CJK-safe terminal table renderer, and progress_label() fixed-width bar label helper. Start with the free tier to try Unicode-aware terminal formatting code generation.