diff --git a/Cargo.toml b/Cargo.toml
index 69305c9..c17374e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,9 +27,9 @@ pkg-url = "{ repo }/releases/download/v{ version }/whiskers-{ target }{ archive-
pkg-fmt = "bin"
[lints.clippy]
-all = "warn"
-pedantic = "warn"
-nursery = "warn"
+all = { level = "warn", priority = -1 }
+pedantic = { level = "warn", priority = -1 }
+nursery = { level = "warn", priority = -1 }
unwrap_used = "warn"
missing_errors_doc = "allow"
implicit_hasher = "allow"
diff --git a/README.md b/README.md
index 2f830df..7976e52 100644
--- a/README.md
+++ b/README.md
@@ -27,13 +27,13 @@ into.
You can install Whiskers using one of the methods below:
-| Installation Method | Instructions |
-| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
-| crates.io | `cargo install catppuccin-whiskers` |
-| Source | `cargo install --git https://github.com/catppuccin/whiskers catppuccin-whiskers` |
-| Homebrew | `brew install catppuccin/tap/whiskers` |
-| Nix | `nix profile install github:catppuccin/whiskers`
`nix run github:catppuccin/whiskers -- ` |
-| Binaries
(Windows, MacOS & Linux) | Available from the [latest GitHub release](https://github.com/catppuccin/whiskers/releases?q=whiskers). |
+| Installation Method | Instructions |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| crates.io | `cargo install catppuccin-whiskers` |
+| Source | `cargo install --git https://github.com/catppuccin/whiskers catppuccin-whiskers` |
+| Homebrew | `brew install catppuccin/tap/whiskers` |
+| Nix | `nix profile install github:catppuccin/whiskers`
`nix run github:catppuccin/whiskers -- ` |
+| Binaries
(Windows, MacOS & Linux) | Available from the [latest GitHub release](https://github.com/catppuccin/whiskers/releases?q=whiskers). |
## Usage
@@ -50,7 +50,7 @@ Arguments:
Options:
-f, --flavor
Render a single flavor instead of all four
-
+
[possible values: latte, frappe, macchiato, mocha]
--color-overrides
@@ -61,7 +61,7 @@ Options:
--check []
Instead of creating an output, check it against an example
-
+
In single-output mode, a path to the example file must be provided. In multi-output mode, no path is required and, if one is provided, it will be ignored.
--dry-run
@@ -78,7 +78,7 @@ Options:
-o, --output-format
Output format of --list-functions
-
+
[default: json]
[possible values: json, yaml, markdown, markdown-table, plain]
@@ -111,18 +111,18 @@ The following variables are available for use in your templates:
#### Single-Flavor Mode
-| Variable | Description |
-| - | - |
-| `flavor` ([`Flavor`](#flavor)) | The flavor being templated. |
-| `rosewater`, `flamingo`, `pink`, [etc.](https://github.com/catppuccin/catppuccin#-palette) ([`Color`](#color)) | All colors of the flavor being templated. |
-| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. |
+| Variable | Description |
+| -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| `flavor` ([`Flavor`](#flavor)) | The flavor being templated. |
+| `rosewater`, `flamingo`, `pink`, [etc.](https://github.com/catppuccin/catppuccin#-palette) ([`Color`](#color)) | All colors of the flavor being templated. |
+| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. |
#### Multi-Flavor Mode
-| Variable | Description |
-| - | - |
-| `flavors` (Map\) | An array containing all of the named flavors, with every other context variable. |
-| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. |
+| Variable | Description |
+| --------------------------------------------- | ---------------------------------------------------------------------------------- |
+| `flavors` (Map\) | An array containing all of the named flavors, with every other context variable. |
+| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. |
#### Types
@@ -130,71 +130,71 @@ These types are designed to closely match the [palette.json](https://github.com/
##### Flavor
-| Field | Type | Description | Examples |
-| - | - | - | - |
-| `name` | `String` | The name of the flavor. | `"Latte"`, `"FrappΓ©"`, `"Macchiato"`, `"Mocha"` |
-| `identifier` | `String` | The identifier of the flavor. | `"latte"`, `"frappe"`, `"macchiato"`, `"mocha"` |
-| `emoji` | `char` | Emoji associated with the flavor. | `'π»'`, `'πͺ΄'`, `'πΊ'`, `'πΏ'` |
-| `order` | `u32` | Order of the flavor in the palette spec. | `0` to `3` |
-| `dark` | `bool` | Whether the flavor is dark. | `false` for Latte, `true` for others |
-| `light` | `bool` | Whether the flavor is light. | `true` for Latte, `false` for others |
-| `colors` | `Map` | A map of color identifiers to their respective values. | |
+| Field | Type | Description | Examples |
+| ------------ | -------------------- | ------------------------------------------------------ | ----------------------------------------------- |
+| `name` | `String` | The name of the flavor. | `"Latte"`, `"FrappΓ©"`, `"Macchiato"`, `"Mocha"` |
+| `identifier` | `String` | The identifier of the flavor. | `"latte"`, `"frappe"`, `"macchiato"`, `"mocha"` |
+| `emoji` | `char` | Emoji associated with the flavor. | `'π»'`, `'πͺ΄'`, `'πΊ'`, `'πΏ'` |
+| `order` | `u32` | Order of the flavor in the palette spec. | `0` to `3` |
+| `dark` | `bool` | Whether the flavor is dark. | `false` for Latte, `true` for others |
+| `light` | `bool` | Whether the flavor is light. | `true` for Latte, `false` for others |
+| `colors` | `Map` | A map of color identifiers to their respective values. | |
##### Color
-| Field | Type | Description | Examples |
-| - | - | - | - |
-| `name` | `String` | The name of the color. | `"Rosewater"`, `"Surface 0"`, `"Base"` |
-| `identifier` | `String` | The identifier of the color. | `"rosewater"`, `"surface0"`, `"base"` |
-| `order` | `u32` | Order of the color in the palette spec. | `0` to `25` |
-| `accent` | `bool` | Whether the color is an accent color. | |
-| `hex` | `String` | The color in hexadecimal format. | `"1e1e2e"` |
-| `rgb` | `RGB` | The color in RGB format. | |
-| `hsl` | `HSL` | The color in HSL format. | |
-| `opacity` | `u8` | The opacity of the color. | `0` to `255` |
+| Field | Type | Description | Examples |
+| ------------ | -------- | --------------------------------------- | -------------------------------------- |
+| `name` | `String` | The name of the color. | `"Rosewater"`, `"Surface 0"`, `"Base"` |
+| `identifier` | `String` | The identifier of the color. | `"rosewater"`, `"surface0"`, `"base"` |
+| `order` | `u32` | Order of the color in the palette spec. | `0` to `25` |
+| `accent` | `bool` | Whether the color is an accent color. | |
+| `hex` | `String` | The color in hexadecimal format. | `"1e1e2e"` |
+| `rgb` | `RGB` | The color in RGB format. | |
+| `hsl` | `HSL` | The color in HSL format. | |
+| `opacity` | `u8` | The opacity of the color. | `0` to `255` |
##### RGB
-| Field | Type | Description |
-| - | - | - |
-| `r` | `u8` | The red channel of the color. |
-| `g` | `u8` | The green channel of the color. |
-| `b` | `u8` | The blue channel of the color. |
+| Field | Type | Description |
+| ----- | ---- | ------------------------------- |
+| `r` | `u8` | The red channel of the color. |
+| `g` | `u8` | The green channel of the color. |
+| `b` | `u8` | The blue channel of the color. |
##### HSL
-| Field | Type | Description |
-| - | - | - |
-| `h` | `u16` | The hue of the color. |
-| `s` | `u8` | The saturation of the color. |
-| `l` | `u8` | The lightness of the color. |
+| Field | Type | Description |
+| ----- | ----- | ---------------------------- |
+| `h` | `u16` | The hue of the color. |
+| `s` | `u8` | The saturation of the color. |
+| `l` | `u8` | The lightness of the color. |
### Functions
-| Name | Description | Examples |
-| ----------- | ------------------------------------------------------------------------------ | ----------------------------------------------------- |
-| `if` | Return one value if a condition is true, and another if it's false | `if(cond=true, t=1, f=0)` β `1` |
-| `object` | Create an object from the input | `object(a=1, b=2)` β `{a: 1, b: 2}` |
-| `css_rgb` | Convert a color to an RGB CSS string | `css_rgb(color=red)` β `rgb(210, 15, 57)` |
-| `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` β `rgba(210, 15, 57, 1.00)` |
-| `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` β `hsl(347, 87%, 44%)` |
-| `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` β `hsla(347, 87%, 44%, 1.00)` |
-| `read_file` | Read and include the contents of a file, path is relative to the template file | `read_file(path="abc.txt")` β `abc` |
+| Name | Description | Examples |
+| ----------- | ------------------------------------------------------------------------------ | --------------------------------------------------- |
+| `if` | Return one value if a condition is true, and another if it's false | `if(cond=true, t=1, f=0)` β `1` |
+| `object` | Create an object from the input | `object(a=1, b=2)` β `{a: 1, b: 2}` |
+| `css_rgb` | Convert a color to an RGB CSS string | `css_rgb(color=red)` β `rgb(210, 15, 57)` |
+| `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` β `rgba(210, 15, 57, 1.00)` |
+| `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` β `hsl(347, 87%, 44%)` |
+| `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` β `hsla(347, 87%, 44%, 1.00)` |
+| `read_file` | Read and include the contents of a file, path is relative to the template file | `read_file(path="abc.txt")` β `abc` |
### Filters
-| Name | Description | Examples |
-| ---------------- | ---------------------------------------------------------------- | -------------------------------------------------- |
-| `add` | Add a value to a color | `red \| add(hue=30)` β `#ff6666` |
-| `sub` | Subtract a value from a color | `red \| sub(hue=30)` β `#d30f9b` |
-| `mod` | Modify a color | `red \| mod(lightness=80)` β `#f8a0b3` |
-| `mix` | Mix two colors together | `red \| mix(color=base, amount=0.5)` β `#e08097` |
-| `urlencode_lzma` | Serialize an object into a URL-safe string with LZMA compression | `red \| urlencode_lzma` β `#ff6666` |
-| `trunc` | Truncate a number to a certain number of places | `1.123456 \| trunc(places=3)` β `1.123` |
-| `css_rgb` | Convert a color to an RGB CSS string | `red \| css_rgb` β `rgb(210, 15, 57)` |
-| `css_rgba` | Convert a color to an RGBA CSS string | `red \| css_rgba` β `rgba(210, 15, 57, 1.00)` |
-| `css_hsl` | Convert a color to an HSL CSS string | `red \| css_hsl` β `hsl(347, 87%, 44%)` |
-| `css_hsla` | Convert a color to an HSLA CSS string | `red \| css_hsla` β `hsla(347, 87%, 44%, 1.00)` |
+| Name | Description | Examples |
+| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------ |
+| `add` | Add a value to a color | `red \| add(hue=30)` β `#ff6666` |
+| `sub` | Subtract a value from a color | `red \| sub(hue=30)` β `#d30f9b` |
+| `mod` | Modify a color | `red \| mod(lightness=80)` β `#f8a0b3` |
+| `mix` | Mix two colors together | `red \| mix(color=base, amount=0.5)` β `#e08097` |
+| `urlencode_lzma` | Serialize an object into a URL-safe string with LZMA compression | `red \| urlencode_lzma` β `#ff6666` |
+| `trunc` | Truncate a number to a certain number of places | `1.123456 \| trunc(places=3)` β `1.123` |
+| `css_rgb` | Convert a color to an RGB CSS string | `red \| css_rgb` β `rgb(210, 15, 57)` |
+| `css_rgba` | Convert a color to an RGBA CSS string | `red \| css_rgba` β `rgba(210, 15, 57, 1.00)` |
+| `css_hsl` | Convert a color to an HSL CSS string | `red \| css_hsl` β `hsl(347, 87%, 44%)` |
+| `css_hsla` | Convert a color to an HSLA CSS string | `red \| css_hsla` β `hsla(347, 87%, 44%, 1.00)` |
> [!NOTE]
> You also have access to all of Tera's own built-in filters and functions.
@@ -228,6 +228,32 @@ If the version key is not present, Whiskers will display a warning and attempt
to render the template anyway. However, it is recommended to always include the
version key to ensure compatibility with future versions of Whiskers.
+### Hex Format
+
+The format used for rendering colors in hexadecimal can be customised with the `hex_format` frontmatter variable.
+
+This string is rendered as a Tera template with the following context variables:
+
+- `r`, `g`, `b`, `a`: The red, green, blue, and alpha channels of the color as lowercase 2-digit hexadecimal strings.
+- `R`, `G`, `B`, `A`: As above, but uppercase.
+- `z`: The same as `a` if the color is not fully opaque, otherwise an empty string.
+- `Z`: As above, but uppercase.
+
+The default value of `hex_format` is `{{r}}{{g}}{{b}}{{z}}`.
+
+Example:
+
+```
+---
+whiskers:
+ version: "2.0.0"
+ hex_format: "0x{{B}}{{G}}{{R}}{{A}}"
+---
+{{red.hex}}
+```
+
+Running `whiskers example.tera -f mocha` produces the following output: `0xA88BF3FF`
+
### Frontmatter Variables
You can also include additional context variables in the templating process by
@@ -237,8 +263,8 @@ As a simple example, given the following template (`example.tera`):
```yaml
---
-app: 'Pepperjack'
-author: 'winston'
+app: "Pepperjack"
+author: "winston"
---
# Catppuccin for {{app}}
# by {{author}}
@@ -312,7 +338,7 @@ flag. This flag takes a JSON string like the following:
"mocha": {
"base": "000000",
"mantle": "010101",
- "crust": "020202",
+ "crust": "020202"
}
}
```
@@ -405,7 +431,7 @@ for each combination of the matrix iterables.
## Check Mode
-You can use Whiskers as a linter with *check mode*. To do so, set the `--check`
+You can use Whiskers as a linter with _check mode_. To do so, set the `--check`
option. Whiskers will render your template as per usual, but then instead of
printing the result it will check it against the expected output and fail with
exit code 1 if they differ.
diff --git a/src/filters.rs b/src/filters.rs
index 025220f..08ce5d7 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -23,7 +23,7 @@ pub fn mix(
.as_f64()
.ok_or_else(|| tera::Error::msg("blend amount must be a number"))?;
- let result = Color::mix(&base, &blend, amount);
+ let result = Color::mix(&base, &blend, amount)?;
Ok(tera::to_value(result)?)
}
@@ -35,16 +35,16 @@ pub fn modify(
let color: Color = tera::from_value(value.clone())?;
if let Some(hue) = args.get("hue") {
let hue = tera::from_value(hue.clone())?;
- Ok(tera::to_value(color.mod_hue(hue))?)
+ Ok(tera::to_value(color.mod_hue(hue)?)?)
} else if let Some(saturation) = args.get("saturation") {
let saturation = tera::from_value(saturation.clone())?;
- Ok(tera::to_value(color.mod_saturation(saturation))?)
+ Ok(tera::to_value(color.mod_saturation(saturation)?)?)
} else if let Some(lightness) = args.get("lightness") {
let lightness = tera::from_value(lightness.clone())?;
- Ok(tera::to_value(color.mod_lightness(lightness))?)
+ Ok(tera::to_value(color.mod_lightness(lightness)?)?)
} else if let Some(opacity) = args.get("opacity") {
let opacity = tera::from_value(opacity.clone())?;
- Ok(tera::to_value(color.mod_opacity(opacity))?)
+ Ok(tera::to_value(color.mod_opacity(opacity)?)?)
} else {
Ok(value.clone())
}
@@ -57,16 +57,16 @@ pub fn add(
let color: Color = tera::from_value(value.clone())?;
if let Some(hue) = args.get("hue") {
let hue = tera::from_value(hue.clone())?;
- Ok(tera::to_value(color.add_hue(hue))?)
+ Ok(tera::to_value(color.add_hue(hue)?)?)
} else if let Some(saturation) = args.get("saturation") {
let saturation = tera::from_value(saturation.clone())?;
- Ok(tera::to_value(color.add_saturation(saturation))?)
+ Ok(tera::to_value(color.add_saturation(saturation)?)?)
} else if let Some(lightness) = args.get("lightness") {
let lightness = tera::from_value(lightness.clone())?;
- Ok(tera::to_value(color.add_lightness(lightness))?)
+ Ok(tera::to_value(color.add_lightness(lightness)?)?)
} else if let Some(opacity) = args.get("opacity") {
let opacity = tera::from_value(opacity.clone())?;
- Ok(tera::to_value(color.add_opacity(opacity))?)
+ Ok(tera::to_value(color.add_opacity(opacity)?)?)
} else {
Ok(value.clone())
}
@@ -79,16 +79,16 @@ pub fn sub(
let color: Color = tera::from_value(value.clone())?;
if let Some(hue) = args.get("hue") {
let hue = tera::from_value(hue.clone())?;
- Ok(tera::to_value(color.sub_hue(hue))?)
+ Ok(tera::to_value(color.sub_hue(hue)?)?)
} else if let Some(saturation) = args.get("saturation") {
let saturation = tera::from_value(saturation.clone())?;
- Ok(tera::to_value(color.sub_saturation(saturation))?)
+ Ok(tera::to_value(color.sub_saturation(saturation)?)?)
} else if let Some(lightness) = args.get("lightness") {
let lightness = tera::from_value(lightness.clone())?;
- Ok(tera::to_value(color.sub_lightness(lightness))?)
+ Ok(tera::to_value(color.sub_lightness(lightness)?)?)
} else if let Some(opacity) = args.get("opacity") {
let opacity = tera::from_value(opacity.clone())?;
- Ok(tera::to_value(color.sub_opacity(opacity))?)
+ Ok(tera::to_value(color.sub_opacity(opacity)?)?)
} else {
Ok(value.clone())
}
diff --git a/src/main.rs b/src/main.rs
index df6ef34..93457d1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,19 +16,22 @@ use whiskers::{
context::merge_values,
frontmatter, markdown,
matrix::{self, Matrix},
- models, templating,
+ models::{self, HEX_FORMAT},
+ templating,
};
const FRONTMATTER_OPTIONS_SECTION: &str = "whiskers";
+fn default_hex_format() -> String {
+ "{{r}}{{g}}{{b}}{{z}}".to_string()
+}
+
#[derive(Default, Debug, serde::Deserialize)]
struct TemplateOptions {
version: Option,
matrix: Option,
filename: Option,
- hex_prefix: Option,
- #[serde(default)]
- capitalize_hex: bool,
+ hex_format: String,
}
impl TemplateOptions {
@@ -42,6 +45,7 @@ impl TemplateOptions {
version: Option,
matrix: Option>,
filename: Option,
+ hex_format: Option,
hex_prefix: Option,
#[serde(default)]
capitalize_hex: bool,
@@ -50,20 +54,47 @@ impl TemplateOptions {
if let Some(opts) = frontmatter.get(FRONTMATTER_OPTIONS_SECTION) {
let opts: RawTemplateOptions = tera::from_value(opts.clone())
.context("Frontmatter `whiskers` section is invalid")?;
+
let matrix = opts
.matrix
.map(|m| matrix::from_values(m, only_flavor))
.transpose()
.context("Frontmatter matrix is invalid")?;
+
+ // if there's no hex_format but there is hex_prefix and/or capitalize_hex,
+ // we can construct a hex_format from those.
+ let hex_format = if let Some(hex_format) = opts.hex_format {
+ hex_format
+ } else {
+ // throw a deprecation warning for hex_prefix and capitalize_hex
+ if opts.hex_prefix.is_some() {
+ eprintln!("Warning: `hex_prefix` is deprecated and will be removed in a future version. Use `hex_format` instead.");
+ }
+
+ if opts.capitalize_hex {
+ eprintln!("Warning: `capitalize_hex` is deprecated and will be removed in a future version. Use `hex_format` instead.");
+ }
+
+ let prefix = opts.hex_prefix.unwrap_or_default();
+ let components = default_hex_format();
+ if opts.capitalize_hex {
+ format!("{prefix}{}", components.to_uppercase())
+ } else {
+ format!("{prefix}{components}")
+ }
+ };
+
Ok(Self {
version: opts.version,
matrix,
filename: opts.filename,
- hex_prefix: opts.hex_prefix,
- capitalize_hex: opts.capitalize_hex,
+ hex_format,
})
} else {
- Ok(Self::default())
+ Ok(Self {
+ hex_format: default_hex_format(),
+ ..Default::default()
+ })
}
}
}
@@ -126,13 +157,13 @@ fn main() -> anyhow::Result<()> {
ctx.insert(key, &value);
}
+ HEX_FORMAT
+ .set(template_opts.hex_format)
+ .expect("can always set HEX_FORMAT");
+
// build the palette and add it to the templating context
- let palette = models::build_palette(
- template_opts.capitalize_hex,
- template_opts.hex_prefix.as_deref(),
- args.color_overrides.as_ref(),
- )
- .context("Palette context cannot be built")?;
+ let palette = models::build_palette(args.color_overrides.as_ref())
+ .context("Palette context cannot be built")?;
ctx.insert("flavors", &palette.flavors);
if let Some(flavor) = args.flavor {
diff --git a/src/models.rs b/src/models.rs
index b084de1..cdaa791 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -1,5 +1,9 @@
+use std::sync::OnceLock;
+
use css_colors::Color as _;
use indexmap::IndexMap;
+use serde_json::json;
+use tera::Tera;
use crate::cli::ColorOverrides;
@@ -49,39 +53,63 @@ pub struct HSL {
#[derive(Debug, thiserror::Error)]
pub enum Error {
+ #[error("Hex formatting failed: {0}")]
+ HexFormat(#[from] tera::Error),
#[error("Failed to parse hex color: {0}")]
ParseHex(#[from] std::num::ParseIntError),
}
-/// attempt to canonicalize a hex string, optionally capitalizing it and adding a prefix.
-fn format_hex(hex: &str, capitalize_hex_strings: bool, hex_prefix: Option<&str>) -> String {
- let hex = hex.trim_start_matches('#');
- let hex = if capitalize_hex_strings {
- hex.to_uppercase()
- } else {
- hex.to_string()
+// we have many functions that need to know how to format hex colors.
+// they can't know this at build time, as the format may be provided by the template.
+// we have little to no available state in many of these functions to store this information.
+// these possible solutions were evaluated:
+// 1. pass the format string to every function that needs it. this is cumbersome and error-prone.
+// 2. store the format string in the `Colour` struct, thus duplicating it for every color. this is wasteful.
+// 3. store it in a global static, and initialize it when the template frontmatter is read.
+// we opted for the third option, with a convenience macro for accessing it.
+pub static HEX_FORMAT: OnceLock = OnceLock::new();
+macro_rules! format_hex {
+ ($r:expr, $g:expr, $b: expr, $a: expr) => {
+ format_hex(
+ $r,
+ $g,
+ $b,
+ $a,
+ &*HEX_FORMAT.get().expect("HEX_FORMAT was never set"),
+ )
};
- if let Some(prefix) = hex_prefix {
- format!("{prefix}{hex}")
- } else {
- hex
- }
}
-fn color_from_hex(
- hex: &str,
- blueprint: &catppuccin::Color,
- capitalize_hex_strings: bool,
- hex_prefix: Option<&str>,
-) -> Result {
+/// attempt to canonicalize a hex string, using the provided format string.
+fn format_hex(r: u8, g: u8, b: u8, a: u8, hex_format: &str) -> tera::Result {
+ Tera::one_off(
+ hex_format,
+ &tera::Context::from_serialize(json!({
+ "r": format!("{r:02x}"),
+ "g": format!("{g:02x}"),
+ "b": format!("{b:02x}"),
+ "a": format!("{a:02x}"),
+ "z": if a == 0xFF { String::new() } else { format!("{a:02x}") },
+ "R": format!("{r:02X}"),
+ "G": format!("{g:02X}"),
+ "B": format!("{b:02X}"),
+ "A": format!("{a:02X}"),
+ "Z": if a == 0xFF { String::new() } else { format!("{a:02X}") },
+ }))
+ .expect("hardcoded context is always valid"),
+ true,
+ )
+}
+
+fn color_from_hex_override(hex: &str, blueprint: &catppuccin::Color) -> Result {
let i = u32::from_str_radix(hex, 16)?;
let rgb = RGB {
- r: ((i >> 16) & 0xff) as u8,
- g: ((i >> 8) & 0xff) as u8,
- b: (i & 0xff) as u8,
+ r: ((i >> 16) & 0xFF) as u8,
+ g: ((i >> 8) & 0xFF) as u8,
+ b: (i & 0xFF) as u8,
};
let hsl = css_colors::rgb(rgb.r, rgb.g, rgb.b).to_hsl();
- let hex = format_hex(hex, capitalize_hex_strings, hex_prefix);
+ let hex = format_hex!(rgb.r, rgb.g, rgb.b, 0xFF)?;
Ok(Color {
name: blueprint.name.to_string(),
identifier: blueprint.name.identifier().to_string(),
@@ -94,17 +122,13 @@ fn color_from_hex(
s: hsl.s.as_f32(),
l: hsl.l.as_f32(),
},
- opacity: 255,
+ opacity: 0xFF,
})
}
-fn color_from_catppuccin(
- color: &catppuccin::Color,
- capitalize_hex_strings: bool,
- hex_prefix: Option<&str>,
-) -> Color {
- let hex = format_hex(&color.hex.to_string(), capitalize_hex_strings, hex_prefix);
- Color {
+fn color_from_catppuccin(color: &catppuccin::Color) -> tera::Result {
+ let hex = format_hex!(color.rgb.r, color.rgb.g, color.rgb.b, 0xFF)?;
+ Ok(Color {
name: color.name.to_string(),
identifier: color.name.identifier().to_string(),
order: color.order,
@@ -121,15 +145,11 @@ fn color_from_catppuccin(
l: color.hsl.l as f32,
},
opacity: 255,
- }
+ })
}
/// Build a [`Palette`] from [`catppuccin::PALETTE`], optionally applying color overrides.
-pub fn build_palette(
- capitalize_hex_strings: bool,
- hex_prefix: Option<&str>,
- color_overrides: Option<&ColorOverrides>,
-) -> Result {
+pub fn build_palette(color_overrides: Option<&ColorOverrides>) -> Result {
// make a `Color` from a `catppuccin::Color`, taking into account `color_overrides`.
// overrides apply in this order:
// 1. base color
@@ -145,17 +165,17 @@ pub fn build_palette(
catppuccin::FlavorName::Mocha => &co.mocha,
})
.and_then(|o| o.get(color.name.identifier()).cloned())
- .map(|s| color_from_hex(&s, color, capitalize_hex_strings, hex_prefix))
+ .map(|s| color_from_hex_override(&s, color))
.transpose()?;
let all_override = color_overrides
.and_then(|co| co.all.get(color.name.identifier()).cloned())
- .map(|s| color_from_hex(&s, color, capitalize_hex_strings, hex_prefix))
+ .map(|s| color_from_hex_override(&s, color))
.transpose()?;
- Ok(flavor_override.or(all_override).unwrap_or_else(|| {
- color_from_catppuccin(color, capitalize_hex_strings, hex_prefix)
- }))
+ let base_color = color_from_catppuccin(color)?;
+
+ Ok(flavor_override.or(all_override).unwrap_or(base_color))
};
let mut flavors = IndexMap::new();
@@ -215,16 +235,12 @@ impl<'a> IntoIterator for &'a Flavor {
}
}
-fn rgb_to_hex(rgb: &RGB, opacity: u8) -> String {
- if opacity < 255 {
- format!("{:02x}{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b, opacity)
- } else {
- format!("{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b)
- }
+fn rgb_to_hex(rgb: &RGB, opacity: u8) -> tera::Result {
+ format_hex!(rgb.r, rgb.g, rgb.b, opacity)
}
impl Color {
- fn from_hsla(hsla: css_colors::HSLA, blueprint: &Self) -> Self {
+ fn from_hsla(hsla: css_colors::HSLA, blueprint: &Self) -> tera::Result {
let rgb = hsla.to_rgb();
let rgb = RGB {
r: rgb.r.as_u8(),
@@ -237,19 +253,19 @@ impl Color {
l: hsla.l.as_f32(),
};
let opacity = hsla.a.as_u8();
- Self {
+ Ok(Self {
name: blueprint.name.clone(),
identifier: blueprint.identifier.clone(),
order: blueprint.order,
accent: blueprint.accent,
- hex: rgb_to_hex(&rgb, opacity),
+ hex: rgb_to_hex(&rgb, opacity)?,
rgb,
hsl,
opacity,
- }
+ })
}
- fn from_rgba(rgba: css_colors::RGBA, blueprint: &Self) -> Self {
+ fn from_rgba(rgba: css_colors::RGBA, blueprint: &Self) -> tera::Result {
let hsl = rgba.to_hsl();
let rgb = RGB {
r: rgba.r.as_u8(),
@@ -262,20 +278,19 @@ impl Color {
l: hsl.l.as_f32(),
};
let opacity = rgba.a.as_u8();
- Self {
+ Ok(Self {
name: blueprint.name.clone(),
identifier: blueprint.identifier.clone(),
order: blueprint.order,
accent: blueprint.accent,
- hex: rgb_to_hex(&rgb, opacity),
+ hex: rgb_to_hex(&rgb, opacity)?,
rgb,
hsl,
opacity,
- }
+ })
}
- #[must_use]
- pub fn mix(base: &Self, blend: &Self, amount: f64) -> Self {
+ pub fn mix(base: &Self, blend: &Self, amount: f64) -> tera::Result {
let amount = (amount * 100.0).clamp(0.0, 100.0).round() as u8;
let blueprint = base;
let base: css_colors::RGBA = base.into();
@@ -285,99 +300,87 @@ impl Color {
Self::from_rgba(result, blueprint)
}
- #[must_use]
- pub fn mod_hue(&self, hue: i32) -> Self {
+ pub fn mod_hue(&self, hue: i32) -> tera::Result {
let mut hsl: css_colors::HSL = self.into();
hsl.h = css_colors::deg(hue);
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn add_hue(&self, hue: i32) -> Self {
+ pub fn add_hue(&self, hue: i32) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.spin(css_colors::deg(hue));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn sub_hue(&self, hue: i32) -> Self {
+ pub fn sub_hue(&self, hue: i32) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.spin(-css_colors::deg(hue));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn mod_saturation(&self, saturation: u8) -> Self {
+ pub fn mod_saturation(&self, saturation: u8) -> tera::Result {
let mut hsl: css_colors::HSL = self.into();
hsl.s = css_colors::percent(saturation);
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn add_saturation(&self, saturation: u8) -> Self {
+ pub fn add_saturation(&self, saturation: u8) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.saturate(css_colors::percent(saturation));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn sub_saturation(&self, saturation: u8) -> Self {
+ pub fn sub_saturation(&self, saturation: u8) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.desaturate(css_colors::percent(saturation));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn mod_lightness(&self, lightness: u8) -> Self {
+ pub fn mod_lightness(&self, lightness: u8) -> tera::Result {
let mut hsl: css_colors::HSL = self.into();
hsl.l = css_colors::percent(lightness);
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn add_lightness(&self, lightness: u8) -> Self {
+ pub fn add_lightness(&self, lightness: u8) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.lighten(css_colors::percent(lightness));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn sub_lightness(&self, lightness: u8) -> Self {
+ pub fn sub_lightness(&self, lightness: u8) -> tera::Result {
let hsl: css_colors::HSL = self.into();
let hsl = hsl.darken(css_colors::percent(lightness));
Self::from_hsla(hsl.to_hsla(), self)
}
- #[must_use]
- pub fn mod_opacity(&self, opacity: f32) -> Self {
+ pub fn mod_opacity(&self, opacity: f32) -> tera::Result {
let opacity = (opacity * 255.0).round() as u8;
- Self {
+ Ok(Self {
opacity,
- hex: rgb_to_hex(&self.rgb, opacity),
+ hex: rgb_to_hex(&self.rgb, opacity)?,
..self.clone()
- }
+ })
}
- #[must_use]
- pub fn add_opacity(&self, opacity: f32) -> Self {
+ pub fn add_opacity(&self, opacity: f32) -> tera::Result {
let opacity = (opacity * 255.0).round() as u8;
let opacity = self.opacity.saturating_add(opacity);
- Self {
+ Ok(Self {
opacity,
- hex: rgb_to_hex(&self.rgb, opacity),
+ hex: rgb_to_hex(&self.rgb, opacity)?,
..self.clone()
- }
+ })
}
- #[must_use]
- pub fn sub_opacity(&self, opacity: f32) -> Self {
+ pub fn sub_opacity(&self, opacity: f32) -> tera::Result {
let opacity = (opacity * 255.0).round() as u8;
let opacity = self.opacity.saturating_sub(opacity);
- Self {
+ Ok(Self {
opacity,
- hex: rgb_to_hex(&self.rgb, opacity),
+ hex: rgb_to_hex(&self.rgb, opacity)?,
..self.clone()
- }
+ })
}
}
diff --git a/tests/cli.rs b/tests/cli.rs
index 26570b6..930c50d 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -88,6 +88,28 @@ mod happy_path {
.success()
.stdout(predicate::str::contains("it worked!"));
}
+
+ /// Test that the default hex format is rrggbb and full alpha is hidden
+ #[test]
+ fn test_default_hex_format() {
+ let mut cmd = Command::cargo_bin("whiskers").expect("binary exists");
+ let assert = cmd.args(["tests/fixtures/hexformat/default.tera"]).assert();
+ assert
+ .success()
+ .stdout(include_str!("fixtures/hexformat/default.txt"));
+ }
+
+ /// Test that the CLI can render a template with a custom hex format
+ #[test]
+ fn test_custom_hex_format() {
+ let mut cmd = Command::cargo_bin("whiskers").expect("binary exists");
+ let assert = cmd
+ .args(["tests/fixtures/hexformat/custom.tera", "-f", "latte"])
+ .assert();
+ assert
+ .success()
+ .stdout(include_str!("fixtures/hexformat/custom.txt"));
+ }
}
#[cfg(test)]
diff --git a/tests/fixtures/hexformat/custom.tera b/tests/fixtures/hexformat/custom.tera
new file mode 100644
index 0000000..9b732db
--- /dev/null
+++ b/tests/fixtures/hexformat/custom.tera
@@ -0,0 +1,10 @@
+---
+# test a custom hex format
+whiskers:
+ version: "2"
+ hex_format: "0x{{B}}{{G}}{{R}}{{A}}"
+---
+{%- set translucent_red = flavors.latte.colors.red | mod(opacity=0.5) -%}
+{{flavors.latte.colors.red.hex}} == 0x390FD2FF
+{{flavors.macchiato.colors.sky.hex}} == 0xE3D791FF
+{{translucent_red.hex}} == 0x390FD280
diff --git a/tests/fixtures/hexformat/custom.txt b/tests/fixtures/hexformat/custom.txt
new file mode 100644
index 0000000..29614e9
--- /dev/null
+++ b/tests/fixtures/hexformat/custom.txt
@@ -0,0 +1,3 @@
+0x390FD2FF == 0x390FD2FF
+0xE3D791FF == 0xE3D791FF
+0x390FD280 == 0x390FD280
diff --git a/tests/fixtures/hexformat/default.tera b/tests/fixtures/hexformat/default.tera
new file mode 100644
index 0000000..fbbb300
--- /dev/null
+++ b/tests/fixtures/hexformat/default.tera
@@ -0,0 +1,9 @@
+---
+# test the default hex format
+whiskers:
+ version: "2"
+---
+{%- set translucent_red = flavors.latte.colors.red | mod(opacity=0.5) -%}
+{{flavors.latte.colors.red.hex}} == d20f39
+{{flavors.macchiato.colors.sky.hex}} == 91d7e3
+{{translucent_red.hex}} == d20f3980
diff --git a/tests/fixtures/hexformat/default.txt b/tests/fixtures/hexformat/default.txt
new file mode 100644
index 0000000..dcfa168
--- /dev/null
+++ b/tests/fixtures/hexformat/default.txt
@@ -0,0 +1,3 @@
+d20f39 == d20f39
+91d7e3 == 91d7e3
+d20f3980 == d20f3980