feat(themes): generate base16 palette from wallpaper

This commit adds a new Wallpaper configuration option to the
WorkspaceConfig, allowing the user to specify a path to a wallpaper
image file, and to specify whether to generate a base16 palette from the
colours of that image file.

Theme generation is enabled by default when a wallpaper is selected.

A set of theme options can be given to customize the colours of various
borders and accents.

The themes generated are also plumbed through to the komorebi-bar.

The palette generation algorithm from "flavours" (which has been forked
and updated) is quite slow, so the outputs are cached to file in
DATA_DIR, and keyed by ThemeVariant (Light or Dark).

The Win32 COM API to set the desktop wallpaper is also quite slow,
however this calls is async so it doesn't block komorebi's main thread.
This commit is contained in:
LGUG2Z
2025-04-01 17:43:33 -07:00
parent b4b400b236
commit 4968b0fe37
11 changed files with 1227 additions and 189 deletions

View File

@@ -13,6 +13,7 @@ serde = { workspace = true }
serde_variant = "0.1"
strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]

View File

@@ -57,7 +57,7 @@ impl From<Colour> for Color32 {
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(HexColor);
pub struct Hex(pub HexColor);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex {

View File

@@ -0,0 +1,77 @@
use crate::colour::Colour;
use crate::colour::Hex;
use crate::Base16ColourPalette;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ThemeVariant {
#[default]
Dark,
Light,
}
impl Display for ThemeVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ThemeVariant::Dark => write!(f, "dark"),
ThemeVariant::Light => write!(f, "light"),
}
}
}
impl From<ThemeVariant> for flavours::operations::generate::Mode {
fn from(value: ThemeVariant) -> Self {
match value {
ThemeVariant::Dark => Self::Dark,
ThemeVariant::Light => Self::Light,
}
}
}
pub fn generate_base16_palette(
image_path: &Path,
variant: ThemeVariant,
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
Base16ColourPalette::try_from(
&flavours::operations::generate::generate(image_path, variant.into(), false)
.unwrap_or_default(),
)
}
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
type Error = hex_color::ParseHexColorError;
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
if fixed.len() != 16 {
return Err(hex_color::ParseHexColorError::Empty);
}
Ok(Self {
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
})
}
}

View File

@@ -2,6 +2,10 @@
#![allow(clippy::missing_errors_doc)]
pub mod colour;
mod generator;
pub use generator::generate_base16_palette;
pub use generator::ThemeVariant;
use schemars::JsonSchema;
use serde::Deserialize;