aboutsummaryrefslogtreecommitdiff
path: root/xtask/src/themelint.rs
diff options
context:
space:
mode:
Diffstat (limited to 'xtask/src/themelint.rs')
-rw-r--r--xtask/src/themelint.rs199
1 files changed, 199 insertions, 0 deletions
diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs
new file mode 100644
index 00000000..26d1cb04
--- /dev/null
+++ b/xtask/src/themelint.rs
@@ -0,0 +1,199 @@
+use crate::path;
+use crate::DynError;
+use helix_view::theme::Loader;
+use helix_view::theme::Modifier;
+use helix_view::Theme;
+
+struct Rule {
+ fg: Option<&'static str>,
+ bg: Option<&'static str>,
+ check_both: bool,
+}
+
+enum Require {
+ Existence(Rule),
+ Difference(&'static str, &'static str),
+}
+
+// Placed in an fn here, so it's the first thing you see
+fn get_rules() -> Vec<Require> {
+ vec![
+ // Check for ui.selection, which is required
+ Require::Existence(Rule::has_either("ui.selection")),
+ Require::Existence(Rule::has_either("ui.selection.primary")),
+ Require::Difference("ui.selection", "ui.selection.primary"),
+ // Check for planned readable text
+ Require::Existence(Rule::has_fg("ui.text")),
+ Require::Existence(Rule::has_bg("ui.background")),
+ // Check for complete editor.statusline bare minimum
+ Require::Existence(Rule::has_both("ui.statusline")),
+ Require::Existence(Rule::has_both("ui.statusline.inactive")),
+ // Check for editor.color-modes
+ Require::Existence(Rule::has_either("ui.statusline.normal")),
+ Require::Existence(Rule::has_either("ui.statusline.insert")),
+ Require::Existence(Rule::has_either("ui.statusline.select")),
+ Require::Difference("ui.statusline.normal", "ui.statusline.insert"),
+ Require::Difference("ui.statusline.normal", "ui.statusline.select"),
+ // Check for editor.cursorline
+ Require::Existence(Rule::has_bg("ui.cursorline.primary")),
+ // Check for editor.whitespace
+ Require::Existence(Rule::has_fg("ui.virtual.whitespace")),
+ // Check fir rulers
+ Require::Existence(Rule::has_either("ui.virtual.indent-guide")),
+ // Check for editor.rulers
+ Require::Existence(Rule::has_either("ui.virtual.ruler")),
+ // Check for menus and prompts
+ Require::Existence(Rule::has_both("ui.menu")),
+ Require::Existence(Rule::has_both("ui.help")),
+ Require::Existence(Rule::has_bg("ui.popup")),
+ Require::Existence(Rule::has_either("ui.window")),
+ // Check for visible cursor
+ Require::Existence(Rule::has_bg("ui.cursor.primary")),
+ Require::Existence(Rule::has_either("ui.cursor.match")),
+ ]
+}
+
+impl Rule {
+ fn has_bg(bg: &'static str) -> Rule {
+ Rule {
+ fg: None,
+ bg: Some(bg),
+ check_both: true,
+ }
+ }
+ fn has_fg(fg: &'static str) -> Rule {
+ Rule {
+ fg: Some(fg),
+ bg: None,
+ check_both: true,
+ }
+ }
+ fn has_either(item: &'static str) -> Rule {
+ Rule {
+ fg: Some(item),
+ bg: Some(item),
+ check_both: false,
+ }
+ }
+ fn has_both(item: &'static str) -> Rule {
+ Rule {
+ fg: Some(item),
+ bg: Some(item),
+ check_both: true,
+ }
+ }
+ fn found_fg(&self, theme: &Theme) -> bool {
+ if let Some(fg) = &self.fg {
+ if theme.get(fg).fg.is_none() && theme.get(fg).add_modifier == Modifier::empty() {
+ return false;
+ }
+ }
+ true
+ }
+ fn found_bg(&self, theme: &Theme) -> bool {
+ if let Some(bg) = &self.bg {
+ if theme.get(bg).bg.is_none() && theme.get(bg).add_modifier == Modifier::empty() {
+ return false;
+ }
+ }
+ true
+ }
+ fn rule_name(&self) -> &'static str {
+ if self.fg.is_some() {
+ self.fg.unwrap()
+ } else if self.bg.is_some() {
+ self.bg.unwrap()
+ } else {
+ "LINTER_ERROR_NO_RULE"
+ }
+ }
+
+ fn check_difference(
+ theme: &Theme,
+ a: &'static str,
+ b: &'static str,
+ messages: &mut Vec<String>,
+ ) {
+ let theme_a = theme.get(a);
+ let theme_b = theme.get(b);
+ if theme_a == theme_b {
+ messages.push(format!("$THEME: `{}` and `{}` cannot be equal", a, b));
+ }
+ }
+
+ fn check_existence(rule: &Rule, theme: &Theme, messages: &mut Vec<String>) {
+ let found_fg = rule.found_fg(theme);
+ let found_bg = rule.found_bg(theme);
+
+ if !rule.check_both && (found_fg || found_bg) {
+ return;
+ }
+ if !found_fg || !found_bg {
+ let mut missing = vec![];
+ if !found_fg {
+ missing.push("`fg`");
+ }
+ if !found_bg {
+ missing.push("`bg`");
+ }
+ let entry = if !rule.check_both && !found_fg && !found_bg {
+ missing.join(" or ")
+ } else {
+ missing.join(" and ")
+ };
+ messages.push(format!(
+ "$THEME: missing {} for `{}`",
+ entry,
+ rule.rule_name()
+ ))
+ }
+ }
+}
+
+pub fn lint(file: String) -> Result<(), DynError> {
+ if file.contains("base16") {
+ println!("Skipping base16: {}", file);
+ return Ok(());
+ }
+ let path = path::themes().join(file.clone() + ".toml");
+ let theme = std::fs::read(&path).unwrap();
+ let theme: Theme = toml::from_slice(&theme).expect("Failed to parse theme");
+
+ let mut messages: Vec<String> = vec![];
+ get_rules().iter().for_each(|lint| match lint {
+ Require::Existence(rule) => Rule::check_existence(rule, &theme, &mut messages),
+ Require::Difference(a, b) => Rule::check_difference(&theme, a, b, &mut messages),
+ });
+
+ if !messages.is_empty() {
+ messages.iter().for_each(|m| {
+ let theme = file.clone();
+ let message = m.replace("$THEME", theme.as_str());
+ println!("{}", message);
+ });
+ Err(format!("{} has issues", file.clone().as_str()).into())
+ } else {
+ Ok(())
+ }
+}
+
+pub fn lint_all() -> Result<(), DynError> {
+ let files = Loader::read_names(path::themes().as_path());
+ let files_count = files.len();
+ let ok_files_count = files
+ .into_iter()
+ .filter_map(|path| lint(path.replace(".toml", "")).ok())
+ .collect::<Vec<()>>()
+ .len();
+
+ if files_count != ok_files_count {
+ Err(format!(
+ "{} of {} themes had issues",
+ files_count - ok_files_count,
+ files_count
+ )
+ .into())
+ } else {
+ Ok(())
+ }
+}