1use std::{collections::BTreeMap, fmt, path::PathBuf, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5pub type HexColor = String;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "kebab-case")]
9pub enum ThemeMode {
10 Light,
11 #[default]
12 Dark,
13}
14
15impl ThemeMode {
16 pub const fn as_str(self) -> &'static str {
17 match self {
18 Self::Light => "light",
19 Self::Dark => "dark",
20 }
21 }
22
23 pub const fn default_background_tone(self) -> u8 {
24 match self {
25 Self::Light => 98,
26 Self::Dark => 10,
27 }
28 }
29
30 pub const fn default_surface_tone(self) -> u8 {
31 match self {
32 Self::Light => 95,
33 Self::Dark => 14,
34 }
35 }
36
37 pub const fn default_text_tone(self) -> u8 {
38 match self {
39 Self::Light => 12,
40 Self::Dark => 94,
41 }
42 }
43
44 pub const fn default_muted_text_tone(self) -> u8 {
45 match self {
46 Self::Light => 30,
47 Self::Dark => 80,
48 }
49 }
50}
51
52impl fmt::Display for ThemeMode {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(self.as_str())
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
59#[serde(rename_all = "kebab-case")]
60pub enum ContrastStrategy {
61 #[default]
62 RelativeLuminance,
63 ApcaExperimental,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
67#[serde(rename_all = "kebab-case")]
68pub enum ChromaStrategy {
69 Subtle,
70 #[default]
71 Normal,
72 Vibrant,
73 Muted,
74 Industrial,
75}
76
77impl ChromaStrategy {
78 pub const fn as_str(self) -> &'static str {
79 match self {
80 Self::Subtle => "subtle",
81 Self::Normal => "normal",
82 Self::Vibrant => "vibrant",
83 Self::Muted => "muted",
84 Self::Industrial => "industrial",
85 }
86 }
87}
88
89impl fmt::Display for ChromaStrategy {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95impl ContrastStrategy {
96 pub const fn as_str(self) -> &'static str {
97 match self {
98 Self::RelativeLuminance => "relative-luminance",
99 Self::ApcaExperimental => "apca-experimental",
100 }
101 }
102}
103
104impl fmt::Display for ContrastStrategy {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str(self.as_str())
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
111#[serde(rename_all = "kebab-case")]
112pub enum RenderTarget {
113 Gtk,
114 Gtk3,
115 Gtk4,
116 Hyprland,
117 HyprlandLua,
118 Kitty,
119 Css,
120 Waybar,
121 Rofi,
122 Alacritty,
123 Foot,
124 Ghostty,
125 Editor,
126 Zed,
127 Chromium,
128 GoogleChrome,
129 HeliumBrowser,
130 #[serde(rename = "kcolorscheme")]
131 KColorScheme,
132 Micro,
133 Qt5,
134 Qt6,
135 #[serde(rename = "vscode")]
136 VsCode,
137 #[serde(rename = "vscode-insiders")]
138 VsCodeInsiders,
139}
140
141impl RenderTarget {
142 pub const MVP: [Self; 4] = [Self::Gtk, Self::Hyprland, Self::Kitty, Self::Css];
143
144 pub const fn as_str(self) -> &'static str {
145 match self {
146 Self::Gtk => "gtk",
147 Self::Gtk3 => "gtk3",
148 Self::Gtk4 => "gtk4",
149 Self::Hyprland => "hyprland",
150 Self::HyprlandLua => "hyprland-lua",
151 Self::Kitty => "kitty",
152 Self::Css => "css",
153 Self::Waybar => "waybar",
154 Self::Rofi => "rofi",
155 Self::Alacritty => "alacritty",
156 Self::Foot => "foot",
157 Self::Ghostty => "ghostty",
158 Self::Editor => "editor",
159 Self::Zed => "zed",
160 Self::Chromium => "chromium",
161 Self::GoogleChrome => "google-chrome",
162 Self::HeliumBrowser => "helium-browser",
163 Self::KColorScheme => "kcolorscheme",
164 Self::Micro => "micro",
165 Self::Qt5 => "qt5",
166 Self::Qt6 => "qt6",
167 Self::VsCode => "vscode",
168 Self::VsCodeInsiders => "vscode-insiders",
169 }
170 }
171
172 pub const fn file_name(self) -> &'static str {
173 match self {
174 Self::Gtk => "gtk.css",
175 Self::Gtk3 | Self::Gtk4 => "gtk.css",
176 Self::Hyprland => "hyprland.conf",
177 Self::HyprlandLua => "hypr-chromasync.lua",
178 Self::Kitty => "kitty.conf",
179 Self::Css => "theme.css",
180 Self::Waybar => "style.css",
181 Self::Rofi => "config.rasi",
182 Self::Alacritty => "alacritty.toml",
183 Self::Foot => "foot.ini",
184 Self::Ghostty => "chromasync.ghostty",
185 Self::Editor => "theme.json",
186 Self::Zed => "chromasync.json",
187 Self::Chromium | Self::GoogleChrome | Self::HeliumBrowser => "manifest.json",
188 Self::KColorScheme => "chromasync.colors",
189 Self::Micro => "chromasync.micro",
190 Self::Qt5 | Self::Qt6 => "chromasync.conf",
191 Self::VsCode | Self::VsCodeInsiders => "package.json",
192 }
193 }
194}
195
196impl fmt::Display for RenderTarget {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 f.write_str(self.as_str())
199 }
200}
201
202#[derive(
203 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
204)]
205#[serde(rename_all = "snake_case")]
206pub enum PaletteFamilyName {
207 #[default]
208 Primary,
209 Secondary,
210 Tertiary,
211 Neutral,
212 NeutralVariant,
213 Error,
214 Success,
215 Warning,
216 Info,
217}
218
219impl PaletteFamilyName {
220 pub const ALL: [Self; 9] = [
221 Self::Primary,
222 Self::Secondary,
223 Self::Tertiary,
224 Self::Neutral,
225 Self::NeutralVariant,
226 Self::Error,
227 Self::Success,
228 Self::Warning,
229 Self::Info,
230 ];
231
232 pub const fn as_str(self) -> &'static str {
233 match self {
234 Self::Primary => "primary",
235 Self::Secondary => "secondary",
236 Self::Tertiary => "tertiary",
237 Self::Neutral => "neutral",
238 Self::NeutralVariant => "neutral_variant",
239 Self::Error => "error",
240 Self::Success => "success",
241 Self::Warning => "warning",
242 Self::Info => "info",
243 }
244 }
245}
246
247impl fmt::Display for PaletteFamilyName {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 f.write_str(self.as_str())
250 }
251}
252
253impl FromStr for PaletteFamilyName {
254 type Err = ();
255
256 fn from_str(value: &str) -> Result<Self, Self::Err> {
257 match value {
258 "primary" => Ok(Self::Primary),
259 "secondary" => Ok(Self::Secondary),
260 "tertiary" => Ok(Self::Tertiary),
261 "neutral" => Ok(Self::Neutral),
262 "neutral_variant" => Ok(Self::NeutralVariant),
263 "error" => Ok(Self::Error),
264 "success" => Ok(Self::Success),
265 "warning" => Ok(Self::Warning),
266 "info" => Ok(Self::Info),
267 _ => Err(()),
268 }
269 }
270}
271
272#[derive(
273 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
274)]
275#[serde(rename_all = "snake_case")]
276pub enum SemanticTokenName {
277 #[default]
278 Bg,
279 BgSecondary,
280 Surface,
281 SurfaceElevated,
282 Text,
283 TextMuted,
284 Border,
285 BorderStrong,
286 Accent,
287 AccentHover,
288 AccentActive,
289 AccentFg,
290 Selection,
291 Link,
292 Success,
293 Warning,
294 Error,
295}
296
297impl SemanticTokenName {
298 pub const ALL: [Self; 17] = [
299 Self::Bg,
300 Self::BgSecondary,
301 Self::Surface,
302 Self::SurfaceElevated,
303 Self::Text,
304 Self::TextMuted,
305 Self::Border,
306 Self::BorderStrong,
307 Self::Accent,
308 Self::AccentHover,
309 Self::AccentActive,
310 Self::AccentFg,
311 Self::Selection,
312 Self::Link,
313 Self::Success,
314 Self::Warning,
315 Self::Error,
316 ];
317
318 pub const fn as_str(self) -> &'static str {
319 match self {
320 Self::Bg => "bg",
321 Self::BgSecondary => "bg_secondary",
322 Self::Surface => "surface",
323 Self::SurfaceElevated => "surface_elevated",
324 Self::Text => "text",
325 Self::TextMuted => "text_muted",
326 Self::Border => "border",
327 Self::BorderStrong => "border_strong",
328 Self::Accent => "accent",
329 Self::AccentHover => "accent_hover",
330 Self::AccentActive => "accent_active",
331 Self::AccentFg => "accent_fg",
332 Self::Selection => "selection",
333 Self::Link => "link",
334 Self::Success => "success",
335 Self::Warning => "warning",
336 Self::Error => "error",
337 }
338 }
339}
340
341impl fmt::Display for SemanticTokenName {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 f.write_str(self.as_str())
344 }
345}
346
347impl FromStr for SemanticTokenName {
348 type Err = ();
349
350 fn from_str(value: &str) -> Result<Self, Self::Err> {
351 match value {
352 "bg" => Ok(Self::Bg),
353 "bg_secondary" => Ok(Self::BgSecondary),
354 "surface" => Ok(Self::Surface),
355 "surface_elevated" => Ok(Self::SurfaceElevated),
356 "text" => Ok(Self::Text),
357 "text_muted" => Ok(Self::TextMuted),
358 "border" => Ok(Self::Border),
359 "border_strong" => Ok(Self::BorderStrong),
360 "accent" => Ok(Self::Accent),
361 "accent_hover" => Ok(Self::AccentHover),
362 "accent_active" => Ok(Self::AccentActive),
363 "accent_fg" => Ok(Self::AccentFg),
364 "selection" => Ok(Self::Selection),
365 "link" => Ok(Self::Link),
366 "success" => Ok(Self::Success),
367 "warning" => Ok(Self::Warning),
368 "error" => Ok(Self::Error),
369 _ => Err(()),
370 }
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375pub struct ToneSample {
376 pub tone: u8,
377 pub hex: HexColor,
378}
379
380#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
381pub struct PaletteFamily {
382 pub name: PaletteFamilyName,
383 pub hue: f32,
384 pub base_chroma: f32,
385 #[serde(default)]
386 pub tones: Vec<ToneSample>,
387 pub dominance: Option<f32>,
388 pub source_region: Option<String>,
389 pub seed_index: Option<usize>,
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
393pub struct GeneratedPalette {
394 pub seed: HexColor,
395 pub mode: ThemeMode,
396 pub chroma: ChromaStrategy,
397 pub families: BTreeMap<PaletteFamilyName, PaletteFamily>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct GenerationRequest {
402 pub seed: Option<String>,
403 pub wallpaper: Option<PathBuf>,
404 pub template: Option<String>,
405 pub mode: ThemeMode,
406 #[serde(default)]
407 pub contrast: ContrastStrategy,
408 #[serde(default)]
409 pub chroma: ChromaStrategy,
410 #[serde(default)]
411 pub targets: Vec<String>,
412 pub output_dir: PathBuf,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416pub struct GeneratedArtifact {
417 pub target: String,
418 pub file_name: String,
419 pub content: String,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
423pub struct GenerationContext {
424 pub mode: ThemeMode,
425 pub template_name: String,
426 pub chroma: ChromaStrategy,
427 pub output_dir: PathBuf,
428 pub seed: Option<String>,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
432pub struct ThemePack {
433 pub name: String,
434 pub version: String,
435 pub description: Option<String>,
436 pub author: Option<String>,
437 pub license: Option<String>,
438 pub homepage: Option<String>,
439 pub root_dir: PathBuf,
440 #[serde(default)]
441 pub template_dirs: Vec<PathBuf>,
442 #[serde(default)]
443 pub target_dirs: Vec<PathBuf>,
444}
445
446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
447pub struct TemplateTokenRule {
448 pub family: PaletteFamilyName,
449 pub tone: f32,
450 pub chroma: Option<f32>,
451 pub chroma_scale: Option<f32>,
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
455pub struct TemplateDefinition {
456 pub name: String,
457 pub mode: ThemeMode,
458 pub description: Option<String>,
459 #[serde(default)]
460 pub tokens: BTreeMap<SemanticTokenName, TemplateTokenRule>,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
464pub struct SemanticTokens {
465 pub bg: HexColor,
466 pub bg_secondary: HexColor,
467 pub surface: HexColor,
468 pub surface_elevated: HexColor,
469 pub text: HexColor,
470 pub text_muted: HexColor,
471 pub border: HexColor,
472 pub border_strong: HexColor,
473 pub accent: HexColor,
474 pub accent_hover: HexColor,
475 pub accent_active: HexColor,
476 pub accent_fg: HexColor,
477 pub selection: HexColor,
478 pub link: HexColor,
479 pub success: HexColor,
480 pub warning: HexColor,
481 pub error: HexColor,
482}