From af541604b2200bc43c90b37815adf933c943399c Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 24 Aug 2026 20:31:16 +0200 Subject: [PATCH 1/5] refactor(theme): depend on colony-ui instead of carrying the palettes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/ui/theme.rs was 2935 lines, 100 KB of it hand-maintained colour constants. It is now 14 lines that re-export colony-ui, whose palettes are generated from the design tokens in Project-Colony-Resources. That file was the reason the Resources repository exists: SphereCord downloaded it over HTTP and regex-parsed the Rust source to recover the palettes, because there was no other way to reach them. There is now. Every call site keeps working — the module re-exports the same ThemePalette, Palette façade, active_palette, app_tint and contrast_on that were defined here, so this is an import-level change rather than a rewrite. 119 tests still pass. Adding a theme family no longer touches this repository at all: add the TOML upstream, regenerate, bump the tag. colony-ui is pinned to v0.1.0 rather than tracking main, so an upstream change cannot break this build without a deliberate bump. --- Cargo.lock | 12 + Cargo.toml | 5 + src/ui/theme.rs | 2949 +---------------------------------------------- 3 files changed, 31 insertions(+), 2935 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f7d3af..63d7693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,6 +536,7 @@ version = "0.9.2" dependencies = [ "anyhow", "base64 0.23.0", + "colony-ui", "dirs", "ed25519-dalek", "flate2", @@ -559,6 +560,16 @@ dependencies = [ "zip", ] +[[package]] +name = "colony-ui" +version = "0.1.0" +source = "git+https://github.com/Project-Colony/Project-Colony-Resources?tag=v0.1.0#108ee8010997e69e06929ae84e112a4d8b90ae80" +dependencies = [ + "dirs", + "iced", + "serde_json", +] + [[package]] name = "combine" version = "4.6.7" @@ -3510,6 +3521,7 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ + "indexmap", "itoa", "memchr", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8f02689..2100904 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,11 @@ rust-version = "1.80" [dependencies] anyhow = "1.0.100" +# The shared theme, palettes, accents, labels and widgets, generated from the +# design tokens in Project-Colony-Resources. Pinned to a tag rather than +# tracking main: a change upstream cannot break this build without a deliberate +# bump here. +colony-ui = { git = "https://github.com/Project-Colony/Project-Colony-Resources", tag = "v0.1.0" } iced = { version = "0.14.0", features = ["tokio", "markdown", "advanced", "image-without-codecs"] } shell-words = "1.1.1" serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index c2ddfeb..c400ac9 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -1,2935 +1,14 @@ -use iced::Color; -use std::sync::RwLock; - -/// Runtime theme palette — all semantic UI colors. -#[derive(Debug, Clone, Copy)] -pub struct ThemePalette { - // --- Backgrounds --- - pub bg_primary: Color, - pub bg_sidebar: Color, - pub bg_card: Color, - pub bg_card_hover: Color, - pub bg_card_pressed: Color, - pub bg_selected: Color, - pub bg_input: Color, - pub bg_progress: Color, - - // --- Text --- - pub text_primary: Color, - pub text_secondary: Color, - pub text_muted: Color, - pub text_dim: Color, - pub text_dimmer: Color, - pub text_dimmest: Color, - pub text_placeholder: Color, - - // --- Accent --- - pub accent_blue: Color, - pub accent_icon: Color, - pub accent_progress: Color, - - // --- Buttons --- - pub btn_default: Color, - pub btn_hover: Color, - pub btn_pressed: Color, - - // --- Success --- - pub success: Color, - pub success_bg: Color, - pub btn_success: Color, - pub btn_success_hover: Color, - pub btn_success_pressed: Color, - - // --- Warning --- - pub warning: Color, - pub warning_bg: Color, - - // --- Error --- - pub error: Color, - pub error_light: Color, - pub error_bg: Color, - pub btn_danger_bg: Color, - pub btn_danger_hover: Color, - pub btn_trash_hover: Color, - pub btn_trash_pressed: Color, - - // --- Modal --- - pub bg_modal_section: Color, - pub border_subtle: Color, - pub divider: Color, -} - -// ── Global active palette ── - -static ACTIVE_PALETTE: RwLock = RwLock::new(ThemePalette::GRUVBOX_DARK); - -/// User-chosen accent color override. `None` means "auto" (use theme default). -static ACTIVE_ACCENT: RwLock> = RwLock::new(None); - -/// Set the user accent override. Pass `None` for auto (theme default). -pub fn set_active_accent(color: Option) { - *ACTIVE_ACCENT.write().unwrap() = color; -} - -/// Resolve the effective accent color: user override or theme default. -pub fn effective_accent() -> Color { - ACTIVE_ACCENT - .read() - .unwrap() - .unwrap_or_else(|| active_palette().accent_blue) -} - -/// Convert an accent key to its Color, or None for "auto". -pub fn accent_key_to_color(key: &str) -> Option { - let hex: u32 = match key { - "red" => 0xE05555, - "orange" => 0xE0855A, - "yellow" => 0xC8A832, - "green" => 0x55B87A, - "blue" => 0x6B8BD6, - "indigo" => 0x7B6BD6, - "violet" => 0xB06BD6, - "amber" => 0xD4A030, - _ => return None, - }; - Some(Color { - r: ((hex >> 16) & 0xFF) as f32 / 255.0, - g: ((hex >> 8) & 0xFF) as f32 / 255.0, - b: (hex & 0xFF) as f32 / 255.0, - a: 1.0, - }) -} - -/// Deterministic identity tint for an app, derived from its NAME only (stable -/// across install/uninstall and across machines). Buckets the name hash into -/// the 8 accent hues so every app gets a distinct, palette-harmonious color for -/// its hexagon cell — no per-app assets required. -pub fn app_tint(name: &str) -> Color { - const KEYS: [&str; 8] = [ - "red", "orange", "yellow", "green", "blue", "indigo", "violet", "amber", - ]; - // Classic string hash: h = c + h*31, wrapping. - let mut h: i64 = 0; - for b in name.bytes() { - h = (b as i64).wrapping_add(h.wrapping_shl(5)).wrapping_sub(h); - } - let idx = (h.unsigned_abs() % KEYS.len() as u64) as usize; - accent_key_to_color(KEYS[idx]).unwrap_or_else(|| active_palette().accent_blue) -} - -/// Pick a legible foreground (near-black or near-white) for a glyph drawn on top -/// of `bg`, using perceptual (YIQ) luminance. -pub fn contrast_on(bg: Color) -> Color { - let yiq = 0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b; - if yiq > 0.6 { - Color { - r: 0.08, - g: 0.08, - b: 0.10, - a: 1.0, - } - } else { - Color { - r: 0.97, - g: 0.98, - b: 1.0, - a: 1.0, - } - } -} - -/// Set the active palette based on theme + variant keys. -pub fn set_active_theme(theme: &str, variant: &str) { - let palette = match (theme, variant) { - // Catppuccin - ("catppuccin", "latte") => ThemePalette::CATPPUCCIN_LATTE, - ("catppuccin", "frappe") => ThemePalette::CATPPUCCIN_FRAPPE, - ("catppuccin", "macchiato") => ThemePalette::CATPPUCCIN_MACCHIATO, - ("catppuccin", "mocha") => ThemePalette::CATPPUCCIN_MOCHA, - // Gruvbox - ("gruvbox", "light") => ThemePalette::GRUVBOX_LIGHT, - ("gruvbox", "dark") => ThemePalette::GRUVBOX_DARK, - // Everblush - ("everblush", "light") => ThemePalette::EVERBLUSH_LIGHT, - ("everblush", "dark") => ThemePalette::EVERBLUSH_DARK, - // Kanagawa - ("kanagawa", "light") => ThemePalette::KANAGAWA_LOTUS, - ("kanagawa", "dark") => ThemePalette::KANAGAWA_WAVE, - ("kanagawa", "journal") => ThemePalette::KANAGAWA_DRAGON, - // Nord - ("nord", "dark") => ThemePalette::NORD_DARK, - ("nord", "light") => ThemePalette::NORD_LIGHT, - // Dracula - ("dracula", "dark") => ThemePalette::DRACULA_DARK, - ("dracula", "light") => ThemePalette::DRACULA_LIGHT, - // Solarized - ("solarized", "dark") => ThemePalette::SOLARIZED_DARK, - ("solarized", "light") => ThemePalette::SOLARIZED_LIGHT, - // Tokyo Night - ("tokyonight", "night") => ThemePalette::TOKYONIGHT_NIGHT, - ("tokyonight", "day") => ThemePalette::TOKYONIGHT_DAY, - // Rosé Pine - ("rosepine", "main") => ThemePalette::ROSEPINE_MAIN, - ("rosepine", "moon") => ThemePalette::ROSEPINE_MOON, - ("rosepine", "dawn") => ThemePalette::ROSEPINE_DAWN, - // One Dark - ("onedark", "dark") => ThemePalette::ONEDARK_DARK, - ("onedark", "light") => ThemePalette::ONEDARK_LIGHT, - // Monokai Pro - ("monokai", "pro") => ThemePalette::MONOKAI_PRO, - ("monokai", "classic") => ThemePalette::MONOKAI_CLASSIC, - ("monokai", "spectrum") => ThemePalette::MONOKAI_SPECTRUM, - // Ayu - ("ayu", "dark") => ThemePalette::AYU_DARK, - ("ayu", "mirage") => ThemePalette::AYU_MIRAGE, - ("ayu", "light") => ThemePalette::AYU_LIGHT, - // Everforest - ("everforest", "dark") => ThemePalette::EVERFOREST_DARK, - ("everforest", "light") => ThemePalette::EVERFOREST_LIGHT, - // Material - ("material", "oceanic") => ThemePalette::MATERIAL_OCEANIC, - ("material", "palenight") => ThemePalette::MATERIAL_PALENIGHT, - ("material", "deepocean") => ThemePalette::MATERIAL_DEEPOCEAN, - // Flexoki - ("flexoki", "dark") => ThemePalette::FLEXOKI_DARK, - ("flexoki", "light") => ThemePalette::FLEXOKI_LIGHT, - // Nightfox - ("nightfox", "nightfox") => ThemePalette::NIGHTFOX, - ("nightfox", "dawnfox") => ThemePalette::DAWNFOX, - // Sonokai - ("sonokai", "default") => ThemePalette::SONOKAI_DEFAULT, - // Oxocarbon - ("oxocarbon", "dark") => ThemePalette::OXOCARBON_DARK, - ("oxocarbon", "light") => ThemePalette::OXOCARBON_LIGHT, - // Night Owl - ("nightowl", "dark") => ThemePalette::NIGHTOWL_DARK, - ("nightowl", "light") => ThemePalette::NIGHTOWL_LIGHT, - // Iceberg - ("iceberg", "dark") => ThemePalette::ICEBERG_DARK, - ("iceberg", "light") => ThemePalette::ICEBERG_LIGHT, - // Horizon - ("horizon", "dark") => ThemePalette::HORIZON_DARK, - // Melange - ("melange", "dark") => ThemePalette::MELANGE_DARK, - ("melange", "light") => ThemePalette::MELANGE_LIGHT, - // Synthwave '84 - ("synthwave", "dark") => ThemePalette::SYNTHWAVE_DARK, - // Modus - ("modus", "operandi") => ThemePalette::MODUS_OPERANDI, - ("modus", "vivendi") => ThemePalette::MODUS_VIVENDI, - // Stellar Blade (fan-made character set) - ("stellar_blade", "eve") => ThemePalette::STELLAR_EVE, - ("stellar_blade", "tachy") => ThemePalette::STELLAR_TACHY, - ("stellar_blade", "lily") => ThemePalette::STELLAR_LILY, - ("stellar_blade", "enya") => ThemePalette::STELLAR_ENYA, - ("stellar_blade", "kaya") => ThemePalette::STELLAR_KAYA, - // Fallback - _ => ThemePalette::GRUVBOX_DARK, - }; - *ACTIVE_PALETTE.write().unwrap() = palette; -} - -/// High-contrast mode flag. -static HIGH_CONTRAST: RwLock = RwLock::new(false); - -/// Set high-contrast mode. -pub fn set_high_contrast(enabled: bool) { - *HIGH_CONTRAST.write().unwrap() = enabled; -} - -/// Check if high contrast is active. -pub fn is_high_contrast() -> bool { - *HIGH_CONTRAST.read().unwrap() -} - -/// Convenience accessor — read the current palette. -pub fn active_palette() -> ThemePalette { - let base = *ACTIVE_PALETTE.read().unwrap(); - if is_high_contrast() { - base.with_high_contrast() - } else { - base - } -} - -// ── Public façade (drop-in replacement for the old `Palette::CONST` API) ── - -pub struct Palette; - -#[allow(non_snake_case, dead_code)] -impl Palette { - // Backgrounds - pub fn BG_PRIMARY() -> Color { - active_palette().bg_primary - } - pub fn BG_SIDEBAR() -> Color { - active_palette().bg_sidebar - } - pub fn BG_CARD() -> Color { - active_palette().bg_card - } - pub fn BG_CARD_HOVER() -> Color { - active_palette().bg_card_hover - } - pub fn BG_CARD_PRESSED() -> Color { - active_palette().bg_card_pressed - } - pub fn BG_SELECTED() -> Color { - active_palette().bg_selected - } - pub fn BG_INPUT() -> Color { - active_palette().bg_input - } - pub fn BG_PROGRESS() -> Color { - active_palette().bg_progress - } - - // Text - pub fn TEXT_PRIMARY() -> Color { - active_palette().text_primary - } - pub fn TEXT_SECONDARY() -> Color { - active_palette().text_secondary - } - pub fn TEXT_MUTED() -> Color { - active_palette().text_muted - } - pub fn TEXT_DIM() -> Color { - active_palette().text_dim - } - pub fn TEXT_DIMMER() -> Color { - active_palette().text_dimmer - } - pub fn TEXT_DIMMEST() -> Color { - active_palette().text_dimmest - } - pub fn TEXT_PLACEHOLDER() -> Color { - active_palette().text_placeholder - } - - // Accent - /// User-selected accent (or theme default if auto). - pub fn ACCENT() -> Color { - effective_accent() - } - pub fn ACCENT_ICON() -> Color { - active_palette().accent_icon - } - pub fn ACCENT_PROGRESS() -> Color { - active_palette().accent_progress - } - - // Buttons - pub fn BTN_DEFAULT() -> Color { - active_palette().btn_default - } - pub fn BTN_HOVER() -> Color { - active_palette().btn_hover - } - pub fn BTN_PRESSED() -> Color { - active_palette().btn_pressed - } - - // Success - pub fn SUCCESS() -> Color { - active_palette().success - } - pub fn SUCCESS_BG() -> Color { - active_palette().success_bg - } - pub fn BTN_SUCCESS() -> Color { - active_palette().btn_success - } - pub fn BTN_SUCCESS_HOVER() -> Color { - active_palette().btn_success_hover - } - pub fn BTN_SUCCESS_PRESSED() -> Color { - active_palette().btn_success_pressed - } - - // Warning - pub fn WARNING() -> Color { - active_palette().warning - } - pub fn WARNING_BG() -> Color { - active_palette().warning_bg - } - - // Error - pub fn ERROR() -> Color { - active_palette().error - } - pub fn ERROR_LIGHT() -> Color { - active_palette().error_light - } - pub fn ERROR_BG() -> Color { - active_palette().error_bg - } - pub fn BTN_DANGER_BG() -> Color { - active_palette().btn_danger_bg - } - pub fn BTN_DANGER_HOVER() -> Color { - active_palette().btn_danger_hover - } - pub fn BTN_TRASH_HOVER() -> Color { - active_palette().btn_trash_hover - } - pub fn BTN_TRASH_PRESSED() -> Color { - active_palette().btn_trash_pressed - } - - // Modal - pub fn BG_MODAL_SECTION() -> Color { - active_palette().bg_modal_section - } - pub fn BORDER_SUBTLE() -> Color { - active_palette().border_subtle - } - pub fn DIVIDER() -> Color { - active_palette().divider - } -} - -// ── Compile-time hex → Color helper ── - -const fn hex(h: u32) -> Color { - let r = ((h >> 16) & 0xFF) as f32 / 255.0; - let g = ((h >> 8) & 0xFF) as f32 / 255.0; - let b = (h & 0xFF) as f32 / 255.0; - Color { r, g, b, a: 1.0 } -} - -// ════════════════════════════════════════════════════════════════════════ -// Theme palette definitions -// ════════════════════════════════════════════════════════════════════════ - -impl ThemePalette { - /// Boost contrast for accessibility: brighten text, darken backgrounds, sharpen borders. - pub fn with_high_contrast(mut self) -> Self { - fn boost(c: Color, amount: f32) -> Color { - Color { - r: (c.r + amount).min(1.0), - g: (c.g + amount).min(1.0), - b: (c.b + amount).min(1.0), - a: c.a, - } - } - fn darken(c: Color, amount: f32) -> Color { - Color { - r: (c.r - amount).max(0.0), - g: (c.g - amount).max(0.0), - b: (c.b - amount).max(0.0), - a: c.a, - } - } - // Detect light vs dark theme by bg brightness - let bg_luma = - self.bg_primary.r * 0.299 + self.bg_primary.g * 0.587 + self.bg_primary.b * 0.114; - let is_light = bg_luma > 0.5; - - if is_light { - // Light theme: darken text more, lighten backgrounds - self.text_primary = darken(self.text_primary, 0.15); - self.text_secondary = darken(self.text_secondary, 0.12); - self.text_muted = darken(self.text_muted, 0.10); - self.text_dim = darken(self.text_dim, 0.10); - self.text_dimmer = darken(self.text_dimmer, 0.08); - self.border_subtle = darken(self.border_subtle, 0.15); - self.divider = darken(self.divider, 0.15); - } else { - // Dark theme: brighten text, sharpen borders - self.text_primary = boost(self.text_primary, 0.12); - self.text_secondary = boost(self.text_secondary, 0.10); - self.text_muted = boost(self.text_muted, 0.10); - self.text_dim = boost(self.text_dim, 0.08); - self.text_dimmer = boost(self.text_dimmer, 0.08); - self.border_subtle = boost(self.border_subtle, 0.12); - self.divider = boost(self.divider, 0.12); - } - self - } - - // ── Catppuccin Latte (light) ── - pub const CATPPUCCIN_LATTE: Self = Self { - bg_primary: hex(0xeff1f5), - bg_sidebar: hex(0xe6e9ef), - bg_card: hex(0xe6e9ef), - bg_card_hover: hex(0xdce0e8), - bg_card_pressed: hex(0xccd0da), - bg_selected: hex(0xccd0da), - bg_input: hex(0xdce0e8), - bg_progress: hex(0xe6e9ef), - - text_primary: hex(0x4c4f69), - text_secondary: hex(0x5c5f77), - text_muted: hex(0x6c6f85), - text_dim: hex(0x7c7f93), - text_dimmer: hex(0x8c8fa1), - text_dimmest: hex(0x9ca0b0), - text_placeholder: hex(0x8c8fa1), - - accent_blue: hex(0x1e66f5), - accent_icon: hex(0x7287fd), - accent_progress: hex(0x7287fd), - - btn_default: hex(0xdce0e8), - btn_hover: hex(0xccd0da), - btn_pressed: hex(0xbcc0cc), - - success: hex(0x40a02b), - success_bg: hex(0xd5f0cd), - btn_success: hex(0x40a02b), - btn_success_hover: hex(0x369222), - btn_success_pressed: hex(0x2c8219), - - warning: hex(0xdf8e1d), - warning_bg: hex(0xf5e6c8), - - error: hex(0xd20f39), - error_light: hex(0xe64553), - error_bg: hex(0xf5d0d6), - btn_danger_bg: hex(0xf5d0d6), - btn_danger_hover: hex(0xeab0ba), - btn_trash_hover: hex(0xd20f39), - btn_trash_pressed: hex(0xb00c30), - - bg_modal_section: hex(0xe6e9ef), - border_subtle: hex(0xccd0da), - divider: hex(0xccd0da), - }; - - // ── Catppuccin Frappé ── - pub const CATPPUCCIN_FRAPPE: Self = Self { - bg_primary: hex(0x303446), - bg_sidebar: hex(0x292c3c), - bg_card: hex(0x292c3c), - bg_card_hover: hex(0x414559), - bg_card_pressed: hex(0x51576d), - bg_selected: hex(0x51576d), - bg_input: hex(0x414559), - bg_progress: hex(0x292c3c), - - text_primary: hex(0xc6d0f5), - text_secondary: hex(0xb5bfe2), - text_muted: hex(0xa5adce), - text_dim: hex(0x949cbb), - text_dimmer: hex(0x838ba7), - text_dimmest: hex(0x737994), - text_placeholder: hex(0x838ba7), - - accent_blue: hex(0x8caaee), - accent_icon: hex(0xbabbf1), - accent_progress: hex(0xbabbf1), - - btn_default: hex(0x414559), - btn_hover: hex(0x51576d), - btn_pressed: hex(0x626880), - - success: hex(0xa6d189), - success_bg: hex(0x2a3a28), - btn_success: hex(0x549444), - btn_success_hover: hex(0x468838), - btn_success_pressed: hex(0x387a2c), - - warning: hex(0xe5c890), - warning_bg: hex(0x3a3828), - - error: hex(0xe78284), - error_light: hex(0xea999c), - error_bg: hex(0x3a2628), - btn_danger_bg: hex(0x3a2628), - btn_danger_hover: hex(0x5a3638), - btn_trash_hover: hex(0x9a3234), - btn_trash_pressed: hex(0x7a2224), - - bg_modal_section: hex(0x292c3c), - border_subtle: hex(0x414559), - divider: hex(0x414559), - }; - - // ── Catppuccin Macchiato ── - pub const CATPPUCCIN_MACCHIATO: Self = Self { - bg_primary: hex(0x24273a), - bg_sidebar: hex(0x1e2030), - bg_card: hex(0x1e2030), - bg_card_hover: hex(0x363a4f), - bg_card_pressed: hex(0x494d64), - bg_selected: hex(0x494d64), - bg_input: hex(0x363a4f), - bg_progress: hex(0x1e2030), - - text_primary: hex(0xcad3f5), - text_secondary: hex(0xb8c0e0), - text_muted: hex(0xa5adcb), - text_dim: hex(0x939ab7), - text_dimmer: hex(0x8087a2), - text_dimmest: hex(0x6e738d), - text_placeholder: hex(0x8087a2), - - accent_blue: hex(0x8aadf4), - accent_icon: hex(0xb7bdf8), - accent_progress: hex(0xb7bdf8), - - btn_default: hex(0x363a4f), - btn_hover: hex(0x494d64), - btn_pressed: hex(0x5b6078), - - success: hex(0xa6da95), - success_bg: hex(0x243826), - btn_success: hex(0x4e9240), - btn_success_hover: hex(0x408634), - btn_success_pressed: hex(0x327a28), - - warning: hex(0xeed49f), - warning_bg: hex(0x383428), - - error: hex(0xed8796), - error_light: hex(0xee99a0), - error_bg: hex(0x382428), - btn_danger_bg: hex(0x382428), - btn_danger_hover: hex(0x583438), - btn_trash_hover: hex(0x983038), - btn_trash_pressed: hex(0x782028), - - bg_modal_section: hex(0x1e2030), - border_subtle: hex(0x363a4f), - divider: hex(0x363a4f), - }; - - // ── Catppuccin Mocha ── - pub const CATPPUCCIN_MOCHA: Self = Self { - bg_primary: hex(0x1e1e2e), - bg_sidebar: hex(0x181825), - bg_card: hex(0x181825), - bg_card_hover: hex(0x313244), - bg_card_pressed: hex(0x45475a), - bg_selected: hex(0x45475a), - bg_input: hex(0x313244), - bg_progress: hex(0x181825), - - text_primary: hex(0xcdd6f4), - text_secondary: hex(0xbac2de), - text_muted: hex(0xa6adc8), - text_dim: hex(0x9399b2), - text_dimmer: hex(0x7f849c), - text_dimmest: hex(0x6c7086), - text_placeholder: hex(0x7f849c), - - accent_blue: hex(0x89b4fa), - accent_icon: hex(0xb4befe), - accent_progress: hex(0xb4befe), - - btn_default: hex(0x313244), - btn_hover: hex(0x45475a), - btn_pressed: hex(0x585b70), - - success: hex(0xa6e3a1), - success_bg: hex(0x1e3a1e), - btn_success: hex(0x48904a), - btn_success_hover: hex(0x3a843c), - btn_success_pressed: hex(0x2c782e), - - warning: hex(0xf9e2af), - warning_bg: hex(0x3a3420), - - error: hex(0xf38ba8), - error_light: hex(0xf5a0b8), - error_bg: hex(0x3a1e28), - btn_danger_bg: hex(0x3a1e28), - btn_danger_hover: hex(0x5a2e38), - btn_trash_hover: hex(0x952e3a), - btn_trash_pressed: hex(0x751e2a), - - bg_modal_section: hex(0x181825), - border_subtle: hex(0x313244), - divider: hex(0x313244), - }; - - // ── Gruvbox Dark ── - pub const GRUVBOX_DARK: Self = Self { - bg_primary: hex(0x282828), - bg_sidebar: hex(0x1d2021), - bg_card: hex(0x3c3836), - bg_card_hover: hex(0x504945), - bg_card_pressed: hex(0x665c54), - bg_selected: hex(0x504945), - bg_input: hex(0x3c3836), - bg_progress: hex(0x32302f), - - text_primary: hex(0xebdbb2), - text_secondary: hex(0xd5c4a1), - text_muted: hex(0xbdae93), - text_dim: hex(0xa89984), - text_dimmer: hex(0x928374), - text_dimmest: hex(0x7c6f64), - text_placeholder: hex(0x928374), - - accent_blue: hex(0x83a598), - accent_icon: hex(0x83a598), - accent_progress: hex(0x83a598), - - btn_default: hex(0x3c3836), - btn_hover: hex(0x504945), - btn_pressed: hex(0x665c54), - - success: hex(0xb8bb26), - success_bg: hex(0x2a3020), - btn_success: hex(0x689d6a), - btn_success_hover: hex(0x5a8f5c), - btn_success_pressed: hex(0x4c814e), - - warning: hex(0xfabd2f), - warning_bg: hex(0x3a3420), - - error: hex(0xfb4934), - error_light: hex(0xfe6050), - error_bg: hex(0x3c1f1f), - btn_danger_bg: hex(0x3c1f1f), - btn_danger_hover: hex(0x5c2f2f), - btn_trash_hover: hex(0x9d3030), - btn_trash_pressed: hex(0x7d2020), - - bg_modal_section: hex(0x282828), - border_subtle: hex(0x3c3836), - divider: hex(0x3c3836), - }; - - // ── Gruvbox Light ── - pub const GRUVBOX_LIGHT: Self = Self { - bg_primary: hex(0xfbf1c7), - bg_sidebar: hex(0xf2e5bc), - bg_card: hex(0xebdbb2), - bg_card_hover: hex(0xd5c4a1), - bg_card_pressed: hex(0xbdae93), - bg_selected: hex(0xd5c4a1), - bg_input: hex(0xebdbb2), - bg_progress: hex(0xf2e5bc), - - text_primary: hex(0x3c3836), - text_secondary: hex(0x504945), - text_muted: hex(0x665c54), - text_dim: hex(0x7c6f64), - text_dimmer: hex(0x928374), - text_dimmest: hex(0xa89984), - text_placeholder: hex(0x928374), - - accent_blue: hex(0x458588), - accent_icon: hex(0x458588), - accent_progress: hex(0x458588), - - btn_default: hex(0xebdbb2), - btn_hover: hex(0xd5c4a1), - btn_pressed: hex(0xbdae93), - - success: hex(0x98971a), - success_bg: hex(0xdde6b0), - btn_success: hex(0x689d6a), - btn_success_hover: hex(0x5a8f5c), - btn_success_pressed: hex(0x4c814e), - - warning: hex(0xd79921), - warning_bg: hex(0xf0e0b0), - - error: hex(0xcc241d), - error_light: hex(0xd44040), - error_bg: hex(0xf0c8c8), - btn_danger_bg: hex(0xf0c8c8), - btn_danger_hover: hex(0xe0a8a8), - btn_trash_hover: hex(0xcc241d), - btn_trash_pressed: hex(0xaa1818), - - bg_modal_section: hex(0xfbf1c7), - border_subtle: hex(0xd5c4a1), - divider: hex(0xd5c4a1), - }; - - // ── Everblush Dark ── - pub const EVERBLUSH_DARK: Self = Self { - bg_primary: hex(0x141b1e), - bg_sidebar: hex(0x1a2124), - bg_card: hex(0x232a2d), - bg_card_hover: hex(0x2c3538), - bg_card_pressed: hex(0x354043), - bg_selected: hex(0x2c3538), - bg_input: hex(0x232a2d), - bg_progress: hex(0x1a2124), - - text_primary: hex(0xdadada), - text_secondary: hex(0xc4c4c4), - text_muted: hex(0xb3b9b8), - text_dim: hex(0x9aa0a0), - text_dimmer: hex(0x808888), - text_dimmest: hex(0x667070), - text_placeholder: hex(0x808888), - - accent_blue: hex(0x67b0e8), - accent_icon: hex(0x67b0e8), - accent_progress: hex(0x67b0e8), - - btn_default: hex(0x232a2d), - btn_hover: hex(0x2c3538), - btn_pressed: hex(0x354043), - - success: hex(0x8ccf7e), - success_bg: hex(0x1a2e1e), - btn_success: hex(0x5aaa50), - btn_success_hover: hex(0x4c9e44), - btn_success_pressed: hex(0x3e9238), - - warning: hex(0xe5c76b), - warning_bg: hex(0x2e2a1a), - - error: hex(0xe57474), - error_light: hex(0xf08888), - error_bg: hex(0x2e1a1a), - btn_danger_bg: hex(0x2e1a1a), - btn_danger_hover: hex(0x4e2a2a), - btn_trash_hover: hex(0x9a3030), - btn_trash_pressed: hex(0x7a2020), - - bg_modal_section: hex(0x232a2d), - border_subtle: hex(0x2c3538), - divider: hex(0x2c3538), - }; - - // ── Everblush Light ── - pub const EVERBLUSH_LIGHT: Self = Self { - bg_primary: hex(0xe8eded), - bg_sidebar: hex(0xdce3e3), - bg_card: hex(0xd0d8d8), - bg_card_hover: hex(0xc4cece), - bg_card_pressed: hex(0xb8c4c4), - bg_selected: hex(0xc4cece), - bg_input: hex(0xd0d8d8), - bg_progress: hex(0xdce3e3), - - text_primary: hex(0x2d3437), - text_secondary: hex(0x3a4448), - text_muted: hex(0x4a5558), - text_dim: hex(0x5a6668), - text_dimmer: hex(0x6a7878), - text_dimmest: hex(0x7a8a8a), - text_placeholder: hex(0x6a7878), - - accent_blue: hex(0x3a88c0), - accent_icon: hex(0x3a88c0), - accent_progress: hex(0x3a88c0), - - btn_default: hex(0xd0d8d8), - btn_hover: hex(0xc4cece), - btn_pressed: hex(0xb8c4c4), - - success: hex(0x5aaa4e), - success_bg: hex(0xc8e8c4), - btn_success: hex(0x5aaa4e), - btn_success_hover: hex(0x4c9e42), - btn_success_pressed: hex(0x3e9236), - - warning: hex(0xc0a030), - warning_bg: hex(0xe8e0c0), - - error: hex(0xc85050), - error_light: hex(0xd86868), - error_bg: hex(0xf0d0d0), - btn_danger_bg: hex(0xf0d0d0), - btn_danger_hover: hex(0xe0b0b0), - btn_trash_hover: hex(0xc85050), - btn_trash_pressed: hex(0xa84040), - - bg_modal_section: hex(0xe8eded), - border_subtle: hex(0xc4cece), - divider: hex(0xc4cece), - }; - - // ── Kanagawa Wave (dark — default) ── - pub const KANAGAWA_WAVE: Self = Self { - bg_primary: hex(0x1F1F28), - bg_sidebar: hex(0x181820), - bg_card: hex(0x2A2A37), - bg_card_hover: hex(0x363646), - bg_card_pressed: hex(0x54546D), - bg_selected: hex(0x363646), - bg_input: hex(0x2A2A37), - bg_progress: hex(0x223249), - - text_primary: hex(0xDCD7BA), - text_secondary: hex(0xC8C093), - text_muted: hex(0x9CABCA), - text_dim: hex(0x938AA9), - text_dimmer: hex(0x727169), - text_dimmest: hex(0x54546D), - text_placeholder: hex(0x727169), - - accent_blue: hex(0x7E9CD8), - accent_icon: hex(0x7FB4CA), - accent_progress: hex(0x7FB4CA), - - btn_default: hex(0x2A2A37), - btn_hover: hex(0x363646), - btn_pressed: hex(0x54546D), - - success: hex(0x98BB6C), - success_bg: hex(0x2B3328), - btn_success: hex(0x76946A), - btn_success_hover: hex(0x68885c), - btn_success_pressed: hex(0x5a7c4e), - - warning: hex(0xE6C384), - warning_bg: hex(0x49443C), - - error: hex(0xE46876), - error_light: hex(0xFF5D62), - error_bg: hex(0x43242B), - btn_danger_bg: hex(0x43242B), - btn_danger_hover: hex(0x63343B), - btn_trash_hover: hex(0xC34043), - btn_trash_pressed: hex(0xA33033), - - bg_modal_section: hex(0x1F1F28), - border_subtle: hex(0x2A2A37), - divider: hex(0x2A2A37), - }; - - // ── Kanagawa Journal (warm parchment light) ── - // Inspired by the Grape "Mode journal" — warm beige/sepia paper tones - pub const KANAGAWA_DRAGON: Self = Self { - bg_primary: hex(0xd5cea3), // warm parchment - bg_sidebar: hex(0xc8b98e), // slightly darker sidebar - bg_card: hex(0xc8b98e), - bg_card_hover: hex(0xbdae80), - bg_card_pressed: hex(0xb0a070), - bg_selected: hex(0xbdae80), - bg_input: hex(0xc8b98e), - bg_progress: hex(0xc8b98e), - - text_primary: hex(0x43412e), // dark brown - text_secondary: hex(0x5c5840), - text_muted: hex(0x736e55), - text_dim: hex(0x8a856c), - text_dimmer: hex(0xa09a80), - text_dimmest: hex(0xb5ae94), - text_placeholder: hex(0x8a856c), - - accent_blue: hex(0x7a6840), // warm brown accent (matching screenshot) - accent_icon: hex(0x7a6840), - accent_progress: hex(0x7a6840), - - btn_default: hex(0xc8b98e), - btn_hover: hex(0xbdae80), - btn_pressed: hex(0xb0a070), - - success: hex(0x6f894e), - success_bg: hex(0xcbd8b8), - btn_success: hex(0x6e915f), - btn_success_hover: hex(0x608554), - btn_success_pressed: hex(0x527949), - - warning: hex(0xc49a20), - warning_bg: hex(0xe0d4a0), - - error: hex(0xc84053), - error_light: hex(0xd7474b), - error_bg: hex(0xe0bfb0), - btn_danger_bg: hex(0xe0bfb0), - btn_danger_hover: hex(0xd0a898), - btn_trash_hover: hex(0xc84053), - btn_trash_pressed: hex(0xa83043), - - bg_modal_section: hex(0xd5cea3), - border_subtle: hex(0xb8a878), - divider: hex(0xb8a878), - }; - - // ── Kanagawa Lotus (light) ── - pub const KANAGAWA_LOTUS: Self = Self { - bg_primary: hex(0xf2ecbc), - bg_sidebar: hex(0xe5ddb0), - bg_card: hex(0xe7dba0), - bg_card_hover: hex(0xd5cea3), - bg_card_pressed: hex(0xc8c093), - bg_selected: hex(0xd5cea3), - bg_input: hex(0xe4d794), - bg_progress: hex(0xe5ddb0), - - text_primary: hex(0x545464), - text_secondary: hex(0x43436c), - text_muted: hex(0x716e61), - text_dim: hex(0x8a8980), - text_dimmer: hex(0xa09cac), - text_dimmest: hex(0xb0acb8), - text_placeholder: hex(0x8a8980), - - accent_blue: hex(0x4d699b), - accent_icon: hex(0x5d57a3), - accent_progress: hex(0x5d57a3), - - btn_default: hex(0xe7dba0), - btn_hover: hex(0xd5cea3), - btn_pressed: hex(0xc8c093), - - success: hex(0x6f894e), - success_bg: hex(0xb7d0ae), - btn_success: hex(0x6e915f), - btn_success_hover: hex(0x608554), - btn_success_pressed: hex(0x527949), - - warning: hex(0xde9800), - warning_bg: hex(0xf9d791), - - error: hex(0xc84053), - error_light: hex(0xd7474b), - error_bg: hex(0xd9a594), - btn_danger_bg: hex(0xd9a594), - btn_danger_hover: hex(0xc89484), - btn_trash_hover: hex(0xc84053), - btn_trash_pressed: hex(0xa83043), - - bg_modal_section: hex(0xf2ecbc), - border_subtle: hex(0xd5cea3), - divider: hex(0xd5cea3), - }; -} - -// ════════════════════════════════════════════════════════════════════════ -// Additional theme palettes -// ════════════════════════════════════════════════════════════════════════ - -impl ThemePalette { - // ── Nord Dark ── - pub const NORD_DARK: Self = Self { - bg_primary: hex(0x2E3440), - bg_sidebar: hex(0x252B37), - bg_card: hex(0x3B4252), - bg_card_hover: hex(0x434C5E), - bg_card_pressed: hex(0x4C566A), - bg_selected: hex(0x434C5E), - bg_input: hex(0x3B4252), - bg_progress: hex(0x252B37), - text_primary: hex(0xD8DEE9), - text_secondary: hex(0xC0C8D4), - text_muted: hex(0xA0AEC0), - text_dim: hex(0x7E8FA4), - text_dimmer: hex(0x616E88), - text_dimmest: hex(0x4C566A), - text_placeholder: hex(0x616E88), - accent_blue: hex(0x88C0D0), - accent_icon: hex(0x81A1C1), - accent_progress: hex(0x81A1C1), - btn_default: hex(0x3B4252), - btn_hover: hex(0x434C5E), - btn_pressed: hex(0x4C566A), - success: hex(0xA3BE8C), - success_bg: hex(0x2A3425), - btn_success: hex(0x809B6C), - btn_success_hover: hex(0x728F60), - btn_success_pressed: hex(0x648354), - warning: hex(0xEBCB8B), - warning_bg: hex(0x3A3828), - error: hex(0xBF616A), - error_light: hex(0xD08080), - error_bg: hex(0x3C2830), - btn_danger_bg: hex(0x3C2830), - btn_danger_hover: hex(0x5C3840), - btn_trash_hover: hex(0xBF616A), - btn_trash_pressed: hex(0x9F5158), - bg_modal_section: hex(0x2E3440), - border_subtle: hex(0x3B4252), - divider: hex(0x3B4252), - }; - - // ── Nord Light ── - pub const NORD_LIGHT: Self = Self { - bg_primary: hex(0xECEFF4), - bg_sidebar: hex(0xE5E9F0), - bg_card: hex(0xD8DEE9), - bg_card_hover: hex(0xCCD4DF), - bg_card_pressed: hex(0xB8C2D0), - bg_selected: hex(0xCCD4DF), - bg_input: hex(0xD8DEE9), - bg_progress: hex(0xE5E9F0), - text_primary: hex(0x2E3440), - text_secondary: hex(0x3B4252), - text_muted: hex(0x4C566A), - text_dim: hex(0x636E80), - text_dimmer: hex(0x7E8FA4), - text_dimmest: hex(0xA0AAB8), - text_placeholder: hex(0x7E8FA4), - accent_blue: hex(0x5E81AC), - accent_icon: hex(0x81A1C1), - accent_progress: hex(0x81A1C1), - btn_default: hex(0xD8DEE9), - btn_hover: hex(0xCCD4DF), - btn_pressed: hex(0xB8C2D0), - success: hex(0x6B8C56), - success_bg: hex(0xD0E0C8), - btn_success: hex(0x6B8C56), - btn_success_hover: hex(0x5E8048), - btn_success_pressed: hex(0x50743A), - warning: hex(0xB5A050), - warning_bg: hex(0xE8DFC0), - error: hex(0xA04048), - error_light: hex(0xBF616A), - error_bg: hex(0xE8C8C8), - btn_danger_bg: hex(0xE8C8C8), - btn_danger_hover: hex(0xD8B0B0), - btn_trash_hover: hex(0xA04048), - btn_trash_pressed: hex(0x883438), - bg_modal_section: hex(0xECEFF4), - border_subtle: hex(0xCCD4DF), - divider: hex(0xCCD4DF), - }; - - // ── Dracula Dark ── - pub const DRACULA_DARK: Self = Self { - bg_primary: hex(0x282A36), - bg_sidebar: hex(0x21222C), - bg_card: hex(0x44475A), - bg_card_hover: hex(0x515470), - bg_card_pressed: hex(0x606382), - bg_selected: hex(0x44475A), - bg_input: hex(0x44475A), - bg_progress: hex(0x21222C), - text_primary: hex(0xF8F8F2), - text_secondary: hex(0xE8E8E0), - text_muted: hex(0xC8C8C0), - text_dim: hex(0x9898A0), - text_dimmer: hex(0x6272A4), - text_dimmest: hex(0x4C5478), - text_placeholder: hex(0x6272A4), - accent_blue: hex(0xBD93F9), - accent_icon: hex(0xFF79C6), - accent_progress: hex(0xBD93F9), - btn_default: hex(0x44475A), - btn_hover: hex(0x515470), - btn_pressed: hex(0x606382), - success: hex(0x50FA7B), - success_bg: hex(0x1E3228), - btn_success: hex(0x40C862), - btn_success_hover: hex(0x38B858), - btn_success_pressed: hex(0x30A84E), - warning: hex(0xF1FA8C), - warning_bg: hex(0x3A382A), - error: hex(0xFF5555), - error_light: hex(0xFF7777), - error_bg: hex(0x3C222A), - btn_danger_bg: hex(0x3C222A), - btn_danger_hover: hex(0x5C323A), - btn_trash_hover: hex(0xFF5555), - btn_trash_pressed: hex(0xDD4444), - bg_modal_section: hex(0x282A36), - border_subtle: hex(0x44475A), - divider: hex(0x44475A), - }; - - // ── Dracula Light ── - pub const DRACULA_LIGHT: Self = Self { - bg_primary: hex(0xFFFBEB), - bg_sidebar: hex(0xF4EFD8), - bg_card: hex(0xE8E4CF), - bg_card_hover: hex(0xDCD8C0), - bg_card_pressed: hex(0xD0CCB8), - bg_selected: hex(0xDCD8C0), - bg_input: hex(0xE8E4CF), - bg_progress: hex(0xF4EFD8), - text_primary: hex(0x1F1F1F), - text_secondary: hex(0x333333), - text_muted: hex(0x555555), - text_dim: hex(0x777777), - text_dimmer: hex(0x7970A9), - text_dimmest: hex(0xA09CA0), - text_placeholder: hex(0x7970A9), - accent_blue: hex(0x7C5FC2), - accent_icon: hex(0xE04886), - accent_progress: hex(0x7C5FC2), - btn_default: hex(0xE8E4CF), - btn_hover: hex(0xDCD8C0), - btn_pressed: hex(0xD0CCB8), - success: hex(0x2BA479), - success_bg: hex(0xD0E8D8), - btn_success: hex(0x2BA479), - btn_success_hover: hex(0x24966C), - btn_success_pressed: hex(0x1D8860), - warning: hex(0xD5A212), - warning_bg: hex(0xEDE0C0), - error: hex(0xDE3535), - error_light: hex(0xE85858), - error_bg: hex(0xEEDCDC), - btn_danger_bg: hex(0xEEDCDC), - btn_danger_hover: hex(0xDEC8C8), - btn_trash_hover: hex(0xDE3535), - btn_trash_pressed: hex(0xC02828), - bg_modal_section: hex(0xFFFBEB), - border_subtle: hex(0xDCD8C0), - divider: hex(0xDCD8C0), - }; - - // ── Solarized Dark ── - pub const SOLARIZED_DARK: Self = Self { - bg_primary: hex(0x002B36), - bg_sidebar: hex(0x00222C), - bg_card: hex(0x073642), - bg_card_hover: hex(0x0A4858), - bg_card_pressed: hex(0x1A5A6C), - bg_selected: hex(0x073642), - bg_input: hex(0x073642), - bg_progress: hex(0x00222C), - text_primary: hex(0x839496), - text_secondary: hex(0x93A1A1), - text_muted: hex(0x738C90), - text_dim: hex(0x657B83), - text_dimmer: hex(0x586E75), - text_dimmest: hex(0x475860), - text_placeholder: hex(0x586E75), - accent_blue: hex(0x268BD2), - accent_icon: hex(0x2AA198), - accent_progress: hex(0x268BD2), - btn_default: hex(0x073642), - btn_hover: hex(0x0A4858), - btn_pressed: hex(0x1A5A6C), - success: hex(0x859900), - success_bg: hex(0x0A2A18), - btn_success: hex(0x6A7A00), - btn_success_hover: hex(0x5C6C00), - btn_success_pressed: hex(0x4E5E00), - warning: hex(0xB58900), - warning_bg: hex(0x1A2818), - error: hex(0xDC322F), - error_light: hex(0xE8504E), - error_bg: hex(0x2A1A1A), - btn_danger_bg: hex(0x2A1A1A), - btn_danger_hover: hex(0x3A2A2A), - btn_trash_hover: hex(0xDC322F), - btn_trash_pressed: hex(0xBC2220), - bg_modal_section: hex(0x002B36), - border_subtle: hex(0x073642), - divider: hex(0x073642), - }; - - // ── Solarized Light ── - pub const SOLARIZED_LIGHT: Self = Self { - bg_primary: hex(0xFDF6E3), - bg_sidebar: hex(0xEEE8D5), - bg_card: hex(0xE6E0CA), - bg_card_hover: hex(0xDCD6C0), - bg_card_pressed: hex(0xD0CAB4), - bg_selected: hex(0xDCD6C0), - bg_input: hex(0xE6E0CA), - bg_progress: hex(0xEEE8D5), - text_primary: hex(0x657B83), - text_secondary: hex(0x586E75), - text_muted: hex(0x7C8E94), - text_dim: hex(0x93A1A1), - text_dimmer: hex(0xA8B4B8), - text_dimmest: hex(0xC0C8CA), - text_placeholder: hex(0xA8B4B8), - accent_blue: hex(0x268BD2), - accent_icon: hex(0x2AA198), - accent_progress: hex(0x268BD2), - btn_default: hex(0xE6E0CA), - btn_hover: hex(0xDCD6C0), - btn_pressed: hex(0xD0CAB4), - success: hex(0x859900), - success_bg: hex(0xDAE8C8), - btn_success: hex(0x6A7A00), - btn_success_hover: hex(0x5C6C00), - btn_success_pressed: hex(0x4E5E00), - warning: hex(0xB58900), - warning_bg: hex(0xECE0C0), - error: hex(0xDC322F), - error_light: hex(0xE8504E), - error_bg: hex(0xECCCCC), - btn_danger_bg: hex(0xECCCCC), - btn_danger_hover: hex(0xDCB8B8), - btn_trash_hover: hex(0xDC322F), - btn_trash_pressed: hex(0xBC2220), - bg_modal_section: hex(0xFDF6E3), - border_subtle: hex(0xDCD6C0), - divider: hex(0xDCD6C0), - }; - - // ── Tokyo Night ── - pub const TOKYONIGHT_NIGHT: Self = Self { - bg_primary: hex(0x1A1B26), - bg_sidebar: hex(0x16161E), - bg_card: hex(0x292E42), - bg_card_hover: hex(0x3B4261), - bg_card_pressed: hex(0x414868), - bg_selected: hex(0x2D3F76), - bg_input: hex(0x292E42), - bg_progress: hex(0x16161E), - text_primary: hex(0xC0CAF5), - text_secondary: hex(0xA9B1D6), - text_muted: hex(0x8890B0), - text_dim: hex(0x737AA2), - text_dimmer: hex(0x565F89), - text_dimmest: hex(0x414868), - text_placeholder: hex(0x565F89), - accent_blue: hex(0x7AA2F7), - accent_icon: hex(0x7DCFFF), - accent_progress: hex(0x7AA2F7), - btn_default: hex(0x292E42), - btn_hover: hex(0x3B4261), - btn_pressed: hex(0x414868), - success: hex(0x9ECE6A), - success_bg: hex(0x202830), - btn_success: hex(0x7EAA50), - btn_success_hover: hex(0x709E44), - btn_success_pressed: hex(0x629238), - warning: hex(0xE0AF68), - warning_bg: hex(0x302820), - error: hex(0xF7768E), - error_light: hex(0xDB4B4B), - error_bg: hex(0x302025), - btn_danger_bg: hex(0x302025), - btn_danger_hover: hex(0x503035), - btn_trash_hover: hex(0xF7768E), - btn_trash_pressed: hex(0xD75E76), - bg_modal_section: hex(0x1A1B26), - border_subtle: hex(0x292E42), - divider: hex(0x292E42), - }; - - // ── Tokyo Night Day ── - pub const TOKYONIGHT_DAY: Self = Self { - bg_primary: hex(0xE1E2E7), - bg_sidebar: hex(0xD0D5E3), - bg_card: hex(0xC4C8DA), - bg_card_hover: hex(0xB7C1E3), - bg_card_pressed: hex(0xA8B4D4), - bg_selected: hex(0xB7C1E3), - bg_input: hex(0xC4C8DA), - bg_progress: hex(0xD0D5E3), - text_primary: hex(0x3760BF), - text_secondary: hex(0x4A6CA8), - text_muted: hex(0x6172B0), - text_dim: hex(0x848CB5), - text_dimmer: hex(0xA1A6C5), - text_dimmest: hex(0xB4B5B9), - text_placeholder: hex(0xA1A6C5), - accent_blue: hex(0x2E7DE9), - accent_icon: hex(0x007197), - accent_progress: hex(0x2E7DE9), - btn_default: hex(0xC4C8DA), - btn_hover: hex(0xB7C1E3), - btn_pressed: hex(0xA8B4D4), - success: hex(0x587539), - success_bg: hex(0xC8DCC0), - btn_success: hex(0x587539), - btn_success_hover: hex(0x4C692E), - btn_success_pressed: hex(0x405D24), - warning: hex(0x8C6C3E), - warning_bg: hex(0xDCD8C0), - error: hex(0xF52A65), - error_light: hex(0xC64343), - error_bg: hex(0xE8C0C8), - btn_danger_bg: hex(0xE8C0C8), - btn_danger_hover: hex(0xD8A8B4), - btn_trash_hover: hex(0xF52A65), - btn_trash_pressed: hex(0xD52050), - bg_modal_section: hex(0xE1E2E7), - border_subtle: hex(0xC4C8DA), - divider: hex(0xC4C8DA), - }; - - // ── Rosé Pine ── - pub const ROSEPINE_MAIN: Self = Self { - bg_primary: hex(0x191724), - bg_sidebar: hex(0x1F1D2E), - bg_card: hex(0x26233A), - bg_card_hover: hex(0x403D52), - bg_card_pressed: hex(0x524F67), - bg_selected: hex(0x403D52), - bg_input: hex(0x26233A), - bg_progress: hex(0x1F1D2E), - text_primary: hex(0xE0DEF4), - text_secondary: hex(0xCCCADD), - text_muted: hex(0x908CAA), - text_dim: hex(0x7E7A96), - text_dimmer: hex(0x6E6A86), - text_dimmest: hex(0x555168), - text_placeholder: hex(0x6E6A86), - accent_blue: hex(0x9CCFD8), - accent_icon: hex(0xC4A7E7), - accent_progress: hex(0x9CCFD8), - btn_default: hex(0x26233A), - btn_hover: hex(0x403D52), - btn_pressed: hex(0x524F67), - success: hex(0x31748F), - success_bg: hex(0x1E2830), - btn_success: hex(0x2A6478), - btn_success_hover: hex(0x24586C), - btn_success_pressed: hex(0x1E4C60), - warning: hex(0xF6C177), - warning_bg: hex(0x302820), - error: hex(0xEB6F92), - error_light: hex(0xF08098), - error_bg: hex(0x301828), - btn_danger_bg: hex(0x301828), - btn_danger_hover: hex(0x502838), - btn_trash_hover: hex(0xEB6F92), - btn_trash_pressed: hex(0xCB5878), - bg_modal_section: hex(0x191724), - border_subtle: hex(0x26233A), - divider: hex(0x26233A), - }; - - // ── Rosé Pine Moon ── - pub const ROSEPINE_MOON: Self = Self { - bg_primary: hex(0x232136), - bg_sidebar: hex(0x2A273F), - bg_card: hex(0x393552), - bg_card_hover: hex(0x44415A), - bg_card_pressed: hex(0x56526E), - bg_selected: hex(0x44415A), - bg_input: hex(0x393552), - bg_progress: hex(0x2A273F), - text_primary: hex(0xE0DEF4), - text_secondary: hex(0xCCCADD), - text_muted: hex(0x908CAA), - text_dim: hex(0x7E7A96), - text_dimmer: hex(0x6E6A86), - text_dimmest: hex(0x555168), - text_placeholder: hex(0x6E6A86), - accent_blue: hex(0x9CCFD8), - accent_icon: hex(0xC4A7E7), - accent_progress: hex(0x9CCFD8), - btn_default: hex(0x393552), - btn_hover: hex(0x44415A), - btn_pressed: hex(0x56526E), - success: hex(0x3E8FB0), - success_bg: hex(0x202830), - btn_success: hex(0x347A98), - btn_success_hover: hex(0x2C6E88), - btn_success_pressed: hex(0x246278), - warning: hex(0xF6C177), - warning_bg: hex(0x382820), - error: hex(0xEB6F92), - error_light: hex(0xF08098), - error_bg: hex(0x381828), - btn_danger_bg: hex(0x381828), - btn_danger_hover: hex(0x582838), - btn_trash_hover: hex(0xEB6F92), - btn_trash_pressed: hex(0xCB5878), - bg_modal_section: hex(0x232136), - border_subtle: hex(0x393552), - divider: hex(0x393552), - }; - - // ── Rosé Pine Dawn (light) ── - pub const ROSEPINE_DAWN: Self = Self { - bg_primary: hex(0xFAF4ED), - bg_sidebar: hex(0xF2E9E1), - bg_card: hex(0xF4EDE8), - bg_card_hover: hex(0xDFDAD9), - bg_card_pressed: hex(0xCECACD), - bg_selected: hex(0xDFDAD9), - bg_input: hex(0xF4EDE8), - bg_progress: hex(0xF2E9E1), - text_primary: hex(0x575279), - text_secondary: hex(0x6E6A86), - text_muted: hex(0x797593), - text_dim: hex(0x9893A5), - text_dimmer: hex(0xB0ACC0), - text_dimmest: hex(0xC0BCC8), - text_placeholder: hex(0xB0ACC0), - accent_blue: hex(0x56949F), - accent_icon: hex(0x907AA9), - accent_progress: hex(0x56949F), - btn_default: hex(0xF4EDE8), - btn_hover: hex(0xDFDAD9), - btn_pressed: hex(0xCECACD), - success: hex(0x286983), - success_bg: hex(0xD0E0D8), - btn_success: hex(0x286983), - btn_success_hover: hex(0x205C74), - btn_success_pressed: hex(0x184F65), - warning: hex(0xEA9D34), - warning_bg: hex(0xECE0C8), - error: hex(0xB4637A), - error_light: hex(0xC87890), - error_bg: hex(0xE8D0D4), - btn_danger_bg: hex(0xE8D0D4), - btn_danger_hover: hex(0xD8BCC4), - btn_trash_hover: hex(0xB4637A), - btn_trash_pressed: hex(0x965268), - bg_modal_section: hex(0xFAF4ED), - border_subtle: hex(0xDFDAD9), - divider: hex(0xDFDAD9), - }; - - // ── One Dark ── - pub const ONEDARK_DARK: Self = Self { - bg_primary: hex(0x282C34), - bg_sidebar: hex(0x21252B), - bg_card: hex(0x3E4452), - bg_card_hover: hex(0x4B5162), - bg_card_pressed: hex(0x5C6370), - bg_selected: hex(0x3E4452), - bg_input: hex(0x3E4452), - bg_progress: hex(0x21252B), - text_primary: hex(0xABB2BF), - text_secondary: hex(0x9DA4B0), - text_muted: hex(0x848B98), - text_dim: hex(0x6B7280), - text_dimmer: hex(0x5C6370), - text_dimmest: hex(0x4B5162), - text_placeholder: hex(0x5C6370), - accent_blue: hex(0x61AFEF), - accent_icon: hex(0x56B6C2), - accent_progress: hex(0x61AFEF), - btn_default: hex(0x3E4452), - btn_hover: hex(0x4B5162), - btn_pressed: hex(0x5C6370), - success: hex(0x98C379), - success_bg: hex(0x1E3428), - btn_success: hex(0x78A060), - btn_success_hover: hex(0x6C9454), - btn_success_pressed: hex(0x608848), - warning: hex(0xE5C07B), - warning_bg: hex(0x343020), - error: hex(0xE06C75), - error_light: hex(0xE88888), - error_bg: hex(0x342028), - btn_danger_bg: hex(0x342028), - btn_danger_hover: hex(0x543038), - btn_trash_hover: hex(0xE06C75), - btn_trash_pressed: hex(0xC05860), - bg_modal_section: hex(0x282C34), - border_subtle: hex(0x3E4452), - divider: hex(0x3E4452), - }; - - // ── One Dark Light ── - pub const ONEDARK_LIGHT: Self = Self { - bg_primary: hex(0xFAFAFA), - bg_sidebar: hex(0xF0F0F0), - bg_card: hex(0xE2E2E2), - bg_card_hover: hex(0xD4D4D4), - bg_card_pressed: hex(0xC8C8C8), - bg_selected: hex(0xD4D4D4), - bg_input: hex(0xE2E2E2), - bg_progress: hex(0xF0F0F0), - text_primary: hex(0x383A42), - text_secondary: hex(0x4A4C56), - text_muted: hex(0x686A76), - text_dim: hex(0x818387), - text_dimmer: hex(0xA0A1A7), - text_dimmest: hex(0xB8B9BC), - text_placeholder: hex(0xA0A1A7), - accent_blue: hex(0x4078F2), - accent_icon: hex(0x0184BC), - accent_progress: hex(0x4078F2), - btn_default: hex(0xE2E2E2), - btn_hover: hex(0xD4D4D4), - btn_pressed: hex(0xC8C8C8), - success: hex(0x50A14F), - success_bg: hex(0xD0E8D0), - btn_success: hex(0x50A14F), - btn_success_hover: hex(0x449444), - btn_success_pressed: hex(0x388838), - warning: hex(0xC18401), - warning_bg: hex(0xECE0C0), - error: hex(0xE45649), - error_light: hex(0xF06860), - error_bg: hex(0xEACCCC), - btn_danger_bg: hex(0xEACCCC), - btn_danger_hover: hex(0xDAB8B8), - btn_trash_hover: hex(0xE45649), - btn_trash_pressed: hex(0xC44038), - bg_modal_section: hex(0xFAFAFA), - border_subtle: hex(0xD4D4D4), - divider: hex(0xD4D4D4), - }; - - // ── Monokai Pro ── - pub const MONOKAI_PRO: Self = Self { - bg_primary: hex(0x2D2A2E), - bg_sidebar: hex(0x221F22), - bg_card: hex(0x403E41), - bg_card_hover: hex(0x525052), - bg_card_pressed: hex(0x5B595C), - bg_selected: hex(0x403E41), - bg_input: hex(0x403E41), - bg_progress: hex(0x221F22), - text_primary: hex(0xFCFCFA), - text_secondary: hex(0xE0E0DE), - text_muted: hex(0xC1C0C0), - text_dim: hex(0x939293), - text_dimmer: hex(0x727072), - text_dimmest: hex(0x5B595C), - text_placeholder: hex(0x727072), - accent_blue: hex(0x78DCE8), - accent_icon: hex(0xAB9DF2), - accent_progress: hex(0x78DCE8), - btn_default: hex(0x403E41), - btn_hover: hex(0x525052), - btn_pressed: hex(0x5B595C), - success: hex(0xA9DC76), - success_bg: hex(0x222820), - btn_success: hex(0x88B860), - btn_success_hover: hex(0x7CAC54), - btn_success_pressed: hex(0x70A048), - warning: hex(0xFFD866), - warning_bg: hex(0x302A20), - error: hex(0xFF6188), - error_light: hex(0xFF8098), - error_bg: hex(0x341E28), - btn_danger_bg: hex(0x341E28), - btn_danger_hover: hex(0x542E38), - btn_trash_hover: hex(0xFF6188), - btn_trash_pressed: hex(0xDF5070), - bg_modal_section: hex(0x2D2A2E), - border_subtle: hex(0x403E41), - divider: hex(0x403E41), - }; - - // ── Monokai Classic ── - pub const MONOKAI_CLASSIC: Self = Self { - bg_primary: hex(0x272822), - bg_sidebar: hex(0x1A1A18), - bg_card: hex(0x49483E), - bg_card_hover: hex(0x5A5950), - bg_card_pressed: hex(0x6B6A62), - bg_selected: hex(0x49483E), - bg_input: hex(0x49483E), - bg_progress: hex(0x1A1A18), - text_primary: hex(0xF8F8F2), - text_secondary: hex(0xE0E0D8), - text_muted: hex(0xC1C0C0), - text_dim: hex(0x939293), - text_dimmer: hex(0x75715E), - text_dimmest: hex(0x5B595C), - text_placeholder: hex(0x75715E), - accent_blue: hex(0x66D9EF), - accent_icon: hex(0xAE81FF), - accent_progress: hex(0x66D9EF), - btn_default: hex(0x49483E), - btn_hover: hex(0x5A5950), - btn_pressed: hex(0x6B6A62), - success: hex(0xA6E22E), - success_bg: hex(0x1E2E18), - btn_success: hex(0x86C020), - btn_success_hover: hex(0x7AB418), - btn_success_pressed: hex(0x6EA810), - warning: hex(0xE6DB74), - warning_bg: hex(0x302A18), - error: hex(0xF92672), - error_light: hex(0xFF5090), - error_bg: hex(0x341828), - btn_danger_bg: hex(0x341828), - btn_danger_hover: hex(0x542838), - btn_trash_hover: hex(0xF92672), - btn_trash_pressed: hex(0xD91860), - bg_modal_section: hex(0x272822), - border_subtle: hex(0x49483E), - divider: hex(0x49483E), - }; - - // ── Monokai Spectrum ── - pub const MONOKAI_SPECTRUM: Self = Self { - bg_primary: hex(0x222222), - bg_sidebar: hex(0x191919), - bg_card: hex(0x363537), - bg_card_hover: hex(0x484749), - bg_card_pressed: hex(0x5A595B), - bg_selected: hex(0x363537), - bg_input: hex(0x363537), - bg_progress: hex(0x191919), - text_primary: hex(0xF7F1FF), - text_secondary: hex(0xE0DAE8), - text_muted: hex(0xC0BAC8), - text_dim: hex(0x938EA0), - text_dimmer: hex(0x69676C), - text_dimmest: hex(0x4A484C), - text_placeholder: hex(0x69676C), - accent_blue: hex(0x5AD4E6), - accent_icon: hex(0x948AE3), - accent_progress: hex(0x5AD4E6), - btn_default: hex(0x363537), - btn_hover: hex(0x484749), - btn_pressed: hex(0x5A595B), - success: hex(0x7BD88F), - success_bg: hex(0x1A2820), - btn_success: hex(0x62B074), - btn_success_hover: hex(0x58A468), - btn_success_pressed: hex(0x4E985C), - warning: hex(0xFCE566), - warning_bg: hex(0x2E2A1A), - error: hex(0xFC618D), - error_light: hex(0xFF80A0), - error_bg: hex(0x2E1A22), - btn_danger_bg: hex(0x2E1A22), - btn_danger_hover: hex(0x4E2A32), - btn_trash_hover: hex(0xFC618D), - btn_trash_pressed: hex(0xDC5078), - bg_modal_section: hex(0x222222), - border_subtle: hex(0x363537), - divider: hex(0x363537), - }; - - // ── Ayu Dark ── - pub const AYU_DARK: Self = Self { - bg_primary: hex(0x0B0E14), - bg_sidebar: hex(0x070A10), - bg_card: hex(0x1B2028), - bg_card_hover: hex(0x252B35), - bg_card_pressed: hex(0x303842), - bg_selected: hex(0x1B3A4B), - bg_input: hex(0x1B2028), - bg_progress: hex(0x070A10), - text_primary: hex(0xBFBDB6), - text_secondary: hex(0xA8A6A0), - text_muted: hex(0x8A8880), - text_dim: hex(0x7A786E), - text_dimmer: hex(0x626A73), - text_dimmest: hex(0x4A505A), - text_placeholder: hex(0x626A73), - accent_blue: hex(0xE6B450), - accent_icon: hex(0x59C2FF), - accent_progress: hex(0xE6B450), - btn_default: hex(0x1B2028), - btn_hover: hex(0x252B35), - btn_pressed: hex(0x303842), - success: hex(0xAAD94C), - success_bg: hex(0x0C1A10), - btn_success: hex(0x88B040), - btn_success_hover: hex(0x7CA436), - btn_success_pressed: hex(0x70982C), - warning: hex(0xFFB454), - warning_bg: hex(0x1A1808), - error: hex(0xD95757), - error_light: hex(0xE87070), - error_bg: hex(0x1A0C0C), - btn_danger_bg: hex(0x1A0C0C), - btn_danger_hover: hex(0x2A1C1C), - btn_trash_hover: hex(0xD95757), - btn_trash_pressed: hex(0xB94848), - bg_modal_section: hex(0x0B0E14), - border_subtle: hex(0x1B2028), - divider: hex(0x1B2028), - }; - - // ── Ayu Mirage ── - pub const AYU_MIRAGE: Self = Self { - bg_primary: hex(0x1F2430), - bg_sidebar: hex(0x1A1F2B), - bg_card: hex(0x2A303E), - bg_card_hover: hex(0x33415E), - bg_card_pressed: hex(0x3D4A68), - bg_selected: hex(0x33415E), - bg_input: hex(0x2A303E), - bg_progress: hex(0x1A1F2B), - text_primary: hex(0xCCCAC2), - text_secondary: hex(0xB4B2AA), - text_muted: hex(0x9A988E), - text_dim: hex(0x858380), - text_dimmer: hex(0x707A8C), - text_dimmest: hex(0x555D6E), - text_placeholder: hex(0x707A8C), - accent_blue: hex(0xFFCC66), - accent_icon: hex(0x73D0FF), - accent_progress: hex(0xFFCC66), - btn_default: hex(0x2A303E), - btn_hover: hex(0x33415E), - btn_pressed: hex(0x3D4A68), - success: hex(0xBAE67E), - success_bg: hex(0x1A2A1E), - btn_success: hex(0x98C066), - btn_success_hover: hex(0x8CB45A), - btn_success_pressed: hex(0x80A84E), - warning: hex(0xFFD580), - warning_bg: hex(0x2A2818), - error: hex(0xF28779), - error_light: hex(0xFF9E90), - error_bg: hex(0x2A1E1E), - btn_danger_bg: hex(0x2A1E1E), - btn_danger_hover: hex(0x4A2E2E), - btn_trash_hover: hex(0xF28779), - btn_trash_pressed: hex(0xD27060), - bg_modal_section: hex(0x1F2430), - border_subtle: hex(0x2A303E), - divider: hex(0x2A303E), - }; - - // ── Ayu Light ── - pub const AYU_LIGHT: Self = Self { - bg_primary: hex(0xFAFAFA), - bg_sidebar: hex(0xF0EEE4), - bg_card: hex(0xE8E6DC), - bg_card_hover: hex(0xDCD8CC), - bg_card_pressed: hex(0xD0CCC0), - bg_selected: hex(0xD1E4F4), - bg_input: hex(0xE8E6DC), - bg_progress: hex(0xF0EEE4), - text_primary: hex(0x575F66), - text_secondary: hex(0x6B737A), - text_muted: hex(0x848C94), - text_dim: hex(0x9CA4AC), - text_dimmer: hex(0xABB0B6), - text_dimmest: hex(0xC0C4C8), - text_placeholder: hex(0xABB0B6), - accent_blue: hex(0xFF9940), - accent_icon: hex(0x36A3D9), - accent_progress: hex(0xFF9940), - btn_default: hex(0xE8E6DC), - btn_hover: hex(0xDCD8CC), - btn_pressed: hex(0xD0CCC0), - success: hex(0x86B300), - success_bg: hex(0xD8E8C8), - btn_success: hex(0x86B300), - btn_success_hover: hex(0x78A400), - btn_success_pressed: hex(0x6A9600), - warning: hex(0xF29718), - warning_bg: hex(0xEDE0C0), - error: hex(0xF51818), - error_light: hex(0xF04040), - error_bg: hex(0xEEC8C8), - btn_danger_bg: hex(0xEEC8C8), - btn_danger_hover: hex(0xDEB0B0), - btn_trash_hover: hex(0xF51818), - btn_trash_pressed: hex(0xD50E0E), - bg_modal_section: hex(0xFAFAFA), - border_subtle: hex(0xDCD8CC), - divider: hex(0xDCD8CC), - }; - - // ── Everforest Dark ── - pub const EVERFOREST_DARK: Self = Self { - bg_primary: hex(0x2D353B), - bg_sidebar: hex(0x232A2E), - bg_card: hex(0x343F44), - bg_card_hover: hex(0x3D484D), - bg_card_pressed: hex(0x475258), - bg_selected: hex(0x543A48), - bg_input: hex(0x343F44), - bg_progress: hex(0x232A2E), - text_primary: hex(0xD3C6AA), - text_secondary: hex(0xC0B498), - text_muted: hex(0xA09880), - text_dim: hex(0x859289), - text_dimmer: hex(0x7A8478), - text_dimmest: hex(0x5C6A62), - text_placeholder: hex(0x7A8478), - accent_blue: hex(0x7FBBB3), - accent_icon: hex(0xA7C080), - accent_progress: hex(0x7FBBB3), - btn_default: hex(0x343F44), - btn_hover: hex(0x3D484D), - btn_pressed: hex(0x475258), - success: hex(0xA7C080), - success_bg: hex(0x1E2E24), - btn_success: hex(0x86A066), - btn_success_hover: hex(0x7A945A), - btn_success_pressed: hex(0x6E884E), - warning: hex(0xDBBC7F), - warning_bg: hex(0x2E2A1E), - error: hex(0xE67E80), - error_light: hex(0xF09090), - error_bg: hex(0x2E2020), - btn_danger_bg: hex(0x2E2020), - btn_danger_hover: hex(0x4E3030), - btn_trash_hover: hex(0xE67E80), - btn_trash_pressed: hex(0xC66868), - bg_modal_section: hex(0x2D353B), - border_subtle: hex(0x343F44), - divider: hex(0x343F44), - }; - - // ── Everforest Light ── - pub const EVERFOREST_LIGHT: Self = Self { - bg_primary: hex(0xFDF6E3), - bg_sidebar: hex(0xEFECD4), - bg_card: hex(0xF4F0D9), - bg_card_hover: hex(0xE6E2CC), - bg_card_pressed: hex(0xE0DCC7), - bg_selected: hex(0xEADDC0), - bg_input: hex(0xF4F0D9), - bg_progress: hex(0xEFECD4), - text_primary: hex(0x5C6A72), - text_secondary: hex(0x6E7A80), - text_muted: hex(0x829181), - text_dim: hex(0x939F91), - text_dimmer: hex(0xA6B0A0), - text_dimmest: hex(0xBCC5B8), - text_placeholder: hex(0xA6B0A0), - accent_blue: hex(0x3A94C5), - accent_icon: hex(0x8DA101), - accent_progress: hex(0x3A94C5), - btn_default: hex(0xF4F0D9), - btn_hover: hex(0xE6E2CC), - btn_pressed: hex(0xE0DCC7), - success: hex(0x8DA101), - success_bg: hex(0xD0E0C4), - btn_success: hex(0x8DA101), - btn_success_hover: hex(0x7E9200), - btn_success_pressed: hex(0x6F8300), - warning: hex(0xDFA000), - warning_bg: hex(0xECE0B8), - error: hex(0xF85552), - error_light: hex(0xE86868), - error_bg: hex(0xECC8C4), - btn_danger_bg: hex(0xECC8C4), - btn_danger_hover: hex(0xDCB0AC), - btn_trash_hover: hex(0xF85552), - btn_trash_pressed: hex(0xD84040), - bg_modal_section: hex(0xFDF6E3), - border_subtle: hex(0xE6E2CC), - divider: hex(0xE6E2CC), - }; - - // ── Material Oceanic ── - pub const MATERIAL_OCEANIC: Self = Self { - bg_primary: hex(0x263238), - bg_sidebar: hex(0x1E272C), - bg_card: hex(0x2E3C42), - bg_card_hover: hex(0x3A4A52), - bg_card_pressed: hex(0x465862), - bg_selected: hex(0x546E7A), - bg_input: hex(0x2E3C42), - bg_progress: hex(0x1E272C), - text_primary: hex(0xB0BEC5), - text_secondary: hex(0x9AAAB2), - text_muted: hex(0x849AA4), - text_dim: hex(0x6E8490), - text_dimmer: hex(0x546E7A), - text_dimmest: hex(0x405A64), - text_placeholder: hex(0x546E7A), - accent_blue: hex(0x89DDFF), - accent_icon: hex(0x80CBC4), - accent_progress: hex(0x89DDFF), - btn_default: hex(0x2E3C42), - btn_hover: hex(0x3A4A52), - btn_pressed: hex(0x465862), - success: hex(0xC3E88D), - success_bg: hex(0x1A3028), - btn_success: hex(0xA0C070), - btn_success_hover: hex(0x94B464), - btn_success_pressed: hex(0x88A858), - warning: hex(0xFFCB6B), - warning_bg: hex(0x2E2E1E), - error: hex(0xFF5370), - error_light: hex(0xFF7088), - error_bg: hex(0x2E1E22), - btn_danger_bg: hex(0x2E1E22), - btn_danger_hover: hex(0x4E2E32), - btn_trash_hover: hex(0xFF5370), - btn_trash_pressed: hex(0xDF4058), - bg_modal_section: hex(0x263238), - border_subtle: hex(0x2E3C42), - divider: hex(0x2E3C42), - }; - - // ── Material Palenight ── - pub const MATERIAL_PALENIGHT: Self = Self { - bg_primary: hex(0x292D3E), - bg_sidebar: hex(0x232838), - bg_card: hex(0x343A4E), - bg_card_hover: hex(0x414662), - bg_card_pressed: hex(0x515772), - bg_selected: hex(0x717CB4), - bg_input: hex(0x343A4E), - bg_progress: hex(0x232838), - text_primary: hex(0xA6ACCD), - text_secondary: hex(0x929ABE), - text_muted: hex(0x7E86AE), - text_dim: hex(0x717CB4), - text_dimmer: hex(0x676E95), - text_dimmest: hex(0x515772), - text_placeholder: hex(0x676E95), - accent_blue: hex(0xC792EA), - accent_icon: hex(0x82AAFF), - accent_progress: hex(0xC792EA), - btn_default: hex(0x343A4E), - btn_hover: hex(0x414662), - btn_pressed: hex(0x515772), - success: hex(0xC3E88D), - success_bg: hex(0x202E28), - btn_success: hex(0xA0C070), - btn_success_hover: hex(0x94B464), - btn_success_pressed: hex(0x88A858), - warning: hex(0xFFCB6B), - warning_bg: hex(0x2E2C1E), - error: hex(0xFF5370), - error_light: hex(0xFF7088), - error_bg: hex(0x2E1E22), - btn_danger_bg: hex(0x2E1E22), - btn_danger_hover: hex(0x4E2E32), - btn_trash_hover: hex(0xFF5370), - btn_trash_pressed: hex(0xDF4058), - bg_modal_section: hex(0x292D3E), - border_subtle: hex(0x343A4E), - divider: hex(0x343A4E), - }; - - // ── Material Deep Ocean ── - pub const MATERIAL_DEEPOCEAN: Self = Self { - bg_primary: hex(0x0F111A), - bg_sidebar: hex(0x090B10), - bg_card: hex(0x1A1C28), - bg_card_hover: hex(0x252836), - bg_card_pressed: hex(0x3B3F51), - bg_selected: hex(0x44475A), - bg_input: hex(0x1A1C28), - bg_progress: hex(0x090B10), - text_primary: hex(0x8F93A2), - text_secondary: hex(0xA0A4B4), - text_muted: hex(0x7880A0), - text_dim: hex(0x606888), - text_dimmer: hex(0x464B5D), - text_dimmest: hex(0x3B3F51), - text_placeholder: hex(0x464B5D), - accent_blue: hex(0x84FFFF), - accent_icon: hex(0x82AAFF), - accent_progress: hex(0x84FFFF), - btn_default: hex(0x1A1C28), - btn_hover: hex(0x252836), - btn_pressed: hex(0x3B3F51), - success: hex(0xC3E88D), - success_bg: hex(0x0C1A14), - btn_success: hex(0xA0C070), - btn_success_hover: hex(0x94B464), - btn_success_pressed: hex(0x88A858), - warning: hex(0xFFCB6B), - warning_bg: hex(0x1A180C), - error: hex(0xFF5370), - error_light: hex(0xFF7088), - error_bg: hex(0x1A0C10), - btn_danger_bg: hex(0x1A0C10), - btn_danger_hover: hex(0x3A1C20), - btn_trash_hover: hex(0xFF5370), - btn_trash_pressed: hex(0xDF4058), - bg_modal_section: hex(0x0F111A), - border_subtle: hex(0x1A1C28), - divider: hex(0x1A1C28), - }; - - // ── Flexoki Dark ── - pub const FLEXOKI_DARK: Self = Self { - bg_primary: hex(0x100F0F), - bg_sidebar: hex(0x1C1B1A), - bg_card: hex(0x282726), - bg_card_hover: hex(0x343331), - bg_card_pressed: hex(0x403E3C), - bg_selected: hex(0x403E3C), - bg_input: hex(0x282726), - bg_progress: hex(0x1C1B1A), - text_primary: hex(0xCECDC3), - text_secondary: hex(0xB7B5AC), - text_muted: hex(0x9F9D96), - text_dim: hex(0x878580), - text_dimmer: hex(0x6F6E69), - text_dimmest: hex(0x575653), - text_placeholder: hex(0x6F6E69), - accent_blue: hex(0x4385BE), - accent_icon: hex(0xDA702C), - accent_progress: hex(0x4385BE), - btn_default: hex(0x282726), - btn_hover: hex(0x343331), - btn_pressed: hex(0x403E3C), - success: hex(0x879A39), - success_bg: hex(0x141A10), - btn_success: hex(0x6E8030), - btn_success_hover: hex(0x627428), - btn_success_pressed: hex(0x566820), - warning: hex(0xD0A215), - warning_bg: hex(0x1E1A0C), - error: hex(0xD14D41), - error_light: hex(0xE06858), - error_bg: hex(0x1E100C), - btn_danger_bg: hex(0x1E100C), - btn_danger_hover: hex(0x3E201C), - btn_trash_hover: hex(0xD14D41), - btn_trash_pressed: hex(0xB13830), - bg_modal_section: hex(0x100F0F), - border_subtle: hex(0x282726), - divider: hex(0x282726), - }; - - // ── Flexoki Light ── - pub const FLEXOKI_LIGHT: Self = Self { - bg_primary: hex(0xFFFCF0), - bg_sidebar: hex(0xF2F0E5), - bg_card: hex(0xE6E4D9), - bg_card_hover: hex(0xDAD8CE), - bg_card_pressed: hex(0xCECDC3), - bg_selected: hex(0xE6E4D9), - bg_input: hex(0xE6E4D9), - bg_progress: hex(0xF2F0E5), - text_primary: hex(0x403E3C), - text_secondary: hex(0x575653), - text_muted: hex(0x6F6E69), - text_dim: hex(0x878580), - text_dimmer: hex(0x9F9D96), - text_dimmest: hex(0xB7B5AC), - text_placeholder: hex(0x9F9D96), - accent_blue: hex(0x205EA6), - accent_icon: hex(0xBC5215), - accent_progress: hex(0x205EA6), - btn_default: hex(0xE6E4D9), - btn_hover: hex(0xDAD8CE), - btn_pressed: hex(0xCECDC3), - success: hex(0x66800B), - success_bg: hex(0xD0E0C0), - btn_success: hex(0x66800B), - btn_success_hover: hex(0x587208), - btn_success_pressed: hex(0x4A6405), - warning: hex(0xAD8301), - warning_bg: hex(0xE0DCC0), - error: hex(0xAF3029), - error_light: hex(0xD14D41), - error_bg: hex(0xE0C8C0), - btn_danger_bg: hex(0xE0C8C0), - btn_danger_hover: hex(0xD0B4AC), - btn_trash_hover: hex(0xAF3029), - btn_trash_pressed: hex(0x902420), - bg_modal_section: hex(0xFFFCF0), - border_subtle: hex(0xDAD8CE), - divider: hex(0xDAD8CE), - }; - - // ── Nightfox ── - pub const NIGHTFOX: Self = Self { - bg_primary: hex(0x192330), - bg_sidebar: hex(0x131A24), - bg_card: hex(0x2B3B51), - bg_card_hover: hex(0x3C5372), - bg_card_pressed: hex(0x39506D), - bg_selected: hex(0x2B3B51), - bg_input: hex(0x2B3B51), - bg_progress: hex(0x131A24), - text_primary: hex(0xCDCECF), - text_secondary: hex(0xAEAFB0), - text_muted: hex(0x8E9098), - text_dim: hex(0x738091), - text_dimmer: hex(0x5C6A7C), - text_dimmest: hex(0x3E4E62), - text_placeholder: hex(0x5C6A7C), - accent_blue: hex(0x719CD6), - accent_icon: hex(0x63CDCF), - accent_progress: hex(0x719CD6), - btn_default: hex(0x2B3B51), - btn_hover: hex(0x3C5372), - btn_pressed: hex(0x39506D), - success: hex(0x81B29A), - success_bg: hex(0x142820), - btn_success: hex(0x68907E), - btn_success_hover: hex(0x5C8472), - btn_success_pressed: hex(0x507866), - warning: hex(0xDBC074), - warning_bg: hex(0x282418), - error: hex(0xC94F6D), - error_light: hex(0xD66880), - error_bg: hex(0x281820), - btn_danger_bg: hex(0x281820), - btn_danger_hover: hex(0x482830), - btn_trash_hover: hex(0xC94F6D), - btn_trash_pressed: hex(0xA9405C), - bg_modal_section: hex(0x192330), - border_subtle: hex(0x2B3B51), - divider: hex(0x2B3B51), - }; - - // ── Dawnfox (light) ── - pub const DAWNFOX: Self = Self { - bg_primary: hex(0xFAF4ED), - bg_sidebar: hex(0xEBE5DF), - bg_card: hex(0xEBD8CE), - bg_card_hover: hex(0xDACDC3), - bg_card_pressed: hex(0xC8BEB4), - bg_selected: hex(0xEBD8CE), - bg_input: hex(0xEBD8CE), - bg_progress: hex(0xEBE5DF), - text_primary: hex(0x575279), - text_secondary: hex(0x625C87), - text_muted: hex(0x6E6A86), - text_dim: hex(0x9893A5), - text_dimmer: hex(0xAEA8B8), - text_dimmest: hex(0xC0BAC8), - text_placeholder: hex(0xAEA8B8), - accent_blue: hex(0x286983), - accent_icon: hex(0x907AA9), - accent_progress: hex(0x286983), - btn_default: hex(0xEBD8CE), - btn_hover: hex(0xDACDC3), - btn_pressed: hex(0xC8BEB4), - success: hex(0x618774), - success_bg: hex(0xD0E0D4), - btn_success: hex(0x618774), - btn_success_hover: hex(0x547A68), - btn_success_pressed: hex(0x476D5C), - warning: hex(0xEA9D34), - warning_bg: hex(0xECE0C8), - error: hex(0xB4637A), - error_light: hex(0xC87890), - error_bg: hex(0xE8D0D4), - btn_danger_bg: hex(0xE8D0D4), - btn_danger_hover: hex(0xD8BCC4), - btn_trash_hover: hex(0xB4637A), - btn_trash_pressed: hex(0x965268), - bg_modal_section: hex(0xFAF4ED), - border_subtle: hex(0xDACDC3), - divider: hex(0xDACDC3), - }; - - // ── Sonokai ── - pub const SONOKAI_DEFAULT: Self = Self { - bg_primary: hex(0x2C2E34), - bg_sidebar: hex(0x242529), - bg_card: hex(0x33353F), - bg_card_hover: hex(0x3B3E48), - bg_card_pressed: hex(0x414550), - bg_selected: hex(0x3B3E48), - bg_input: hex(0x33353F), - bg_progress: hex(0x242529), - text_primary: hex(0xE2E2E3), - text_secondary: hex(0xCCCCD0), - text_muted: hex(0xA8A8B0), - text_dim: hex(0x8C8C96), - text_dimmer: hex(0x7F8490), - text_dimmest: hex(0x585C68), - text_placeholder: hex(0x7F8490), - accent_blue: hex(0x76CCE0), - accent_icon: hex(0xB39DF3), - accent_progress: hex(0x76CCE0), - btn_default: hex(0x33353F), - btn_hover: hex(0x3B3E48), - btn_pressed: hex(0x414550), - success: hex(0x9ED072), - success_bg: hex(0x1E2E22), - btn_success: hex(0x7EAA5A), - btn_success_hover: hex(0x729E4E), - btn_success_pressed: hex(0x669242), - warning: hex(0xE7C664), - warning_bg: hex(0x2E2A1A), - error: hex(0xFC5D7C), - error_light: hex(0xFF7890), - error_bg: hex(0x2E1A20), - btn_danger_bg: hex(0x2E1A20), - btn_danger_hover: hex(0x4E2A30), - btn_trash_hover: hex(0xFC5D7C), - btn_trash_pressed: hex(0xDC4C66), - bg_modal_section: hex(0x2C2E34), - border_subtle: hex(0x33353F), - divider: hex(0x33353F), - }; - - // ── Oxocarbon Dark ── - pub const OXOCARBON_DARK: Self = Self { - bg_primary: hex(0x161616), - bg_sidebar: hex(0x0E0E0E), - bg_card: hex(0x262626), - bg_card_hover: hex(0x393939), - bg_card_pressed: hex(0x525252), - bg_selected: hex(0x393939), - bg_input: hex(0x262626), - bg_progress: hex(0x0E0E0E), - text_primary: hex(0xF2F4F8), - text_secondary: hex(0xDDE1E6), - text_muted: hex(0xB0B4BC), - text_dim: hex(0x8A8E96), - text_dimmer: hex(0x6E7278), - text_dimmest: hex(0x525252), - text_placeholder: hex(0x6E7278), - accent_blue: hex(0x78A9FF), - accent_icon: hex(0xBE95FF), - accent_progress: hex(0x78A9FF), - btn_default: hex(0x262626), - btn_hover: hex(0x393939), - btn_pressed: hex(0x525252), - success: hex(0x42BE65), - success_bg: hex(0x0C1C12), - btn_success: hex(0x359E52), - btn_success_hover: hex(0x2C9048), - btn_success_pressed: hex(0x24823E), - warning: hex(0x08BDBA), - warning_bg: hex(0x0C1A1A), - error: hex(0xEE5396), - error_light: hex(0xFF7EB6), - error_bg: hex(0x1C0C14), - btn_danger_bg: hex(0x1C0C14), - btn_danger_hover: hex(0x3C1C24), - btn_trash_hover: hex(0xEE5396), - btn_trash_pressed: hex(0xCE4480), - bg_modal_section: hex(0x161616), - border_subtle: hex(0x262626), - divider: hex(0x262626), - }; - - // ── Oxocarbon Light ── - pub const OXOCARBON_LIGHT: Self = Self { - bg_primary: hex(0xFFFFFF), - bg_sidebar: hex(0xF2F4F8), - bg_card: hex(0xDDE1E6), - bg_card_hover: hex(0xC8CCD2), - bg_card_pressed: hex(0xB4B8C0), - bg_selected: hex(0xDDE1E6), - bg_input: hex(0xDDE1E6), - bg_progress: hex(0xF2F4F8), - text_primary: hex(0x262626), - text_secondary: hex(0x393939), - text_muted: hex(0x525252), - text_dim: hex(0x6E7278), - text_dimmer: hex(0x8A8E96), - text_dimmest: hex(0xB0B4BC), - text_placeholder: hex(0x8A8E96), - accent_blue: hex(0x0F62FE), - accent_icon: hex(0x8A3FFC), - accent_progress: hex(0x0F62FE), - btn_default: hex(0xDDE1E6), - btn_hover: hex(0xC8CCD2), - btn_pressed: hex(0xB4B8C0), - success: hex(0x198038), - success_bg: hex(0xD0F0D8), - btn_success: hex(0x198038), - btn_success_hover: hex(0x14702E), - btn_success_pressed: hex(0x106024), - warning: hex(0x005D5D), - warning_bg: hex(0xD0E8E8), - error: hex(0xDA1E28), - error_light: hex(0xEE5396), - error_bg: hex(0xF0D0D8), - btn_danger_bg: hex(0xF0D0D8), - btn_danger_hover: hex(0xE0B8C4), - btn_trash_hover: hex(0xDA1E28), - btn_trash_pressed: hex(0xBA1420), - bg_modal_section: hex(0xFFFFFF), - border_subtle: hex(0xC8CCD2), - divider: hex(0xC8CCD2), - }; - - // ── Night Owl Dark ── - pub const NIGHTOWL_DARK: Self = Self { - bg_primary: hex(0x011627), - bg_sidebar: hex(0x011221), - bg_card: hex(0x0B2942), - bg_card_hover: hex(0x1D3B53), - bg_card_pressed: hex(0x2A4F6C), - bg_selected: hex(0x1D3B53), - bg_input: hex(0x0B2942), - bg_progress: hex(0x011221), - text_primary: hex(0xD6DEEB), - text_secondary: hex(0xB4C0D0), - text_muted: hex(0x8CA0B4), - text_dim: hex(0x7E94A8), - text_dimmer: hex(0x637777), - text_dimmest: hex(0x4A6060), - text_placeholder: hex(0x637777), - accent_blue: hex(0x82AAFF), - accent_icon: hex(0x7FDBCA), - accent_progress: hex(0x82AAFF), - btn_default: hex(0x0B2942), - btn_hover: hex(0x1D3B53), - btn_pressed: hex(0x2A4F6C), - success: hex(0xADDB67), - success_bg: hex(0x011E14), - btn_success: hex(0x8CB852), - btn_success_hover: hex(0x80AC48), - btn_success_pressed: hex(0x74A03E), - warning: hex(0xECC48D), - warning_bg: hex(0x1A1808), - error: hex(0xEF5350), - error_light: hex(0xFF6E6A), - error_bg: hex(0x1A0808), - btn_danger_bg: hex(0x1A0808), - btn_danger_hover: hex(0x3A1818), - btn_trash_hover: hex(0xEF5350), - btn_trash_pressed: hex(0xCF4040), - bg_modal_section: hex(0x011627), - border_subtle: hex(0x0B2942), - divider: hex(0x0B2942), - }; - - // ── Night Owl Light ── - pub const NIGHTOWL_LIGHT: Self = Self { - bg_primary: hex(0xFBFBFB), - bg_sidebar: hex(0xF0F0F0), - bg_card: hex(0xE8E8E8), - bg_card_hover: hex(0xE0E0E0), - bg_card_pressed: hex(0xD4D4D4), - bg_selected: hex(0xE0E0E0), - bg_input: hex(0xE8E8E8), - bg_progress: hex(0xF0F0F0), - text_primary: hex(0x403F53), - text_secondary: hex(0x565570), - text_muted: hex(0x6E6D88), - text_dim: hex(0x8888A0), - text_dimmer: hex(0x989FB1), - text_dimmest: hex(0xB0B4C0), - text_placeholder: hex(0x989FB1), - accent_blue: hex(0x4876D6), - accent_icon: hex(0x0C969B), - accent_progress: hex(0x4876D6), - btn_default: hex(0xE8E8E8), - btn_hover: hex(0xE0E0E0), - btn_pressed: hex(0xD4D4D4), - success: hex(0x2AA298), - success_bg: hex(0xD0E8E0), - btn_success: hex(0x2AA298), - btn_success_hover: hex(0x22948C), - btn_success_pressed: hex(0x1A8680), - warning: hex(0xD98E24), - warning_bg: hex(0xECE0C8), - error: hex(0xDE3D3B), - error_light: hex(0xE85858), - error_bg: hex(0xECC8C8), - btn_danger_bg: hex(0xECC8C8), - btn_danger_hover: hex(0xDCB0B0), - btn_trash_hover: hex(0xDE3D3B), - btn_trash_pressed: hex(0xBE2E2C), - bg_modal_section: hex(0xFBFBFB), - border_subtle: hex(0xE0E0E0), - divider: hex(0xE0E0E0), - }; - - // ── Iceberg Dark ── - pub const ICEBERG_DARK: Self = Self { - bg_primary: hex(0x161821), - bg_sidebar: hex(0x0F1117), - bg_card: hex(0x1E2132), - bg_card_hover: hex(0x2E313F), - bg_card_pressed: hex(0x3D425B), - bg_selected: hex(0x2E313F), - bg_input: hex(0x1E2132), - bg_progress: hex(0x0F1117), - text_primary: hex(0xC6C8D1), - text_secondary: hex(0xB0B4C0), - text_muted: hex(0x9498A8), - text_dim: hex(0x818596), - text_dimmer: hex(0x6B7089), - text_dimmest: hex(0x4E5268), - text_placeholder: hex(0x6B7089), - accent_blue: hex(0x84A0C6), - accent_icon: hex(0x89B8C2), - accent_progress: hex(0x84A0C6), - btn_default: hex(0x1E2132), - btn_hover: hex(0x2E313F), - btn_pressed: hex(0x3D425B), - success: hex(0xB4BE82), - success_bg: hex(0x141E18), - btn_success: hex(0x949E68), - btn_success_hover: hex(0x88925C), - btn_success_pressed: hex(0x7C8650), - warning: hex(0xE2A478), - warning_bg: hex(0x201C14), - error: hex(0xE27878), - error_light: hex(0xF09090), - error_bg: hex(0x201418), - btn_danger_bg: hex(0x201418), - btn_danger_hover: hex(0x402428), - btn_trash_hover: hex(0xE27878), - btn_trash_pressed: hex(0xC26060), - bg_modal_section: hex(0x161821), - border_subtle: hex(0x1E2132), - divider: hex(0x1E2132), - }; - - // ── Iceberg Light ── - pub const ICEBERG_LIGHT: Self = Self { - bg_primary: hex(0xE8E9EC), - bg_sidebar: hex(0xDCDFE7), - bg_card: hex(0xD0D4DE), - bg_card_hover: hex(0xCAD0DE), - bg_card_pressed: hex(0xBEC4D2), - bg_selected: hex(0xCAD0DE), - bg_input: hex(0xD0D4DE), - bg_progress: hex(0xDCDFE7), - text_primary: hex(0x33374C), - text_secondary: hex(0x444862), - text_muted: hex(0x5C6080), - text_dim: hex(0x747896), - text_dimmer: hex(0x8B98B6), - text_dimmest: hex(0xA8B0C8), - text_placeholder: hex(0x8B98B6), - accent_blue: hex(0x2D539E), - accent_icon: hex(0x33635C), - accent_progress: hex(0x2D539E), - btn_default: hex(0xD0D4DE), - btn_hover: hex(0xCAD0DE), - btn_pressed: hex(0xBEC4D2), - success: hex(0x668E3D), - success_bg: hex(0xD0E0C8), - btn_success: hex(0x668E3D), - btn_success_hover: hex(0x588032), - btn_success_pressed: hex(0x4A7228), - warning: hex(0xC57339), - warning_bg: hex(0xE4D8C8), - error: hex(0xCC517A), - error_light: hex(0xDD6890), - error_bg: hex(0xE4C8D4), - btn_danger_bg: hex(0xE4C8D4), - btn_danger_hover: hex(0xD4B4C0), - btn_trash_hover: hex(0xCC517A), - btn_trash_pressed: hex(0xAC4068), - bg_modal_section: hex(0xE8E9EC), - border_subtle: hex(0xCAD0DE), - divider: hex(0xCAD0DE), - }; - - // ── Horizon Dark ── - pub const HORIZON_DARK: Self = Self { - bg_primary: hex(0x1C1E26), - bg_sidebar: hex(0x16161C), - bg_card: hex(0x2E303E), - bg_card_hover: hex(0x3A3C4E), - bg_card_pressed: hex(0x484A60), - bg_selected: hex(0x2E303E), - bg_input: hex(0x2E303E), - bg_progress: hex(0x16161C), - text_primary: hex(0xD5D8DA), - text_secondary: hex(0xBEC0C4), - text_muted: hex(0xA0A2A8), - text_dim: hex(0x888A92), - text_dimmer: hex(0x6C6F93), - text_dimmest: hex(0x50526E), - text_placeholder: hex(0x6C6F93), - accent_blue: hex(0x26BBD9), - accent_icon: hex(0xB877DB), - accent_progress: hex(0x26BBD9), - btn_default: hex(0x2E303E), - btn_hover: hex(0x3A3C4E), - btn_pressed: hex(0x484A60), - success: hex(0x29D398), - success_bg: hex(0x141E1C), - btn_success: hex(0x22B480), - btn_success_hover: hex(0x1CA874), - btn_success_pressed: hex(0x169C68), - warning: hex(0xFAC29A), - warning_bg: hex(0x2A2218), - error: hex(0xE95678), - error_light: hex(0xF07090), - error_bg: hex(0x2A1620), - btn_danger_bg: hex(0x2A1620), - btn_danger_hover: hex(0x4A2630), - btn_trash_hover: hex(0xE95678), - btn_trash_pressed: hex(0xC94460), - bg_modal_section: hex(0x1C1E26), - border_subtle: hex(0x2E303E), - divider: hex(0x2E303E), - }; - - // ── Melange Dark ── - pub const MELANGE_DARK: Self = Self { - bg_primary: hex(0x292522), - bg_sidebar: hex(0x221E1B), - bg_card: hex(0x34302C), - bg_card_hover: hex(0x403A36), - bg_card_pressed: hex(0x4C4640), - bg_selected: hex(0x403A36), - bg_input: hex(0x34302C), - bg_progress: hex(0x221E1B), - text_primary: hex(0xECE1D7), - text_secondary: hex(0xD4C8BC), - text_muted: hex(0xBAB0A4), - text_dim: hex(0x9E9488), - text_dimmer: hex(0x867462), - text_dimmest: hex(0x6A5E50), - text_placeholder: hex(0x867462), - accent_blue: hex(0xA3A9CE), - accent_icon: hex(0x89B3B6), - accent_progress: hex(0xA3A9CE), - btn_default: hex(0x34302C), - btn_hover: hex(0x403A36), - btn_pressed: hex(0x4C4640), - success: hex(0x85B695), - success_bg: hex(0x1E2820), - btn_success: hex(0x6C9A7C), - btn_success_hover: hex(0x608E70), - btn_success_pressed: hex(0x548264), - warning: hex(0xEBC06D), - warning_bg: hex(0x2A261A), - error: hex(0xD47766), - error_light: hex(0xE89080), - error_bg: hex(0x2A1C18), - btn_danger_bg: hex(0x2A1C18), - btn_danger_hover: hex(0x4A2C28), - btn_trash_hover: hex(0xD47766), - btn_trash_pressed: hex(0xB46050), - bg_modal_section: hex(0x292522), - border_subtle: hex(0x34302C), - divider: hex(0x34302C), - }; - - // ── Melange Light ── - pub const MELANGE_LIGHT: Self = Self { - bg_primary: hex(0xF4F0ED), - bg_sidebar: hex(0xE9E1DB), - bg_card: hex(0xDDD2C8), - bg_card_hover: hex(0xD0C6BA), - bg_card_pressed: hex(0xC4BAAE), - bg_selected: hex(0xDDD2C8), - bg_input: hex(0xDDD2C8), - bg_progress: hex(0xE9E1DB), - text_primary: hex(0x54433A), - text_secondary: hex(0x6B5C4D), - text_muted: hex(0x7E6E5E), - text_dim: hex(0x948472), - text_dimmer: hex(0xA89888), - text_dimmest: hex(0xBEB0A0), - text_placeholder: hex(0xA89888), - accent_blue: hex(0x5E6DAB), - accent_icon: hex(0x3F7C82), - accent_progress: hex(0x5E6DAB), - btn_default: hex(0xDDD2C8), - btn_hover: hex(0xD0C6BA), - btn_pressed: hex(0xC4BAAE), - success: hex(0x4E7548), - success_bg: hex(0xD0E0D0), - btn_success: hex(0x4E7548), - btn_success_hover: hex(0x42683C), - btn_success_pressed: hex(0x365C30), - warning: hex(0x9A7C24), - warning_bg: hex(0xE0DCC0), - error: hex(0xA44C36), - error_light: hex(0xC06048), - error_bg: hex(0xE0CCC4), - btn_danger_bg: hex(0xE0CCC4), - btn_danger_hover: hex(0xD0B8B0), - btn_trash_hover: hex(0xA44C36), - btn_trash_pressed: hex(0x843C28), - bg_modal_section: hex(0xF4F0ED), - border_subtle: hex(0xD0C6BA), - divider: hex(0xD0C6BA), - }; - - // ── Synthwave '84 ── - pub const SYNTHWAVE_DARK: Self = Self { - bg_primary: hex(0x262335), - bg_sidebar: hex(0x241B2F), - bg_card: hex(0x34294F), - bg_card_hover: hex(0x3E3460), - bg_card_pressed: hex(0x463465), - bg_selected: hex(0x463465), - bg_input: hex(0x2A2139), - bg_progress: hex(0x241B2F), - text_primary: hex(0xFFFFFF), - text_secondary: hex(0xE2E2E2), - text_muted: hex(0xC0BCD0), - text_dim: hex(0xA09CB4), - text_dimmer: hex(0x848BBD), - text_dimmest: hex(0x614D85), - text_placeholder: hex(0x848BBD), - accent_blue: hex(0xFF7EDB), - accent_icon: hex(0x36F9F6), - accent_progress: hex(0xFF7EDB), - btn_default: hex(0x34294F), - btn_hover: hex(0x3E3460), - btn_pressed: hex(0x463465), - success: hex(0x72F1B8), - success_bg: hex(0x1A2828), - btn_success: hex(0x5CC898), - btn_success_hover: hex(0x50BC8C), - btn_success_pressed: hex(0x44B080), - warning: hex(0xFEDE5D), - warning_bg: hex(0x2A281A), - error: hex(0xFE4450), - error_light: hex(0xFF6670), - error_bg: hex(0x2A1A1E), - btn_danger_bg: hex(0x2A1A1E), - btn_danger_hover: hex(0x4A2A2E), - btn_trash_hover: hex(0xFE4450), - btn_trash_pressed: hex(0xDE3440), - bg_modal_section: hex(0x262335), - border_subtle: hex(0x34294F), - divider: hex(0x34294F), - }; - - // ── Modus Operandi (light, WCAG AAA) ── - pub const MODUS_OPERANDI: Self = Self { - bg_primary: hex(0xFFFFFF), - bg_sidebar: hex(0xF0F0F0), - bg_card: hex(0xE0E0E0), - bg_card_hover: hex(0xD0D0D0), - bg_card_pressed: hex(0xC4C4C4), - bg_selected: hex(0xD0D0D0), - bg_input: hex(0xE0E0E0), - bg_progress: hex(0xF0F0F0), - text_primary: hex(0x000000), - text_secondary: hex(0x1A1A1A), - text_muted: hex(0x333333), - text_dim: hex(0x595959), - text_dimmer: hex(0x7F7F7F), - text_dimmest: hex(0x9F9F9F), - text_placeholder: hex(0x7F7F7F), - accent_blue: hex(0x0031A9), - accent_icon: hex(0x005E8B), - accent_progress: hex(0x0031A9), - btn_default: hex(0xE0E0E0), - btn_hover: hex(0xD0D0D0), - btn_pressed: hex(0xC4C4C4), - success: hex(0x006800), - success_bg: hex(0xD0F0D0), - btn_success: hex(0x006800), - btn_success_hover: hex(0x005800), - btn_success_pressed: hex(0x004800), - warning: hex(0x6F5500), - warning_bg: hex(0xF0E8C8), - error: hex(0xA60000), - error_light: hex(0xD00000), - error_bg: hex(0xF0C8C8), - btn_danger_bg: hex(0xF0C8C8), - btn_danger_hover: hex(0xE0B0B0), - btn_trash_hover: hex(0xA60000), - btn_trash_pressed: hex(0x860000), - bg_modal_section: hex(0xFFFFFF), - border_subtle: hex(0xD0D0D0), - divider: hex(0xD0D0D0), - }; - - // ── Modus Vivendi (dark, WCAG AAA) ── - pub const MODUS_VIVENDI: Self = Self { - bg_primary: hex(0x000000), - bg_sidebar: hex(0x1E1E1E), - bg_card: hex(0x303030), - bg_card_hover: hex(0x404040), - bg_card_pressed: hex(0x535353), - bg_selected: hex(0x404040), - bg_input: hex(0x303030), - bg_progress: hex(0x1E1E1E), - text_primary: hex(0xFFFFFF), - text_secondary: hex(0xE0E0E0), - text_muted: hex(0xC0C0C0), - text_dim: hex(0x989898), - text_dimmer: hex(0x707070), - text_dimmest: hex(0x535353), - text_placeholder: hex(0x707070), - accent_blue: hex(0x2FAFFF), - accent_icon: hex(0x00D3D0), - accent_progress: hex(0x2FAFFF), - btn_default: hex(0x303030), - btn_hover: hex(0x404040), - btn_pressed: hex(0x535353), - success: hex(0x44BC44), - success_bg: hex(0x0A1A0A), - btn_success: hex(0x38A038), - btn_success_hover: hex(0x30942E), - btn_success_pressed: hex(0x288824), - warning: hex(0xD0BC00), - warning_bg: hex(0x1A1A00), - error: hex(0xFF5F59), - error_light: hex(0xFF7F86), - error_bg: hex(0x1A0808), - btn_danger_bg: hex(0x1A0808), - btn_danger_hover: hex(0x3A1818), - btn_trash_hover: hex(0xFF5F59), - btn_trash_pressed: hex(0xDF4F48), - bg_modal_section: hex(0x000000), - border_subtle: hex(0x303030), - divider: hex(0x303030), - }; - - // ── Stellar Blade (fan-made character set) ───────────────────────────── - // Duotone design language: one surface material + the character's signature - // color as the ink (text + accent). Derived from each character's on-screen - // design; Shift Up publishes no official codes. Same palettes as SphereCord's - // bundled sbThemes — keep the two in sync. - - /// EVE — her Planet Diving Suit: white/grey ceramic, green legs as the ink. Light. - pub const STELLAR_EVE: Self = Self { - bg_primary: hex(0xECEEEC), - bg_sidebar: hex(0xD3D7D3), - bg_card: hex(0xFFFFFF), - bg_card_hover: hex(0xE2E6E2), - bg_card_pressed: hex(0xD7DCD7), - bg_selected: hex(0xD9E4DA), - bg_input: hex(0xE0E4E0), - bg_progress: hex(0xDEE1DE), - text_primary: hex(0x1A6B3B), - text_secondary: hex(0x2B7D4A), - text_muted: hex(0x4E8A63), - text_dim: hex(0x6FA082), - text_dimmer: hex(0x84AD94), - text_dimmest: hex(0x9ABDA7), - text_placeholder: hex(0x84AD94), - accent_blue: hex(0x21A04D), - accent_icon: hex(0x21A04D), - accent_progress: hex(0x21A04D), - btn_default: hex(0xDFE3DF), - btn_hover: hex(0xD2D8D3), - btn_pressed: hex(0xC5CDC7), - success: hex(0x2F9E5C), - success_bg: hex(0xD3ECD9), - btn_success: hex(0x2F9E5C), - btn_success_hover: hex(0x288B50), - btn_success_pressed: hex(0x227A46), - warning: hex(0xC8861E), - warning_bg: hex(0xF0E6C8), - error: hex(0xD5493C), - error_light: hex(0xE05A4E), - error_bg: hex(0xF0CDC8), - btn_danger_bg: hex(0xF0CDC8), - btn_danger_hover: hex(0xE3B6B0), - btn_trash_hover: hex(0xC23A2E), - btn_trash_pressed: hex(0xA93227), - bg_modal_section: hex(0xE7EAE7), - border_subtle: hex(0xC0C8C1), - divider: hex(0xC0C8C1), - }; - - /// Tachy — carbon-navy suit, amber text, orange-glow accent; teal glints as success. - pub const STELLAR_TACHY: Self = Self { - bg_primary: hex(0x10121A), - bg_sidebar: hex(0x0A0B10), - bg_card: hex(0x171A24), - bg_card_hover: hex(0x1E2230), - bg_card_pressed: hex(0x252A3A), - bg_selected: hex(0x202536), - bg_input: hex(0x191C28), - bg_progress: hex(0x0D0E15), - text_primary: hex(0xF2BC4E), - text_secondary: hex(0xD9A133), - text_muted: hex(0xA58136), - text_dim: hex(0x75612F), - text_dimmer: hex(0x5C4D28), - text_dimmest: hex(0x453A20), - text_placeholder: hex(0x5C4D28), - accent_blue: hex(0xFF7C1E), - accent_icon: hex(0xEAA72C), - accent_progress: hex(0xFF7C1E), - btn_default: hex(0x171A24), - btn_hover: hex(0x1E2230), - btn_pressed: hex(0x252A3A), - success: hex(0x3FBFA8), - success_bg: hex(0x0E2420), - btn_success: hex(0x2F9D8A), - btn_success_hover: hex(0x298C7B), - btn_success_pressed: hex(0x237B6C), - warning: hex(0xEAA72C), - warning_bg: hex(0x2A2210), - error: hex(0xF0503C), - error_light: hex(0xFF6A55), - error_bg: hex(0x2A120E), - btn_danger_bg: hex(0x2A120E), - btn_danger_hover: hex(0x41201A), - btn_trash_hover: hex(0xB8402F), - btn_trash_pressed: hex(0x96331F), - bg_modal_section: hex(0x141720), - border_subtle: hex(0x2B3040), - divider: hex(0x2B3040), - }; - - /// Lily — strict duotone: amber-gold ink on neutral blacks. - pub const STELLAR_LILY: Self = Self { - bg_primary: hex(0x111010), - bg_sidebar: hex(0x0A0909), - bg_card: hex(0x191817), - bg_card_hover: hex(0x201E1C), - bg_card_pressed: hex(0x262421), - bg_selected: hex(0x232120), - bg_input: hex(0x1A1918), - bg_progress: hex(0x0D0C0C), - text_primary: hex(0xF6C832), - text_secondary: hex(0xD4A92C), - text_muted: hex(0xA5842A), - text_dim: hex(0x7A6526), - text_dimmer: hex(0x5F4F21), - text_dimmest: hex(0x483C1B), - text_placeholder: hex(0x5F4F21), - accent_blue: hex(0xFFD23E), - accent_icon: hex(0xFFD23E), - accent_progress: hex(0xFFD23E), - btn_default: hex(0x191817), - btn_hover: hex(0x201E1C), - btn_pressed: hex(0x262421), - success: hex(0x5FB55A), - success_bg: hex(0x15200F), - btn_success: hex(0x4C9D48), - btn_success_hover: hex(0x428C3F), - btn_success_pressed: hex(0x397B36), - warning: hex(0xE08A2A), - warning_bg: hex(0x261C0C), - error: hex(0xE0503C), - error_light: hex(0xF0654F), - error_bg: hex(0x26100C), - btn_danger_bg: hex(0x26100C), - btn_danger_hover: hex(0x3C1D16), - btn_trash_hover: hex(0xB8402F), - btn_trash_pressed: hex(0x96331F), - bg_modal_section: hex(0x151413), - border_subtle: hex(0x2E2B26), - divider: hex(0x2E2B26), - }; - - /// Enya — white ceramic armour, ice-blue sheen. Light. - pub const STELLAR_ENYA: Self = Self { - bg_primary: hex(0xEEF0F3), - bg_sidebar: hex(0xD5DAE1), - bg_card: hex(0xFFFFFF), - bg_card_hover: hex(0xE4E8EE), - bg_card_pressed: hex(0xD9DFE7), - bg_selected: hex(0xDCE3EC), - bg_input: hex(0xE2E6EC), - bg_progress: hex(0xDFE3EA), - text_primary: hex(0x171C23), - text_secondary: hex(0x3F4A58), - text_muted: hex(0x66727F), - text_dim: hex(0x8D97A3), - text_dimmer: hex(0xA3ADB8), - text_dimmest: hex(0xBAC2CC), - text_placeholder: hex(0xA3ADB8), - accent_blue: hex(0x3E90C9), - accent_icon: hex(0x3E90C9), - accent_progress: hex(0x3E90C9), - btn_default: hex(0xE1E5EB), - btn_hover: hex(0xD4DAE2), - btn_pressed: hex(0xC8CFD9), - success: hex(0x2F9E5C), - success_bg: hex(0xD3ECD9), - btn_success: hex(0x2F9E5C), - btn_success_hover: hex(0x288B50), - btn_success_pressed: hex(0x227A46), - warning: hex(0xC8861E), - warning_bg: hex(0xF0E6C8), - error: hex(0xD5493C), - error_light: hex(0xE05A4E), - error_bg: hex(0xF0CDC8), - btn_danger_bg: hex(0xF0CDC8), - btn_danger_hover: hex(0xE3B6B0), - btn_trash_hover: hex(0xC23A2E), - btn_trash_pressed: hex(0xA93227), - bg_modal_section: hex(0xE9ECF0), - border_subtle: hex(0xC2CAD4), - divider: hex(0xC2CAD4), - }; - - /// Kaya — lavender ink on deep violet; her khaki coat survives as the success state. - pub const STELLAR_KAYA: Self = Self { - bg_primary: hex(0x1A1622), - bg_sidebar: hex(0x14111B), - bg_card: hex(0x221D2E), - bg_card_hover: hex(0x292338), - bg_card_pressed: hex(0x302A42), - bg_selected: hex(0x2C2540), - bg_input: hex(0x241F31), - bg_progress: hex(0x171320), - text_primary: hex(0xD9C8F0), - text_secondary: hex(0xB49AE0), - text_muted: hex(0x8D7BAE), - text_dim: hex(0x6B5C88), - text_dimmer: hex(0x55486E), - text_dimmest: hex(0x423857), - text_placeholder: hex(0x55486E), - accent_blue: hex(0xCFA8FF), - accent_icon: hex(0xCFA8FF), - accent_progress: hex(0xCFA8FF), - btn_default: hex(0x221D2E), - btn_hover: hex(0x292338), - btn_pressed: hex(0x302A42), - success: hex(0x8A9455), - success_bg: hex(0x1D2012), - btn_success: hex(0x767F48), - btn_success_hover: hex(0x68703F), - btn_success_pressed: hex(0x5A6136), - warning: hex(0xD9A441), - warning_bg: hex(0x271F0E), - error: hex(0xE06A6A), - error_light: hex(0xEF8080), - error_bg: hex(0x291214), - btn_danger_bg: hex(0x291214), - btn_danger_hover: hex(0x3E1E21), - btn_trash_hover: hex(0xB84848), - btn_trash_pressed: hex(0x963A3A), - bg_modal_section: hex(0x1E1928), - border_subtle: hex(0x382F4C), - divider: hex(0x382F4C), - }; -} +//! Colony's theme — now shared with the rest of the ecosystem. +//! +//! The 38-field palette, all 57 theme palettes, the `(family, variant)` +//! resolver, the picker catalog, the accent overrides and the high-contrast +//! derivation live in `colony-ui`, generated from the design tokens in +//! Project-Colony-Resources. +//! +//! This module re-exports them so every existing `crate::ui::theme::…` call +//! site keeps working unchanged. +//! +//! **Adding a theme family no longer touches this repository.** Add the TOML +//! upstream, regenerate, and bump the `colony-ui` tag in `Cargo.toml`. + +pub use colony_ui::theme::*; From b166d47ec830a99969769fe71f8c71865025a797 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 24 Aug 2026 20:32:11 +0200 Subject: [PATCH 2/5] refactor(settings): render the theme picker from the generated catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picker held a 367-line vec listing every family, its Nerd Font glyph, its i18n key, and every variant's swatch colours — the same facts colony-ui already generates from the design tokens, written out a second time by hand. It now iterates colony_ui::THEME_FAMILIES. The card-drawing code below is untouched: the loop binds the same names it did before, so the diff is the catalog disappearing rather than the rendering changing. Swatch colours come typed from the crate now too, which removes two hand-rolled hex-to-Color conversions that did what colony-ui's swatch_bg_color() does. Together with the previous commit this is what the migration was for: adding a theme family used to mean four edits in this repository — a palette const, a resolver arm, an entry in this vec, and both locale files. It now means none. 119 tests still pass. --- src/ui/settings.rs | 394 +-------------------------------------------- 1 file changed, 8 insertions(+), 386 deletions(-) diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 7f74883..bcb1976 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -8,9 +8,6 @@ use crate::message::Message; use crate::state::App; use crate::ui::theme::Palette; -/// A theme variant: (variant_key, i18n_label_key, bg_hex, accent_hex). -type ThemeVariant = (&'static str, &'static str, u32, u32); - /// Settings category names (keys for i18n). const SETTINGS_CATEGORIES: &[&str] = &[ "settings_cat_general", @@ -813,377 +810,12 @@ impl App { let font = self.app_font(); let medium = self.app_font_with_weight(Weight::Medium); - // Theme families: (key, i18n_label_key, variants) - // Each variant: (variant_key, i18n_label_key, bg_hex, accent_hex) - // Theme families: (key, i18n_label_key, icon, variants) - // Each variant: (variant_key, i18n_label_key, bg_hex, accent_hex) - // Icons use Nerd Font codepoints for themed themes - let theme_families: Vec<(&str, &str, &str, Vec)> = vec![ - // ── Existing themes ── - ( - "catppuccin", - "settings_theme_catppuccin", - "\u{f0f4}", - vec![ - // coffee - ( - "latte", - "settings_theme_catppuccin_latte", - 0xeff1f5, - 0x1e66f5, - ), - ( - "frappe", - "settings_theme_catppuccin_frappe", - 0x303446, - 0x8caaee, - ), - ( - "macchiato", - "settings_theme_catppuccin_macchiato", - 0x24273a, - 0x8aadf4, - ), - ( - "mocha", - "settings_theme_catppuccin_mocha", - 0x1e1e2e, - 0x89b4fa, - ), - ], - ), - ( - "gruvbox", - "settings_theme_gruvbox", - "", - vec![ - ("light", "settings_theme_light", 0xfbf1c7, 0x458588), - ("dark", "settings_theme_dark_mode", 0x282828, 0x83a598), - ], - ), - ( - "everblush", - "settings_theme_everblush", - "\u{f06c}", - vec![ - // leaf - ("light", "settings_theme_light", 0xe8eded, 0x3a88c0), - ("dark", "settings_theme_dark_mode", 0x141b1e, 0x67b0e8), - ], - ), - ( - "kanagawa", - "settings_theme_kanagawa", - "\u{f073e}", - vec![ - // wave (torii) - ("light", "settings_theme_light", 0xf2ecbc, 0x4d699b), - ("dark", "settings_theme_dark_mode", 0x1F1F28, 0x7E9CD8), - ( - "journal", - "settings_theme_kanagawa_journal", - 0xd5cea3, - 0x7a6840, - ), - ], - ), - // ── New themes ── - ( - "nord", - "settings_theme_nord", - "\u{f2dc}", - vec![ - // snowflake - ("dark", "settings_theme_dark_mode", 0x2E3440, 0x88C0D0), - ("light", "settings_theme_light", 0xECEFF4, 0x5E81AC), - ], - ), - ( - "dracula", - "settings_theme_dracula", - "\u{f6e2}", - vec![ - // ghost - ("dark", "settings_theme_dark_mode", 0x282A36, 0xBD93F9), - ("light", "settings_theme_light", 0xFFFBEB, 0x7C5FC2), - ], - ), - ( - "solarized", - "settings_theme_solarized", - "\u{f185}", - vec![ - // sun - ("dark", "settings_theme_dark_mode", 0x002B36, 0x268BD2), - ("light", "settings_theme_light", 0xFDF6E3, 0x268BD2), - ], - ), - ( - "tokyonight", - "settings_theme_tokyonight", - "\u{f0219}", - vec![ - // city - ( - "night", - "settings_theme_tokyonight_night", - 0x1A1B26, - 0x7AA2F7, - ), - ("day", "settings_theme_tokyonight_day", 0xE1E2E7, 0x2E7DE9), - ], - ), - ( - "rosepine", - "settings_theme_rosepine", - "\u{f46d}", - vec![ - // rose/flower - ("main", "settings_theme_rosepine_main", 0x191724, 0x9CCFD8), - ("moon", "settings_theme_rosepine_moon", 0x232136, 0x9CCFD8), - ("dawn", "settings_theme_rosepine_dawn", 0xFAF4ED, 0x56949F), - ], - ), - ( - "onedark", - "settings_theme_onedark", - "", - vec![ - ("dark", "settings_theme_dark_mode", 0x282C34, 0x61AFEF), - ("light", "settings_theme_light", 0xFAFAFA, 0x4078F2), - ], - ), - ( - "monokai", - "settings_theme_monokai", - "\u{f121}", - vec![ - // code - ("pro", "settings_theme_monokai_pro", 0x2D2A2E, 0x78DCE8), - ( - "classic", - "settings_theme_monokai_classic", - 0x272822, - 0x66D9EF, - ), - ( - "spectrum", - "settings_theme_monokai_spectrum", - 0x222222, - 0x5AD4E6, - ), - ], - ), - ( - "ayu", - "settings_theme_ayu", - "\u{f06c0}", - vec![ - // sunrise - ("dark", "settings_theme_dark_mode", 0x0B0E14, 0xE6B450), - ("mirage", "settings_theme_ayu_mirage", 0x1F2430, 0xFFCC66), - ("light", "settings_theme_light", 0xFAFAFA, 0xFF9940), - ], - ), - ( - "everforest", - "settings_theme_everforest", - "\u{f1bb}", - vec![ - // tree - ("dark", "settings_theme_dark_mode", 0x2D353B, 0x7FBBB3), - ("light", "settings_theme_light", 0xFDF6E3, 0x3A94C5), - ], - ), - ( - "material", - "settings_theme_material", - "\u{f0509}", - vec![ - // material-design - ( - "oceanic", - "settings_theme_material_oceanic", - 0x263238, - 0x89DDFF, - ), - ( - "palenight", - "settings_theme_material_palenight", - 0x292D3E, - 0xC792EA, - ), - ( - "deepocean", - "settings_theme_material_deepocean", - 0x0F111A, - 0x84FFFF, - ), - ], - ), - ( - "flexoki", - "settings_theme_flexoki", - "\u{f02d}", - vec![ - // book - ("dark", "settings_theme_dark_mode", 0x100F0F, 0x4385BE), - ("light", "settings_theme_light", 0xFFFCF0, 0x205EA6), - ], - ), - ( - "nightfox", - "settings_theme_nightfox", - "\u{f0139}", - vec![ - // fox - ( - "nightfox", - "settings_theme_nightfox_nightfox", - 0x192330, - 0x719CD6, - ), - ( - "dawnfox", - "settings_theme_nightfox_dawnfox", - 0xFAF4ED, - 0x286983, - ), - ], - ), - ( - "sonokai", - "settings_theme_sonokai", - "", - vec![( - "default", - "settings_theme_sonokai_default", - 0x2C2E34, - 0x76CCE0, - )], - ), - ( - "oxocarbon", - "settings_theme_oxocarbon", - "\u{f0620}", - vec![ - // molecule - ("dark", "settings_theme_dark_mode", 0x161616, 0x78A9FF), - ("light", "settings_theme_light", 0xFFFFFF, 0x0F62FE), - ], - ), - ( - "nightowl", - "settings_theme_nightowl", - "\u{f19e}", - vec![ - // owl (moon) - ("dark", "settings_theme_dark_mode", 0x011627, 0x82AAFF), - ("light", "settings_theme_light", 0xFBFBFB, 0x4876D6), - ], - ), - ( - "iceberg", - "settings_theme_iceberg", - "\u{f2dc}", - vec![ - // snowflake - ("dark", "settings_theme_dark_mode", 0x161821, 0x84A0C6), - ("light", "settings_theme_light", 0xE8E9EC, 0x2D539E), - ], - ), - ( - "horizon", - "settings_theme_horizon", - "\u{f06c0}", - vec![ - // sunrise - ("dark", "settings_theme_dark_mode", 0x1C1E26, 0x26BBD9), - ], - ), - ( - "melange", - "settings_theme_melange", - "\u{f0f4}", - vec![ - // coffee - ("dark", "settings_theme_dark_mode", 0x292522, 0xA3A9CE), - ("light", "settings_theme_light", 0xF4F0ED, 0x5E6DAB), - ], - ), - ( - "synthwave", - "settings_theme_synthwave", - "\u{f001}", - vec![ - // music - ("dark", "settings_theme_dark_mode", 0x262335, 0xFF7EDB), - ], - ), - ( - "modus", - "settings_theme_modus", - "\u{f06e}", - vec![ - // eye (accessibility) - ( - "operandi", - "settings_theme_modus_operandi", - 0xFFFFFF, - 0x0031A9, - ), - ( - "vivendi", - "settings_theme_modus_vivendi", - 0x000000, - 0x2FAFFF, - ), - ], - ), - // ── Fan-made character sets ── - ( - "stellar_blade", - "settings_theme_stellar_blade", - "\u{f04e5}", - vec![ - // sword - ( - "eve", - "settings_theme_stellar_blade_eve", - 0xECEEEC, - 0x21A04D, - ), - ( - "tachy", - "settings_theme_stellar_blade_tachy", - 0x10121A, - 0xFF7C1E, - ), - ( - "lily", - "settings_theme_stellar_blade_lily", - 0x111010, - 0xFFD23E, - ), - ( - "enya", - "settings_theme_stellar_blade_enya", - 0xEEF0F3, - 0x3E90C9, - ), - ( - "kaya", - "settings_theme_stellar_blade_kaya", - 0x1A1622, - 0xCFA8FF, - ), - ], - ), - ]; - let mut col = column![].spacing(12); - for (theme_key, label_key, icon, variants) in theme_families { + // Rendered straight from colony-ui's generated catalog: adding a theme + // family upstream needs no change here. + for family in colony_ui::THEME_FAMILIES { + let (theme_key, label_key, icon) = (family.key, family.label_key, family.icon); let is_selected_family = self.selected_theme == theme_key; let label = i18n::t(label_key); @@ -1206,24 +838,14 @@ impl App { // Variant cards as a horizontal row of mini color-swatch cards let mut variant_row = row![].spacing(8); - for (var_key, var_label_key, bg_hex, accent_hex) in &variants { + for variant in family.variants { + let (var_key, var_label_key) = (&variant.key, variant.label_key); let is_active = is_selected_family && self.selected_variant == *var_key; let theme_owned = theme_key.to_string(); let var_owned = var_key.to_string(); - // Parse swatch colors - let bg_color = iced::Color { - r: ((*bg_hex >> 16) & 0xFF) as f32 / 255.0, - g: ((*bg_hex >> 8) & 0xFF) as f32 / 255.0, - b: (*bg_hex & 0xFF) as f32 / 255.0, - a: 1.0, - }; - let accent_color = iced::Color { - r: ((*accent_hex >> 16) & 0xFF) as f32 / 255.0, - g: ((*accent_hex >> 8) & 0xFF) as f32 / 255.0, - b: (*accent_hex & 0xFF) as f32 / 255.0, - a: 1.0, - }; + let bg_color = variant.swatch_bg_color(); + let accent_color = variant.swatch_accent_color(); // Color swatch: bg stripe + accent dot let swatch_bg = container(text("")) From f74b92751f2ac0f2f8918c3467dc6695bca80e04 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 24 Aug 2026 20:35:14 +0200 Subject: [PATCH 3/5] refactor(persistence): use the shared Colony// filesystem layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colony resolved its own directories, and got Windows wrong: dirs::config_dir() is Roaming there, while Digger and Grape both used Local. On Linux the two are the same function, so nobody could see the disagreement. The layout is defined once in colony-ui now — see design/filesystem.md upstream — and Local is what it picks. Two things move, both handled by migrate_legacy_paths() at startup: - the config directory, on Windows only (Roaming -> Local); a no-op on Linux and macOS, where the old and new resolvers return the same path - the caches, on every platform: repos_cache.json and scan_cache.json lived inside the config directory and belong in the cache root The migration never deletes the source. It renames when it can, falls back to a recursive copy across filesystems (~/.config and ~/.cache are not guaranteed to share one), and on failure removes the half-written destination so the next start retries instead of finding an empty directory and skipping. A user who ends up with a copy in both places has lost nothing; a user whose preferences were deleted by a half-finished migration has. Also fixes real test pollution this exposed: with_temp_dirs isolated XDG_CONFIG_HOME and XDG_DATA_HOME but not XDG_CACHE_HOME, so once the caches moved, the update tests read and wrote the developer's actual ~/.cache — and github_error_only_toasts_when_the_catalog_is_empty started failing depending on what an earlier run had left there. That was a latent hole in the isolation, not a consequence of the move. 125 tests pass, up from 119. --- src/main.rs | 4 + src/persistence.rs | 268 ++++++++++++++++++++++++++++++++++++++++++--- src/update/mod.rs | 10 ++ 3 files changed, 264 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index ab03270..5eb497f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,10 @@ pub fn main() -> iced::Result { ) .init(); + // Earlier versions wrote to different directories; move them before + // anything reads a path. No-op once done, and on a fresh install. + crate::persistence::migrate_legacy_paths(); + // Honor the saved language preference over environment locale detection, // and reopen at the last persisted window size (clamped to sanity). let prefs = crate::persistence::load_preferences(); diff --git a/src/persistence.rs b/src/persistence.rs index 10afdc4..abaada0 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -4,18 +4,108 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::github::{current_platform_key, ColonyRepo}; -/// Central data directory for all Colony files: `~/.config/Colony/Colony/` +/// Colony's own name in the shared `Colony//` tree. +const PROGRAM: &str = "Colony"; + +/// Central config directory for Colony's own files. +/// +/// The layout — which root on which platform — is defined once in colony-ui; +/// see `design/filesystem.md` in Project-Colony-Resources. On Linux this is +/// `~/.config/Colony/Colony/`. pub fn colony_data_dir() -> Result { - let base = dirs::config_dir() - .ok_or_else(|| anyhow::anyhow!("No config directory"))? - .join("Colony") - .join("Colony"); - std::fs::create_dir_all(&base)?; - Ok(base) + Ok(colony_ui::paths::config_dir(PROGRAM)?) +} + +/// Regenerable state: the repo listing and the scan results. +fn colony_cache_dir() -> Result { + Ok(colony_ui::paths::cache_dir(PROGRAM)?) +} + +/// Move state written by earlier versions to where the shared layout puts it. +/// +/// Must run before anything reads a path — `main` calls it first. +/// +/// Deliberately conservative: it only moves when the old location exists and +/// the new one does not, and it **never deletes the source**. A user who ends +/// up with a copy in both places has lost nothing; a user whose preferences +/// were deleted by a half-finished migration has. +pub fn migrate_legacy_paths() { + // Windows used to resolve config to Roaming (dirs::config_dir), and the + // layout is Local. Identical on Linux and macOS, so this is a no-op there. + if let (Some(legacy_root), Ok(current)) = ( + dirs::config_dir(), + colony_ui::paths::locate::config_dir(PROGRAM), + ) { + relocate( + &legacy_root.join("Colony").join(PROGRAM), + ¤t, + "config directory", + ); + } + + // The cache used to live inside the config directory. It is regenerable, so + // a failure here costs a re-fetch and nothing more. + if let (Ok(config), Ok(cache)) = ( + colony_ui::paths::locate::config_dir(PROGRAM), + colony_ui::paths::locate::cache_dir(PROGRAM), + ) { + relocate(&config.join("cache"), &cache, "cache"); + } +} + +/// Move `from` to `to`, once, without ever destroying `from`. +fn relocate(from: &Path, to: &Path, what: &str) { + if from == to || !from.is_dir() || to.exists() { + return; + } + let Some(parent) = to.parent() else { return }; + if let Err(e) = std::fs::create_dir_all(parent) { + tracing::warn!("cannot prepare {} for the {what}: {e}", parent.display()); + return; + } + + // A rename is atomic and cheap, but fails across filesystems (EXDEV) — and + // ~/.config and ~/.cache are not guaranteed to be on the same one. + if std::fs::rename(from, to).is_ok() { + tracing::info!("moved the {what} to {}", to.display()); + return; + } + match copy_tree(from, to) { + Ok(()) => tracing::info!( + "copied the {what} to {}; the old copy at {} is left in place and can be deleted by hand", + to.display(), + from.display() + ), + Err(e) => { + // Leave no half-copied directory behind: the next start would see + // `to` existing and skip the migration, stranding the real data. + let _ = std::fs::remove_dir_all(to); + tracing::error!( + "could not move the {what} from {}: {e}. Nothing was lost — the old location still holds it — but Colony will start with an empty one.", + from.display() + ); + } + } +} + +/// Recursive copy. Files and directories only; anything else is skipped. +fn copy_tree(from: &Path, to: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(to)?; + for entry in std::fs::read_dir(from)? { + let entry = entry?; + let target = to.join(entry.file_name()); + let kind = entry.file_type()?; + if kind.is_dir() { + copy_tree(&entry.path(), &target)?; + } else if kind.is_file() { + std::fs::copy(entry.path(), &target)?; + } + } + Ok(()) } /// Join a repo name onto `base` as a single directory component. @@ -76,11 +166,13 @@ pub fn load_repo_icon(repo_name: &str) -> Option> { std::fs::read(dir.join("icon.png")).ok() } -/// Return the Colony apps directory: `/Colony/apps/` +/// The shared install root: `/Colony/apps/`. +/// +/// Deliberately a sibling of Colony's own directory rather than a child — +/// installed programs belong to the ecosystem, not to the launcher. Does not +/// create the directory; callers that write do that themselves. pub fn colony_apps_dir() -> Result { - let base = dirs::data_local_dir() - .ok_or_else(|| anyhow::anyhow!("Cannot determine local data directory"))?; - Ok(base.join("Colony").join("apps")) + Ok(colony_ui::paths::locate::apps_dir()?) } /// Check if a Colony app is installed for the current platform. @@ -199,9 +291,7 @@ pub fn load_installed_asset(repo_name: &str) -> Option { } fn repos_cache_path() -> Result { - let cache_dir = colony_data_dir()?.join("cache"); - std::fs::create_dir_all(&cache_dir)?; - Ok(cache_dir.join("repos_cache.json")) + Ok(colony_cache_dir()?.join("repos_cache.json")) } /// Save Colony repos to local cache for offline use. @@ -300,9 +390,7 @@ pub fn save_preferences(prefs: &UserPreferences) -> Result<()> { } fn scan_cache_path() -> Result { - let cache_dir = colony_data_dir()?.join("cache"); - std::fs::create_dir_all(&cache_dir)?; - Ok(cache_dir.join("scan_cache.json")) + Ok(colony_cache_dir()?.join("scan_cache.json")) } /// Cached scan entry. @@ -549,3 +637,147 @@ mod tests { assert_eq!(loaded.first_launch_done, Some(true)); } } + +#[cfg(test)] +mod path_migration_tests { + use super::*; + + fn scratch(name: &str) -> PathBuf { + let dir = + std::env::temp_dir().join(format!("colony_migrate_{name}_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("scratch dir"); + dir + } + + fn seed(dir: &Path, rel: &str, contents: &str) { + let path = dir.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, contents).unwrap(); + } + + #[test] + fn moves_a_legacy_directory_and_keeps_its_contents() { + let root = scratch("moves"); + let (from, to) = (root.join("old"), root.join("new")); + seed( + &from, + "preferences/preferences.json", + "{\"theme\":\"gruvbox\"}", + ); + seed(&from, "auth/github_token.json", "token"); + + relocate(&from, &to, "config directory"); + + assert!(!from.exists(), "the source should have been renamed away"); + assert_eq!( + std::fs::read_to_string(to.join("preferences/preferences.json")).unwrap(), + "{\"theme\":\"gruvbox\"}" + ); + assert!(to.join("auth/github_token.json").exists()); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn never_clobbers_an_existing_destination() { + let root = scratch("clobber"); + let (from, to) = (root.join("old"), root.join("new")); + seed(&from, "preferences.json", "old"); + seed(&to, "preferences.json", "current"); + + relocate(&from, &to, "config directory"); + + // The current data wins and the old copy is left untouched, not merged. + assert_eq!( + std::fs::read_to_string(to.join("preferences.json")).unwrap(), + "current" + ); + assert_eq!( + std::fs::read_to_string(from.join("preferences.json")).unwrap(), + "old" + ); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn does_nothing_on_a_fresh_install_or_a_second_run() { + let root = scratch("noop"); + let (from, to) = (root.join("absent"), root.join("new")); + + relocate(&from, &to, "config directory"); + + assert!(!to.exists(), "nothing to migrate should create nothing"); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn a_source_equal_to_the_destination_is_left_alone() { + // This is the Linux case for the config directory: the old and new + // resolvers return the same path, so the migration must be inert. + let root = scratch("same"); + let dir = root.join("config"); + seed(&dir, "preferences.json", "kept"); + + relocate(&dir, &dir, "config directory"); + + assert_eq!( + std::fs::read_to_string(dir.join("preferences.json")).unwrap(), + "kept" + ); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn copy_tree_reproduces_nested_contents() { + let root = scratch("copy"); + let (from, to) = (root.join("src"), root.join("dst")); + seed(&from, "a.json", "a"); + seed(&from, "deep/b.json", "b"); + seed(&from, "deep/deeper/c.json", "c"); + + copy_tree(&from, &to).expect("copy"); + + for (rel, want) in [ + ("a.json", "a"), + ("deep/b.json", "b"), + ("deep/deeper/c.json", "c"), + ] { + assert_eq!( + std::fs::read_to_string(to.join(rel)).unwrap(), + want, + "{rel}" + ); + } + // The source survives a copy — that is the whole point of the fallback. + assert!(from.join("a.json").exists()); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn colony_lives_under_the_shared_tree() { + let config = colony_ui::paths::locate::config_dir(PROGRAM).unwrap(); + assert!( + config.ends_with("Colony/Colony") || config.ends_with("Colony\\Colony"), + "{config:?}" + ); + + // apps/ is a SIBLING of Colony's own data directory, not a child of + // it: uninstalling the launcher must not look like it takes the + // installed programs with it. + let apps = colony_apps_dir().unwrap(); + let data = colony_ui::paths::locate::data_dir(PROGRAM).unwrap(); + assert!( + apps.ends_with("Colony/apps") || apps.ends_with("Colony\\apps"), + "{apps:?}" + ); + assert!( + !apps.starts_with(&data), + "installed programs must not live inside Colony's own directory" + ); + assert_eq!( + apps.parent(), + data.parent(), + "apps/ and Colony/ share a parent" + ); + } +} diff --git a/src/update/mod.rs b/src/update/mod.rs index d5bf0b0..35f31da 100644 --- a/src/update/mod.rs +++ b/src/update/mod.rs @@ -573,8 +573,14 @@ mod tests { let tmp = tempfile::tempdir().expect("tempdir"); let old_config = std::env::var_os("XDG_CONFIG_HOME"); let old_data = std::env::var_os("XDG_DATA_HOME"); + // XDG_CACHE_HOME matters since the caches moved out of the config + // directory: without it these tests read and write the developer's real + // ~/.cache, which makes them pass or fail depending on what a previous + // run left behind. + let old_cache = std::env::var_os("XDG_CACHE_HOME"); std::env::set_var("XDG_CONFIG_HOME", tmp.path().join("config")); std::env::set_var("XDG_DATA_HOME", tmp.path().join("data")); + std::env::set_var("XDG_CACHE_HOME", tmp.path().join("cache")); f(); match old_config { Some(v) => std::env::set_var("XDG_CONFIG_HOME", v), @@ -584,6 +590,10 @@ mod tests { Some(v) => std::env::set_var("XDG_DATA_HOME", v), None => std::env::remove_var("XDG_DATA_HOME"), } + match old_cache { + Some(v) => std::env::set_var("XDG_CACHE_HOME", v), + None => std::env::remove_var("XDG_CACHE_HOME"), + } } #[test] From 47ad519178760b4a8a85892f140cd33ca97dd25d Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 24 Aug 2026 20:37:37 +0200 Subject: [PATCH 4/5] refactor(persistence): move the rest of the regenerable state to the cache root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit repo-docs, repo-icons and update-staging were still under the config directory. All three are re-fetched or recreated when missing — cached documentation, downloaded icons, and a download staging area — so they belong in the cache root with repos_cache and scan_cache. Leaving them behind would have meant a program that half-follows its own documented layout. This surfaced a bug that compiled cleanly: both orphan-pruning functions walked `/repo-docs` and `/repo-icons` to delete caches for repos that no longer exist. Once those directories moved, the pruners would have found nothing and silently stopped reclaiming anything, growing the cache forever. Colony's own test caught the second one after the first was fixed. Also corrects the documentation, which was already wrong before this change and which the move would have made worse. docs/faq.md and docs/architecture.md claimed preferences lived at ~/.config/colony/preferences.json; the code has written ~/.config/Colony/Colony/preferences/preferences.json for some time. Both tables now match the code, and say what the Windows and macOS roots are instead of implying Linux is the only platform. docs/release-signing.md keeps ~/.config/colony/release-signing/ deliberately: that is where a maintainer keeps their own signing key, not state the program owns, and it is the default sign-release.sh already looks for. --- docs/architecture.md | 15 ++++++++------- docs/faq.md | 46 +++++++++++++++++++++++++------------------- docs/tutorial.md | 2 +- src/download.rs | 5 +++-- src/persistence.rs | 24 ++++++++++++++--------- src/update/mod.rs | 2 +- 6 files changed, 54 insertions(+), 40 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 7d5833f..907a2cd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -76,16 +76,17 @@ All async operations (API calls, downloads, scanning) return a `Task` t | Data | Location | Duration | |------|----------|----------| -| Colony repos (cache) | `~/.cache/colony/repos_cache.json` | Offline fallback | -| Scanned apps (cache) | `~/.cache/colony/scan_cache.json` | Session | -| Repo docs (cache) | `~/.cache/colony/docs//` | Offline fallback | -| Preferences | `~/.config/colony/preferences.json` | Permanent | -| Favorites | `~/.config/colony/favorites.json` | Permanent | -| OAuth token | OS Keychain / `~/.config/colony/github_token.json` | Permanent | +| Colony repos (cache) | `~/.cache/Colony/Colony/repos_cache.json` | Offline fallback | +| Scanned apps (cache) | `~/.cache/Colony/Colony/scan_cache.json` | Session | +| Repo docs (cache) | `~/.cache/Colony/Colony/repo-docs//` | Offline fallback | +| App icons (cache) | `~/.cache/Colony/Colony/repo-icons//icon.png` | Offline fallback | +| Preferences | `~/.config/Colony/Colony/preferences/preferences.json` | Permanent | +| Favorites | `~/.config/Colony/Colony/preferences/favorites.json` | Permanent | +| OAuth token | OS Keychain / `~/.config/Colony/Colony/auth/github_token.json` | Permanent | | Installed versions | `~/.local/share/Colony/apps//.colony_version` | Permanent | | Resolved asset | `~/.local/share/Colony/apps//.colony_asset` | Permanent | | Colony binaries | `~/.local/share/Colony/apps//` | Permanent | -| Self-update staging | `~/.local/share/Colony/update-staging/` | Temporary | +| Self-update staging | `~/.cache/Colony/Colony/update-staging/` | Temporary | ## GitHub API diff --git a/docs/faq.md b/docs/faq.md index 6cd7a45..96644fe 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -56,7 +56,7 @@ Open the app in Colony → detail view → **Uninstall**. Or delete the app's di ### How do I uninstall Colony itself? -AUR: `sudo pacman -R colony-bin` (or `colony-git`). Manual install: delete the binary plus `~/.config/colony/`, `~/.cache/colony/`, and `~/.local/share/Colony/`. +AUR: `sudo pacman -R colony-bin` (or `colony-git`). Manual install: delete the binary plus `~/.config/Colony/`, `~/.cache/Colony/`, and `~/.local/share/Colony/`. --- @@ -72,7 +72,7 @@ You get a 5000 req/h limit and Colony can read private repos you have access to ### Where is the token stored? -In your OS keychain (GNOME keyring, KWallet, macOS Keychain, Windows Credential Manager) via the [keyring](https://crates.io/crates/keyring) crate. If no keychain is available (headless Linux, CI) it falls back to `~/.config/colony/github_token.json` with `chmod 600`. +In your OS keychain (GNOME keyring, KWallet, macOS Keychain, Windows Credential Manager) via the [keyring](https://crates.io/crates/keyring) crate. If no keychain is available (headless Linux, CI) it falls back to `~/.config/Colony/Colony/auth/github_token.json` with `chmod 600`. ### How do I disconnect? @@ -93,7 +93,7 @@ Most common causes: 5. The repo is not under the `Project-Colony` org (Colony only scans that org). 6. GitHub rate limit hit — connect your account. -Cache file to inspect: `~/.cache/colony/repos_cache.json`. +Cache file to inspect: `~/.cache/Colony/Colony/repos_cache.json`. ### Does Colony support private repos? @@ -119,20 +119,26 @@ If your app is installed but not in any of these (portable binary without `.desk ### Where does Colony store its stuff? -| What | Path | -|----------------------------|---------------------------------------------------------| -| Preferences | `~/.config/colony/preferences.json` | -| Favorites | `~/.config/colony/favorites.json` | -| GitHub token (fallback) | `~/.config/colony/github_token.json` (`chmod 600`) | -| Installed app binaries | `~/.local/share/Colony/apps//` | -| Installed version marker | `~/.local/share/Colony/apps//.colony_version` | -| Resolved asset filename | `~/.local/share/Colony/apps//.colony_asset` | -| Repo list cache | `~/.cache/colony/repos_cache.json` | -| Manifest docs cache | `~/.cache/colony/docs//` | -| System app scan cache | `~/.cache/colony/scan_cache.json` | -| Self-update staging | `~/.local/share/Colony/update-staging/` | - -Purging the `~/.cache/colony/` directory forces a full re-scan and re-fetch. +| What | Path (Linux) | +|----------------------------|----------------------------------------------------------| +| Preferences | `~/.config/Colony/Colony/preferences/preferences.json` | +| Favorites | `~/.config/Colony/Colony/preferences/favorites.json` | +| GitHub token (fallback) | `~/.config/Colony/Colony/auth/github_token.json` (`600`) | +| Installed app binaries | `~/.local/share/Colony/apps//` | +| Installed version marker | `~/.local/share/Colony/apps//.colony_version` | +| Resolved asset filename | `~/.local/share/Colony/apps//.colony_asset` | +| Repo list cache | `~/.cache/Colony/Colony/repos_cache.json` | +| Manifest docs cache | `~/.cache/Colony/Colony/repo-docs//` | +| App icon cache | `~/.cache/Colony/Colony/repo-icons//icon.png` | +| System app scan cache | `~/.cache/Colony/Colony/scan_cache.json` | +| Self-update staging | `~/.cache/Colony/Colony/update-staging/` | + +On Windows all three roots are `%LOCALAPPDATA%\Colony\`, and on macOS config +and data share `~/Library/Application Support/Colony/` while the cache sits in +`~/Library/Caches/Colony/`. The layout is defined once in +[Project-Colony-Resources](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/design/filesystem.md). + +Purging the `~/.cache/Colony/` directory forces a full re-scan and re-fetch. ### Can I edit `preferences.json` by hand? @@ -140,7 +146,7 @@ Yes. Colony re-reads it on launch. The file is JSON with field names matching wh ### Can I override the scan directories? -Yes. Edit `~/.config/colony/preferences.json` and add custom paths under the platform-appropriate scan settings. The Settings UI also exposes this for Linux. +Yes. Edit `~/.config/Colony/Colony/preferences/preferences.json` and add custom paths under the platform-appropriate scan settings. The Settings UI also exposes this for Linux. ### I changed a theme and don't see it — why? @@ -161,8 +167,8 @@ colony 2>&1 | tee colony.log Common causes: - Missing Linux runtime libs (GTK, dbus, xdo) — on AUR the deps are pulled automatically. For manual downloads, install them via your package manager. -- Corrupted cache — `rm -rf ~/.cache/colony` and relaunch. -- Corrupted preferences — `mv ~/.config/colony/preferences.json ~/.config/colony/preferences.json.bak` and relaunch to regenerate defaults. +- Corrupted cache — `rm -rf ~/.cache/Colony` and relaunch. +- Corrupted preferences — `mv ~/.config/Colony/Colony/preferences/preferences.json{,.bak}` and relaunch to regenerate defaults. ### Download stuck / very slow diff --git a/docs/tutorial.md b/docs/tutorial.md index 92dc8f2..35fb3b3 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -151,5 +151,5 @@ upgraded alongside the rest of your system. [colony-spec.md](colony-spec.md) for the full manifest reference. - **Rate limit hit?** Connect a GitHub account (section 6). - **Download fails?** Check your network. Colony retries automatically; logs - go to `~/.cache/colony/` if you need to dig in. + go to `~/.cache/Colony/Colony/` if you need to dig in. - **More questions?** See the [FAQ](faq.md). diff --git a/src/download.rs b/src/download.rs index 04f384d..ba32269 100644 --- a/src/download.rs +++ b/src/download.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use std::time::Duration; use crate::github::{APP_VERSION, CONNECT_TIMEOUT, GITHUB_ACCOUNT, LAUNCHER_OWNER, LAUNCHER_REPO}; -use crate::persistence::colony_data_dir; +use crate::persistence::colony_cache_dir; /// Build the HTTP client used for large asset downloads (longer read timeout /// than the API client). @@ -555,7 +555,8 @@ pub async fn download_launcher_asset( filename: String, progress_tx: Option)>>, ) -> Result { - let temp_dir = colony_data_dir()?.join("update-staging"); + // Transient by definition: staging lives in the cache. + let temp_dir = colony_cache_dir()?.join("update-staging"); std::fs::create_dir_all(&temp_dir)?; let dest_path = temp_dir.join(&filename); diff --git a/src/persistence.rs b/src/persistence.rs index abaada0..a64139c 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -21,7 +21,7 @@ pub fn colony_data_dir() -> Result { } /// Regenerable state: the repo listing and the scan results. -fn colony_cache_dir() -> Result { +pub(crate) fn colony_cache_dir() -> Result { Ok(colony_ui::paths::cache_dir(PROGRAM)?) } @@ -47,13 +47,17 @@ pub fn migrate_legacy_paths() { ); } - // The cache used to live inside the config directory. It is regenerable, so - // a failure here costs a re-fetch and nothing more. + // Everything regenerable used to live inside the config directory. All of + // it is re-fetched when missing, so a failure here costs a round trip to + // GitHub and nothing more. if let (Ok(config), Ok(cache)) = ( colony_ui::paths::locate::config_dir(PROGRAM), colony_ui::paths::locate::cache_dir(PROGRAM), ) { relocate(&config.join("cache"), &cache, "cache"); + for sub in ["repo-docs", "repo-icons", "update-staging"] { + relocate(&config.join(sub), &cache.join(sub), sub); + } } } @@ -126,9 +130,10 @@ pub(crate) fn colony_app_dir(repo_name: &str) -> Result { join_repo_component(colony_apps_dir()?, repo_name) } -/// Directory for cached repo documentation files: `~/.config/Colony/Colony/repo-docs/{repo_name}/` +/// Cached repo documentation: `/repo-docs/{repo_name}/`. Re-fetched from +/// GitHub when missing, so it is cache rather than config. fn repo_docs_dir(repo_name: &str) -> Result { - let base = join_repo_component(colony_data_dir()?.join("repo-docs"), repo_name)?; + let base = join_repo_component(colony_cache_dir()?.join("repo-docs"), repo_name)?; std::fs::create_dir_all(&base)?; Ok(base) } @@ -146,9 +151,10 @@ pub fn read_repo_doc(repo_name: &str, filename: &str) -> Option { std::fs::read_to_string(dir.join(filename)).ok() } -/// Directory for the cached per-repo app icon: `~/.config/Colony/Colony/repo-icons/{repo_name}/` +/// Cached per-repo app icon: `/repo-icons/{repo_name}/`. Re-downloaded +/// when missing, so it is cache rather than config. fn repo_icon_dir(repo_name: &str) -> Result { - let base = join_repo_component(colony_data_dir()?.join("repo-icons"), repo_name)?; + let base = join_repo_component(colony_cache_dir()?.join("repo-icons"), repo_name)?; std::fs::create_dir_all(&base)?; Ok(base) } @@ -502,7 +508,7 @@ fn desktop_entry_filename(repo_name: &str) -> Result { /// management from Settings > Storage; installs and preferences are NOT /// touched. Returns the number of cache directories removed. pub fn clear_store_caches() -> usize { - let Ok(base) = colony_data_dir() else { + let Ok(base) = colony_cache_dir() else { return 0; }; let mut removed = 0; @@ -525,7 +531,7 @@ pub fn clear_store_caches() -> usize { /// transient absence must not purge anything). Uninstalling a still-listed /// app deliberately keeps its caches - they render the catalog entry. pub fn prune_orphaned_repo_caches(live_repo_names: &[String]) { - let Ok(base) = colony_data_dir() else { + let Ok(base) = colony_cache_dir() else { return; }; for parent in ["repo-docs", "repo-icons"] { diff --git a/src/update/mod.rs b/src/update/mod.rs index 35f31da..6e1ebbd 100644 --- a/src/update/mod.rs +++ b/src/update/mod.rs @@ -735,7 +735,7 @@ mod tests { assert!(matches!(app.github_state, GitHubState::Disconnected)); // Seed an orphaned doc cache for a repo that no longer exists. - let orphan = crate::persistence::colony_data_dir() + let orphan = crate::persistence::colony_cache_dir() .unwrap() .join("repo-docs") .join("Ghost"); From e8fca0295f9caed491311e959fd7fdcf77dbe4cb Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Wed, 26 Aug 2026 00:43:08 +0200 Subject: [PATCH 5/5] refactor(ui): take the shared vocabulary and widgets from colony-ui Colony already gets its palettes, its resolver, its accents and its filesystem layout from colony-ui. Two things it kept its own copy of, and both were quietly broken. ## The theme picker promised something it could not deliver `view_theme_section` renders from `THEME_FAMILIES`, so a theme family added upstream reaches the screen with no code change here - that is the whole point of the generated catalog. But NAMING it still took a hand-written line in `src/i18n/{fr,en}.rs`, and an upstream change never touches this repository. A new family would therefore have appeared in the picker labelled `settings_theme_whatever`. All 62 shared strings were duplicated in both locale files. They had not drifted yet, which is the only reason deleting them is safe rather than a merge. The shared table is now seeded first in `Locale::new`, and a test fails if either locale file ever redefines one of those keys again - an override is precisely how this comes back. `set_language` also moves colony-ui's own active locale now. The shared widgets call `colony_ui::i18n::t` directly and cannot reach Colony's table, so without that the theme picker would have stayed English while the rest of the page turned French. ## The accent list was a local copy of an order that must not move `view_colors_section` held the eight accents as a `vec![]` of hex literals. They now come from `ACCENT_OVERRIDES`, generated from `tokens/accents.toml`. The line count is the least of it: Colony buckets a hash of each installed app's NAME into that list to pick its identity tint, so an edit here that reordered or inserted an entry would have silently re-coloured every icon on every user's machine. The token file says so in a comment; this file said nothing. ## What the widgets do now `theme_picker`, `accent_picker`, `collapsible_section` and `functional_toggle` were ported INTO colony-ui from this file, so the two copies were identical character for character. The four `App` methods stay as one-line facades over the crate: the expanded set, the messages and the fonts are the host's, the drawing is the crate's. That split is why none of the twenty-seven call sites changed. `App::typo()` is the bridge - a widget in the crate cannot reach `App`, so the three fonts and the combined font scale are handed over explicitly. 481 lines deleted, 202 added, 130 tests green. One of them had to be serialised: the active locale is process-wide, so a test that sets it and reads it back cannot run beside another that sets it too. --- src/i18n/en.rs | 82 +--------- src/i18n/fr.rs | 82 +--------- src/i18n/mod.rs | 139 +++++++++++++++++ src/state.rs | 15 ++ src/ui/settings.rs | 365 ++++++--------------------------------------- 5 files changed, 202 insertions(+), 481 deletions(-) diff --git a/src/i18n/en.rs b/src/i18n/en.rs index b6efead..2f0bef2 100644 --- a/src/i18n/en.rs +++ b/src/i18n/en.rs @@ -376,77 +376,9 @@ pub(super) fn insert_all(strings: &mut HashMap) { "Overall application appearance.".into(), ); strings.insert("settings_theme_dark".into(), "Dark".into()); - // Theme families - strings.insert("settings_theme_catppuccin".into(), "Catppuccin".into()); - strings.insert("settings_theme_catppuccin_latte".into(), "Latte".into()); - strings.insert("settings_theme_catppuccin_frappe".into(), "Frappé".into()); - strings.insert( - "settings_theme_catppuccin_macchiato".into(), - "Macchiato".into(), - ); - strings.insert("settings_theme_catppuccin_mocha".into(), "Mocha".into()); - strings.insert("settings_theme_gruvbox".into(), "Gruvbox".into()); - strings.insert("settings_theme_light".into(), "Light mode".into()); - strings.insert("settings_theme_dark_mode".into(), "Dark mode".into()); - strings.insert("settings_theme_everblush".into(), "Everblush".into()); - strings.insert("settings_theme_kanagawa".into(), "Kanagawa".into()); - strings.insert( - "settings_theme_kanagawa_journal".into(), - "Journal mode".into(), - ); - // New theme families - strings.insert("settings_theme_nord".into(), "Nord".into()); - strings.insert("settings_theme_dracula".into(), "Dracula".into()); - strings.insert("settings_theme_solarized".into(), "Solarized".into()); - strings.insert("settings_theme_tokyonight".into(), "Tokyo Night".into()); - strings.insert("settings_theme_tokyonight_night".into(), "Night".into()); - strings.insert("settings_theme_tokyonight_day".into(), "Day".into()); - strings.insert("settings_theme_rosepine".into(), "Rosé Pine".into()); - strings.insert("settings_theme_rosepine_main".into(), "Main".into()); - strings.insert("settings_theme_rosepine_moon".into(), "Moon".into()); - strings.insert("settings_theme_rosepine_dawn".into(), "Dawn".into()); - strings.insert("settings_theme_onedark".into(), "One Dark".into()); - strings.insert("settings_theme_monokai".into(), "Monokai Pro".into()); - strings.insert("settings_theme_monokai_pro".into(), "Pro".into()); - strings.insert("settings_theme_monokai_classic".into(), "Classic".into()); - strings.insert("settings_theme_monokai_spectrum".into(), "Spectrum".into()); - strings.insert("settings_theme_ayu".into(), "Ayu".into()); - strings.insert("settings_theme_ayu_mirage".into(), "Mirage".into()); - strings.insert("settings_theme_everforest".into(), "Everforest".into()); - strings.insert("settings_theme_material".into(), "Material".into()); - strings.insert("settings_theme_material_oceanic".into(), "Oceanic".into()); - strings.insert( - "settings_theme_material_palenight".into(), - "Palenight".into(), - ); - strings.insert( - "settings_theme_material_deepocean".into(), - "Deep Ocean".into(), - ); - strings.insert("settings_theme_flexoki".into(), "Flexoki".into()); - strings.insert("settings_theme_nightfox".into(), "Nightfox".into()); - strings.insert("settings_theme_nightfox_nightfox".into(), "Nightfox".into()); - strings.insert("settings_theme_nightfox_dawnfox".into(), "Dawnfox".into()); - strings.insert("settings_theme_sonokai".into(), "Sonokai".into()); - strings.insert("settings_theme_sonokai_default".into(), "Default".into()); - strings.insert("settings_theme_oxocarbon".into(), "Oxocarbon".into()); - strings.insert("settings_theme_nightowl".into(), "Night Owl".into()); - strings.insert("settings_theme_iceberg".into(), "Iceberg".into()); - strings.insert("settings_theme_horizon".into(), "Horizon".into()); - strings.insert("settings_theme_melange".into(), "Melange".into()); - strings.insert("settings_theme_synthwave".into(), "Synthwave '84".into()); - strings.insert("settings_theme_modus".into(), "Modus".into()); - strings.insert("settings_theme_modus_operandi".into(), "Operandi".into()); - strings.insert("settings_theme_modus_vivendi".into(), "Vivendi".into()); - strings.insert( - "settings_theme_stellar_blade".into(), - "Stellar Blade".into(), - ); - strings.insert("settings_theme_stellar_blade_eve".into(), "EVE".into()); - strings.insert("settings_theme_stellar_blade_tachy".into(), "Tachy".into()); - strings.insert("settings_theme_stellar_blade_lily".into(), "Lily".into()); - strings.insert("settings_theme_stellar_blade_enya".into(), "Enya".into()); - strings.insert("settings_theme_stellar_blade_kaya".into(), "Kaya".into()); + // The theme family and variant names are NOT here: they come from + // colony-ui, generated from the design tokens. A family added upstream + // must not need a line in this file - see `Locale::new`. // Colors & accents strings.insert("settings_section_colors".into(), "Colors & accents".into()); strings.insert( @@ -458,14 +390,6 @@ pub(super) fn insert_all(strings: &mut HashMap) { "settings_accent_color_desc".into(), "Color used for interactive elements.".into(), ); - strings.insert("settings_accent_red".into(), "Red".into()); - strings.insert("settings_accent_orange".into(), "Orange".into()); - strings.insert("settings_accent_yellow".into(), "Yellow".into()); - strings.insert("settings_accent_green".into(), "Green".into()); - strings.insert("settings_accent_blue".into(), "Blue".into()); - strings.insert("settings_accent_indigo".into(), "Indigo".into()); - strings.insert("settings_accent_violet".into(), "Violet".into()); - strings.insert("settings_accent_amber".into(), "Amber".into()); strings.insert( "settings_auto_accent".into(), "Auto accent from background".into(), diff --git a/src/i18n/fr.rs b/src/i18n/fr.rs index 85263fe..b2fd5ff 100644 --- a/src/i18n/fr.rs +++ b/src/i18n/fr.rs @@ -417,77 +417,9 @@ pub(super) fn insert_all(strings: &mut HashMap) { "Apparence globale de l'application.".into(), ); strings.insert("settings_theme_dark".into(), "Sombre".into()); - // Theme families - strings.insert("settings_theme_catppuccin".into(), "Catppuccin".into()); - strings.insert("settings_theme_catppuccin_latte".into(), "Latte".into()); - strings.insert("settings_theme_catppuccin_frappe".into(), "Frappé".into()); - strings.insert( - "settings_theme_catppuccin_macchiato".into(), - "Macchiato".into(), - ); - strings.insert("settings_theme_catppuccin_mocha".into(), "Mocha".into()); - strings.insert("settings_theme_gruvbox".into(), "Gruvbox".into()); - strings.insert("settings_theme_light".into(), "Mode clair".into()); - strings.insert("settings_theme_dark_mode".into(), "Mode sombre".into()); - strings.insert("settings_theme_everblush".into(), "Everblush".into()); - strings.insert("settings_theme_kanagawa".into(), "Kanagawa".into()); - strings.insert( - "settings_theme_kanagawa_journal".into(), - "Mode journal".into(), - ); - // New theme families - strings.insert("settings_theme_nord".into(), "Nord".into()); - strings.insert("settings_theme_dracula".into(), "Dracula".into()); - strings.insert("settings_theme_solarized".into(), "Solarized".into()); - strings.insert("settings_theme_tokyonight".into(), "Tokyo Night".into()); - strings.insert("settings_theme_tokyonight_night".into(), "Nuit".into()); - strings.insert("settings_theme_tokyonight_day".into(), "Jour".into()); - strings.insert("settings_theme_rosepine".into(), "Rosé Pine".into()); - strings.insert("settings_theme_rosepine_main".into(), "Principal".into()); - strings.insert("settings_theme_rosepine_moon".into(), "Lune".into()); - strings.insert("settings_theme_rosepine_dawn".into(), "Aurore".into()); - strings.insert("settings_theme_onedark".into(), "One Dark".into()); - strings.insert("settings_theme_monokai".into(), "Monokai Pro".into()); - strings.insert("settings_theme_monokai_pro".into(), "Pro".into()); - strings.insert("settings_theme_monokai_classic".into(), "Classic".into()); - strings.insert("settings_theme_monokai_spectrum".into(), "Spectrum".into()); - strings.insert("settings_theme_ayu".into(), "Ayu".into()); - strings.insert("settings_theme_ayu_mirage".into(), "Mirage".into()); - strings.insert("settings_theme_everforest".into(), "Everforest".into()); - strings.insert("settings_theme_material".into(), "Material".into()); - strings.insert("settings_theme_material_oceanic".into(), "Oceanic".into()); - strings.insert( - "settings_theme_material_palenight".into(), - "Palenight".into(), - ); - strings.insert( - "settings_theme_material_deepocean".into(), - "Deep Ocean".into(), - ); - strings.insert("settings_theme_flexoki".into(), "Flexoki".into()); - strings.insert("settings_theme_nightfox".into(), "Nightfox".into()); - strings.insert("settings_theme_nightfox_nightfox".into(), "Nightfox".into()); - strings.insert("settings_theme_nightfox_dawnfox".into(), "Dawnfox".into()); - strings.insert("settings_theme_sonokai".into(), "Sonokai".into()); - strings.insert("settings_theme_sonokai_default".into(), "Défaut".into()); - strings.insert("settings_theme_oxocarbon".into(), "Oxocarbon".into()); - strings.insert("settings_theme_nightowl".into(), "Night Owl".into()); - strings.insert("settings_theme_iceberg".into(), "Iceberg".into()); - strings.insert("settings_theme_horizon".into(), "Horizon".into()); - strings.insert("settings_theme_melange".into(), "Mélange".into()); - strings.insert("settings_theme_synthwave".into(), "Synthwave '84".into()); - strings.insert("settings_theme_modus".into(), "Modus".into()); - strings.insert("settings_theme_modus_operandi".into(), "Operandi".into()); - strings.insert("settings_theme_modus_vivendi".into(), "Vivendi".into()); - strings.insert( - "settings_theme_stellar_blade".into(), - "Stellar Blade".into(), - ); - strings.insert("settings_theme_stellar_blade_eve".into(), "EVE".into()); - strings.insert("settings_theme_stellar_blade_tachy".into(), "Tachy".into()); - strings.insert("settings_theme_stellar_blade_lily".into(), "Lily".into()); - strings.insert("settings_theme_stellar_blade_enya".into(), "Enya".into()); - strings.insert("settings_theme_stellar_blade_kaya".into(), "Kaya".into()); + // The theme family and variant names are NOT here: they come from + // colony-ui, generated from the design tokens. A family added upstream + // must not need a line in this file - see `Locale::new`. // Colors & accents strings.insert( "settings_section_colors".into(), @@ -502,14 +434,6 @@ pub(super) fn insert_all(strings: &mut HashMap) { "settings_accent_color_desc".into(), "Couleur utilisée pour les éléments interactifs.".into(), ); - strings.insert("settings_accent_red".into(), "Rouge".into()); - strings.insert("settings_accent_orange".into(), "Orange".into()); - strings.insert("settings_accent_yellow".into(), "Jaune".into()); - strings.insert("settings_accent_green".into(), "Vert".into()); - strings.insert("settings_accent_blue".into(), "Bleu".into()); - strings.insert("settings_accent_indigo".into(), "Indigo".into()); - strings.insert("settings_accent_violet".into(), "Violet".into()); - strings.insert("settings_accent_amber".into(), "Ambre".into()); strings.insert( "settings_auto_accent".into(), "Accent automatique selon le fond".into(), diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs index 815f7c9..636900d 100644 --- a/src/i18n/mod.rs +++ b/src/i18n/mod.rs @@ -15,6 +15,26 @@ impl Locale { fn new(lang: &str) -> Self { let mut strings = HashMap::new(); + // The shared vocabulary FIRST: theme families, theme variants and accent + // names, generated from the design tokens in Project-Colony-Resources. + // + // They used to be copied into `fr.rs` and `en.rs` by hand, which made + // the picker's promise false: `THEME_FAMILIES` renders whatever the + // catalog holds, so a family added upstream reached the screen with no + // code change here - and then displayed its raw key, because no hand + // written line existed to name it. Seeding from the crate closes that. + // + // Colony's own strings load second and would win a collision, but + // `no_shared_key_is_redefined_locally` fails if one ever exists: an + // override is how the drift this deletion removed would come back. + let shared = match lang { + "fr" => colony_ui::i18n::Locale::Fr, + _ => colony_ui::i18n::Locale::En, + }; + strings.extend( + colony_ui::i18n::all(shared).map(|(k, v)| (k.to_string(), v.to_string())), + ); + match lang { "fr" => fr::insert_all(&mut strings), _ => en::insert_all(&mut strings), @@ -50,6 +70,10 @@ pub fn set_language(lang: &str) { "en" }; tracing::info!("Locale: {lang}"); + // The shared widgets call `colony_ui::i18n::t` directly - they cannot reach + // Colony's table - so the crate's own active locale has to move in step or + // the theme picker stays English while the rest of the page turns French. + colony_ui::i18n::set_locale(colony_ui::i18n::Locale::from_tag(lang)); if let Ok(mut locale) = LOCALE.write() { *locale = Some(Locale::new(lang)); } @@ -134,6 +158,19 @@ fn detect_language() -> String { mod tests { use super::*; + /// The active locale is process-wide, so a test that SETS it and then reads + /// it back cannot run beside another that sets it too - the second write + /// lands between the first test's write and its assertion. Every test that + /// calls `set_language` takes this first. + /// + /// The lock is poisoned by a failing test, which would cascade into a second + /// misleading failure, so the guard is taken through `unwrap_or_else`. + static LOCALE: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + fn locale_guard() -> std::sync::MutexGuard<'static, ()> { + LOCALE.lock().unwrap_or_else(|e| e.into_inner()) + } + #[test] fn english_locale_has_keys() { let locale = Locale::new("en"); @@ -177,8 +214,110 @@ mod tests { ); } + /// Colony's own locale files must not carry a string colony-ui already + /// ships. They did - all 62 of them, in both locales - and the copies had + /// not drifted *yet*, which is the only reason the deletion was safe. + /// + /// `Locale::new` loads Colony's table second, so a re-added line would win + /// silently and put the drift back. This is what makes that loud. + #[test] + fn no_shared_key_is_redefined_locally() { + for (lang, insert_all) in [ + ("fr", fr::insert_all as fn(&mut HashMap)), + ("en", en::insert_all as fn(&mut HashMap)), + ] { + let mut own = HashMap::new(); + insert_all(&mut own); + + let shared = colony_ui::i18n::Locale::from_tag(lang); + let clashes: Vec<&str> = colony_ui::i18n::all(shared) + .map(|(k, _)| k) + .filter(|k| own.contains_key(*k)) + .collect(); + + assert!( + clashes.is_empty(), + "{lang}.rs redefines strings colony-ui already ships: {clashes:?}. \ + Delete them - the shared table is seeded first in Locale::new." + ); + } + } + + /// The point of the deletion: the names still reach the screen, localized, + /// through exactly the same `t()` every call site already uses. + #[test] + fn shared_theme_and_accent_labels_still_resolve() { + let fr = Locale::new("fr"); + let en = Locale::new("en"); + + // Translated. + assert_eq!(fr.strings.get("settings_accent_red").unwrap(), "Rouge"); + assert_eq!(en.strings.get("settings_accent_red").unwrap(), "Red"); + assert_eq!(fr.strings.get("settings_theme_light").unwrap(), "Mode clair"); + + // A proper noun, identical in both - and never hand-typed again. + assert_eq!(fr.strings.get("settings_theme_gruvbox").unwrap(), "Gruvbox"); + assert_eq!(en.strings.get("settings_theme_gruvbox").unwrap(), "Gruvbox"); + } + + /// A family added upstream must reach the picker NAMED. Before the seeding + /// it reached it as its raw key, because naming it took a hand-written line + /// in a file the upstream change never touched. + #[test] + fn every_catalog_entry_has_a_name_in_both_locales() { + let fr = Locale::new("fr"); + let en = Locale::new("en"); + + for family in colony_ui::THEME_FAMILIES { + for key in std::iter::once(family.label_key) + .chain(family.variants.iter().map(|v| v.label_key)) + { + for (lang, locale) in [("fr", &fr), ("en", &en)] { + let name = locale.strings.get(key); + assert!( + name.is_some_and(|n| n != key), + "{lang}: theme catalog key {key} would render as itself" + ); + } + } + } + } + + /// Same for the accents, whose order is load-bearing (each app's identity + /// tint is a hash bucketed into this list). + #[test] + fn every_accent_has_a_name_in_both_locales() { + let fr = Locale::new("fr"); + let en = Locale::new("en"); + for accent in colony_ui::ACCENT_OVERRIDES { + for (lang, locale) in [("fr", &fr), ("en", &en)] { + assert!( + locale.strings.contains_key(accent.label_key), + "{lang}: accent {} has no name", accent.key + ); + } + } + } + + /// `set_language` has to move BOTH tables. The shared widgets never touch + /// Colony's, so if this regresses the theme picker silently stays English. + #[test] + fn set_language_moves_the_shared_locale_too() { + let _guard = locale_guard(); + + set_language("fr"); + assert_eq!(colony_ui::i18n::locale(), colony_ui::i18n::Locale::Fr); + assert_eq!(colony_ui::i18n::t("settings_accent_red"), "Rouge"); + + set_language("en"); + assert_eq!(colony_ui::i18n::locale(), colony_ui::i18n::Locale::En); + assert_eq!(colony_ui::i18n::t("settings_accent_red"), "Red"); + } + #[test] fn t_fmt_substitution() { + let _guard = locale_guard(); + // Initialize with English for test set_language("en"); let result = t_fmt("apps_found", &[("count", "42")]); diff --git a/src/state.rs b/src/state.rs index 3ad2c85..40bd370 100644 --- a/src/state.rs +++ b/src/state.rs @@ -416,6 +416,21 @@ impl App { (base as f32 * self.font_scale()).round() } + /// What the shared colony-ui widgets need to know about this program's text. + /// + /// A widget in that crate cannot reach `App`, so the three fonts and the + /// combined scale are handed over explicitly. `Typography::sz` is + /// `(base * scale).round()` - the same arithmetic as [`Self::sz`], which is + /// what keeps a shared widget the same size as the rows around it. + pub fn typo(&self) -> colony_ui::Typography { + colony_ui::Typography { + scale: self.font_scale(), + regular: self.app_font(), + medium: self.app_font_with_weight(Weight::Medium), + bold: self.app_font_with_weight(Weight::Bold), + } + } + /// Duration of the sidebar slide animation. pub const SIDEBAR_ANIM_MS: f32 = 200.0; diff --git a/src/ui/settings.rs b/src/ui/settings.rs index bcb1976..79461b5 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -7,6 +7,7 @@ use crate::i18n; use crate::message::Message; use crate::state::App; use crate::ui::theme::Palette; +use colony_ui::widgets; /// Settings category names (keys for i18n). const SETTINGS_CATEGORIES: &[&str] = &[ @@ -806,208 +807,38 @@ impl App { } // ── Theme sub-section ── + /// The theme picker, drawn by colony-ui straight from the generated + /// catalog. This used to be 135 lines here, character for character what + /// the crate now draws - the crate's copy was ported FROM this one. fn view_theme_section(&self) -> Element<'_, Message> { - let font = self.app_font(); - let medium = self.app_font_with_weight(Weight::Medium); - - let mut col = column![].spacing(12); - - // Rendered straight from colony-ui's generated catalog: adding a theme - // family upstream needs no change here. - for family in colony_ui::THEME_FAMILIES { - let (theme_key, label_key, icon) = (family.key, family.label_key, family.icon); - let is_selected_family = self.selected_theme == theme_key; - let label = i18n::t(label_key); - - // Family name label with optional themed icon - let label_text = if icon.is_empty() { - label.clone() - } else { - format!("{} {}", icon, label) - }; - let family_label = - text(label_text) - .size(self.sz(13)) - .font(medium) - .color(if is_selected_family { - Palette::TEXT_PRIMARY() - } else { - Palette::TEXT_SECONDARY() - }); - - // Variant cards as a horizontal row of mini color-swatch cards - let mut variant_row = row![].spacing(8); - - for variant in family.variants { - let (var_key, var_label_key) = (&variant.key, variant.label_key); - let is_active = is_selected_family && self.selected_variant == *var_key; - let theme_owned = theme_key.to_string(); - let var_owned = var_key.to_string(); - - let bg_color = variant.swatch_bg_color(); - let accent_color = variant.swatch_accent_color(); - - // Color swatch: bg stripe + accent dot - let swatch_bg = container(text("")) - .width(Length::Fill) - .height(Length::Fixed(4.0)) - .style(move |_theme| container::Style { - background: Some(accent_color.into()), - border: iced::Border::default().rounded(2), - ..Default::default() - }); - - let swatch = container(swatch_bg) - .width(Length::Fill) - .height(Length::Fixed(28.0)) - .padding(iced::Padding { - top: 20.0, - right: 6.0, - bottom: 4.0, - left: 6.0, - }) - .style(move |_theme| container::Style { - background: Some(bg_color.into()), - border: iced::Border::default().rounded(6), - ..Default::default() - }); - - // Variant label below the swatch - let var_label = text(i18n::t(var_label_key)) - .size(self.sz(10)) - .font(font) - .color(if is_active { - Palette::TEXT_PRIMARY() - } else { - Palette::TEXT_MUTED() - }); - - // Check indicator for active variant - let indicator: Element<'_, Message> = if is_active { - text("\u{f00c}") - .size(self.sz(8)) - .font(font) - .color(Palette::ACCENT()) - .into() - } else { - text("").size(self.sz(8)).into() - }; - - let card_content = column![ - swatch, - container( - row![var_label, indicator] - .spacing(4) - .align_y(iced::Alignment::Center) - ) - .padding(iced::Padding { - top: 4.0, - right: 0.0, - bottom: 0.0, - left: 2.0 - }), - ] - .spacing(0) - .width(Length::Fill); - - let card = button(card_content) - .on_press(Message::SelectThemeVariant(theme_owned, var_owned)) - .padding(4) - .width(Length::Fill) - .style(move |_theme, status| { - let border_color = match status { - _ if is_active => Palette::ACCENT(), - button::Status::Hovered => Palette::TEXT_DIMMER(), - _ => Palette::BORDER_SUBTLE(), - }; - button::Style { - background: Some(Palette::BG_CARD().into()), - text_color: Palette::TEXT_PRIMARY(), - border: iced::Border { - color: border_color, - width: if is_active { 2.0 } else { 1.0 }, - radius: 8.0.into(), - }, - ..Default::default() - } - }); - - variant_row = variant_row.push(card); - } - - col = col.push(column![family_label, variant_row,].spacing(6)); - } - - col.into() + widgets::theme_picker( + &self.typo(), + &self.selected_theme, + &self.selected_variant, + |family, variant| { + Message::SelectThemeVariant(family.to_string(), variant.to_string()) + }, + ) } // ── Colors & accents sub-section ── + /// The accent swatches, plus the separate "derive the accent from the + /// background" behaviour toggle. + /// + /// The eight colours were hardcoded here as a `vec![]` of literals. They + /// now come from `ACCENT_OVERRIDES`, generated from `tokens/accents.toml`, + /// which matters more than the line count: **the order is load-bearing**. + /// Colony buckets a hash of each installed app's name into that list to + /// pick its identity tint, so a local edit that reordered or inserted an + /// entry would silently re-colour every icon on every machine - and there + /// was nothing here to say so. fn view_colors_section(&self) -> Element<'_, Message> { - let font = self.app_font(); - - // Accent colors: (key, i18n_label_key, hex_color) - let accent_colors: Vec<(&str, &str, u32)> = vec![ - ("red", "settings_accent_red", 0xE05555), - ("orange", "settings_accent_orange", 0xE0855A), - ("yellow", "settings_accent_yellow", 0xC8A832), - ("green", "settings_accent_green", 0x55B87A), - ("blue", "settings_accent_blue", 0x6B8BD6), - ("indigo", "settings_accent_indigo", 0x7B6BD6), - ("violet", "settings_accent_violet", 0xB06BD6), - ("amber", "settings_accent_amber", 0xD4A030), - ]; - - let mut color_row = row![].spacing(8).align_y(iced::Alignment::Center); - - for (color_key, _label_key, hex) in &accent_colors { - let is_active = self.selected_accent == *color_key; - let color_key_owned = color_key.to_string(); - let r = ((*hex >> 16) & 0xFF) as f32 / 255.0; - let g = ((*hex >> 8) & 0xFF) as f32 / 255.0; - let b = (*hex & 0xFF) as f32 / 255.0; - let dot_color = iced::Color { r, g, b, a: 1.0 }; - - // Circular color swatch button - let check_icon: Element<'_, Message> = if is_active { - text("\u{f00c}") - .size(self.sz(8)) - .font(font) - .color(iced::Color::WHITE) - .into() - } else { - text("").size(self.sz(8)).into() - }; - let swatch = button( - container(check_icon) - .center_x(Length::Fill) - .center_y(Length::Fill), - ) - .on_press(Message::SelectAccentColor(color_key_owned)) - .width(Length::Fixed(28.0)) - .height(Length::Fixed(28.0)) - .padding(0) - .style(move |_theme, status| { - let border_color = match status { - _ if is_active => Palette::TEXT_PRIMARY(), - button::Status::Hovered => Palette::TEXT_DIMMER(), - _ => iced::Color::TRANSPARENT, - }; - button::Style { - background: Some(dot_color.into()), - text_color: iced::Color::WHITE, - border: iced::Border { - color: border_color, - width: if is_active { 2.0 } else { 0.0 }, - radius: 14.0.into(), - }, - ..Default::default() - } - }); - - color_row = color_row.push(swatch); - } + let swatches = widgets::accent_picker( + &self.typo(), + Some(self.selected_accent.as_str()), + |key| Message::SelectAccentColor(key.to_string()), + ); - // Auto accent toggle let auto_accent_row = self.view_functional_toggle( &i18n::t("settings_auto_accent"), &i18n::t("settings_auto_accent_desc"), @@ -1015,82 +846,34 @@ impl App { Message::ToggleAutoAccent, ); - column![color_row, container(text("")).height(12), auto_accent_row,] + column![swatches, container(text("")).height(12), auto_accent_row] .spacing(0) .into() } // ── Collapsible section ── + /// A collapsible settings section. + /// + /// Kept as a method rather than calling the crate from all sixteen sites: + /// the expanded set and the message are the HOST's, the drawing is the + /// crate's. That split is why no call site changed. fn view_collapsible_section<'a>( &self, key: &str, title: &str, content: Element<'a, Message>, ) -> Element<'a, Message> { - let is_expanded = self.settings_expanded_sections.contains(key); - let arrow = if is_expanded { "\u{f078}" } else { "\u{f054}" }; // chevron down / right - let key_owned = key.to_string(); - let title_owned = title.to_string(); - - // Header: clean flat style, no box — just text + chevron - let header_btn = button( - row![ - text(title_owned) - .size(self.sz(15)) - .font(self.app_font_with_weight(Weight::Bold)) - .color(Palette::TEXT_PRIMARY()), - container(text("")).width(Fill), - text(arrow) - .size(self.sz(9)) - .font(self.app_font()) - .color(Palette::TEXT_DIMMER()), - ] - .spacing(8) - .align_y(iced::Alignment::Center), + widgets::collapsible_section( + &self.typo(), + title, + self.settings_expanded_sections.contains(key), + Message::SettingsToggleSection(key.to_string()), + content, ) - .on_press(Message::SettingsToggleSection(key_owned)) - .padding([12, 4]) - .width(Fill) - .style(move |_theme, status| { - let bg = match status { - button::Status::Hovered => Palette::BG_CARD_HOVER(), - _ => iced::Color::TRANSPARENT, - }; - button::Style { - background: Some(bg.into()), - text_color: Palette::TEXT_PRIMARY(), - border: iced::Border::default().rounded(6), - ..Default::default() - } - }); - - if is_expanded { - // Thin divider line under header - let divider = - container(text("")) - .width(Fill) - .height(1) - .style(|_theme| container::Style { - background: Some(Palette::DIVIDER().into()), - ..Default::default() - }); - - let body = container(content) - .padding(iced::Padding { - top: 12.0, - right: 4.0, - bottom: 4.0, - left: 4.0, - }) - .width(Fill); - - column![header_btn, divider, body].spacing(0).into() - } else { - header_btn.into() - } } - /// A functional toggle: clicking sends the given message. + /// A labelled on/off row. Same split as above: eleven call sites, none of + /// which changed. fn view_functional_toggle( &self, title: &str, @@ -1098,71 +881,7 @@ impl App { on: bool, msg: Message, ) -> Element<'_, Message> { - let font = self.app_font(); - let track_bg = if on { - Palette::ACCENT() - } else { - Palette::BG_CARD_HOVER() - }; - let knob_offset: f32 = if on { 16.0 } else { 2.0 }; - - let knob = container(text("")) - .width(Length::Fixed(14.0)) - .height(Length::Fixed(14.0)) - .style(move |_theme| container::Style { - background: Some(Palette::TEXT_PRIMARY().into()), - border: iced::Border::default().rounded(7), - ..Default::default() - }); - let toggle_visual = container(container(knob).padding(iced::Padding { - top: 1.0, - right: 0.0, - bottom: 0.0, - left: knob_offset, - })) - .width(Length::Fixed(34.0)) - .height(Length::Fixed(18.0)) - .style(move |_theme| container::Style { - background: Some(track_bg.into()), - border: iced::Border::default().rounded(9), - ..Default::default() - }); - - button( - row![ - column![ - text(title.to_string()) - .size(self.sz(13)) - .font(font) - .color(Palette::TEXT_PRIMARY()), - text(desc.to_string()) - .size(self.sz(11)) - .font(font) - .color(Palette::TEXT_DIMMER()), - ] - .spacing(2), - container(text("")).width(Fill), - toggle_visual, - ] - .spacing(10) - .align_y(iced::Alignment::Center), - ) - .on_press(msg) - .padding([6, 4]) - .width(Fill) - .style(|_theme, status| { - let bg = match status { - button::Status::Hovered => Palette::BG_CARD_HOVER(), - _ => iced::Color::TRANSPARENT, - }; - button::Style { - background: Some(bg.into()), - text_color: Palette::TEXT_PRIMARY(), - border: iced::Border::default().rounded(6), - ..Default::default() - } - }) - .into() + widgets::functional_toggle(&self.typo(), title, desc, on, msg) } /// A setting row with a pick_list dropdown for selecting from options.