Skip to main content

alf/tui/
syntax.rs

1//! Syntax highlighting for shell scripts using syntect.
2
3#[cfg(feature = "syntax-highlighting")]
4use syntect::easy::HighlightLines;
5#[cfg(feature = "syntax-highlighting")]
6use syntect::highlighting::ThemeSet;
7#[cfg(feature = "syntax-highlighting")]
8use syntect::parsing::SyntaxSet;
9#[cfg(feature = "syntax-highlighting")]
10use syntect::util::LinesWithEndings;
11
12use ratatui::style::{Color, Style};
13use ratatui::text::{Line, Span, Text};
14
15/// Convert syntect color to ratatui color
16#[cfg(feature = "syntax-highlighting")]
17fn syntect_color_to_ratatui(color: syntect::highlighting::Color) -> Color {
18   Color::Rgb(color.r, color.g, color.b)
19}
20
21/// Highlight shell script code with syntax highlighting
22#[cfg(feature = "syntax-highlighting")]
23pub fn highlight_shell_script(code: &str) -> Text<'static> {
24   let ps = SyntaxSet::load_defaults_newlines();
25   let ts = ThemeSet::load_defaults();
26
27   // Use a dark theme that works well with our color scheme
28   let theme = &ts.themes["base16-ocean.dark"];
29
30   // Try to find the best syntax for shell scripts
31   let syntax = ps
32      .find_syntax_by_extension("sh")
33      .or_else(|| ps.find_syntax_by_extension("bash"))
34      .or_else(|| ps.find_syntax_by_extension("zsh"))
35      .unwrap_or_else(|| ps.find_syntax_plain_text());
36
37   let mut highlighter = HighlightLines::new(syntax, theme);
38   let mut lines = Vec::new();
39
40   for line in LinesWithEndings::from(code) {
41      let ranges = highlighter.highlight_line(line, &ps).unwrap_or_default();
42
43      let mut spans = Vec::new();
44      for (style, text) in ranges {
45         let fg = syntect_color_to_ratatui(style.foreground);
46         let ratatui_style = Style::default().fg(fg);
47         spans.push(Span::styled(text.to_string(), ratatui_style));
48      }
49
50      lines.push(Line::from(spans));
51   }
52
53   Text::from(lines)
54}
55
56/// Highlight shell script code (fallback when feature is disabled)
57#[cfg(not(feature = "syntax-highlighting"))]
58pub fn highlight_shell_script(code: &str) -> Text<'static> {
59   Text::from(code.to_string())
60}
61
62/// Highlight shell script code with line numbers and optional dimming for inactive panels
63pub fn highlight_shell_script_with_style(
64   code: &str,
65   dim: bool,
66) -> Text<'static> {
67   let text = highlight_shell_script(code);
68
69   // Add line numbers to each line
70   let mut numbered_lines = Vec::new();
71   let line_count = text.lines.len();
72   let line_num_width = line_count.to_string().len().max(2); // At least 2 digits width
73
74   for (idx, line) in text.lines.into_iter().enumerate() {
75      let line_num = idx + 1;
76      let line_num_str = format!("{:>width$} ", line_num, width = line_num_width);
77
78      // Create line number span with dimmed style
79      let line_num_span = Span::styled(
80         line_num_str,
81         Style::default()
82            .fg(Color::Rgb(100, 100, 100)) // Dark gray color for line numbers
83            .add_modifier(if dim { ratatui::style::Modifier::DIM } else { ratatui::style::Modifier::empty() }),
84      );
85
86      // Combine line number with the existing line spans
87      let mut new_spans = vec![line_num_span];
88      new_spans.extend(line.spans.into_iter().map(|span| {
89         if dim {
90            Span::styled(span.content, span.style.add_modifier(ratatui::style::Modifier::DIM))
91         } else {
92            span
93         }
94      }));
95
96      numbered_lines.push(Line::from(new_spans));
97   }
98
99   Text::from(numbered_lines)
100}