Skip to content

Commit 4968b0f

Browse files
committed
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.
1 parent b4b400b commit 4968b0f

File tree

11 files changed

+1226
-188
lines changed

11 files changed

+1226
-188
lines changed

Cargo.lock

Lines changed: 653 additions & 158 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deny.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ feature-depth = 1
1313
[advisories]
1414
ignore = [
1515
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
16-
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" }
16+
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
17+
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
1718
]
1819

1920
[licenses]
@@ -93,4 +94,6 @@ allow-git = [
9394
"https://github.com/LGUG2Z/catppuccin-egui",
9495
"https://github.com/LGUG2Z/windows-icons",
9596
"https://github.com/LGUG2Z/win32-display-data",
97+
"https://github.com/LGUG2Z/flavours",
98+
"https://github.com/LGUG2Z/base16_color_scheme",
9699
]

komorebi-themes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ serde = { workspace = true }
1313
serde_variant = "0.1"
1414
strum = { workspace = true }
1515
hex_color = { version = "3", features = ["serde"] }
16+
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
1617

1718
[features]
1819
default = ["schemars"]

komorebi-themes/src/colour.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl From<Colour> for Color32 {
5757
}
5858

5959
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
60-
pub struct Hex(HexColor);
60+
pub struct Hex(pub HexColor);
6161

6262
#[cfg(feature = "schemars")]
6363
impl schemars::JsonSchema for Hex {

komorebi-themes/src/generator.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::colour::Colour;
2+
use crate::colour::Hex;
3+
use crate::Base16ColourPalette;
4+
use hex_color::HexColor;
5+
use std::collections::VecDeque;
6+
use std::fmt::Display;
7+
use std::fmt::Formatter;
8+
use std::path::Path;
9+
10+
use serde::Deserialize;
11+
use serde::Serialize;
12+
13+
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
14+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15+
pub enum ThemeVariant {
16+
#[default]
17+
Dark,
18+
Light,
19+
}
20+
21+
impl Display for ThemeVariant {
22+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23+
match self {
24+
ThemeVariant::Dark => write!(f, "dark"),
25+
ThemeVariant::Light => write!(f, "light"),
26+
}
27+
}
28+
}
29+
30+
impl From<ThemeVariant> for flavours::operations::generate::Mode {
31+
fn from(value: ThemeVariant) -> Self {
32+
match value {
33+
ThemeVariant::Dark => Self::Dark,
34+
ThemeVariant::Light => Self::Light,
35+
}
36+
}
37+
}
38+
39+
pub fn generate_base16_palette(
40+
image_path: &Path,
41+
variant: ThemeVariant,
42+
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
43+
Base16ColourPalette::try_from(
44+
&flavours::operations::generate::generate(image_path, variant.into(), false)
45+
.unwrap_or_default(),
46+
)
47+
}
48+
49+
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
50+
type Error = hex_color::ParseHexColorError;
51+
52+
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
53+
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
54+
if fixed.len() != 16 {
55+
return Err(hex_color::ParseHexColorError::Empty);
56+
}
57+
58+
Ok(Self {
59+
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
60+
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
61+
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
62+
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
63+
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
64+
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
65+
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
66+
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
67+
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
68+
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
69+
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
70+
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
71+
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
72+
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
73+
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
74+
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
75+
})
76+
}
77+
}

komorebi-themes/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
#![allow(clippy::missing_errors_doc)]
33

44
pub mod colour;
5+
mod generator;
6+
7+
pub use generator::generate_base16_palette;
8+
pub use generator::ThemeVariant;
59

610
use schemars::JsonSchema;
711
use serde::Deserialize;

komorebi/src/static_config.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,57 @@ pub struct BorderColours {
124124
pub unfocused_locked: Option<Colour>,
125125
}
126126

127+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
128+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
129+
pub struct ThemeOptions {
130+
/// Specify Light or Dark variant for theme generation (default: Dark)
131+
#[serde(skip_serializing_if = "Option::is_none")]
132+
pub theme_variant: Option<komorebi_themes::ThemeVariant>,
133+
/// Border colour when the container contains a single window (default: Base0D)
134+
#[serde(skip_serializing_if = "Option::is_none")]
135+
pub single_border: Option<komorebi_themes::Base16Value>,
136+
/// Border colour when the container contains multiple windows (default: Base0B)
137+
#[serde(skip_serializing_if = "Option::is_none")]
138+
pub stack_border: Option<komorebi_themes::Base16Value>,
139+
/// Border colour when the container is in monocle mode (default: Base0F)
140+
#[serde(skip_serializing_if = "Option::is_none")]
141+
pub monocle_border: Option<komorebi_themes::Base16Value>,
142+
/// Border colour when the window is floating (default: Base09)
143+
#[serde(skip_serializing_if = "Option::is_none")]
144+
pub floating_border: Option<komorebi_themes::Base16Value>,
145+
/// Border colour when the container is unfocused (default: Base01)
146+
#[serde(skip_serializing_if = "Option::is_none")]
147+
pub unfocused_border: Option<komorebi_themes::Base16Value>,
148+
/// Border colour when the container is unfocused and locked (default: Base08)
149+
#[serde(skip_serializing_if = "Option::is_none")]
150+
pub unfocused_locked_border: Option<komorebi_themes::Base16Value>,
151+
/// Stackbar focused tab text colour (default: Base0B)
152+
#[serde(skip_serializing_if = "Option::is_none")]
153+
pub stackbar_focused_text: Option<komorebi_themes::Base16Value>,
154+
/// Stackbar unfocused tab text colour (default: Base05)
155+
#[serde(skip_serializing_if = "Option::is_none")]
156+
pub stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
157+
/// Stackbar tab background colour (default: Base01)
158+
#[serde(skip_serializing_if = "Option::is_none")]
159+
pub stackbar_background: Option<komorebi_themes::Base16Value>,
160+
/// Komorebi status bar accent (default: Base0D)
161+
#[serde(skip_serializing_if = "Option::is_none")]
162+
pub bar_accent: Option<komorebi_themes::Base16Value>,
163+
}
164+
165+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
166+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
167+
pub struct Wallpaper {
168+
/// Path to the wallpaper image file
169+
pub path: PathBuf,
170+
/// Generate and apply Base16 theme for this wallpaper (default: true)
171+
#[serde(skip_serializing_if = "Option::is_none")]
172+
pub generate_theme: Option<bool>,
173+
/// Specify Light or Dark variant for theme generation (default: Dark)
174+
#[serde(skip_serializing_if = "Option::is_none")]
175+
pub theme_options: Option<ThemeOptions>,
176+
}
177+
127178
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
128179
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
129180
pub struct WorkspaceConfig {
@@ -171,6 +222,9 @@ pub struct WorkspaceConfig {
171222
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
172223
#[serde(skip_serializing_if = "Option::is_none")]
173224
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
225+
/// Specify a wallpaper for this workspace
226+
#[serde(skip_serializing_if = "Option::is_none")]
227+
pub wallpaper: Option<Wallpaper>,
174228
}
175229

176230
impl From<&Workspace> for WorkspaceConfig {
@@ -247,6 +301,7 @@ impl From<&Workspace> for WorkspaceConfig {
247301
float_override: *value.float_override(),
248302
layout_flip: value.layout_flip(),
249303
floating_layer_behaviour: Option::from(*value.floating_layer_behaviour()),
304+
wallpaper: None,
250305
}
251306
}
252307
}

komorebi/src/window_manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ impl From<&WindowManager> for State {
348348
floating_layer_behaviour: workspace.floating_layer_behaviour,
349349
globals: workspace.globals,
350350
locked_containers: workspace.locked_containers.clone(),
351+
wallpaper: workspace.wallpaper.clone(),
351352
workspace_config: None,
352353
})
353354
.collect::<VecDeque<_>>();

komorebi/src/windows_api.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
use color_eyre::eyre::anyhow;
2+
use color_eyre::eyre::bail;
3+
use color_eyre::eyre::Error;
4+
use color_eyre::Result;
15
use core::ffi::c_void;
26
use std::collections::HashMap;
37
use std::collections::VecDeque;
48
use std::convert::TryFrom;
59
use std::mem::size_of;
6-
7-
use color_eyre::eyre::anyhow;
8-
use color_eyre::eyre::bail;
9-
use color_eyre::eyre::Error;
10-
use color_eyre::Result;
10+
use std::path::Path;
1111
use windows::core::Result as WindowsCrateResult;
1212
use windows::core::PCWSTR;
1313
use windows::core::PWSTR;
@@ -47,6 +47,8 @@ use windows::Win32::Graphics::Gdi::HMONITOR;
4747
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
4848
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
4949
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
50+
use windows::Win32::System::Com::CoCreateInstance;
51+
use windows::Win32::System::Com::CLSCTX_ALL;
5052
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
5153
use windows::Win32::System::Power::RegisterPowerSettingNotification;
5254
use windows::Win32::System::Power::HPOWERNOTIFY;
@@ -72,6 +74,9 @@ use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
7274
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
7375
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
7476
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
77+
use windows::Win32::UI::Shell::DesktopWallpaper;
78+
use windows::Win32::UI::Shell::IDesktopWallpaper;
79+
use windows::Win32::UI::Shell::DWPOS_FILL;
7580
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
7681
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
7782
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
@@ -141,6 +146,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
141146
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
142147
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
143148
use windows_core::BOOL;
149+
use windows_core::HSTRING;
144150

145151
use crate::core::Rect;
146152

@@ -1345,4 +1351,22 @@ impl WindowsApi {
13451351
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
13461352
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
13471353
}
1354+
1355+
pub fn set_wallpaper(path: &Path) -> Result<()> {
1356+
let path = path.canonicalize()?;
1357+
1358+
let wallpaper: IDesktopWallpaper =
1359+
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
1360+
1361+
let wallpaper_path = HSTRING::from(path.to_str().unwrap_or_default());
1362+
unsafe {
1363+
wallpaper.SetPosition(DWPOS_FILL)?;
1364+
}
1365+
1366+
// Set the wallpaper
1367+
unsafe {
1368+
wallpaper.SetWallpaper(PCWSTR::null(), PCWSTR::from_raw(wallpaper_path.as_ptr()))?;
1369+
}
1370+
Ok(())
1371+
}
13481372
}

0 commit comments

Comments
 (0)