feat: read helix themes
This commit is contained in:
parent
cac6d0ae31
commit
30ca17e17f
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1427,6 +1427,7 @@ dependencies = [
|
||||
"tar",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@ -28,3 +28,4 @@ typst-utils = "0.15.1"
|
||||
tree-house = "0.4.0"
|
||||
tree-house-bindings = { version = "0.3.2", features = ["ropey"] }
|
||||
ropey = "1.6.1"
|
||||
toml = "0.8.23"
|
||||
|
||||
@ -26,6 +26,7 @@ use tracing_subscriber::FmtSubscriber;
|
||||
use crate::typst::TypstContext;
|
||||
|
||||
mod syntax;
|
||||
mod themes;
|
||||
mod typst;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -64,6 +65,10 @@ struct AppConfig {
|
||||
#[arg(short = 'g', long, value_name = "DIR", default_value = None)]
|
||||
syntax_root: Option<PathBuf>,
|
||||
|
||||
/// The directory where Helix theme `.toml` files are stored.
|
||||
#[arg(short = 't', long, value_name = "DIR", default_value = None)]
|
||||
themes_root: Option<PathBuf>,
|
||||
|
||||
/// Grammars to load, separated by commas.
|
||||
/// Useful to avoid the memory required to load all languages.
|
||||
#[arg(short = 'L', long = "lang", value_name = "NAME", value_delimiter = ',')]
|
||||
@ -105,6 +110,7 @@ async fn main() {
|
||||
.init();
|
||||
|
||||
syntax::init(&state.config);
|
||||
themes::init(&state.config);
|
||||
|
||||
// TODO: Replace with config option
|
||||
let mut favicon = state.config.get_full_assets_path();
|
||||
|
||||
216
src/themes.rs
Normal file
216
src/themes.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fs,
|
||||
path::Path,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use toml::{Table, Value};
|
||||
use tracing::{info, warn};
|
||||
use typst::foundations::{Content, Func, NativeFunc, func};
|
||||
|
||||
use crate::AppConfig;
|
||||
|
||||
static REGISTRY: OnceLock<ThemeRegistry> = OnceLock::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ThemeRegistry {
|
||||
themes: HashMap<String, Theme>,
|
||||
}
|
||||
|
||||
impl ThemeRegistry {
|
||||
fn new(theme_root: &Path) -> Self {
|
||||
let mut helix_themes = HashMap::new();
|
||||
for entry in fs::read_dir(theme_root).into_iter().flatten().flatten() {
|
||||
let name = entry
|
||||
.path()
|
||||
.file_prefix()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if let Ok(text) = fs::read_to_string(entry.path())
|
||||
&& let Ok(toml) = toml::from_str::<Table>(&text)
|
||||
{
|
||||
helix_themes.insert(name, toml);
|
||||
}
|
||||
}
|
||||
|
||||
// resolve inheritance
|
||||
while helix_themes
|
||||
.values()
|
||||
.any(|table| table.contains_key("inherits"))
|
||||
{
|
||||
let old_themes = helix_themes.clone();
|
||||
old_themes
|
||||
.keys()
|
||||
.filter(|name| old_themes.get(*name).unwrap().contains_key("inherits"))
|
||||
.for_each(|name| {
|
||||
let parent_name = old_themes
|
||||
.get(name)
|
||||
.unwrap()
|
||||
.get("inherits")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap();
|
||||
let parent = old_themes.get(parent_name).unwrap();
|
||||
if !parent.contains_key("inherits") {
|
||||
let table = helix_themes.get_mut(name).unwrap();
|
||||
parent.into_iter().for_each(|(k, v)| {
|
||||
if k == "palette" {
|
||||
let parent_palette = v.as_table();
|
||||
let child_palette = table
|
||||
.entry(k.clone())
|
||||
.or_insert_with(|| Value::Table(Table::new()))
|
||||
.as_table_mut();
|
||||
if let (Some(parent_palette), Some(child_palette)) =
|
||||
(parent_palette, child_palette)
|
||||
{
|
||||
for (pk, pv) in parent_palette {
|
||||
child_palette
|
||||
.entry(pk.clone())
|
||||
.or_insert_with(|| pv.clone());
|
||||
}
|
||||
}
|
||||
} else if !table.contains_key(k) {
|
||||
table.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
table.remove("inherits");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let themes = helix_themes
|
||||
.into_iter()
|
||||
.flat_map(|(name, table)| Theme::from_toml(table).map(|t| (name, t)))
|
||||
.collect();
|
||||
|
||||
Self { themes }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Color(String);
|
||||
|
||||
impl Color {
|
||||
fn from_str(color: &str) -> Option<Self> {
|
||||
if color.starts_with('#') {
|
||||
Some(Self(color.into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn from_palette(color: &str, palette: &HashMap<String, Color>) -> Option<Self> {
|
||||
if color.starts_with('#') {
|
||||
Self::from_str(color)
|
||||
} else {
|
||||
palette.get(color).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Style {
|
||||
fg: Option<Color>,
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
underlined: bool,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
fn from_toml(style: Value, palette: &HashMap<String, Color>) -> Option<Self> {
|
||||
let resolve = |s: &str| Color::from_palette(s, palette);
|
||||
match style {
|
||||
Value::String(s) => Some(Self {
|
||||
fg: resolve(&s),
|
||||
bold: false,
|
||||
italic: false,
|
||||
underlined: false,
|
||||
}),
|
||||
Value::Table(table) => {
|
||||
let fg = table.get("fg").and_then(Value::as_str).and_then(resolve);
|
||||
let modifiers: HashSet<&str> = table
|
||||
.get("modifiers")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(Value::as_str)
|
||||
.collect();
|
||||
Some(Self {
|
||||
fg,
|
||||
bold: modifiers.contains("bold"),
|
||||
italic: modifiers.contains("italic"),
|
||||
underlined: modifiers.contains("underlined"),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Theme {
|
||||
foreground: Option<Color>,
|
||||
background: Option<Color>,
|
||||
styles: HashMap<String, Style>,
|
||||
}
|
||||
|
||||
static IGNORED_SCOPES: &[&str] = &["warning", "error", "info", "hint"];
|
||||
|
||||
impl Theme {
|
||||
fn from_toml(theme: Table) -> Option<Self> {
|
||||
let palette = theme
|
||||
.get("palette")
|
||||
.and_then(Value::as_table)
|
||||
.cloned()
|
||||
.map(|table| {
|
||||
table
|
||||
.into_iter()
|
||||
.flat_map(|(k, v)| v.as_str().and_then(Color::from_str).map(|v| (k, v)))
|
||||
.collect::<HashMap<String, Color>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let resolve = |c: &str| Color::from_palette(c, &palette);
|
||||
|
||||
let ui_background = theme.get("ui.background").and_then(Value::as_table)?;
|
||||
let foreground = ui_background
|
||||
.get("fg")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(resolve);
|
||||
let background = ui_background
|
||||
.get("bg")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(resolve);
|
||||
|
||||
let styles = theme
|
||||
.into_iter()
|
||||
.filter(|(k, _)| {
|
||||
!k.starts_with("ui.")
|
||||
&& !k.starts_with("diagnostic.")
|
||||
&& !IGNORED_SCOPES.contains(&k.as_str())
|
||||
})
|
||||
.flat_map(|(k, v)| Style::from_toml(v, &palette).map(|v| (k, v)))
|
||||
.collect();
|
||||
|
||||
Some(Self {
|
||||
foreground,
|
||||
background,
|
||||
styles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(config: &AppConfig) {
|
||||
match &config.themes_root {
|
||||
Some(themes_root) => {
|
||||
info!("Loading themes from {}", themes_root.display());
|
||||
REGISTRY.get_or_init(|| ThemeRegistry::new(themes_root));
|
||||
}
|
||||
None => {
|
||||
warn!("No themes root has been configured, theme-based colouring will not be possible.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user