Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/theme.rs')
-rw-r--r--helix-view/src/theme.rs697
1 files changed, 107 insertions, 590 deletions
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 173a40f3..757316bd 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -1,191 +1,51 @@
use std::{
- collections::{HashMap, HashSet},
+ collections::HashMap,
path::{Path, PathBuf},
- str,
};
-use anyhow::{anyhow, Result};
-use helix_core::{hashmap, syntax::Highlight};
-use helix_loader::merge_toml_values;
+use anyhow::Context;
+use helix_core::hashmap;
use log::warn;
use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer};
-use toml::{map::Map, Value};
+use toml::Value;
-use crate::graphics::UnderlineStyle;
pub use crate::graphics::{Color, Modifier, Style};
-pub static DEFAULT_THEME_DATA: Lazy<Value> = Lazy::new(|| {
- let bytes = include_bytes!("../../theme.toml");
- toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base default theme")
+pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
+ toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
-pub static BASE16_DEFAULT_THEME_DATA: Lazy<Value> = Lazy::new(|| {
- let bytes = include_bytes!("../../base16_theme.toml");
- toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base 16 default theme")
-});
-
-pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
- name: "default".into(),
- ..Theme::from(DEFAULT_THEME_DATA.clone())
-});
-
-pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
- name: "base16_default".into(),
- ..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
-});
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum Mode {
- Dark,
- Light,
-}
-
-#[cfg(feature = "term")]
-impl From<termina::escape::csi::ThemeMode> for Mode {
- fn from(mode: termina::escape::csi::ThemeMode) -> Self {
- match mode {
- termina::escape::csi::ThemeMode::Dark => Self::Dark,
- termina::escape::csi::ThemeMode::Light => Self::Light,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Config {
- light: String,
- dark: String,
- /// A theme to choose when the terminal did not declare either light or dark mode.
- /// When not specified the dark theme is preferred.
- fallback: Option<String>,
-}
-
-impl Config {
- pub fn choose(&self, preference: Option<Mode>) -> &str {
- match preference {
- Some(Mode::Light) => &self.light,
- Some(Mode::Dark) => &self.dark,
- None => self.fallback.as_ref().unwrap_or(&self.dark),
- }
- }
-}
-
-impl<'de> Deserialize<'de> for Config {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- #[derive(Deserialize)]
- #[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")]
- enum InnerConfig {
- Constant(String),
- Adaptive {
- dark: String,
- light: String,
- fallback: Option<String>,
- },
- }
-
- let inner = InnerConfig::deserialize(deserializer)?;
-
- let (light, dark, fallback) = match inner {
- InnerConfig::Constant(theme) => (theme.clone(), theme.clone(), None),
- InnerConfig::Adaptive {
- light,
- dark,
- fallback,
- } => (light, dark, fallback),
- };
-
- Ok(Self {
- light,
- dark,
- fallback,
- })
- }
-}
#[derive(Clone, Debug)]
pub struct Loader {
- /// Theme directories to search from highest to lowest priority
- theme_dirs: Vec<PathBuf>,
+ user_dir: PathBuf,
+ default_dir: PathBuf,
}
impl Loader {
- /// Creates a new loader that can load themes from multiple directories.
- ///
- /// The provided directories should be ordered from highest to lowest priority.
- /// The directories will have their "themes" subdirectory searched.
- pub fn new(dirs: &[PathBuf]) -> Self {
+ /// Creates a new loader that can load themes from two directories.
+ pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
Self {
- theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(),
+ user_dir: user_dir.as_ref().join("themes"),
+ default_dir: default_dir.as_ref().join("themes"),
}
}
- /// Loads a theme searching directories in priority order.
- pub fn load(&self, name: &str) -> Result<Theme> {
- let (theme, warnings) = self.load_with_warnings(name)?;
-
- for warning in warnings {
- warn!("Theme '{}': {}", name, warning);
- }
-
- Ok(theme)
- }
-
- /// Loads a theme searching directories in priority order, returning any warnings
- pub fn load_with_warnings(&self, name: &str) -> Result<(Theme, Vec<String>)> {
+ /// Loads a theme first looking in the `user_dir` then in `default_dir`
+ pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
if name == "default" {
- return Ok((self.default(), Vec::new()));
- }
- if name == "base16_default" {
- return Ok((self.base16_default(), Vec::new()));
+ return Ok(self.default());
}
+ let filename = format!("{}.toml", name);
- let mut visited_paths = HashSet::new();
- let (theme, warnings) = self
- .load_theme(name, &mut visited_paths)
- .map(Theme::from_toml)?;
-
- let theme = Theme {
- name: name.into(),
- ..theme
- };
- Ok((theme, warnings))
- }
-
- /// Recursively load a theme, merging with any inherited parent themes.
- ///
- /// The paths that have been visited in the inheritance hierarchy are tracked
- /// to detect and avoid cycling.
- ///
- /// It is possible for one file to inherit from another file with the same name
- /// so long as the second file is in a themes directory with lower priority.
- /// However, it is not recommended that users do this as it will make tracing
- /// errors more difficult.
- fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
- let path = self.path(name, visited_paths)?;
-
- let theme_toml = self.load_toml(path)?;
-
- let inherits = theme_toml.get("inherits");
-
- let theme_toml = if let Some(parent_theme_name) = inherits {
- let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
- anyhow!("Expected 'inherits' to be a string: {}", parent_theme_name)
- })?;
-
- let parent_theme_toml = match parent_theme_name {
- // load default themes's toml from const.
- "default" => DEFAULT_THEME_DATA.clone(),
- "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
- _ => self.load_theme(parent_theme_name, visited_paths)?,
- };
-
- self.merge_themes(parent_theme_toml, theme_toml)
+ let user_path = self.user_dir.join(&filename);
+ let path = if user_path.exists() {
+ user_path
} else {
- theme_toml
+ self.default_dir.join(filename)
};
- Ok(theme_toml)
+ let data = std::fs::read(&path)?;
+ toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
}
pub fn read_names(path: &Path) -> Vec<String> {
@@ -203,110 +63,23 @@ impl Loader {
.unwrap_or_default()
}
- // merge one theme into the parent theme
- fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value {
- let parent_palette = parent_theme_toml.get("palette");
- let palette = theme_toml.get("palette");
-
- // handle the table separately since it needs a `merge_depth` of 2
- // this would conflict with the rest of the theme merge strategy
- let palette_values = match (parent_palette, palette) {
- (Some(parent_palette), Some(palette)) => {
- merge_toml_values(parent_palette.clone(), palette.clone(), 2)
- }
- (Some(parent_palette), None) => parent_palette.clone(),
- (None, Some(palette)) => palette.clone(),
- (None, None) => Map::new().into(),
- };
-
- // add the palette correctly as nested table
- let mut palette = Map::new();
- palette.insert(String::from("palette"), palette_values);
-
- // merge the theme into the parent theme
- let theme = merge_toml_values(parent_theme_toml, theme_toml, 1);
- // merge the before specially handled palette into the theme
- merge_toml_values(theme, palette.into(), 1)
- }
-
- // Loads the theme data as `toml::Value`
- fn load_toml(&self, path: PathBuf) -> Result<Value> {
- let data = std::fs::read_to_string(path)?;
- let value = toml::from_str(&data)?;
-
- Ok(value)
- }
-
- /// Returns the path to the theme with the given name
- ///
- /// Ignores paths already visited and follows directory priority order.
- fn path(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<PathBuf> {
- let filename = format!("{}.toml", name);
-
- let mut cycle_found = false; // track if there was a path, but it was in a cycle
- self.theme_dirs
- .iter()
- .find_map(|dir| {
- let path = dir.join(&filename);
- if !path.exists() {
- None
- } else if visited_paths.contains(&path) {
- // Avoiding cycle, continuing to look in lower priority directories
- cycle_found = true;
- None
- } else {
- visited_paths.insert(path.clone());
- Some(path)
- }
- })
- .ok_or_else(|| {
- if cycle_found {
- anyhow!("Cycle found in inheriting: {}", name)
- } else {
- anyhow!("File not found for: {}", name)
- }
- })
- }
-
- pub fn default_theme(&self, true_color: bool) -> Theme {
- if true_color {
- self.default()
- } else {
- self.base16_default()
- }
+ /// Lists all theme names available in default and user directory
+ pub fn names(&self) -> Vec<String> {
+ let mut names = Self::read_names(&self.user_dir);
+ names.extend(Self::read_names(&self.default_dir));
+ names
}
/// Returns the default theme
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
-
- /// Returns the alternative 16-color default theme
- pub fn base16_default(&self) -> Theme {
- BASE16_DEFAULT_THEME.clone()
- }
}
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug)]
pub struct Theme {
- name: String,
-
- // UI styles are stored in a HashMap
- styles: HashMap<String, Style>,
- // tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
- highlights: Vec<Style>,
- rainbow_length: usize,
-}
-
-impl From<Value> for Theme {
- fn from(value: Value) -> Self {
- let (theme, warnings) = Theme::from_toml(value);
- for warning in warnings {
- warn!("{}", warning);
- }
- theme
- }
+ styles: HashMap<String, Style>,
}
impl<'de> Deserialize<'de> for Theme {
@@ -314,147 +87,42 @@ impl<'de> Deserialize<'de> for Theme {
where
D: Deserializer<'de>,
{
- let values = Map::<String, Value>::deserialize(deserializer)?;
- let (theme, warnings) = Theme::from_keys(values);
- for warning in warnings {
- warn!("{}", warning);
- }
- Ok(theme)
- }
-}
-
-#[allow(clippy::type_complexity)]
-fn build_theme_values(
- mut values: Map<String, Value>,
-) -> (
- HashMap<String, Style>,
- Vec<String>,
- Vec<Style>,
- usize,
- Vec<String>,
-) {
- let mut styles = HashMap::new();
- let mut scopes = Vec::new();
- let mut highlights = Vec::new();
- let mut rainbow_length = 0;
-
- let mut warnings = Vec::new();
-
- // TODO: alert user of parsing failures in editor
- let palette = values
- .remove("palette")
- .map(|value| {
- ThemePalette::try_from(value).unwrap_or_else(|err| {
- warnings.push(err);
- ThemePalette::default()
- })
- })
- .unwrap_or_default();
- // remove inherits from value to prevent errors
- let _ = values.remove("inherits");
- styles.reserve(values.len());
- scopes.reserve(values.len());
- highlights.reserve(values.len());
-
- for (i, style) in values
- .remove("rainbow")
- .and_then(|value| match palette.parse_style_array(value) {
- Ok(styles) => Some(styles),
- Err(err) => {
- warnings.push(err);
- None
+ let mut styles = HashMap::new();
+
+ if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
+ // TODO: alert user of parsing failures in editor
+ let palette = colors
+ .remove("palette")
+ .map(|value| {
+ ThemePalette::try_from(value).unwrap_or_else(|err| {
+ warn!("{}", err);
+ ThemePalette::default()
+ })
+ })
+ .unwrap_or_default();
+
+ styles.reserve(colors.len());
+ for (name, style_value) in colors {
+ let mut style = Style::default();
+ if let Err(err) = palette.parse_style(&mut style, style_value) {
+ warn!("{}", err);
+ }
+ styles.insert(name, style);
}
- })
- .unwrap_or_else(default_rainbow)
- .into_iter()
- .enumerate()
- {
- let name = format!("rainbow.{i}");
- styles.insert(name.clone(), style);
- scopes.push(name);
- highlights.push(style);
- rainbow_length += 1;
- }
-
- for (name, style_value) in values {
- let mut style = Style::default();
- if let Err(err) = palette.parse_style(&mut style, style_value) {
- warnings.push(format!("Failed to parse style for key {name:?}. {err}"));
}
- // these are used both as UI and as highlights
- styles.insert(name.clone(), style);
- scopes.push(name);
- highlights.push(style);
+ let scopes = styles.keys().map(ToString::to_string).collect();
+ Ok(Self { scopes, styles })
}
-
- (styles, scopes, highlights, rainbow_length, warnings)
}
-fn default_rainbow() -> Vec<Style> {
- vec![
- Style::default().fg(Color::Red),
- Style::default().fg(Color::Yellow),
- Style::default().fg(Color::Green),
- Style::default().fg(Color::Blue),
- Style::default().fg(Color::Cyan),
- Style::default().fg(Color::Magenta),
- ]
-}
impl Theme {
- /// To allow `Highlight` to represent arbitrary RGB colors without turning it into an enum,
- /// we interpret the last 256^3 numbers as RGB.
- const RGB_START: u32 = (u32::MAX << (8 + 8 + 8)) - 1 - (u32::MAX - Highlight::MAX);
-
- /// Interpret a Highlight with the RGB foreground
- fn decode_rgb_highlight(highlight: Highlight) -> Option<(u8, u8, u8)> {
- (highlight.get() > Self::RGB_START).then(|| {
- let [b, g, r, ..] = (highlight.get() + 1).to_le_bytes();
- (r, g, b)
- })
- }
-
- /// Create a Highlight that represents an RGB color
- pub fn rgb_highlight(r: u8, g: u8, b: u8) -> Highlight {
- // -1 because highlight is "non-max": u32::MAX is reserved for the null pointer
- // optimization.
- Highlight::new(u32::from_le_bytes([b, g, r, u8::MAX]) - 1)
- }
-
- #[inline]
- pub fn highlight(&self, highlight: Highlight) -> Style {
- if let Some((red, green, blue)) = Self::decode_rgb_highlight(highlight) {
- Style::new().fg(Color::Rgb(red, green, blue))
- } else {
- self.highlights[highlight.idx()]
- }
- }
-
- #[inline]
- pub fn scope(&self, highlight: Highlight) -> &str {
- &self.scopes[highlight.idx()]
- }
-
- pub fn name(&self) -> &str {
- &self.name
- }
-
pub fn get(&self, scope: &str) -> Style {
- self.try_get(scope).unwrap_or_default()
+ self.try_get(scope)
+ .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
}
- /// Get the style of a scope, falling back to dot separated broader
- /// scopes. For example if `ui.text.focus` is not defined in the theme,
- /// `ui.text` is tried and then `ui` is tried.
pub fn try_get(&self, scope: &str) -> Option<Style> {
- std::iter::successors(Some(scope), |s| Some(s.rsplit_once('.')?.0))
- .find_map(|s| self.styles.get(s).copied())
- }
-
- /// Get the style of a scope, without falling back to dot separated broader
- /// scopes. For example if `ui.text.focus` is not defined in the theme, it
- /// will return `None`, even if `ui.text` is.
- pub fn try_get_exact(&self, scope: &str) -> Option<Style> {
self.styles.get(scope).copied()
}
@@ -463,59 +131,8 @@ impl Theme {
&self.scopes
}
- pub fn find_highlight_exact(&self, scope: &str) -> Option<Highlight> {
- self.scopes()
- .iter()
- .position(|s| s == scope)
- .map(|idx| Highlight::new(idx as u32))
- }
-
- pub fn find_highlight(&self, mut scope: &str) -> Option<Highlight> {
- loop {
- if let Some(highlight) = self.find_highlight_exact(scope) {
- return Some(highlight);
- }
- if let Some(new_end) = scope.rfind('.') {
- scope = &scope[..new_end];
- } else {
- return None;
- }
- }
- }
-
- pub fn is_16_color(&self) -> bool {
- self.styles.iter().all(|(_, style)| {
- [style.fg, style.bg]
- .into_iter()
- .all(|color| !matches!(color, Some(Color::Rgb(..))))
- })
- }
-
- pub fn rainbow_length(&self) -> usize {
- self.rainbow_length
- }
-
- fn from_toml(value: Value) -> (Self, Vec<String>) {
- if let Value::Table(table) = value {
- Theme::from_keys(table)
- } else {
- warn!("Expected theme TOML value to be a table, found {:?}", value);
- Default::default()
- }
- }
-
- fn from_keys(toml_keys: Map<String, Value>) -> (Self, Vec<String>) {
- let (styles, scopes, highlights, rainbow_length, load_errors) =
- build_theme_values(toml_keys);
-
- let theme = Self {
- styles,
- scopes,
- highlights,
- rainbow_length,
- ..Default::default()
- };
- (theme, load_errors)
+ pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
+ self.scopes().iter().position(|s| s == scope)
}
}
@@ -527,7 +144,6 @@ impl Default for ThemePalette {
fn default() -> Self {
Self {
palette: hashmap! {
- "default".to_string() => Color::Reset,
"black".to_string() => Color::Black,
"red".to_string() => Color::Red,
"green".to_string() => Color::Green,
@@ -559,23 +175,8 @@ impl ThemePalette {
Self { palette: default }
}
- pub fn string_to_rgb(s: &str) -> Result<Color, String> {
- if s.starts_with('#') {
- Self::hex_string_to_rgb(s)
- } else {
- Self::ansi_string_to_rgb(s)
- }
- }
-
- fn ansi_string_to_rgb(s: &str) -> Result<Color, String> {
- if let Ok(index) = s.parse::<u8>() {
- return Ok(Color::Indexed(index));
- }
- Err(format!("Malformed ANSI: {}", s))
- }
-
- fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
- if s.len() >= 7 {
+ pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
+ if s.starts_with('#') && s.len() >= 7 {
if let (Ok(red), Ok(green), Ok(blue)) = (
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
@@ -585,13 +186,13 @@ impl ThemePalette {
}
}
- Err(format!("Malformed hexcode: {}", s))
+ Err(format!("Theme: malformed hexcode: {}", s))
}
fn parse_value_as_str(value: &Value) -> Result<&str, String> {
value
.as_str()
- .ok_or(format!("Unrecognized value: {}", value))
+ .ok_or(format!("Theme: unrecognized value: {}", value))
}
pub fn parse_color(&self, value: Value) -> Result<Color, String> {
@@ -601,54 +202,32 @@ impl ThemePalette {
.get(value)
.copied()
.ok_or("")
- .or_else(|_| Self::string_to_rgb(value))
+ .or_else(|_| Self::hex_string_to_rgb(value))
}
pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
value
.as_str()
.and_then(|s| s.parse().ok())
- .ok_or(format!("Invalid modifier: {}", value))
- }
-
- pub fn parse_underline_style(value: &Value) -> Result<UnderlineStyle, String> {
- value
- .as_str()
- .and_then(|s| s.parse().ok())
- .ok_or(format!("Invalid underline style: {}", value))
+ .ok_or(format!("Theme: invalid modifier: {}", value))
}
pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
if let Value::Table(entries) = value {
- for (name, mut value) in entries {
+ for (name, value) in entries {
match name.as_str() {
"fg" => *style = style.fg(self.parse_color(value)?),
"bg" => *style = style.bg(self.parse_color(value)?),
- "underline" => {
- let table = value.as_table_mut().ok_or("Underline must be table")?;
- if let Some(value) = table.remove("color") {
- *style = style.underline_color(self.parse_color(value)?);
- }
- if let Some(value) = table.remove("style") {
- *style = style.underline_style(Self::parse_underline_style(&value)?);
- }
-
- if let Some(attr) = table.keys().next() {
- return Err(format!("Invalid underline attribute: {attr}"));
- }
- }
"modifiers" => {
- let modifiers = value.as_array().ok_or("Modifiers should be an array")?;
+ let modifiers = value
+ .as_array()
+ .ok_or("Theme: modifiers should be an array")?;
for modifier in modifiers {
- if modifier.as_str() == Some("underlined") {
- *style = style.underline_style(UnderlineStyle::Line);
- } else {
- *style = style.add_modifier(Self::parse_modifier(modifier)?);
- }
+ *style = style.add_modifier(Self::parse_modifier(modifier)?);
}
}
- _ => return Err(format!("Invalid style attribute: {}", name)),
+ _ => return Err(format!("Theme: invalid style attribute: {}", name)),
}
}
} else {
@@ -656,21 +235,6 @@ impl ThemePalette {
}
Ok(())
}
-
- fn parse_style_array(&self, value: Value) -> Result<Vec<Style>, String> {
- let mut styles = Vec::new();
-
- for v in value
- .as_array()
- .ok_or_else(|| format!("Could not parse value as an array: '{value}'"))?
- {
- let mut style = Style::default();
- self.parse_style(&mut style, v.clone())?;
- styles.push(style);
- }
-
- Ok(styles)
- }
}
impl TryFrom<Value> for ThemePalette {
@@ -685,7 +249,7 @@ impl TryFrom<Value> for ThemePalette {
let mut palette = HashMap::with_capacity(map.len());
for (name, value) in map {
let value = Self::parse_value_as_str(&value)?;
- let color = Self::string_to_rgb(value)?;
+ let color = Self::hex_string_to_rgb(value)?;
palette.insert(name, color);
}
@@ -693,100 +257,53 @@ impl TryFrom<Value> for ThemePalette {
}
}
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_style_string() {
- let fg = Value::String("#ffffff".to_string());
+#[test]
+fn test_parse_style_string() {
+ let fg = Value::String("#ffffff".to_string());
- let mut style = Style::default();
- let palette = ThemePalette::default();
- palette.parse_style(&mut style, fg).unwrap();
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ palette.parse_style(&mut style, fg).unwrap();
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
- }
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+}
- #[test]
- fn test_palette() {
- use helix_core::hashmap;
- let fg = Value::String("my_color".to_string());
+#[test]
+fn test_palette() {
+ use helix_core::hashmap;
+ let fg = Value::String("my_color".to_string());
- let mut style = Style::default();
- let palette =
- ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
- palette.parse_style(&mut style, fg).unwrap();
+ let mut style = Style::default();
+ let palette =
+ ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
+ palette.parse_style(&mut style, fg).unwrap();
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
- }
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+}
- #[test]
- fn test_parse_style_table() {
- let table = toml::toml! {
- "keyword" = {
- fg = "#ffffff",
- bg = "#000000",
- modifiers = ["bold"],
- }
- };
+#[test]
+fn test_parse_style_table() {
+ let table = toml::toml! {
+ "keyword" = {
+ fg = "#ffffff",
+ bg = "#000000",
+ modifiers = ["bold"],
+ }
+ };
- let mut style = Style::default();
- let palette = ThemePalette::default();
- for (_name, value) in table {
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ if let Value::Table(entries) = table {
+ for (_name, value) in entries {
palette.parse_style(&mut style, value).unwrap();
}
-
- assert_eq!(
- style,
- Style::default()
- .fg(Color::Rgb(255, 255, 255))
- .bg(Color::Rgb(0, 0, 0))
- .add_modifier(Modifier::BOLD)
- );
}
- // tests for parsing an RGB `Highlight`
-
- #[test]
- fn convert_to_and_from() {
- let (r, g, b) = (0xFF, 0xFE, 0xFA);
- let highlight = Theme::rgb_highlight(r, g, b);
- assert_eq!(Theme::decode_rgb_highlight(highlight), Some((r, g, b)));
- }
-
- /// make sure we can store all the colors at the end
- #[test]
- fn full_numeric_range() {
- assert_eq!(Highlight::MAX - Theme::RGB_START, 256_u32.pow(3));
- }
-
- #[test]
- fn retrieve_color() {
- // color in the middle
- let (r, g, b) = (0x14, 0xAA, 0xF7);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- // pure black
- let (r, g, b) = (0x00, 0x00, 0x00);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- // pure white
- let (r, g, b) = (0xff, 0xff, 0xff);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- }
-
- #[test]
- #[should_panic(expected = "index out of bounds: the len is 0 but the index is 4278190078")]
- fn out_of_bounds() {
- let highlight = Highlight::new(Theme::rgb_highlight(0, 0, 0).get() - 1);
- Theme::default().highlight(highlight);
- }
+ assert_eq!(
+ style,
+ Style::default()
+ .fg(Color::Rgb(255, 255, 255))
+ .bg(Color::Rgb(0, 0, 0))
+ .add_modifier(Modifier::BOLD)
+ );
}