module

TermBuf::Unicode

Character property lookup for the terminal buffer: display width, grapheme cluster break class, and the two auxiliary properties UAX #29 needs.

Every code point falls into exactly one range of Tables::RANGE_START, so a lookup is a binary search with no miss case. Code points below FAST_LIMIT skip the search entirely via a direct-indexed table built at startup, which covers Latin, Greek, Cyrillic, Hebrew, Arabic, the combining diacriticals, and most of the Indic scripts.

Constants

EMOJI_PRESENTATION = '️'

Variation selector 16, forcing emoji presentation and so a width of two.

ENCLOSING_MARKS = StaticArray[1160..1161, 6846..6846, 8413..8416, 8418..8420, 42608..42610]

Enclosing marks, general category Me. There are thirteen of them, so a list costs less than a bit in the generated tables.

They matter only to .code_point_columns: a terminal counting per code point gives an enclosing mark a column of its own, where it gives a nonspacing mark none. That is the difference between 1️⃣ owning one column and owning two.

FAST_LIMIT = 4096

Code points below this are resolved by direct index rather than search.

TEXT_PRESENTATION = '︎'

Variation selector 15, forcing text presentation and so a width of one.

ZERO_WIDTH_JOINER = '\u200D'

Joins emoji into one cluster, and on most terminals into one glyph.

Class methods

ambiguous?(char : Char) : Bool

Whether char is East Asian Ambiguous, and so rendered at a width the terminal's configuration decides.

Source
ambiguous_width

Cells an East Asian Ambiguous character takes. Reads through to .policy, which is where the answer lives now that it can be measured rather than assumed.

Source
char_width(char : Char, ambiguous : Int32 = ambiguous_width) : Int32

Cells char occupies on its own, ignoring any cluster it belongs to. Control characters and default-ignorable code points report zero.

Source
code_point_columns(text : String, policy : WidthPolicy = Unicode.policy) : Int32

Columns a terminal takes for text when it counts them by summing the code points rather than by measuring the grapheme cluster.

Not what any standard says a cluster is worth: what Quirk::PerCodePointColumns terminals do. Measured against Terminal.app 470.2 and GNU screen 5.0.2 over 5,274 clusters, in measurements/survey/.

The rule is East Asian Width per code point, with three departures.

  • An ignorable character is charged what East Asian Width says rather than nothing. A zero width joiner is Neutral, so four joined faces own eleven; a tag character is Neutral, so a subdivision flag owns eight; the hangul fillers are Wide, so they own two apiece. The joiner and the tag were found one at a time and written down as two rules. They were one.
  • An enclosing mark takes one, which is what makes a keycap own two.
  • A regional indicator takes one rather than two, so a flag owns two and five indicators own five.
  • Conjoining jamo are composed before counting, so the vowel and the tail take nothing and a syllable spelled out owns two, like the one it makes. This is the one place the terminal counts a cluster rather than its pieces. A vowel or tail with no lead is not something the samples cover, and is given nothing here.
Source
code_point_columns(char : Char, policy : WidthPolicy = Unicode.policy) : Int32

Columns a terminal takes for text when it counts them by summing the code points rather than by measuring the grapheme cluster.

Not what any standard says a cluster is worth: what Quirk::PerCodePointColumns terminals do. Measured against Terminal.app 470.2 and GNU screen 5.0.2 over 5,274 clusters, in measurements/survey/.

The rule is East Asian Width per code point, with three departures.

  • An ignorable character is charged what East Asian Width says rather than nothing. A zero width joiner is Neutral, so four joined faces own eleven; a tag character is Neutral, so a subdivision flag owns eight; the hangul fillers are Wide, so they own two apiece. The joiner and the tag were found one at a time and written down as two rules. They were one.
  • An enclosing mark takes one, which is what makes a keycap own two.
  • A regional indicator takes one rather than two, so a flag owns two and five indicators own five.
  • Conjoining jamo are composed before counting, so the vowel and the tail take nothing and a syllable spelled out owns two, like the one it makes. This is the one place the terminal counts a cluster rather than its pieces. A vowel or tail with no lead is not something the samples cover, and is given nothing here.
Source
conjunct_class(char : Char) : Tables::Incb

Indic conjunct break class of char, used by rule GB9c.

Source
each_grapheme(string : String, policy : WidthPolicy = Unicode.policy, & : Grapheme -> ) : Nil

Yields each extended grapheme cluster of string in order, without allocating: a Grapheme locates its cluster by byte offset.

Source
east_asian_columns(char : Char, ambiguous : Int32 = ambiguous_width) : Int32

What East Asian Width says char is worth, before anything zeroes it.

Source
ellipsize(text : String, width : Int32, marker : String = "…", policy : WidthPolicy = Unicode.policy) : String

Cuts text down to at most width cells, marking the cut with marker. The marker is measured under policy too, so the result is never wider than width; when the marker alone will not fit, the text is cut without one.

Source
emoji?(char : Char) : Bool

Whether char has the Emoji property.

Narrower than .pictographic? and a different question. U+2691 is pictographic and is not an emoji, so a variation selector after it asks for a presentation it does not have and it stays one column — both terminals measured agree. The digits are the other way about: emoji, not pictographic, which is what makes 1️⃣ two columns.

Source
enclosing_mark?(char : Char) : Bool

Whether char is an enclosing mark.

Source
fit(text : String, width : Int32, align : Align = :left, fill : Char = ' ', policy : WidthPolicy = Unicode.policy) : String

Fits text to exactly width cells: cut when it is too long, padded with fill when it is too short. A cluster dropped rather than split leaves a cell over, which is padded like any other.

Source
grapheme_class(char : Char) : Tables::Gcb

Grapheme cluster break class of char (UAX #29).

Source
graphemes(string : String) : Array(String)

All extended grapheme clusters of string, as separate strings. Convenient for specs and one-off work; the buffer uses each_grapheme instead.

Source
ignorable?(char : Char) : Bool

Number of terminal cells char occupies on its own, ignoring any grapheme cluster it may belong to. Control characters report zero; they are never stored in the buffer. Whether char is a format character or default ignorable: one the tables carry an East Asian Width for and nothing draws.

.char_width answers zero for these. .code_point_columns does not, since a terminal counting per code point charges them anyway.

Source
pictographic?(char : Char) : Bool

Whether char has the Extended_Pictographic property, used by rule GB11.

Source
policy

How clusters are measured when no policy is given. Buffer carries its own, set from what the terminal said when asked; this is for casual callers and for char_width, which has no cluster to consult.

Source
policy=(policy : WidthPolicy)

How clusters are measured when no policy is given. Buffer carries its own, set from what the terminal said when asked; this is for casual callers and for char_width, which has no cluster to consult.

Source
properties(codepoint : Int32) : UInt16

Packed property word for codepoint.

Source
string_width(string : String, policy : WidthPolicy = Unicode.policy) : Int32

Total number of terminal cells string occupies, under policy.

Source
truncate(text : String, width : Int32, policy : WidthPolicy = Unicode.policy) : String

Cuts text down to at most width cells, returning it unchanged when it already fits. Nothing is added to mark the cut; see .ellipsize for that.

Source
utf8_continuation?(byte : UInt8) : Bool

Whether byte continues a character rather than beginning one.

Source
utf8_length(lead : UInt8) : Int32

How many bytes the character starting with lead takes, or zero if lead is not a lead byte at all.

The over-long and surrogate ranges are excluded, so a byte this accepts begins a character that can exist.

Source
utf8_prefix(bytes : Bytes) : Int32

How much of bytes forms whole characters.

A write can end anywhere, including the middle of a character, so the tail this leaves out has to be held until the rest of it arrives.

Source
window(text : String, offset : Int32, width : Int32, policy : WidthPolicy = Unicode.policy) : String

The width cells of text starting offset cells in — a horizontal window over a value too wide to show at once. A cluster crossing either edge is left out whole, and a window running past the end simply comes back short. A negative offset is allowed and starts the window before the text, which is what a marquee scrolling in from the left wants.

Source

Nested types