use rand::prelude::{IndexedRandom, IteratorRandom};
use rand::{RngExt, rng};
use std::collections::HashSet;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
include!(concat!(env!("OUT_DIR"), "/content.rs"));
pub use generated_content::*;
pub struct SyntaxHighlighter {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}
impl Default for SyntaxHighlighter {
fn default() -> Self {
Self::new()
}
}
impl SyntaxHighlighter {
#[must_use]
pub fn new() -> Self {
Self {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
}
}
fn language_to_syntect_scope(&self, lang: &str) -> Option<&syntect::parsing::SyntaxReference> {
let syntect_name = match lang {
"cpp" => "C++",
"go" => "Go",
"python" => "Python",
"rust" => "Rust",
"typescript" => "TypeScript",
_ => return None, };
self.syntax_set.find_syntax_by_name(syntect_name)
}
#[must_use]
pub fn has_support(&self, lang: &str) -> bool {
self.language_to_syntect_scope(lang).is_some()
}
pub fn highlight_code(&self, code: &str, lang: &str) -> Vec<Option<ratatui::style::Color>> {
let Some(syntax) = self.language_to_syntect_scope(lang) else {
return vec![None; code.chars().count()];
};
let theme = self
.theme_set
.themes
.get("base16-ocean.dark")
.cloned()
.unwrap_or_else(|| {
self.theme_set
.themes
.values()
.next()
.cloned()
.expect("No themes available")
});
let default_fg = theme
.settings
.foreground
.unwrap_or(syntect::highlighting::Color::WHITE);
let mut highlighter = HighlightLines::new(syntax, &theme);
let code_char_count = code.chars().count();
let mut result: Vec<Option<ratatui::style::Color>> = vec![None; code_char_count];
let mut char_idx = 0;
for line in LinesWithEndings::from(code) {
let line_len = line.chars().count();
let highlight_result = highlighter.highlight_line(line, &self.syntax_set);
if let Ok(ranges) = highlight_result {
let mut line_offset = 0;
for (syntect_style, text) in &ranges {
let color = syntect_style_to_ratatui(syntect_style, default_fg);
let text_len = text.chars().count();
for i in 0..text_len {
let global_idx = char_idx + line_offset + i;
if global_idx < code_char_count {
result[global_idx] = color;
}
}
line_offset += text_len;
}
} else {
}
char_idx += line_len;
}
result
}
}
fn syntect_style_to_ratatui(
style: &SyntectStyle,
default_fg: syntect::highlighting::Color,
) -> Option<ratatui::style::Color> {
let fg = style.foreground;
if fg.r == default_fg.r && fg.g == default_fg.g && fg.b == default_fg.b {
return None;
}
Some(ratatui::style::Color::Rgb(fg.r, fg.g, fg.b))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Text,
Code,
}
impl Mode {
#[must_use]
pub fn all() -> &'static [Mode] {
&[Mode::Text, Mode::Code]
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Mode::Text => "text",
Mode::Code => "code",
}
}
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"text" => Some(Mode::Text),
"code" => Some(Mode::Code),
_ => None,
}
}
#[must_use]
pub fn languages(&self) -> &'static [&'static str] {
match self {
Mode::Text => TEXT_LANGUAGES,
Mode::Code => CODE_LANGUAGES,
}
}
#[must_use]
pub fn default_language(&self) -> &'static str {
match self {
Mode::Text => TEXT_LANGUAGES.first().copied().unwrap_or("english"),
Mode::Code => CODE_LANGUAGES.first().copied().unwrap_or("python"),
}
}
}
#[derive(Debug, Clone)]
pub struct ContentProvider {
mode: Mode,
language: String,
}
impl ContentProvider {
pub fn new(mode: Mode, language: impl Into<String>) -> Self {
Self {
mode,
language: language.into(),
}
}
#[must_use]
pub fn generate_text(&self, word_count: usize) -> String {
match self.mode {
Mode::Text => self.generate_words(word_count),
Mode::Code => self.generate_code_snippet(),
}
}
fn generate_words(&self, count: usize) -> String {
let words = get_word_list(&self.language);
if words.is_empty() {
return "the quick brown fox jumps over the lazy dog".to_string();
}
let mut rng = rng();
let selected: Vec<&str> = (0..count)
.map(|_| *words.choose(&mut rng).unwrap_or(&"the"))
.collect();
selected.join(" ")
}
#[must_use]
pub fn generate_code_snippet(&self) -> String {
let snippets = get_snippets(&self.language);
if snippets.is_empty() {
return "print('hello world')".to_string();
}
let mut rng = rng();
snippets
.choose(&mut rng)
.unwrap_or(&"print('hello world')")
.to_string()
}
#[must_use]
pub fn generate_code_snippet_slice(
&self,
shown_slices: &HashSet<(usize, usize)>,
) -> Option<(String, (usize, usize))> {
const SNIPPET_LINES: usize = 15;
let snippets = get_snippets(&self.language);
if snippets.is_empty() {
return None;
}
let mut rng = rand::rng();
let is_meaningful = |line: &str| !line.trim().is_empty();
let mut available: Vec<(usize, usize)> = Vec::new();
for (idx, snippet) in snippets.iter().enumerate() {
let lines: Vec<&str> = snippet.lines().collect();
if lines.is_empty() {
continue;
}
let max_start = lines.len().saturating_sub(SNIPPET_LINES);
for start in 0..=max_start {
if !shown_slices.contains(&(idx, start)) && is_meaningful(lines[start]) {
let end = (start + SNIPPET_LINES).min(lines.len());
if lines[start..end].iter().any(|l| is_meaningful(l)) {
available.push((idx, start));
}
}
}
}
let max_attempts = 100;
for _ in 0..max_attempts {
let selected = if available.is_empty() {
let snippet_idx = (0..snippets.len()).choose(&mut rng)?;
let lines: Vec<&str> = snippets[snippet_idx].lines().collect();
if lines.is_empty() {
continue;
}
let max_start = lines.len().saturating_sub(SNIPPET_LINES);
let start = if max_start == 0 {
0
} else {
rng.random_range(0..=max_start)
};
(snippet_idx, start)
} else {
*available.choose(&mut rng)?
};
let (snippet_idx, start_line) = selected;
let snippet: &str = snippets[snippet_idx];
let lines: Vec<&str> = snippet.lines().collect();
let end_line: usize = (start_line + SNIPPET_LINES).min(lines.len());
if !is_meaningful(lines[start_line]) {
continue;
}
let mut slice_lines: Vec<&str> = lines[start_line..end_line].to_vec();
while let Some(last) = slice_lines.last() {
if is_meaningful(last) {
break;
}
slice_lines.pop();
}
if slice_lines.is_empty() {
continue;
}
if !is_meaningful(slice_lines.last().unwrap()) {
continue;
}
let slice = slice_lines.join("\n");
return Some((slice, selected));
}
None
}
#[must_use]
pub fn available_languages(&self) -> &'static [&'static str] {
self.mode.languages()
}
#[must_use]
pub fn validate_language(&self, lang: &str) -> bool {
self.available_languages().contains(&lang)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mode_from_str() {
assert_eq!(Mode::from_str("text"), Some(Mode::Text));
assert_eq!(Mode::from_str("code"), Some(Mode::Code));
assert_eq!(Mode::from_str("unknown"), None);
}
#[test]
fn test_content_provider_text() {
let provider = ContentProvider::new(Mode::Text, "english");
let text = provider.generate_text(10);
assert!(!text.is_empty());
let words: Vec<&str> = text.split_whitespace().collect();
assert!(
words.len() >= 10,
"expected at least 10 words, got {}",
words.len()
);
}
#[test]
fn test_content_provider_code() {
let provider = ContentProvider::new(Mode::Code, "python");
let text = provider.generate_text(10);
assert!(!text.is_empty());
}
}