1use ratatui::style::{Color, Modifier, Style};
5use ratatui::text::{Line, Span};
6
7const KEY: Color = Color::Cyan;
8const STRING: Color = Color::Green;
9const NUMBER: Color = Color::Yellow;
10const LITERAL: Color = Color::Magenta;
11const PUNCT: Color = Color::DarkGray;
12const KEYWORD: Color = Color::Magenta;
13const TYPE: Color = Color::Cyan;
14const MACRO: Color = Color::LightBlue;
15const COMMENT: Color = Color::DarkGray;
16
17fn span(text: impl Into<String>, color: Color) -> Span<'static> {
18 Span::styled(text.into(), Style::new().fg(color))
19}
20
21pub fn json(text: &str) -> Vec<Line<'static>> {
23 text.lines()
24 .map(|line| {
25 let mut spans = vec![Span::raw(" ")];
26 spans.extend(json_spans(line));
27 Line::from(spans)
28 })
29 .collect()
30}
31
32pub fn json_spans(line: &str) -> Vec<Span<'static>> {
34 let chars: Vec<char> = line.chars().collect();
35 let mut spans = Vec::new();
36 let mut i = 0;
37 while i < chars.len() {
38 let c = chars[i];
39 match c {
40 '"' => {
41 let (text, next) = read_string(&chars, i);
42 let is_key = chars[next..]
43 .iter()
44 .find(|c| !c.is_whitespace())
45 .is_some_and(|c| *c == ':');
46 spans.push(span(text, if is_key { KEY } else { STRING }));
47 i = next;
48 }
49 '-' | '0'..='9' => {
50 let start = i;
51 while i < chars.len() && (chars[i].is_ascii_digit() || "-+.eE".contains(chars[i])) {
52 i += 1;
53 }
54 spans.push(span(collect(&chars, start, i), NUMBER));
55 }
56 c if c.is_alphabetic() => {
57 let start = i;
58 while i < chars.len() && chars[i].is_alphabetic() {
59 i += 1;
60 }
61 let word = collect(&chars, start, i);
62 let color = match word.as_str() {
63 "true" | "false" | "null" => LITERAL,
64 _ => Color::Reset,
65 };
66 spans.push(span(word, color));
67 }
68 '{' | '}' | '[' | ']' | ':' | ',' => {
69 spans.push(span(c.to_string(), PUNCT));
70 i += 1;
71 }
72 _ => {
73 let start = i;
74 while i < chars.len() && chars[i].is_whitespace() {
75 i += 1;
76 }
77 if i == start {
78 i += 1;
79 }
80 spans.push(Span::raw(collect(&chars, start, i)));
81 }
82 }
83 }
84 spans
85}
86
87pub fn rust(text: &str) -> Vec<Line<'static>> {
89 text.lines()
90 .map(|line| {
91 let mut spans = vec![Span::raw(" ")];
92 spans.extend(rust_spans(line));
93 Line::from(spans)
94 })
95 .collect()
96}
97
98fn rust_spans(line: &str) -> Vec<Span<'static>> {
99 const KEYWORDS: &[&str] = &[
100 "async", "await", "else", "fn", "for", "if", "impl", "in", "let", "match", "mut", "pub",
101 "return", "struct", "use", "while", "true", "false",
102 ];
103 let chars: Vec<char> = line.chars().collect();
104 let mut spans = Vec::new();
105 let mut i = 0;
106 while i < chars.len() {
107 let c = chars[i];
108 if c == '/' && chars.get(i + 1) == Some(&'/') {
109 spans.push(span(collect(&chars, i, chars.len()), COMMENT));
110 break;
111 }
112 match c {
113 '"' => {
114 let (text, next) = read_string(&chars, i);
115 spans.push(span(text, STRING));
116 i = next;
117 }
118 '0'..='9' => {
119 let start = i;
120 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '.') {
121 i += 1;
122 }
123 spans.push(span(collect(&chars, start, i), NUMBER));
124 }
125 c if c.is_alphabetic() || c == '_' => {
126 let start = i;
127 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
128 i += 1;
129 }
130 let word = collect(&chars, start, i);
131 let macro_call = chars.get(i) == Some(&'!');
132 let color = if macro_call {
133 i += 1;
134 MACRO
135 } else if KEYWORDS.contains(&word.as_str()) {
136 KEYWORD
137 } else if word.starts_with(char::is_uppercase) {
138 TYPE
139 } else {
140 Color::Reset
141 };
142 spans.push(span(
143 if macro_call { format!("{word}!") } else { word },
144 color,
145 ));
146 }
147 '(' | ')' | '{' | '}' | '[' | ']' | ';' | ',' | '.' | ':' | '?' | '&' | '<' | '>' => {
148 spans.push(span(c.to_string(), PUNCT));
149 i += 1;
150 }
151 _ => {
152 spans.push(Span::raw(c.to_string()));
153 i += 1;
154 }
155 }
156 }
157 spans
158}
159
160pub fn command(input: &str, known: impl Fn(&str) -> bool) -> Vec<Span<'static>> {
163 if input.is_empty() {
164 return Vec::new();
165 }
166 if !input.starts_with(':') {
167 return vec![Span::raw(input.to_owned())];
169 }
170 let (cmd, rest) = match input.split_once(char::is_whitespace) {
171 Some((c, r)) => (c, Some(r)),
172 None => (input, None),
173 };
174 let cmd_style = if known(cmd) {
175 Style::new()
176 .fg(Color::LightBlue)
177 .add_modifier(Modifier::BOLD)
178 } else {
179 Style::new().fg(Color::Red)
180 };
181 let mut spans = vec![Span::styled(cmd.to_owned(), cmd_style)];
182 let Some(rest) = rest else { return spans };
183 spans.push(Span::raw(" "));
184
185 let takes_name = matches!(
186 cmd,
187 ":noul" | ":choice" | ":score" | ":raw" | ":rm" | ":drop"
188 );
189 let mut body = rest;
190 if takes_name {
191 if let Some((name, tail)) = rest.split_once(char::is_whitespace) {
192 spans.push(Span::styled(
193 name.to_owned(),
194 Style::new().add_modifier(Modifier::BOLD),
195 ));
196 spans.push(Span::raw(" "));
197 body = tail;
198 } else {
199 spans.push(Span::styled(
200 rest.to_owned(),
201 Style::new().add_modifier(Modifier::BOLD),
202 ));
203 return spans;
204 }
205 }
206
207 for (i, part) in body.split('|').enumerate() {
208 if i > 0 {
209 spans.push(span("|", PUNCT));
210 }
211 if part.trim_start().starts_with('{') || part.trim_start().starts_with('[') {
212 spans.extend(json_spans(part));
213 continue;
214 }
215 match part.split_once('=') {
216 Some((label, desc)) if i > 0 => {
217 spans.push(span(label.to_owned(), LITERAL));
218 spans.push(span("=", PUNCT));
219 spans.push(Span::raw(desc.to_owned()));
220 }
221 _ => match part.split_once(':') {
222 Some((tag, desc)) if matches!(tag.trim(), "yes" | "no" | "true" | "false") => {
223 spans.push(span(tag.to_owned(), KEY));
224 spans.push(span(":", PUNCT));
225 spans.push(Span::raw(desc.to_owned()));
226 }
227 _ => spans.push(Span::raw(part.to_owned())),
228 },
229 }
230 }
231 spans
232}
233
234fn read_string(chars: &[char], start: usize) -> (String, usize) {
235 let mut i = start + 1;
236 while i < chars.len() {
237 match chars[i] {
238 '\\' => i += 2,
239 '"' => {
240 i += 1;
241 break;
242 }
243 _ => i += 1,
244 }
245 }
246 let end = i.min(chars.len());
247 (collect(chars, start, end), end)
248}
249
250fn collect(chars: &[char], start: usize, end: usize) -> String {
251 chars[start..end.min(chars.len())].iter().collect()
252}