Fix/update colorful

This commit is contained in:
Bruce Hill 2025-03-10 13:33:53 -04:00
parent 7c68fc9585
commit 7f75a34bce

View File

@ -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