diff --git a/examples/colorful/colorful.tm b/examples/colorful/colorful.tm index 71b8387..ca34c29 100644 --- a/examples/colorful/colorful.tm +++ b/examples/colorful/colorful.tm @@ -6,8 +6,43 @@ HELP := " CSI := "$\033[" -enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(color:Int32)): - func from_text(text:Text -> Color?): +lang Colorful: + convert(text:Text -> Colorful): + text = text:replace_all({$/@/="@(at)", $/(/="@(lparen)", $/)/="@(rparen)"}) + return Colorful.without_escaping(text) + + func for_terminal(c:Colorful -> Text): + return CSI ++ "m" ++ _for_terminal(c, _TermState()) + + func print(c:Colorful, newline=yes): + say(c:for_terminal(), newline=newline) + + +func main(texts:[Text], files=[:Path], by_line=no): + for i,text in texts: + colorful := Colorful.without_escaping(text) + colorful:print(newline=no) + if i == texts.length: say("") + else: say(" ", newline=no) + + if texts.length == 0 and files.length == 0: + files = [(/dev/stdin)] + + for file in files: + if by_line: + for line in file:by_line() or exit("Could not read file: $(file.text)"): + colorful := Colorful.without_escaping(line) + colorful:print() + else: + colorful := Colorful.without_escaping(file:read() or exit("Could not read file: $(file.text)")) + colorful:print(newline=no) + + +func _for_terminal(c:Colorful, state:_TermState -> Text): + return c.text:map($/@(?)/, func(m:Match): _add_ansi_sequences(m.captures[1], state)) + +enum _Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(color:Int32)): + func from_text(text:Text -> _Color?): if text:matches($/#{3-6 hex}/): hex := text:from(2) return none unless hex.length == 3 or hex.length == 6 @@ -18,26 +53,26 @@ enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(colo else if text:matches($/{1-3 digit}/): n := Int16.parse(text) or return none if n >= 0 and n <= 255: return Color8Bit(n) - else if text == "black": return Color.Color8Bit(0) - else if text == "red": return Color.Color8Bit(1) - else if text == "green": return Color.Color8Bit(2) - else if text == "yellow": return Color.Color8Bit(3) - else if text == "blue": return Color.Color8Bit(4) - else if text == "magenta": return Color.Color8Bit(5) - else if text == "cyan": return Color.Color8Bit(6) - else if text == "white": return Color.Color8Bit(7) - else if text == "default": return Color.Default - else if text == "BLACK": return Color.Bright(0) - else if text == "RED": return Color.Bright(1) - else if text == "GREEN": return Color.Bright(2) - else if text == "YELLOW": return Color.Bright(3) - else if text == "BLUE": return Color.Bright(4) - else if text == "MAGENTA": return Color.Bright(5) - else if text == "CYAN": return Color.Bright(6) - else if text == "WHITE": return Color.Bright(7) + else if text == "black": return _Color.Color8Bit(0) + else if text == "red": return _Color.Color8Bit(1) + else if text == "green": return _Color.Color8Bit(2) + else if text == "yellow": return _Color.Color8Bit(3) + else if text == "blue": return _Color.Color8Bit(4) + else if text == "magenta": return _Color.Color8Bit(5) + else if text == "cyan": return _Color.Color8Bit(6) + else if text == "white": return _Color.Color8Bit(7) + else if text == "default": return _Color.Default + else if text == "BLACK": return _Color.Bright(0) + else if text == "RED": return _Color.Bright(1) + else if text == "GREEN": return _Color.Bright(2) + else if text == "YELLOW": return _Color.Bright(3) + else if text == "BLUE": return _Color.Bright(4) + else if text == "MAGENTA": return _Color.Bright(5) + else if text == "CYAN": return _Color.Bright(6) + else if text == "WHITE": return _Color.Bright(7) return none - func fg(c:Color -> Text): + func fg(c:_Color -> Text): when c is Color8Bit(color): if color >= 0 and color <= 7: return "$(30+color)" else if color >= 0 and color <= 255: return "38;5;$color" @@ -50,7 +85,7 @@ enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(colo return "39" fail("Invalid foreground color: '$c'") - func bg(c:Color -> Text): + func bg(c:_Color -> Text): when c is Color8Bit(color): if color >= 0 and color <= 7: return "$(40+color)" else if color >= 0 and color <= 255: return "48;5;$color" @@ -63,7 +98,7 @@ enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(colo return "49" fail("Invalid background color: '$c'") - func underline(c:Color -> Text): + func underline(c:_Color -> Text): when c is Color8Bit(color): if color >= 0 and color <= 255: return "58;5;$color" is Color24Bit(hex): @@ -75,30 +110,31 @@ enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(colo pass fail("Invalid underline color: '$c'") -struct TermState( +func _toggle(sequences:&[Text], cur,new:Bool, apply,unapply:Text; inline): + if new and not cur: + sequences:insert(apply) + else if cur and not new: + sequences:insert(unapply) + +func _toggle2(sequences:&[Text], cur1,cur2,new1,new2:Bool, apply1,apply2,unapply:Text; inline): + return if new1 == cur1 and new2 == cur2 + if (cur1 and not new1) or (cur2 and not new2): # Gotta wipe at least one + sequences:insert(unapply) + cur1, cur2 = no, no # Wiped out + + if new1 and not cur1: + sequences:insert(apply1) + if new2 and not cur2: + sequences:insert(apply2) + +struct _TermState( bold=no, dim=no, italic=no, underline=no, blink=no, reverse=no, conceal=no, strikethrough=no, fraktur=no, frame=no, encircle=no, overline=no, superscript=no, subscript=no, - bg=Color.Default, fg=Color.Default, underline_color=Color.Default, + bg=_Color.Default, fg=_Color.Default, underline_color=_Color.Default, ): - func _toggle(sequences:&[Text], cur,new:Bool, apply,unapply:Text; inline): - if new and not cur: - sequences:insert(apply) - else if cur and not new: - sequences:insert(unapply) - func _toggle2(sequences:&[Text], cur1,cur2,new1,new2:Bool, apply1,apply2,unapply:Text; inline): - return if new1 == cur1 and new2 == cur2 - if (cur1 and not new1) or (cur2 and not new2): # Gotta wipe at least one - sequences:insert(unapply) - cur1, cur2 = no, no # Wiped out - - if new1 and not cur1: - sequences:insert(apply1) - if new2 and not cur2: - sequences:insert(apply2) - - func apply(old,new:TermState -> Text): + func apply(old,new:_TermState -> Text): sequences := &[:Text] _toggle2(sequences, old.bold, old.dim, new.bold, new.dim, "1", "2", "22") _toggle2(sequences, old.italic, old.fraktur, new.italic, new.fraktur, "3", "20", "23") @@ -124,86 +160,53 @@ struct TermState( return "" return CSI ++ ";":join(sequences) ++ "m" -lang Colorful: - func Colorful(text:Text -> Colorful): - text = text:replace_all({$/@/="@(at)", $/(/="@(lparen)", $/)/="@(rparen)"}) - return Colorful.without_escaping(text) - - func add_ansi_sequences(text:Text, prev_state:TermState -> Text): - if text == "lparen": return "(" - else if text == "rparen": return ")" - else if text == "@" or text == "at": return "@" - parts := ( - text:matches($/{0+..}:{0+..}/) or - return "@("++Colorful.for_terminal(Colorful.without_escaping(text), prev_state)++")" - ) - attributes := parts[1]:split($/{0+space},{0+space}/) - new_state := prev_state - for attr in attributes: - if attr:starts_with("fg="): - new_state.fg = Color.from_text(attr:from(4))! - else if attr:starts_with("bg="): - new_state.bg = Color.from_text(attr:from(4))! - else if attr:starts_with("ul="): - new_state.underline_color = Color.from_text(attr:from(4))! - else if color := Color.from_text(attr): - new_state.fg = color - else if attr == "b" or attr == "bold": - new_state.bold = yes - else if attr == "d" or attr == "dim": - new_state.dim = yes - else if attr == "i" or attr == "italic": - new_state.italic = yes - else if attr == "u" or attr == "underline": - new_state.underline = yes - else if attr == "B" or attr == "blink": - new_state.blink = yes - else if attr == "r" or attr == "reverse": - new_state.reverse = yes - else if attr == "fraktur": - new_state.fraktur = yes - else if attr == "frame": - new_state.frame = yes - else if attr == "encircle": - new_state.encircle = yes - else if attr == "overline": - new_state.overline = yes - else if attr == "super" or attr == "superscript": - new_state.superscript = yes - else if attr == "sub" or attr == "subscript": - new_state.subscript = yes - else: - fail("Invalid attribute: '$attr'") - - result := prev_state:apply(new_state) - result ++= parts[2]:map($/@(?)/, func(m:Match): Colorful.add_ansi_sequences(m.captures[1], new_state)) - result ++= new_state:apply(prev_state) - return result - - func for_terminal(c:Colorful, state=none:TermState -> Text): - cur_state := state or TermState() - result := c.text:map($/@(?)/, func(m:Match): Colorful.add_ansi_sequences(m.captures[1], cur_state)) - if not state: result = CSI ++ "m" ++ result - return result - - func print(c:Colorful, newline=yes): - say(c:for_terminal(), newline=newline) - -func main(texts:[Text], files=[:Path], by_line=no): - for i,text in texts: - colorful := Colorful.without_escaping(text) - colorful:print(newline=no) - if i == texts.length: say("") - else: say(" ", newline=no) - - if texts.length == 0 and files.length == 0: - files = [(/dev/stdin)] - - for file in files: - if by_line: - for line in file:by_line() or exit("Could not read file: $(file.text)"): - colorful := Colorful.without_escaping(line) - colorful:print() +func _add_ansi_sequences(text:Text, prev_state:_TermState -> Text): + if text == "lparen": return "(" + else if text == "rparen": return ")" + else if text == "@" or text == "at": return "@" + parts := ( + text:matches($/{0+..}:{0+..}/) or + return "@("++_for_terminal(Colorful.without_escaping(text), prev_state)++")" + ) + attributes := parts[1]:split($/{0+space},{0+space}/) + new_state := prev_state + for attr in attributes: + if attr:starts_with("fg="): + new_state.fg = _Color.from_text(attr:from(4))! + else if attr:starts_with("bg="): + new_state.bg = _Color.from_text(attr:from(4))! + else if attr:starts_with("ul="): + new_state.underline_color = _Color.from_text(attr:from(4))! + else if color := _Color.from_text(attr): + new_state.fg = color + else if attr == "b" or attr == "bold": + new_state.bold = yes + else if attr == "d" or attr == "dim": + new_state.dim = yes + else if attr == "i" or attr == "italic": + new_state.italic = yes + else if attr == "u" or attr == "underline": + new_state.underline = yes + else if attr == "B" or attr == "blink": + new_state.blink = yes + else if attr == "r" or attr == "reverse": + new_state.reverse = yes + else if attr == "fraktur": + new_state.fraktur = yes + else if attr == "frame": + new_state.frame = yes + else if attr == "encircle": + new_state.encircle = yes + else if attr == "overline": + new_state.overline = yes + else if attr == "super" or attr == "superscript": + new_state.superscript = yes + else if attr == "sub" or attr == "subscript": + new_state.subscript = yes else: - colorful := Colorful.without_escaping(file:read() or exit("Could not read file: $(file.text)")) - colorful:print(newline=no) + fail("Invalid attribute: '$attr'") + + result := prev_state:apply(new_state) + result ++= parts[2]:map($/@(?)/, func(m:Match): _add_ansi_sequences(m.captures[1], new_state)) + result ++= new_state:apply(prev_state) + return result