1 # Pure terminal text: ANSI SGR styling and Unicode 17 display width. 2 import Base 3 import 0x6c784a08486e2e02415e89c5249e9e8a/unicode.bend as Unicode 4 import ./width.bend as W 5 6 # Wrap text in one numeric SGR code and a reset when enabled; otherwise return it unchanged. 7 def style(enabled: Bool, code: U32, text: String) -> String: 8 match enabled: 9 case True{}: 10 String.concat(["\u{1B}[", U32.show(code), "m", text, "\u{1B}[0m"]) 11 case False{}: 12 text 13 14 def vs16(s: String) -> Bool: 15 match s: 16 case SNil{}: 17 False{} 18 case SCon{Chr{+c}, t}: 19 U32.is_eq(c, 65039) || vs16(t) 20 21 # A grapheme cluster takes its first code point's columns (W.w); VS16 makes a narrow base an emoji, 2 wide. 22 def cluster(g: String) -> U32: 23 match g: 24 case SNil{}: 25 0 26 case SCon{Chr{+c}, t}: 27 +w = W.w(c) 28 Bool.pick(U32, U32.is_eq(w, 1) && vs16(t), 2, w) 29 30 def width.go(gs: List<&2, String>, +acc: U32) -> U32: 31 match gs: 32 case Nil{}: 33 acc 34 case Con{g, t}: 35 width.go(t, (acc + cluster(g) : U32)) 36 37 # Display columns: East Asian Wide and Fullwidth clusters are 2, marks and controls 0, Ambiguous 1. 38 # Escape sequences are not stripped: style()'s output is wider than what the terminal shows. 39 def width(text: String) -> U32: 40 width.go(Unicode.graphemes(text), 0) 41 42 # Append spaces until text occupies at least cols display columns. 43 def pad_right(+text: String, +cols: U32) -> String: 44 +w = width(text) 45 +n = Bool.pick(U32, U32.is_lt(w, cols), (cols - w : U32), 0) 46 String.append(text, String.repeat(" ", U32.to_nat(n)))