code / tomo-colorful

Lines259 Tomo191 Markdown65 INI3
(220 lines)
1 # Colorful language
3 HELP := "
4 colorful: A domain-specific language for writing colored text to the terminal
5 Usage: colorful [args...] [--by-line] [--files files...]
6 "
8 CSI := "\033["
10 use patterns
12 lang Colorful
13 convert(text:Text -> Colorful)
14 text = text.translate({"@": "@(at)", "(": "@(lparen)", ")": "@(rparen)"})
15 return Colorful.from_text(text)
17 convert(i:Int -> Colorful) return Colorful.from_text("$i")
18 convert(n:Num -> Colorful) return Colorful.from_text("$n")
20 func for_terminal(c:Colorful -> Text)
21 return CSI ++ "m" ++ _for_terminal(c, _TermState())
23 func print(c:Colorful, newline=yes)
24 say(c.for_terminal(), newline=newline)
27 func main(texts:[Text]=[], files:[Path]=[], by_line=no)
28 for i,text in texts
29 colorful := Colorful.from_text(text)
30 colorful.print(newline=no)
31 if i == texts.length then say("")
32 else say(" ", newline=no)
34 if texts.length == 0 and files.length == 0
35 files = [(/dev/stdin)]
37 for file in files
38 if by_line
39 for line in file.by_line() or exit("Could not read file: $file")
40 colorful := Colorful.from_text(line)
41 colorful.print()
42 else
43 colorful := Colorful.from_text(file.read() or exit("Could not read file: $file"))
44 colorful.print(newline=no)
47 func _for_terminal(c:Colorful, state:_TermState -> Text)
48 return $Pat'@(?)'.map(c.text, recursive=no, func(m:PatternMatch) _add_ansi_sequences(m.captures[1]!, state))
50 enum _Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(color:Int32))
51 func from_text(text:Text -> _Color?)
52 if $Pat'#{3-6 hex}'.matches(text)
53 hex := text.from(2)
54 return none unless hex.length == 3 or hex.length == 6
55 if hex.length == 3
56 hex = hex[1]!++hex[1]!++hex[2]!++hex[2]!++hex[3]!++hex[3]!
57 n := Int32.parse("0x" ++ hex) or return none
58 return Color24Bit(n)
59 else if $Pat'{1-3 digit}'.matches(text)
60 n := Int16.parse(text) or return none
61 if n >= 0 and n <= 255 return Color8Bit(n)
62 else if text == "black" return _Color.Color8Bit(0)
63 else if text == "red" return _Color.Color8Bit(1)
64 else if text == "green" return _Color.Color8Bit(2)
65 else if text == "yellow" return _Color.Color8Bit(3)
66 else if text == "blue" return _Color.Color8Bit(4)
67 else if text == "magenta" return _Color.Color8Bit(5)
68 else if text == "cyan" return _Color.Color8Bit(6)
69 else if text == "white" return _Color.Color8Bit(7)
70 else if text == "default" return _Color.Default
71 else if text == "BLACK" return _Color.Bright(0)
72 else if text == "RED" return _Color.Bright(1)
73 else if text == "GREEN" return _Color.Bright(2)
74 else if text == "YELLOW" return _Color.Bright(3)
75 else if text == "BLUE" return _Color.Bright(4)
76 else if text == "MAGENTA" return _Color.Bright(5)
77 else if text == "CYAN" return _Color.Bright(6)
78 else if text == "WHITE" return _Color.Bright(7)
79 return none
81 func fg(c:_Color -> Text)
82 when c is Color8Bit(color)
83 if color >= 0 and color <= 7 return "$(30+color)"
84 else if color >= 0 and color <= 255 return "38;5;$color"
85 is Color24Bit(hex)
86 if hex >= 0 and hex <= 0xFFFFFF
87 return "38;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)"
88 is Bright(color)
89 if color <= 7 return "$(90+color)"
90 is Default
91 return "39"
92 fail("Invalid foreground color: '$c'")
94 func bg(c:_Color -> Text)
95 when c is Color8Bit(color)
96 if color >= 0 and color <= 7 return "$(40+color)"
97 else if color >= 0 and color <= 255 return "48;5;$color"
98 is Color24Bit(hex)
99 if hex >= 0 and hex <= 0xFFFFFF
100 return "48;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)"
101 is Bright(color)
102 if color <= 7 return "$(90+color)"
103 is Default
104 return "49"
105 fail("Invalid background color: '$c'")
107 func underline(c:_Color -> Text)
108 when c is Color8Bit(color)
109 if color >= 0 and color <= 255 return "58;5;$color"
110 is Color24Bit(hex)
111 if hex >= 0 and hex <= 0xFFFFFF
112 return "58;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)"
113 is Default
114 return "59"
115 is Bright(color)
116 pass
117 fail("Invalid underline color: '$c'")
119 func _toggle(sequences:&[Text], cur,new:Bool, apply,unapply:Text; inline)
120 if new and not cur
121 sequences.insert(apply)
122 else if cur and not new
123 sequences.insert(unapply)
125 func _toggle2(sequences:&[Text], cur1,cur2,new1,new2:Bool, apply1,apply2,unapply:Text; inline)
126 return if new1 == cur1 and new2 == cur2
127 if (cur1 and not new1) or (cur2 and not new2) # Gotta wipe at least one
128 sequences.insert(unapply)
129 cur1, cur2 = no, no # Wiped out
131 if new1 and not cur1
132 sequences.insert(apply1)
133 if new2 and not cur2
134 sequences.insert(apply2)
136 struct _TermState(
137 bold=no, dim=no, italic=no, underline=no, blink=no,
138 reverse=no, conceal=no, strikethrough=no, fraktur=no, frame=no,
139 encircle=no, overline=no, superscript=no, subscript=no,
140 bg=_Color.Default, fg=_Color.Default, underline_color=_Color.Default,
143 func apply(old,new:_TermState -> Text)
144 sequences : &[Text]
145 _toggle2(sequences, old.bold, old.dim, new.bold, new.dim, "1", "2", "22")
146 _toggle2(sequences, old.italic, old.fraktur, new.italic, new.fraktur, "3", "20", "23")
147 _toggle(sequences, old.underline, new.underline, "4", "24")
148 _toggle(sequences, old.blink, new.blink, "5", "25")
149 _toggle(sequences, old.reverse, new.reverse, "7", "27")
150 _toggle(sequences, old.conceal, new.conceal, "8", "28")
151 _toggle(sequences, old.strikethrough, new.strikethrough, "9", "29")
152 _toggle2(sequences, old.frame, old.encircle, new.frame, new.frame, "51", "52", "54")
153 _toggle(sequences, old.overline, new.overline, "53", "55")
154 _toggle2(sequences, old.subscript, old.subscript, new.superscript, new.superscript, "73", "74", "75")
156 if new.bg != old.bg
157 sequences.insert(new.bg.bg())
159 if new.fg != old.fg
160 sequences.insert(new.fg.fg())
162 if new.underline_color != old.underline_color
163 sequences.insert(new.underline_color.underline())
165 if sequences.length == 0
166 return ""
167 return CSI ++ ";".join(sequences) ++ "m"
169 func _add_ansi_sequences(text:Text, prev_state:_TermState -> Text)
170 if text == "lparen" return "("
171 else if text == "rparen" return ")"
172 else if text == "@" or text == "at" return "@"
173 parts := (
174 $Pat'{0+..}:{0+..}'.capture(text) or
175 return "@("++_for_terminal(Colorful.from_text(text), prev_state)++")"
177 attributes := $Pat'{0+space},{0+space}'.split(parts[1]!)
178 new_state := prev_state
179 for attr in attributes
180 if attr.starts_with("fg=")
181 new_state.fg = _Color.from_text(attr.from(4))!
182 else if attr.starts_with("bg=")
183 new_state.bg = _Color.from_text(attr.from(4))!
184 else if attr.starts_with("ul=")
185 new_state.underline_color = _Color.from_text(attr.from(4))!
186 else if color := _Color.from_text(attr)
187 new_state.fg = color
188 else if attr == "b" or attr == "bold"
189 new_state.bold = yes
190 else if attr == "d" or attr == "dim"
191 new_state.dim = yes
192 else if attr == "i" or attr == "italic"
193 new_state.italic = yes
194 else if attr == "u" or attr == "underline"
195 new_state.underline = yes
196 else if attr == "s" or attr == "strikethrough"
197 new_state.strikethrough = yes
198 else if attr == "B" or attr == "blink"
199 new_state.blink = yes
200 else if attr == "r" or attr == "reverse"
201 new_state.reverse = yes
202 else if attr == "fraktur"
203 new_state.fraktur = yes
204 else if attr == "frame"
205 new_state.frame = yes
206 else if attr == "encircle"
207 new_state.encircle = yes
208 else if attr == "overline"
209 new_state.overline = yes
210 else if attr == "super" or attr == "superscript"
211 new_state.superscript = yes
212 else if attr == "sub" or attr == "subscript"
213 new_state.subscript = yes
214 else
215 fail("Invalid attribute: '$attr'")
217 result := prev_state.apply(new_state)
218 result ++= $Pat'@(?)'.map(parts[2]!, recursive=no, func(m:PatternMatch) _add_ansi_sequences(m.captures[1]!, new_state))
219 result ++= new_state.apply(prev_state)
220 return result