libwcwidth¶
A portable C11 library, mainly for CLI/TUI programs that carefully produce output for Terminals.
This project is derived from the Python wcwidth project.
The Python documentation closely matches this C library. The C API adds UTF-8 and codepoint array interfaces.
The lowest-level functions are derived from POSIX.1-2001 and POSIX.1-2008 wcwidth(3) and wcswidth(3), which this library implements as wcwidth_u32() and wcswidth_u32(). These functions return -1 when C0 and C1 control codes other than NUL are present; NUL measures as zero-width. wcstwidth_u8() applies corrections for a specific terminal program, as described in the Python Corrections documentation.
wcwidth_width_u8() is a higher-level wrapper of wcswidth_u8() that also measures terminal control sequences, like colors, bold, tabstops, and horizontal cursor movement.
The text transforms have their own sections:
wcwidth_ljust_u8() with rjust and center,
wcwidth_clip_u8(), wcwidth_wrap_u8(), and
wcwidth_escape_strip().
Quick Start¶
All commands below are run from the libwcwidth/ sub-folder. Build a static library,
build/libwcwidth.a:
make
Example programs:
make examples
Tests:
make test
Format (requires clang-format):
make format
CMake is also supported, and is preferred for embedding this library in a larger build:
cmake -B build-cmake && cmake --build build-cmake
For linking with your own project, from the repository root:
gcc -Ilibwcwidth/include myapp.c -Llibwcwidth/build -lwcwidth
Example Programs¶
Three small CLI utilities demonstrate use of this library.
textwrap: Unicode, CJK, emoji, and terminal sequence-aware text wrapping:
$ textwrap 42 README.rst
==========
libwcwidth
==========
A portable C11 library, mainly for CLI/TUI
programs that carefully produce output for
Terminals.
...
Uses environment value, $COLUMNS, if no width argument is given. Use -v to append a red
carriage-return marker.
width: report the display width of each line:
$ width README.rst
10
10
10
0
96
...
$ echo 'コンニチハ' | width
10
$ width -v <<< "café résumé"
11:café résumé
align: demonstrate left, right, and center alignment:
$ echo "hello" | align 20
hello hello hello
Overview¶
The full function reference is the C11 API page, generated from the headers; this section demonstrates each function by example. Conceptual topics such as ambiguous width, terminal corrections, and grapheme clustering are discussed in the Python documentation.
Memory ownership¶
The text transforms allocate their result and return NULL on failure. The caller must
free() a successful result:
wcwidth_clip_opts_t opts = WCWIDTH_CLIP_OPTS_DEFAULT;
char *out;
opts.v_end = 3;
out = wcwidth_clip_u8("中文字", 9, WCWIDTH_PARSE, &opts, NULL, NULL);
if (out == NULL)
return -1;
/* ... use out ... */
free(out);
wcwidth_encode_u32() and wcwidth_decode_u32() instead return the caller’s scratch buffer
if the result fits, allocating only when it does not. free() the result when it is at a new
address:
char stack[64], *utf8 = wcwidth_encode_u32(cps, n, stack, sizeof(stack), &utf8_len);
if (utf8 == NULL)
return -1;
/* ... use utf8, utf8_len ... */
if (utf8 != stack)
free(utf8);
String length conventions¶
Every string function reads exactly the length you pass:
_u32functionscount codepoints in the array,
_u8functionscount bytes.
Pass strlen(text) when the text is a C string. A NUL counts as an ordinary zero-width character
in any position. Transform functions copy it through and report the full length in *out_len.
Alternate encodings¶
Use _u8 when your text is UTF-8 and _u32 when you hold decoded codepoints; the two
families mirror each other. Auxiliary strings are UTF-8 in both families (the fillchar
padding argument and the initial_indent/subsequent_indent/placeholder wrap options),
because they are short constants.
Other encodings (Latin-1, CP437, Shift-JIS, …) are transcoded by the caller; the library carries
no encoding tables. Either transcode to UTF-8 once with iconv(3) or ICU and use the _u8 forms
throughout, or use the _u32 forms and re-encode the result. wcwidth_encode_u32() and
wcwidth_decode_u32() convert between the two representations:
/* CP437 "caf\x82" decoded to codepoints by the caller. */
uint32_t cps[] = {'c', 'a', 'f', 0xE9}; /* 0x82 in CP437 is U+00E9 */
char stack[64], *utf8;
size_t out_len, utf8_len;
wcwidth_align_opts_t opts = WCWIDTH_ALIGN_OPTS_DEFAULT;
uint32_t *out;
opts.dest_width = 5;
out = wcwidth_ljust_u32(cps, 4, WCWIDTH_PARSE, &opts, &out_len, NULL);
if (out == NULL)
return -1;
/* out is {c, a, f, U+00E9, ' '}; encode for the caller's iconv(3) or ICU. */
utf8 = wcwidth_encode_u32(out, out_len, stack, sizeof(stack), &utf8_len);
if (utf8 == NULL) {
free(out);
return -1;
}
/* ... hand utf8/utf8_len to iconv(3) or ICU ... */
if (utf8 != stack)
free(utf8);
free(out);
Re-encoding to a legacy charset is the caller’s iconv(3) or ICU (ucnv_*) call; a byte cast
works when every codepoint fits the target, and iconv reports EILSEQ when one does not.
wcwidth_u32()¶
Measure the width of a single codepoint; returns 0 for zero-width (combining marks, ZWJ,
NUL), 2 for wide East Asian characters, and -1 for control codes:
wcwidth_u32(0x0301, 1) /* combining acute accent */ 0
wcwidth_u32(0x2630, 1) /* TRIGRAM FOR HEAVEN, wide */ 2
wcwidth_u32(0x2640, 1) /* female sign, ambiguous */ 1
wcwidth_u32(0x2640, 2) /* the same, ambiguous_width=2 */ 2
wcwidth_u32('\n', 1) /* control code */ -1
ambiguous_width (1 or 2) sets the width of East Asian Ambiguous characters, and only of those:
U+2640 above is Ambiguous, so it answers to the second argument, while U+2630 is Wide and measures
2 under either setting. A single codepoint needs no _u8 variant; use wcswidth_u8() to
measure text.
wcswidth_u32() and wcswidth_u8()¶
Measure a string of codepoints or UTF-8 bytes, treating grapheme clusters (ZWJ sequences,
variation selectors, virama conjuncts, regional indicator pairs) as single units; return -1
when any control code other than NUL is present:
uint32_t family[] = {0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467};
wcswidth_u32(family, 5, 1) /* man ZWJ woman ZWJ girl */ 2
wcswidth_u8("café", 5, 1) 4
wcstwidth_u32() and wcstwidth_u8()¶
Terminal-aware variants of wcswidth_u32() and wcswidth_u8(); the term_program
argument applies terminal-specific corrections:
wcswidth_u8("☰", 3, 1) /* U+2630, wide */ 2
wcstwidth_u8("☰", 3, 1, "vte") 1
wcwidth_width_u32() and wcwidth_width_u8()¶
Measure the visible width of text including terminal control sequences: colors, bold, tabstops, horizontal cursor movement, and OSC 66 Text Sizing. wcwidth_width_u32() measures the codepoints directly:
wcwidth_width_opts_t opts = WCWIDTH_WIDTH_OPTS_DEFAULT;
wcwidth_width_u8("\x1b[31mWARN\x1b[0m", 13, WCWIDTH_PARSE, &opts, NULL); /* 4 */
opts.tabsize = 4;
wcwidth_width_u8("\t", 1, WCWIDTH_PARSE, &opts, NULL); /* 4 */
wcwidth_width_u8("\x1b[H\x1b[2J", 7, WCWIDTH_PARSE, &opts, NULL); /* 0 */
wcwidth_width_u8("hello\x1b[5Dworld", 14, WCWIDTH_IGNORE, &opts, NULL); /* 10 */
wcwidth_width_u8("\x1b]66;w=2;XY\x07", 12, WCWIDTH_PARSE, &opts, NULL); /* 2 */
The wcwidth_control_mode_t mode selects how control characters and sequences are treated:
WCWIDTH_PARSE tracks horizontal cursor movement (the default), WCWIDTH_STRICT returns
-1 and sets *error for indeterminate sequences, and WCWIDTH_IGNORE strips all control
codes:
int err = 0;
int w = wcwidth_width_u8("\n", 1, WCWIDTH_STRICT, &WCWIDTH_WIDTH_OPTS_DEFAULT, &err);
if (w < 0) {
/* err is WCWIDTH_ERROR_VERTICAL_CTRL */
}
wcwidth_ljust_u8(), wcwidth_rjust_u8(), and wcwidth_center_u8()¶
Justify UTF-8 text to opts.dest_width display cells, filling with a UTF-8 byte string. Each
returns a malloc‘d NUL-terminated string the caller must free:
wcwidth_align_opts_t opts = WCWIDTH_ALIGN_OPTS_DEFAULT;
char *out;
opts.fillchar = "*";
opts.dest_width = 11;
out = wcwidth_ljust_u8("コンニチハ", 15, WCWIDTH_PARSE, &opts, NULL, NULL);
/* "コンニチハ*" */
free(out);
out = wcwidth_rjust_u8("コンニチハ", 15, WCWIDTH_PARSE, &opts, NULL, NULL);
/* "*コンニチハ" */
free(out);
opts.dest_width = 6;
out = wcwidth_center_u8("cafe\xcc\x81", 6, WCWIDTH_PARSE, &opts, NULL, NULL);
/* "*café*" */
free(out);
Tabs are measured as though text begins at column 0. wcwidth_rjust_u8() and
wcwidth_center_u8() insert padding to the left of the text which move tabstops and return
an incorrect result, always expand tabs or call wcwidth_clip_u8() to opts->dest_width first,
which expands tabs before using these alignment functions.
wcwidth_clip_u8()¶
Clip text to the visible column range [opts.v_start, opts.v_end), filling partially visible
graphemes with a fill string. Returns a malloc‘d NUL-terminated string the caller must
free:
wcwidth_clip_opts_t opts = WCWIDTH_CLIP_OPTS_DEFAULT;
char *out;
opts.v_end = 3;
out = wcwidth_clip_u8("中文字", 9, WCWIDTH_PARSE, &opts, NULL, NULL);
/* "中 " */
free(out);
opts.v_start = 1;
opts.v_end = 5;
opts.fillchar = ".";
out = wcwidth_clip_u8("中文字", 9, WCWIDTH_PARSE, &opts, NULL, NULL);
/* ".文." */
free(out);
Leave opts.v_end at its SIZE_MAX default to clip from v_start through the final column
of text, without measuring it first, matching the -1 default of Python’s clip():
opts = WCWIDTH_CLIP_OPTS_DEFAULT;
opts.v_start = 6; /* v_end stays SIZE_MAX: through the final column */
out = wcwidth_clip_u8("\x1b[1;34mHello world\x1b[0m", 22, WCWIDTH_PARSE, &opts, NULL, NULL);
/* "\x1b[1;34mworld\x1b[0m" */
free(out);
Some sequences are unsupported, and wcwidth_clip_u8() returns NULL with *error set to
WCWIDTH_ERROR_UNSUPPORTED, these sequences are only supported in Python’s clip():
Horizontal cursor movement: BS, CR, and CSI ending in
C(CUF),D(CUB) orG(HPA). Python’s clip() additionally offersovertyping.OSC 8 hyperlinks and OSC 66 text sizing: Python’s clip() covers these.
int error;
opts = WCWIDTH_CLIP_OPTS_DEFAULT;
opts.v_end = 3;
out = wcwidth_clip_u8("abcdef\x1b[3Dxy", 12, WCWIDTH_PARSE, &opts, NULL, &error);
/* NULL, error == WCWIDTH_ERROR_UNSUPPORTED */
Movement is reported wherever it appears; the OSC sequences only where the clip reaches one.
wcwidth_clip_u32() is the codepoint-array form, returning a malloc‘d array of *out_len
codepoints.
wcwidth_wrap_u8() and wcwidth_wrap_u8_text()¶
Wrap UTF-8 text into lines of at most opts.width display cells. wcwidth_wrap_u8() collapses
all whitespace, including newlines; wcwidth_wrap_u8_text() preserves input newlines as paragraph
breaks. Both emit a single malloc‘d buffer of newline-separated lines:
wcwidth_wrap_opts_t opts = WCWIDTH_WRAP_OPTS_DEFAULT;
char *out;
size_t out_len;
opts.width = 5;
wcwidth_wrap_u8("hello world", 11, &opts, &out, &out_len); /* "hello\nworld" */
free(out);
opts.width = 4;
wcwidth_wrap_u8("コンニチハ", 15, &opts, &out, &out_len); /* "コン\nニチ\nハ" */
free(out);
When the placeholder does not fit within the given width (max_lines truncation),
wcwidth_wrap_u8() returns -2, so callers can raise a tailored error.
wcwidth_wrap_lines_u8() additionally reports each line’s start offset in the output buffer, which
matters when a line contains '\n' from the placeholder itself:
size_t *offsets, count;
wcwidth_wrap_lines_u8("one two", 7, &opts, &out, &out_len, &offsets, &count);
/* out is "one\ntwo", offsets = {0, 4} */
free(offsets);
free(out);
OSC 66 text sizing is atomic to the word splitter: a sequence and its display text are one unbreakable unit, so a line is never broken inside one, even at a space or hyphen in the display text. Python’s wrap() behaves the same way. wcwidth_wrap_u32() and wcwidth_wrap_u32_text() are the codepoint-array forms.
wcwidth_escape_strip()¶
Strip all terminal escape sequences from text, preserving OSC 66 display text. The result is written to a caller-supplied buffer and is always NUL-terminated:
char buf[64];
size_t out_len = 0;
size_t needed = wcwidth_escape_strip("\x1b[31mred\x1b[0m", 12, buf, sizeof buf, &out_len);
/* buf is "red", out_len is 3 */
The return value is the byte length the stripped text needs, excluding the terminator, so the
buffer must hold needed + 1 bytes; the output was truncated whenever the return value is
greater than or equal to out_cap. A caller sizing its own buffer can measure first by passing
an out_cap of 0, then allocate and fill. wcwidth_escape_strip_u32() is the
codepoint-array form, and allocates its result instead.
Differences from the Python package¶
wcwidth_width_u32() and wcwidth_width_u8() parse only the sequences that move the cursor
within a line or change how much room text occupies: SGR, horizontal cursor movement (CUF, CUB,
HPA), and OSC 66 text sizing. Every other recognized sequence is zero-width. Screen clears,
scrolls and vertical movement are indeterminate (their column effect depends on terminal state
the text does not carry), so WCWIDTH_STRICT reports them as an error and the other modes
count them as zero-width.
wcswidth_*() and wcstwidth_*() take no wcwidth_control_mode_t and return -1 for any
escape sequence, matching their Python counterparts. wcwidth_ljust_u8(), wcwidth_rjust_u8()
and wcwidth_center_u8() match the Python functions exactly.
The text transforms are simpler:
wcwidth_clip_u8() rejects the unsupported sequences described above; every other sequence but SGR is zero-width and preserved at its original position. It otherwise matches Python’s clip(), SGR included, and Python’s clip() depends on that: it calls here first and runs its own implementation only on
WCWIDTH_ERROR_UNSUPPORTED.wcwidth_wrap_u8() treats an OSC 8 hyperlink as an ordinary zero-width OSC, so the link is not re-opened on each line; callers must re-emit the opener and terminator themselves.
wcwidth_wrap_u8() and wcwidth_wrap_u8_text() split words on the ASCII space alone, where Python’s wrap() splits on any whitespace run.
wcwidth_wrap_opts_toffers nobreak_on_hyphens,fix_sentence_endingsorpropagate_sgr: hyphenated words break mid-word, sentence-ending periods are not widened, and SGR state does not survive a line break.
Supported Terminals¶
The term_program argument selects per-terminal corrections from generated override tables.
The following terminal names are recognized; common TERM/TERM_PROGRAM aliases such as
vscode and xterm-kitty resolve to them:
absolutetelnet/ssh alacritty apple_terminal bobcat contour extraterm foot
ghostty iterm2 kitty konsole mintty mlterm pterm rio st terminology urxvt
vte warp wezterm xterm xterm.js
For the most accurate corrections, query the terminal’s software version via XTVERSION
(CSI > q) and pass the name from the list above. See the Python Corrections documentation
for details.
Unicode Version¶
Tables generated from Unicode 18.0.0.