Something went wrong. Try again.
Measure the width of text in the terminal and build simple layouts!
Something went wrong. Try again.
12 kB · 401 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402// source: https://github.com/unicode-rs/unicode-width/// I mark tests that I think are wrong with @Wrong
import gleam/intimport gleam/ioimport gleam/listimport gleam/stringimport gleeunit/shouldimport string_width
@external(erlang, "string_width_ffi", "utf_codepoint_to_string")@external(javascript, "./string_width_ffi.mjs", "utf_codepoint_to_string")fn utf_codepoint_to_string(cp: Int) -> String
fn t(str: String, expected_normal_width: Int, expected_wide_width: Int) { twc(str, expected_normal_width, expected_wide_width) tg(str, expected_normal_width, expected_wide_width)}
fn twc(str: String, expected_normal_width: Int, expected_wide_width: Int) { let actual_normal_width = string_width.line(str) let actual_wide_width = string_width.line_with( str, string_width.new() |> string_width.ambiguous_as_wide, )
case actual_normal_width == expected_normal_width { True -> Nil False -> io.println( string.inspect(str) <> " (" <> string.to_utf_codepoints(str) |> list.map(string.utf_codepoint_to_int) |> list.map(int.to_base16) |> string.join(" ") <> ") has width " <> string.inspect(actual_normal_width) <> ", but I expected " <> string.inspect(expected_normal_width), ) }
case actual_wide_width == expected_wide_width { True -> Nil False -> io.println( string.inspect(str) <> " (" <> string.to_utf_codepoints(str) |> list.map(string.utf_codepoint_to_int) |> list.map(int.to_base16) |> string.join(" ") <> ") has width " <> string.inspect(actual_wide_width) <> ", but I expected " <> string.inspect(expected_wide_width) <> " (ambiguous_as_wide)", ) }
actual_normal_width |> should.equal(expected_normal_width) actual_wide_width |> should.equal(expected_wide_width)}
fn tg(str: String, expected_normal_width: Int, expected_wide_width: Int) { let actual_normal_width = string_width.line_with( str, string_width.new() |> string_width.mode_2027_ext, ) let actual_wide_width = string_width.line_with( str, string_width.new() |> string_width.mode_2027_ext |> string_width.ambiguous_as_wide, )
case actual_normal_width == expected_normal_width { True -> Nil False -> io.println( string.inspect(str) <> " (" <> string.to_utf_codepoints(str) |> list.map(string.utf_codepoint_to_int) |> list.map(int.to_base16) |> string.join(" ") <> ") has width " <> string.inspect(actual_normal_width) <> ", but I expected " <> string.inspect(expected_normal_width) <> " (2027ext)", ) }
case actual_wide_width == expected_wide_width { True -> Nil False -> io.println( string.inspect(str) <> " (" <> string.to_utf_codepoints(str) |> list.map(string.utf_codepoint_to_int) |> list.map(int.to_base16) |> string.join(" ") <> ") has width " <> string.inspect(actual_wide_width) <> ", but I expected " <> string.inspect(expected_wide_width) <> " (2027ext, ambiguous_as_wide)", ) }
actual_normal_width |> should.equal(expected_normal_width) actual_wide_width |> should.equal(expected_wide_width)}
pub fn str_test() { t("hello", 10, 10) t("", 0, 0) t("\u{2081}\u{2082}\u{2083}\u{2084}", 4, 8)}
pub fn emoji_test() { // Woman t("👩", 2, 2) // Microscope t("🔬", 2, 2) // Woman scientist // t("👩🔬", 2, 2) @Wrong twc("👩🔬", 4, 4) tg("👩🔬", 2, 2)}
// From README
pub fn bad_devanagari_test() { // Devanagari letter Ka t("क", 1, 1) // Devanagari letter Ssa t("ष", 1, 1) // Ka + Virama + Ssa t("क्ष", 2, 2)}
pub fn char_test() { t("h", 2, 2) t("\u{2081}", 1, 2)}
pub fn char2_test() { t("w", 1, 1) t("h", 2, 2) t("\u{AD}", 0, 0) t("\u{1160}", 0, 0) t("\u{a1}", 1, 2) t("\u{300}", 0, 0)}
pub fn unicode_12_test() { t("\u{1F971}", 2, 2)}
pub fn default_ignorable_test() { t("\u{1160}", 0, 0) t("\u{3164}", 0, 0) t("\u{FFA0}", 0, 0) t("\u{E0000}", 0, 0)}
pub fn ambiguous_test() { t("\u{B7}", 1, 2) // t("\u{0387}", 1, 2) @Wrong this one is not ambiguous?? // @Wrong did we get the wrong letter here? // t("\u{A8}", 1, 1) @Wrong this one is ambigous, so it should be 1,2? t("\u{A8}", 1, 2) // t("\u{02C9}", 1, 1) @Wrong aren't we testing ambigous characters here? t("\u{02C9}", 1, 2)}
pub fn jamo_test() { t("\u{1100}", 2, 2) t("\u{A97C}", 2, 2) // Special case: U+115F HANGUL CHOSEONG FILLER t("\u{115F}", 2, 2) t("\u{1160}", 0, 0)
// again these are tricky // they have a width, but render as zero-width on vte t("\u{D7C6}", 0, 0) t("\u{11A8}", 0, 0) t("\u{D7FB}", 0, 0)}
pub fn prepended_concatenation_marks_test() { // list.each( // [ // "\u{0600}", "\u{0601}", "\u{0602}", "\u{0603}", "\u{0604}", "\u{06DD}", // "\u{110BD}", "\u{110CD}", // ], // fn(c) { t(c, 1, 1) }, // ) // @Wrong @Todo // I mark them as zero-width for now. see generate. list.each( [ "\u{0600}", "\u{0601}", "\u{0602}", "\u{0603}", "\u{0604}", "\u{06DD}", "\u{110BD}", "\u{110CD}", ], fn(c) { t(c, 0, 0) }, )
list.each(["\u{0890}", "\u{0891}", "\u{08E2}", "\u{0605}", "\u{070F}"], fn(c) { t(c, 0, 0) })}
pub fn gcb_prepend_test() { Nil // t("ൎഉ", 1, 1) @Wrong prints as 2 characters, and is definitely wide in vscode // t("\u{11A89}", 0, 0) // "𑪉" @Wrong // this one is interesting, since it cannot stand alone but is classified as an other_letter // I do not know what to do about these right now.}
pub fn interlinear_annotation_chars_test() { // @Wrong these render as zero-width on my terminals. // t("\u{FFF9}", 1, 1) // t("\u{FFFA}", 1, 1) // t("\u{FFFB}", 1, 1) t("\u{FFF9}", 0, 0) t("\u{FFFA}", 0, 0) t("\u{FFFB}", 0, 0)}
pub fn hieroglyph_format_controls_test() { // https://github.com/unicode-rs/unicode-width/commit/aae585fbd1a2bcaa621ea85b031bfbbe299007de // @Wrong these render as zero-width on my terminals. // t("\u{13430}", 1, 1) // t("\u{13436}", 1, 1) // t("\u{1343C}", 1, 1)
t("\u{13430}", 0, 0) t("\u{13436}", 0, 0) t("\u{1343C}", 0, 0)}
pub fn marks_test() { // Nonspacing marks have 0 width t("\u{0301}", 0, 0) // Enclosing marks have 0 width t("\u{20DD}", 0, 0) // Some spacing marks have width 1 t("\u{09CB}", 1, 1) // But others have width 0 // t("\u{09BE}", 0, 0) @Wrong still has width!}
pub fn devanagari_caret_test() { // according to the unicode standard, this is a zero-width mark // but, it prints as 1 wide in terminals // https://www.unicode.org/versions/Unicode15.0.0/ch12.pdf#G667447 // t("\u{A8FA}", 0, 0) @Wrong "꣺" clearly has a width. Nil}
pub fn emoji_presentation_test() { t("\u{0023}", 1, 1) t("\u{FE0F}", 0, 0) // t("\u{0023}\u{FE0F}", 2, 2) @Wrong "#️" prints as wide but takes narrow space in my terminal tg("\u{0023}\u{FE0F}", 2, 2) // t("a\u{0023}\u{FE0F}a", 4, 4) @Wrong prints as narrow in my terminal tg("a\u{0023}\u{FE0F}a", 4, 4) twc("\u{0023}a\u{FE0F}", 2, 2) // @WeWrong: we do not handle emoji sequences tg("\u{0023}a\u{FE0F}", 3, 3) twc("a\u{FE0F}", 1, 1) // @WeWrong: we do not handle emoji sequences tg("a\u{FE0F}", 2, 2) // t("\u{0023}\u{0023}\u{FE0F}a", 4, 4) @Wrong prints as narrow in my terminal tg("\u{0023}\u{0023}\u{FE0F}a", 4, 4) // t("\u{002A}\u{FE0F}", 2, 2) @Wrong prints as wide, but takes narrow space tg("\u{002A}\u{FE0F}", 2, 2) // t("\u{23F9}\u{FE0F}", 2, 2) @Wrong prints as wide but takes narrow space tg("\u{23F9}\u{FE0F}", 2, 2) // t("\u{24C2}\u{FE0F}", 2, 2) @Wrong prints as wide but takes narrow space tg("\u{24C2}\u{FE0F}", 2, 2) // t("\u{1F6F3}\u{FE0F}", 2, 2) @Wrong prints as wide but takes narrow space tg("\u{1F6F3}\u{FE0F}", 2, 2) twc("\u{1F700}\u{FE0F}", 1, 1) // @WeWrong emoji sequences tg("\u{1F700}\u{FE0F}", 2, 2) twc("\u{002A}\u{301}\u{FE0F}", 1, 1) // @WeWrong emoji sequences tg("\u{002A}\u{301}\u{FE0F}", 2, 2) twc("\u{002A}\u{200D}\u{FE0F}", 1, 1) // @WeWrong emoji sequences tg("\u{002A}\u{200D}\u{FE0F}", 2, 2) twc("\u{002A}\u{FE0E}\u{FE0F}", 1, 1) // @WeWrong emoji sequences tg("\u{002A}\u{FE0E}\u{FE0F}", 2, 2)}
pub fn text_presentation_test() { t("\u{FE0E}", 0, 0) t("\u{2648}", 2, 2) // t("\u{2648}\u{FE0E}", 1, 2) @Wrong still prints as wide on my terminal, East_Asian_Width=Wide t("\u{1F21A}\u{FE0E}", 2, 2) t("\u{0301}\u{FE0E}", 0, 0) t("a\u{FE0E}", 1, 1) t("𘀀\u{FE0E}", 2, 2) t("\u{2648}\u{0301}\u{FE0E}", 2, 2) t("\u{2648}\u{200D}\u{FE0E}", 2, 2)}
pub fn lisu_tones_test() { use char_code <- list.each(list.range(0xA4F8, 0xA4FD)) let str = utf_codepoint_to_string(char_code) t(str, 1, 1)}
pub fn lisu_tones2_test() { use c1 <- list.each(list.range(0xA4F8, 0xA4FD)) use c2 <- list.each(list.range(0xA4F8, 0xA4FD))
let s = utf_codepoint_to_string(c1) <> utf_codepoint_to_string(c2)
case c1, c2 { // @Wrong ignored // _, _ if c1 >= 0xa4f8 && c1 <= 0xa4fb && c2 >= 0xa4fc && c2 <= 0xa4fd -> // t(s, 1, 1) // these combinations still render as wide on my terminals. // _, _ if c1 >= 0xa4f8 && c1 <= 0xa4fb && c2 >= 0xa4fc && c2 <= 0xa4fd -> // tg(s, 1, 1) _, _ -> t(s, 2, 2) }}
pub fn lisu_tones_others_test() { t("ꓪꓹ", 2, 2) // t("ꓪꓹꓼ", 2, 2) @Wrong prints as 3 in my terminal // t("ꓪꓹ\u{FE0F}ꓼ", 2, 2) @Wrong prints as 3 in my terminal // t("ꓪꓹ\u{200D}ꓼ", 2, 2) @Wrong prints as 3 in my terminal // t("ꓪꓹꓼ\u{FE0F}", 2, 2) @Wrong prints as 3 in my terminal t("ꓪꓹ\u{0301}ꓼ", 3, 3) t("ꓪꓹꓹ", 3, 3) t("ꓪꓼꓼ", 3, 3)}
pub fn tifinagh_biconsonants_test() { t("\u{2D4F}", 1, 1) t("\u{2D3E}", 1, 1) // t("\u{2D7F}", 1, 1) @Wrong this is a nonspacing mark
// t("\u{2D4F}\u{200D}\u{2D3E}", 1, 1) @Wrong prints as 2 in my terminal // t("\u{2D4F}\u{2D7F}\u{2D3E}", 1, 1) @Wrong prints as 2 in my terminal // t("\u{2D4F}\u{200D}\u{2D3E}", 1, 1) @Wrong prints as 2 in my terminal // t("\u{2D4F}\u{FE0F}\u{200D}\u{2D7F}\u{FE0E}\u{200D}\u{17B5}\u{2D3E}", 1, 1) @wrong prints as 2 in my terminal
// t("\u{2D4F}\u{301}\u{2D7F}\u{2D3E}", 3, 3) @Wrong prints as 2 in my terminal t("\u{2D4F}\u{301}\u{200D}\u{2D3E}", 2, 2) t("\u{2D4F}\u{2D3E}", 2, 2) // t("\u{2D4F}\u{2D7F}\u{2D7F}\u{2D3E}", 4, 4) @Wrong prints as 2 in my terminal // t("\u{2D7F}\u{2D3E}", 2, 2) @Wrong prints as 1 in my terminal // t("\u{2D7F}\u{2D7F}\u{2D66}", 3, 3) @Wrong prints as 1 in my terminal // t("\u{2D66}\u{2D7F}\u{2D3E}", 3, 3) @Wrong prints as 2 in my terminal}
pub fn khmer_coeng_test() { t("ល", 1, 1) t("ង", 1, 1) t("លង", 2, 2) // t("ល្ង", 1, 1) @Wrong prints as 2 in my terminal t("ល្ង", 2, 2)}
pub fn khmer_qaa_test() { // t("\u{17A4}", 2, 2) @Wrong prints as 1 in my terminal t("\u{17A4}", 1, 1) t("\u{17A2}\u{17A6}", 2, 2)}
pub fn khmer_sign_beyyal_test() { // t("\u{17D8}", 3, 3)// @Wrong wtf 3 also this prints as 1 in my terminal t("\u{17D8}", 1, 1) t("\u{17D4}\u{179B}\u{17D4}", 3, 3)}
pub fn emoji_modifier_test() { t("\u{1F46A}", 2, 2) t("\u{1F3FB}", 2, 2) // t("\u{1F46A}\u{1F3FB}", 2, 2) @Wrong grapheme cluster handling tg("\u{1F46A}\u{1F3FB}", 2, 2) // despair // twc("\u{1F46A}\u{200D}\u{200D}\u{1F3FB}", 4, 4) // tg("\u{1F46A}\u{200D}\u{200D}\u{1F3FB}", 2, 2)}