use once_cell::sync::Lazy;
use silicon::assets::HighlightingAssets;
use silicon::formatter::ImageFormatterBuilder;
use silicon::utils::{Background, ShadowAdder};
use syntect::easy::HighlightLines;
use syntect::util::LinesWithEndings;
use std::fs;
static ASSETS: Lazy<HighlightingAssets> = Lazy::new(HighlightingAssets::new);
const BG: image::Rgba<u8> = image::Rgba([0x28, 0x2a, 0x36, 0xff]);
const DEFAULT_FONT_SIZE: f32 = 20.0;
const PAD: u32 = 10;
const LINE_PAD: u32 = 2;
const CODE_PAD: u32 = 25;
const LINE_NUMBER_PAD: u32 = 6;
const TAB_WIDTH: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineGeometry {
pub first_line_y: u32,
pub line_height: u32,
}
impl LineGeometry {
pub fn line_center_y(&self, line_index: usize) -> u32 {
self.first_line_y + line_index as u32 * self.line_height + self.line_height / 2
}
pub fn line_center_fraction(&self, line_index: usize, image_height_px: u32) -> f64 {
if image_height_px == 0 {
return 0.5;
}
let y = self.line_center_y(line_index) as f64 / image_height_px as f64;
y.clamp(0.0, 1.0)
}
}
pub fn line_end_x(
font_size: Option<usize>,
show_line_number: bool,
total_lines: usize,
line_offset: usize,
line_text: &str,
) -> u32 {
let size = font_size.map(|s| s as f32).unwrap_or(DEFAULT_FONT_SIZE);
let font = silicon::font::FontCollection::new(&[("Hack", size)])
.expect("Hack font not available for silicon");
let left_pad = CODE_PAD
+ if show_line_number {
let line_number_chars =
(((total_lines + line_offset) as f32).log10() + 1.0).floor() as usize;
let widest = format!("{:>width$}", 0, width = line_number_chars);
2 * LINE_NUMBER_PAD + font.get_text_len(&widest)
} else {
0
};
let expanded = line_text
.trim_end_matches('\n')
.replace('\t', &" ".repeat(TAB_WIDTH));
PAD + left_pad + font.get_text_len(&expanded)
}
pub fn line_geometry(font_size: Option<usize>) -> LineGeometry {
let size = font_size.map(|s| s as f32).unwrap_or(DEFAULT_FONT_SIZE);
let font = silicon::font::FontCollection::new(&[("Hack", size)])
.expect("Hack font not available for silicon");
LineGeometry {
first_line_y: PAD + CODE_PAD,
line_height: font.get_font_height() + LINE_PAD,
}
}
pub fn create_figure(
content: &str,
dest_folder_path: &str,
file_name: &str,
offset: usize,
font_size: Option<usize>,
show_line_number: bool,
) -> String {
let dest_png_path = format!("{dest_folder_path}/{file_name}.png");
let size = font_size.map(|s| s as f32).unwrap_or(DEFAULT_FONT_SIZE);
let ps = &ASSETS.syntax_set;
let theme = &ASSETS.theme_set.themes["Dracula"];
let ext = file_name.rsplit('.').next().unwrap_or("rs");
let syntax = match ext {
"sol" => ps
.find_syntax_by_extension("js")
.or_else(|| ps.find_syntax_by_extension("rs"))
.expect("Syntax not found in syntect"),
other => ps
.find_syntax_by_extension(other)
.or_else(|| ps.find_syntax_by_extension("rs"))
.expect("Syntax not found in syntect"),
};
let mut highlighter = HighlightLines::new(syntax, theme);
let highlight: Vec<Vec<(syntect::highlighting::Style, &str)>> = LinesWithEndings::from(content)
.map(|line| highlighter.highlight_line(line, &ps).unwrap())
.collect();
let shadow = ShadowAdder::default()
.background(Background::Solid(BG))
.shadow_color(image::Rgba([0, 0, 0, 0]))
.blur_radius(0.0)
.pad_horiz(PAD)
.pad_vert(PAD)
.offset_x(0)
.offset_y(0);
let mut formatter = ImageFormatterBuilder::new()
.font(vec![("Hack".to_string(), size)])
.line_number(show_line_number)
.line_offset(offset as u32)
.tab_width(4)
.window_controls(false)
.round_corner(false)
.shadow_adder(shadow)
.build()
.expect("Failed to build silicon ImageFormatter");
let image = formatter.format(&highlight, theme);
image
.save(&dest_png_path)
.expect("Failed to save screenshot PNG");
dest_png_path
}
pub fn delete_png_file(path: String) {
fs::remove_file(path).unwrap();
}
pub fn check_silicon_installed() -> bool {
true
}
#[cfg(test)]
mod line_geometry_test {
use super::*;
#[test]
fn test_line_geometry_matches_rendered_png() {
let dir = std::env::temp_dir().join("bat_cli_line_geometry_test");
std::fs::create_dir_all(&dir).unwrap();
let dir_str = dir.to_str().unwrap();
for font_size in [16usize, 20, 28] {
let geometry = line_geometry(Some(font_size));
let render = |n: usize, name: &str| -> (u32, u32) {
let content = (0..n)
.map(|i| format!("let line_{i} = {i};"))
.collect::<Vec<_>>()
.join("\n");
let path = create_figure(&content, dir_str, name, 1, Some(font_size), true);
let dims = image::image_dimensions(&path).unwrap();
std::fs::remove_file(&path).unwrap();
dims
};
let (_, height_10) = render(10, &format!("probe_10_{font_size}.rs"));
let (_, height_30) = render(30, &format!("probe_30_{font_size}.rs"));
assert_eq!(
height_30 - height_10,
20 * geometry.line_height,
"line_height mismatch at font size {font_size}"
);
let expected_10 = 10 * geometry.line_height + 2 * CODE_PAD + 2 * PAD;
assert_eq!(
height_10, expected_10,
"absolute height mismatch at font size {font_size}"
);
let last_center = geometry.line_center_y(9);
assert!(last_center < height_10 - PAD, "last line center out of bounds");
let fraction = geometry.line_center_fraction(9, height_10);
assert!(
fraction > 0.0 && fraction < 1.0,
"fraction out of range: {fraction}"
);
}
}
#[test]
fn test_line_end_x_matches_rendered_png() {
let dir = std::env::temp_dir().join("bat_cli_line_end_x_test");
std::fs::create_dir_all(&dir).unwrap();
let dir_str = dir.to_str().unwrap();
let font_size = 20usize;
let offset = 1usize;
let geometry = line_geometry(Some(font_size));
let lines = vec![
"let very_long_line_to_widen_the_whole_image = compute(a, b, c, d, e);",
"let short = 1;",
"self.rewarder.accrue(account, shares);",
"",
];
let content = lines.join("\n");
let path = create_figure(&content, dir_str, "line_end_x.rs", offset, Some(font_size), true);
let img = image::open(&path).unwrap().to_rgba8();
let (width, _height) = img.dimensions();
for (line_index, line_text) in lines.iter().enumerate() {
if line_text.is_empty() {
continue;
}
let expected = line_end_x(Some(font_size), true, lines.len(), offset, line_text);
let top = geometry.first_line_y + line_index as u32 * geometry.line_height;
let mut measured = 0u32;
for y in top..(top + geometry.line_height) {
for x in (0..width).rev() {
if img.get_pixel(x, y) != &BG {
measured = measured.max(x);
break;
}
}
}
let char_width = line_end_x(Some(font_size), true, lines.len(), offset, "a")
- line_end_x(Some(font_size), true, lines.len(), offset, "");
let delta = expected as i64 - measured as i64;
assert!(
delta >= 0 && delta <= char_width as i64,
"line {line_index} ({line_text:?}): predicted end x {expected}, \
measured {measured}, char width {char_width}"
);
}
std::fs::remove_file(&path).unwrap();
}
}