go-runewidth

Computes the terminal cell width of runes and strings, handling CJK, emoji, and combining characters correctly.

Library
Go
vv0.0.29
721stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
67/100Good
Development Activity84
Maintenance40
Community64
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
76/100Good
Architecture85
Code Quality85
Innovation80
Learning Curve55

go-runewidth is a small, dependency-light Go library that answers a deceptively hard question: how many terminal columns does this string actually occupy? A plain rune count is wrong the moment a string contains East Asian wide characters, emoji, zero-width combining marks, or grapheme clusters made of several runes that render as one glyph. go-runewidth implements the Unicode East Asian Width property (UTR #11) plus practical heuristics for ambiguous-width and emoji runes, and exposes both single-rune (RuneWidth) and full-string (StringWidth) width calculations.

Beyond raw measurement, it ships the string-manipulation primitives that terminal UIs actually need: Truncate, TruncateLeft, and TruncatePrefix cut a string to fit a target cell width without splitting a grapheme cluster in half, Wrap line-wraps text by visual width rather than byte or rune count, and FillLeft/FillRight pad strings for column alignment. A Condition struct lets callers override the East-Asian-width and strict-emoji-neutral flags per call site (instead of relying on the auto-detected locale), and an optional CreateLUT() precomputes a full lookup table for hot paths that call width functions at high frequency.

What You Get

  • StringWidth/RuneWidth functions that return true terminal cell width per Unicode TR#11, not a naive rune or byte count
  • Grapheme-cluster-aware Truncate, TruncateLeft, and TruncatePrefix that never split a combined character in half
  • Wrap, FillLeft, and FillRight helpers for building aligned, column-based terminal output
  • A Condition type for per-call-site control of East-Asian-width and emoji-neutral behavior, independent of the detected locale
  • Automatic locale detection (IsEastAsian) via POSIX locale env vars or the Windows console output codepage
  • An optional CreateLUT() precomputed lookup table for width-sensitive hot paths, plus a lazily-built internal LUT so importing the package costs no memory unless width functions are actually called

Common Use Cases

  • Terminal UI / TUI toolkits computing box-border and column widths so layouts stay aligned when text contains CJK or emoji
  • CLI tools rendering formatted tables or progress bars over mixed-width Unicode content
  • Truncating long strings (filenames, commit messages, chat text) to fit a fixed terminal width without corrupting a multi-rune glyph
  • Wrapping paragraph text in a terminal app by actual visual width instead of rune count

Under The Hood

Architecture The package is a flat, single-package library: runewidth.go holds the primary API and orchestration logic, runewidth_table.go holds generated static Unicode interval tables (private, nonprint, combining, doublewidth, ambiguous, emoji, narrow, neutral) produced by script/generate.go’s go:generate directive pulling data from unicode.org, and runewidth_posix.go/runewidth_windows.go/runewidth_appengine.go/runewidth_js.go provide build-tag-gated platform implementations of IsEastAsian() (POSIX locale-env parsing vs. a Windows GetConsoleOutputCP syscall vs. no-op stubs). The core data flow merges the static interval tables into zerowidth/widewidth/eastAsianWidth tables lazily via sync.Once, consulted through binary search (inTable/inWidthTable) or a fully materialized two-plane byte lookup table (strictWidthLUT) that fills its low region eagerly at init and its high region lazily behind an atomic, acquire-semantics limit check — a documented concurrency invariant (see the comments around issue #104) that keeps plain package import cheap. Condition is the central strategy object encapsulating the EastAsianWidth/StrictEmojiNeutral flags and an optional precomputed combinedLut, with every package-level convenience function delegating to a shared DefaultCondition. Grapheme-aware operations delegate cluster segmentation to the external uax29/v2 dependency rather than iterating runes directly, so that dependency’s segmentation boundaries flow straight through to every width computation.

Tech Stack Pure Go, module targets Go 1.23, with a single external dependency (github.com/clipperhouse/uax29/v2) providing UAX #29 grapheme-cluster segmentation. Table generation is handled by a go generate-invoked script that fetches interval data from unicode.org and regenerates runewidth_table.go; there is no other build tooling. CI (GitHub Actions) runs go generate ./... followed by git diff --cached --exit-code to guarantee committed tables are never stale, then go test -cover and go test -bench, across a 3x3 matrix of Go 1.24/1.25/1.26 on Windows, macOS, and Linux runners, with coverage uploaded to Codecov.

Code Quality Testing is substantial for a library this size: table-driven tests verify SHA-256 checksums of every generated Unicode interval table to catch silent drift, cross-check the fast lookup-table path against the slow binary-search path for correctness, and exercise StringWidth/Truncate/Wrap across ASCII, CJK, and emoji inputs, plus locale-parsing edge cases on POSIX. Only the standard testing package is used — no external assertion library. Naming is idiomatic Go throughout, and comments are unusually precise about concurrency invariants (documenting exactly which regions of shared state may be read concurrently with a lazy build). There is no linter configuration beyond the generated-file diff check in CI, and error handling is minimal by design since width computation never fails.

API Design The public surface favors progressive disclosure: top-level functions (RuneWidth, StringWidth, Truncate, Wrap, FillLeft, FillRight) work with zero configuration for the common case — a single import and one function call — while advanced use cases construct a Condition to override locale detection per call site, or call CreateLUT() to trade memory for speed on hot paths, without any API redesign. Naming follows a consistent Verb+Modifier pattern (TruncateLeft, TruncatePrefix, FillLeft, FillRight), every exported symbol carries a doc comment, and deprecated fields (ZeroWidthJoiner) are marked with a clear migration note. The README itself is minimal (a single usage line), so onboarding relies on godoc rather than prose documentation.

Used by 5 apps in this directory

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search