chore: added syntax highlighting f0681509
Steve · 2026-02-18 22:26 3 file(s) · +486 −6
Cargo.toml +1 −0
23 23
ratatui = "0.30"
24 24
crossterm = "0.28"
25 25
arboard = "3"
26 +
syntect = "5"
src/ansi.tmTheme (added) +430 −0
1 +
<?xml version="1.0" encoding="UTF-8"?>
2 +
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 +
<plist version="1.0">
4 +
    <dict>
5 +
        <!--
6 +
        The colors in this theme are encoded as #RRGGBBAA where:
7 +
        * If AA is 00, then RR is an ANSI palette number from 00 to 07.
8 +
        * If AA is 01, the terminal's default fg/bg color is used.
9 +
        -->
10 +
        <key>author</key>
11 +
        <string>Template: Chris Kempson, Scheme: Mitchell Kember</string>
12 +
        <key>name</key>
13 +
        <string>ANSI</string>
14 +
        <key>colorSpaceName</key>
15 +
        <string>sRGB</string>
16 +
        <key>settings</key>
17 +
        <array>
18 +
            <dict>
19 +
                <key>settings</key>
20 +
                <dict>
21 +
                    <key>background</key>
22 +
                    <string>#00000001</string>
23 +
                    <key>foreground</key>
24 +
                    <string>#00000001</string>
25 +
                    <!--
26 +
                    Explicitly set the gutter color since bat falls back to a
27 +
                    hardcoded DEFAULT_GUTTER_COLOR otherwise.
28 +
                    -->
29 +
                    <key>gutter</key>
30 +
                    <string>#00000001</string>
31 +
                    <key>gutterForeground</key>
32 +
                    <string>#00000001</string>
33 +
                </dict>
34 +
            </dict>
35 +
            <dict>
36 +
                <key>name</key>
37 +
                <string>Comments</string>
38 +
                <key>scope</key>
39 +
                <string>comment, punctuation.definition.comment</string>
40 +
                <key>settings</key>
41 +
                <dict>
42 +
                    <key>foreground</key>
43 +
                    <string>#02000000</string>
44 +
                </dict>
45 +
            </dict>
46 +
            <dict>
47 +
                <key>name</key>
48 +
                <string>Keywords</string>
49 +
                <key>scope</key>
50 +
                <string>keyword</string>
51 +
                <key>settings</key>
52 +
                <dict>
53 +
                    <key>foreground</key>
54 +
                    <string>#05000000</string>
55 +
                </dict>
56 +
            </dict>
57 +
            <dict>
58 +
                <key>name</key>
59 +
                <string>Functions</string>
60 +
                <key>scope</key>
61 +
                <string>entity.name.function, meta.require, support.function.any-method</string>
62 +
                <key>settings</key>
63 +
                <dict>
64 +
                    <key>foreground</key>
65 +
                    <string>#04000000</string>
66 +
                </dict>
67 +
            </dict>
68 +
            <dict>
69 +
                <key>name</key>
70 +
                <string>Labels</string>
71 +
                <key>scope</key>
72 +
                <string>entity.name.label, variable.parameter</string>
73 +
                <key>settings</key>
74 +
                <dict>
75 +
                    <key>foreground</key>
76 +
                    <string>#06000000</string>
77 +
                </dict>
78 +
            </dict>
79 +
            <dict>
80 +
                <key>name</key>
81 +
                <string>Classes</string>
82 +
                <key>scope</key>
83 +
                <string>support.class, entity.name.class, entity.name.type.class, entity.name</string>
84 +
                <key>settings</key>
85 +
                <dict>
86 +
                    <key>foreground</key>
87 +
                    <string>#03000000</string>
88 +
                </dict>
89 +
            </dict>
90 +
            <dict>
91 +
                <key>name</key>
92 +
                <string>Methods</string>
93 +
                <key>scope</key>
94 +
                <string>keyword.other.special-method</string>
95 +
                <key>settings</key>
96 +
                <dict>
97 +
                    <key>foreground</key>
98 +
                    <string>#04000000</string>
99 +
                </dict>
100 +
            </dict>
101 +
            <dict>
102 +
                <key>name</key>
103 +
                <string>Storage</string>
104 +
                <key>scope</key>
105 +
                <string>storage</string>
106 +
                <key>settings</key>
107 +
                <dict>
108 +
                    <key>foreground</key>
109 +
                    <string>#05000000</string>
110 +
                </dict>
111 +
            </dict>
112 +
            <dict>
113 +
                <key>name</key>
114 +
                <string>Support</string>
115 +
                <key>scope</key>
116 +
                <string>support.function</string>
117 +
                <key>settings</key>
118 +
                <dict>
119 +
                    <key>foreground</key>
120 +
                    <string>#06000000</string>
121 +
                </dict>
122 +
            </dict>
123 +
            <dict>
124 +
                <key>name</key>
125 +
                <string>Strings, Inherited Class</string>
126 +
                <key>scope</key>
127 +
                <string>string, constant.other.symbol, entity.other.inherited-class</string>
128 +
                <key>settings</key>
129 +
                <dict>
130 +
                    <key>foreground</key>
131 +
                    <string>#02000000</string>
132 +
                </dict>
133 +
            </dict>
134 +
            <dict>
135 +
                <key>name</key>
136 +
                <string>Integers</string>
137 +
                <key>scope</key>
138 +
                <string>constant.numeric</string>
139 +
                <key>settings</key>
140 +
                <dict>
141 +
                    <key>foreground</key>
142 +
                    <string>#03000000</string>
143 +
                </dict>
144 +
            </dict>
145 +
            <dict>
146 +
                <key>name</key>
147 +
                <string>Floats</string>
148 +
                <key>scope</key>
149 +
                <string>none</string>
150 +
                <key>settings</key>
151 +
                <dict>
152 +
                    <key>foreground</key>
153 +
                    <string>#03000000</string>
154 +
                </dict>
155 +
            </dict>
156 +
            <dict>
157 +
                <key>name</key>
158 +
                <string>Boolean</string>
159 +
                <key>scope</key>
160 +
                <string>none</string>
161 +
                <key>settings</key>
162 +
                <dict>
163 +
                    <key>foreground</key>
164 +
                    <string>#03000000</string>
165 +
                </dict>
166 +
            </dict>
167 +
            <dict>
168 +
                <key>name</key>
169 +
                <string>Constants</string>
170 +
                <key>scope</key>
171 +
                <string>constant</string>
172 +
                <key>settings</key>
173 +
                <dict>
174 +
                    <key>foreground</key>
175 +
                    <string>#03000000</string>
176 +
                </dict>
177 +
            </dict>
178 +
            <dict>
179 +
                <key>name</key>
180 +
                <string>Tags</string>
181 +
                <key>scope</key>
182 +
                <string>entity.name.tag</string>
183 +
                <key>settings</key>
184 +
                <dict>
185 +
                    <key>foreground</key>
186 +
                    <string>#01000000</string>
187 +
                </dict>
188 +
            </dict>
189 +
            <dict>
190 +
                <key>name</key>
191 +
                <string>Attributes</string>
192 +
                <key>scope</key>
193 +
                <string>entity.other.attribute-name</string>
194 +
                <key>settings</key>
195 +
                <dict>
196 +
                    <key>foreground</key>
197 +
                    <string>#03000000</string>
198 +
                </dict>
199 +
            </dict>
200 +
            <dict>
201 +
                <key>name</key>
202 +
                <string>Attribute IDs</string>
203 +
                <key>scope</key>
204 +
                <string>entity.other.attribute-name.id, punctuation.definition.entity</string>
205 +
                <key>settings</key>
206 +
                <dict>
207 +
                    <key>foreground</key>
208 +
                    <string>#04000000</string>
209 +
                </dict>
210 +
            </dict>
211 +
            <dict>
212 +
                <key>name</key>
213 +
                <string>Selector</string>
214 +
                <key>scope</key>
215 +
                <string>meta.selector</string>
216 +
                <key>settings</key>
217 +
                <dict>
218 +
                    <key>foreground</key>
219 +
                    <string>#05000000</string>
220 +
                </dict>
221 +
            </dict>
222 +
            <dict>
223 +
                <key>name</key>
224 +
                <string>Values</string>
225 +
                <key>scope</key>
226 +
                <string>none</string>
227 +
                <key>settings</key>
228 +
                <dict>
229 +
                    <key>foreground</key>
230 +
                    <string>#03000000</string>
231 +
                </dict>
232 +
            </dict>
233 +
            <dict>
234 +
                <key>name</key>
235 +
                <string>Headings</string>
236 +
                <key>scope</key>
237 +
                <string>markup.heading punctuation.definition.heading, entity.name.section, markup.heading - text.html.markdown, meta.mapping.key string.quoted.double</string>
238 +
                <key>settings</key>
239 +
                <dict>
240 +
                    <key>fontStyle</key>
241 +
                    <string></string>
242 +
                    <key>foreground</key>
243 +
                    <string>#04000000</string>
244 +
                </dict>
245 +
            </dict>
246 +
            <dict>
247 +
                <key>name</key>
248 +
                <string>Units</string>
249 +
                <key>scope</key>
250 +
                <string>keyword.other.unit</string>
251 +
                <key>settings</key>
252 +
                <dict>
253 +
                    <key>foreground</key>
254 +
                    <string>#03000000</string>
255 +
                </dict>
256 +
            </dict>
257 +
            <dict>
258 +
                <key>name</key>
259 +
                <string>Bold</string>
260 +
                <key>scope</key>
261 +
                <string>markup.bold, punctuation.definition.bold</string>
262 +
                <key>settings</key>
263 +
                <dict>
264 +
                    <key>fontStyle</key>
265 +
                    <string>bold</string>
266 +
                    <key>foreground</key>
267 +
                    <string>#03000000</string>
268 +
                </dict>
269 +
            </dict>
270 +
            <dict>
271 +
                <key>name</key>
272 +
                <string>Italic</string>
273 +
                <key>scope</key>
274 +
                <string>markup.italic, punctuation.definition.italic</string>
275 +
                <key>settings</key>
276 +
                <dict>
277 +
                    <key>fontStyle</key>
278 +
                    <string>italic</string>
279 +
                    <key>foreground</key>
280 +
                    <string>#05000000</string>
281 +
                </dict>
282 +
            </dict>
283 +
            <dict>
284 +
                <key>name</key>
285 +
                <string>Code</string>
286 +
                <key>scope</key>
287 +
                <string>markup.raw.inline</string>
288 +
                <key>settings</key>
289 +
                <dict>
290 +
                    <key>foreground</key>
291 +
                    <string>#02000000</string>
292 +
                </dict>
293 +
            </dict>
294 +
            <dict>
295 +
                <key>name</key>
296 +
                <string>Link Text</string>
297 +
                <key>scope</key>
298 +
                <string>string.other.link, punctuation.definition.string.end.markdown, punctuation.definition.string.begin.markdown</string>
299 +
                <key>settings</key>
300 +
                <dict>
301 +
                    <key>foreground</key>
302 +
                    <string>#01000000</string>
303 +
                </dict>
304 +
            </dict>
305 +
            <dict>
306 +
                <key>name</key>
307 +
                <string>Link Url</string>
308 +
                <key>scope</key>
309 +
                <string>meta.link</string>
310 +
                <key>settings</key>
311 +
                <dict>
312 +
                    <key>foreground</key>
313 +
                    <string>#03000000</string>
314 +
                </dict>
315 +
            </dict>
316 +
            <dict>
317 +
                <key>name</key>
318 +
                <string>Quotes</string>
319 +
                <key>scope</key>
320 +
                <string>markup.quote</string>
321 +
                <key>settings</key>
322 +
                <dict>
323 +
                    <key>foreground</key>
324 +
                    <string>#03000000</string>
325 +
                </dict>
326 +
            </dict>
327 +
            <dict>
328 +
                <key>name</key>
329 +
                <string>Inserted</string>
330 +
                <key>scope</key>
331 +
                <string>markup.inserted</string>
332 +
                <key>settings</key>
333 +
                <dict>
334 +
                    <key>foreground</key>
335 +
                    <string>#02000000</string>
336 +
                </dict>
337 +
            </dict>
338 +
            <dict>
339 +
                <key>name</key>
340 +
                <string>Deleted</string>
341 +
                <key>scope</key>
342 +
                <string>markup.deleted</string>
343 +
                <key>settings</key>
344 +
                <dict>
345 +
                    <key>foreground</key>
346 +
                    <string>#01000000</string>
347 +
                </dict>
348 +
            </dict>
349 +
            <dict>
350 +
                <key>name</key>
351 +
                <string>Changed</string>
352 +
                <key>scope</key>
353 +
                <string>markup.changed</string>
354 +
                <key>settings</key>
355 +
                <dict>
356 +
                    <key>foreground</key>
357 +
                    <string>#05000000</string>
358 +
                </dict>
359 +
            </dict>
360 +
            <dict>
361 +
                <key>name</key>
362 +
                <string>Colors</string>
363 +
                <key>scope</key>
364 +
                <string>constant.other.color</string>
365 +
                <key>settings</key>
366 +
                <dict>
367 +
                    <key>foreground</key>
368 +
                    <string>#06000000</string>
369 +
                </dict>
370 +
            </dict>
371 +
            <dict>
372 +
                <key>name</key>
373 +
                <string>Regular Expressions</string>
374 +
                <key>scope</key>
375 +
                <string>string.regexp</string>
376 +
                <key>settings</key>
377 +
                <dict>
378 +
                    <key>foreground</key>
379 +
                    <string>#06000000</string>
380 +
                </dict>
381 +
            </dict>
382 +
            <dict>
383 +
                <key>name</key>
384 +
                <string>Escape Characters</string>
385 +
                <key>scope</key>
386 +
                <string>constant.character.escape</string>
387 +
                <key>settings</key>
388 +
                <dict>
389 +
                    <key>foreground</key>
390 +
                    <string>#06000000</string>
391 +
                </dict>
392 +
            </dict>
393 +
            <dict>
394 +
                <key>name</key>
395 +
                <string>Embedded</string>
396 +
                <key>scope</key>
397 +
                <string>punctuation.section.embedded, variable.interpolation</string>
398 +
                <key>settings</key>
399 +
                <dict>
400 +
                    <key>foreground</key>
401 +
                    <string>#05000000</string>
402 +
                </dict>
403 +
            </dict>
404 +
            <dict>
405 +
                <key>name</key>
406 +
                <string>Illegal</string>
407 +
                <key>scope</key>
408 +
                <string>invalid.illegal</string>
409 +
                <key>settings</key>
410 +
                <dict>
411 +
                    <key>background</key>
412 +
                    <string>#01000000</string>
413 +
                </dict>
414 +
            </dict>
415 +
            <dict>
416 +
                <key>name</key>
417 +
                <string>Broken</string>
418 +
                <key>scope</key>
419 +
                <string>invalid.broken</string>
420 +
                <key>settings</key>
421 +
                <dict>
422 +
                    <key>background</key>
423 +
                    <string>#03000000</string>
424 +
                </dict>
425 +
            </dict>
426 +
        </array>
427 +
        <key>uuid</key>
428 +
        <string>uuid</string>
429 +
    </dict>
430 +
</plist>
src/bin/tui.rs +55 −6
4 4
    DefaultTerminal,
5 5
    layout::{Constraint, Layout},
6 6
    style::{Color, Modifier, Style},
7 -
    text::Text,
7 +
    text::{Line, Span, Text},
8 8
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
9 9
};
10 10
use sipp_rust::db::{self, Snippet};
11 11
use std::time::{Duration, Instant};
12 +
use syntect::easy::HighlightLines;
13 +
use syntect::highlighting::Theme;
14 +
use syntect::parsing::SyntaxSet;
15 +
use syntect::util::LinesWithEndings;
16 +
use std::io::Cursor;
12 17
13 18
enum Focus {
14 19
    List,
22 27
    status_message: Option<(String, Instant)>,
23 28
    focus: Focus,
24 29
    content_scroll: u16,
30 +
    syntax_set: SyntaxSet,
31 +
    theme: Theme,
25 32
}
26 33
27 34
impl App {
30 37
        if !snippets.is_empty() {
31 38
            list_state.select(Some(0));
32 39
        }
40 +
        let syntax_set = SyntaxSet::load_defaults_newlines();
41 +
        let theme_data = include_bytes!("../ansi.tmTheme");
42 +
        let theme =
43 +
            syntect::highlighting::ThemeSet::load_from_reader(&mut Cursor::new(&theme_data[..]))
44 +
                .expect("failed to load base16 theme");
33 45
        Self {
34 46
            snippets,
35 47
            list_state,
37 49
            status_message: None,
38 50
            focus: Focus::List,
39 51
            content_scroll: 0,
52 +
            syntax_set,
53 +
            theme,
40 54
        }
41 55
    }
42 56
96 110
            }
97 111
        }
98 112
    }
113 +
114 +
    fn highlight_content(&self, name: &str, content: &str) -> Text<'static> {
115 +
        let ext = name.rsplit('.').next().unwrap_or("");
116 +
        let syntax = self
117 +
            .syntax_set
118 +
            .find_syntax_by_extension(ext)
119 +
            .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
120 +
        let mut highlighter = HighlightLines::new(syntax, &self.theme);
121 +
122 +
        let lines: Vec<Line<'static>> = LinesWithEndings::from(content)
123 +
            .map(|line| {
124 +
                let ranges = highlighter
125 +
                    .highlight_line(line, &self.syntax_set)
126 +
                    .unwrap_or_default();
127 +
                let spans: Vec<Span<'static>> = ranges
128 +
                    .into_iter()
129 +
                    .map(|(style, text)| {
130 +
                        let color = to_ratatui_color(style.foreground);
131 +
                        Span::styled(text.to_owned(), Style::default().fg(color))
132 +
                    })
133 +
                    .collect();
134 +
                Line::from(spans)
135 +
            })
136 +
            .collect();
137 +
138 +
        Text::from(lines)
139 +
    }
140 +
}
141 +
142 +
fn to_ratatui_color(color: syntect::highlighting::Color) -> Color {
143 +
    if color.a == 0 {
144 +
        Color::Indexed(color.r)
145 +
    } else {
146 +
        Color::Reset
147 +
    }
99 148
}
100 149
101 150
fn main() -> Result<(), Box<dyn std::error::Error>> {
161 210
162 211
            frame.render_stateful_widget(list, chunks[0], &mut app.list_state);
163 212
164 -
            let content = app
165 -
                .selected_snippet()
166 -
                .map(|s| s.content.as_str())
167 -
                .unwrap_or("");
213 +
            let highlighted = match app.selected_snippet() {
214 +
                Some(s) => app.highlight_content(&s.name, &s.content),
215 +
                None => Text::raw(""),
216 +
            };
168 217
169 -
            let paragraph = Paragraph::new(Text::raw(content))
218 +
            let paragraph = Paragraph::new(highlighted)
170 219
                .block(
171 220
                    Block::default()
172 221
                        .title(" Content ")