diff --git a/resources/base.css b/resources/base.css new file mode 100644 index 0000000..ab93575 --- /dev/null +++ b/resources/base.css @@ -0,0 +1,223 @@ +/* + * Owlry Base Styles + * Provides sensible defaults that work with GTK theme + * Colors can be overridden via theme or config + */ + +/* Main window - transparent for layer shell */ +.owlry-window { + background-color: transparent; +} + +/* Main container - needs explicit background for overlay */ +.owlry-main { + background-color: var(--owlry-bg, @theme_bg_color); + border-radius: var(--owlry-border-radius, 12px); + border: 1px solid var(--owlry-border, @borders); + padding: 16px; +} + +/* Search entry */ +.owlry-search { + background-color: var(--owlry-bg-secondary, @theme_base_color); + border: 2px solid var(--owlry-border, @borders); + border-radius: calc(var(--owlry-border-radius, 12px) - 4px); + padding: 12px 16px; + font-size: var(--owlry-font-size, 14px); + color: var(--owlry-text, @theme_fg_color); + min-height: 24px; +} + +.owlry-search:focus { + border-color: var(--owlry-accent, @theme_selected_bg_color); + outline: none; +} + +/* Results list */ +.owlry-results { + background-color: transparent; + border-radius: calc(var(--owlry-border-radius, 12px) - 4px); +} + +/* Individual result row */ +.owlry-result-row { + background-color: transparent; + border-radius: calc(var(--owlry-border-radius, 12px) - 4px); + margin: 2px 0; + padding: 8px 12px; +} + +.owlry-result-row:hover { + background-color: var(--owlry-bg-secondary, alpha(@theme_fg_color, 0.1)); +} + +.owlry-result-row:selected { + background-color: var(--owlry-accent, @theme_selected_bg_color); + color: var(--owlry-accent-bright, @theme_selected_fg_color); +} + +/* Result icon */ +.owlry-result-icon { + color: var(--owlry-text, @theme_fg_color); + opacity: 0.9; +} + +.owlry-result-row:selected .owlry-result-icon { + color: var(--owlry-accent-bright, @theme_selected_fg_color); + opacity: 1; +} + +/* Result name */ +.owlry-result-name { + font-size: var(--owlry-font-size, 14px); + font-weight: 500; + color: var(--owlry-text, @theme_fg_color); +} + +.owlry-result-row:selected .owlry-result-name { + color: var(--owlry-accent-bright, @theme_selected_fg_color); +} + +/* Result description */ +.owlry-result-description { + font-size: calc(var(--owlry-font-size, 14px) - 2px); + color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.7)); + margin-top: 2px; +} + +.owlry-result-row:selected .owlry-result-description { + color: var(--owlry-accent-bright, @theme_selected_fg_color); +} + +/* Provider badges */ +.owlry-result-badge { + font-size: calc(var(--owlry-font-size, 14px) - 4px); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 3px 8px; + border-radius: 6px; + background-color: var(--owlry-bg-secondary, alpha(@theme_fg_color, 0.1)); + color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.7)); +} + +.owlry-badge-app { + background-color: alpha(var(--owlry-badge-app, @blue_3), 0.2); + color: var(--owlry-badge-app, @blue_3); +} + +.owlry-badge-cmd { + background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2); + color: var(--owlry-badge-cmd, @purple_3); +} + +.owlry-badge-dmenu { + background-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.2); + color: var(--owlry-badge-dmenu, @green_3); +} + +.owlry-badge-uuctl { + background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2); + color: var(--owlry-badge-uuctl, @orange_3); +} + +/* Header bar */ +.owlry-header { + margin-bottom: 4px; +} + +/* Mode indicator */ +.owlry-mode-indicator { + font-size: calc(var(--owlry-font-size, 14px) - 2px); + font-weight: 600; + padding: 4px 12px; + border-radius: 6px; + background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.2); + color: var(--owlry-accent, @theme_selected_bg_color); +} + +/* Filter buttons */ +.owlry-filter-button { + font-size: calc(var(--owlry-font-size, 14px) - 3px); + font-weight: 500; + padding: 4px 10px; + border-radius: 6px; + background-color: alpha(var(--owlry-border, @borders), 0.3); + color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.7)); + border: 1px solid transparent; + min-height: 20px; +} + +.owlry-filter-button:hover { + background-color: alpha(var(--owlry-border, @borders), 0.5); + color: var(--owlry-text, @theme_fg_color); +} + +.owlry-filter-button:checked { + background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.2); + color: var(--owlry-accent, @theme_selected_bg_color); + border-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.4); +} + +/* Provider-specific filter button colors */ +.owlry-filter-app:checked { + background-color: alpha(var(--owlry-badge-app, @blue_3), 0.2); + color: var(--owlry-badge-app, @blue_3); + border-color: alpha(var(--owlry-badge-app, @blue_3), 0.4); +} + +.owlry-filter-cmd:checked { + background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2); + color: var(--owlry-badge-cmd, @purple_3); + border-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.4); +} + +.owlry-filter-uuctl:checked { + background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2); + color: var(--owlry-badge-uuctl, @orange_3); + border-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.4); +} + +.owlry-filter-dmenu:checked { + background-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.2); + color: var(--owlry-badge-dmenu, @green_3); + border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4); +} + +/* Hints bar at bottom */ +.owlry-hints { + padding-top: 8px; + border-top: 1px solid var(--owlry-border, @borders); +} + +.owlry-hints-label { + font-size: calc(var(--owlry-font-size, 14px) - 4px); + color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.7)); + letter-spacing: 0.5px; +} + +/* Scrollbar styling */ +scrollbar { + background-color: transparent; +} + +scrollbar slider { + background-color: alpha(var(--owlry-border, @borders), 0.5); + border-radius: 4px; + min-width: 6px; + min-height: 40px; +} + +scrollbar slider:hover { + background-color: alpha(var(--owlry-border, @borders), 0.7); +} + +scrollbar slider:active { + background-color: var(--owlry-accent, @theme_selected_bg_color); +} + +/* Text selection */ +selection { + background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.3); + color: var(--owlry-text, @theme_fg_color); +} diff --git a/resources/style.css b/resources/owl-theme.css similarity index 65% rename from resources/style.css rename to resources/owl-theme.css index 9a7a3aa..23244ba 100644 --- a/resources/style.css +++ b/resources/owl-theme.css @@ -1,7 +1,8 @@ /* - * Owlry - Owl-themed Application Launcher + * Owlry - Owl Theme + * An owl-inspired dark theme with amber accents * - * Color Palette (Owl-inspired): + * Color Palette: * - Deep night sky: #1a1b26 (background) * - Twilight: #24283b (secondary bg) * - Owl feathers: #414868 (borders/muted) @@ -11,14 +12,24 @@ * - Barn owl cream: #f5e0dc (bright accent) */ -/* Main window */ -.owlry-window { - background-color: transparent; +/* Define CSS variables with owl theme defaults */ +:root { + --owlry-bg: #1a1b26; + --owlry-bg-secondary: #24283b; + --owlry-border: #414868; + --owlry-text: #c0caf5; + --owlry-text-secondary: #565f89; + --owlry-accent: #e0af68; + --owlry-accent-bright: #f5e0dc; + --owlry-badge-app: #7aa2f7; + --owlry-badge-cmd: #bb9af7; + --owlry-badge-dmenu: #9ece6a; + --owlry-badge-uuctl: #e0af68; } +/* Main container */ .owlry-main { background-color: rgba(26, 27, 38, 0.95); - border-radius: 16px; border: 1px solid rgba(65, 72, 104, 0.6); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(224, 175, 104, 0.1); @@ -28,35 +39,27 @@ .owlry-search { background-color: rgba(36, 40, 59, 0.8); border: 2px solid rgba(65, 72, 104, 0.5); - border-radius: 12px; - padding: 12px 16px; - font-size: 16px; - color: #c0caf5; - caret-color: #e0af68; - min-height: 24px; + color: var(--owlry-text); + caret-color: var(--owlry-accent); } .owlry-search:focus { - border-color: #e0af68; + border-color: var(--owlry-accent); box-shadow: 0 0 0 2px rgba(224, 175, 104, 0.2); - outline: none; } .owlry-search placeholder { - color: #565f89; + color: var(--owlry-text-secondary); } /* Results list */ .owlry-results { background-color: transparent; - border-radius: 8px; } /* Individual result row */ .owlry-result-row { background-color: transparent; - border-radius: 8px; - margin: 2px 0; transition: background-color 150ms ease; } @@ -66,7 +69,7 @@ .owlry-result-row:selected { background-color: rgba(224, 175, 104, 0.15); - border-left: 3px solid #e0af68; + border-left: 3px solid var(--owlry-accent); } .owlry-result-row:selected:hover { @@ -75,79 +78,60 @@ /* Result icon */ .owlry-result-icon { - color: #c0caf5; - opacity: 0.9; + color: var(--owlry-text); } .owlry-result-row:selected .owlry-result-icon { - color: #e0af68; - opacity: 1; + color: var(--owlry-accent); } /* Result name */ .owlry-result-name { - font-size: 14px; - font-weight: 500; - color: #c0caf5; + color: var(--owlry-text); } .owlry-result-row:selected .owlry-result-name { - color: #f5e0dc; + color: var(--owlry-accent-bright); } /* Result description */ .owlry-result-description { - font-size: 12px; - color: #565f89; - margin-top: 2px; + color: var(--owlry-text-secondary); } .owlry-result-row:selected .owlry-result-description { - color: #7aa2f7; + color: var(--owlry-badge-app); } /* Provider badges */ .owlry-result-badge { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 3px 8px; - border-radius: 6px; background-color: rgba(65, 72, 104, 0.4); - color: #565f89; + color: var(--owlry-text-secondary); } .owlry-badge-app { background-color: rgba(122, 162, 247, 0.2); - color: #7aa2f7; + color: var(--owlry-badge-app); } .owlry-badge-cmd { background-color: rgba(187, 154, 247, 0.2); - color: #bb9af7; + color: var(--owlry-badge-cmd); } .owlry-badge-dmenu { background-color: rgba(158, 206, 106, 0.2); - color: #9ece6a; + color: var(--owlry-badge-dmenu); } .owlry-badge-uuctl { background-color: rgba(224, 175, 104, 0.2); - color: #e0af68; + color: var(--owlry-badge-uuctl); } /* Scrollbar styling */ -scrollbar { - background-color: transparent; -} - scrollbar slider { background-color: rgba(65, 72, 104, 0.5); - border-radius: 4px; - min-width: 6px; - min-height: 40px; } scrollbar slider:hover { @@ -155,92 +139,70 @@ scrollbar slider:hover { } scrollbar slider:active { - background-color: #e0af68; + background-color: var(--owlry-accent); } /* Selection highlighting */ selection { background-color: rgba(224, 175, 104, 0.3); - color: #f5e0dc; -} - -/* Header bar with mode indicator and filter tabs */ -.owlry-header { - margin-bottom: 4px; + color: var(--owlry-accent-bright); } /* Mode indicator */ .owlry-mode-indicator { - font-size: 12px; - font-weight: 600; - color: #e0af68; - padding: 4px 12px; + color: var(--owlry-accent); background-color: rgba(224, 175, 104, 0.15); - border-radius: 6px; -} - -/* Filter tabs container */ -.owlry-filter-tabs { - /* Container spacing handled by GtkBox */ } /* Filter toggle buttons */ .owlry-filter-button { - font-size: 11px; - font-weight: 500; - padding: 4px 10px; - border-radius: 6px; background-color: rgba(65, 72, 104, 0.3); - color: #565f89; + color: var(--owlry-text-secondary); border: 1px solid transparent; - min-height: 20px; transition: all 150ms ease; } .owlry-filter-button:hover { background-color: rgba(65, 72, 104, 0.5); - color: #c0caf5; + color: var(--owlry-text); } .owlry-filter-button:checked { background-color: rgba(224, 175, 104, 0.2); - color: #e0af68; + color: var(--owlry-accent); border-color: rgba(224, 175, 104, 0.4); } /* Provider-specific filter button colors when active */ .owlry-filter-app:checked { background-color: rgba(122, 162, 247, 0.2); - color: #7aa2f7; + color: var(--owlry-badge-app); border-color: rgba(122, 162, 247, 0.4); } .owlry-filter-cmd:checked { background-color: rgba(187, 154, 247, 0.2); - color: #bb9af7; + color: var(--owlry-badge-cmd); border-color: rgba(187, 154, 247, 0.4); } .owlry-filter-uuctl:checked { background-color: rgba(224, 175, 104, 0.2); - color: #e0af68; + color: var(--owlry-badge-uuctl); border-color: rgba(224, 175, 104, 0.4); } .owlry-filter-dmenu:checked { background-color: rgba(158, 206, 106, 0.2); - color: #9ece6a; + color: var(--owlry-badge-dmenu); border-color: rgba(158, 206, 106, 0.4); } /* Hints bar at bottom */ .owlry-hints { - padding-top: 8px; border-top: 1px solid rgba(65, 72, 104, 0.3); } .owlry-hints-label { - font-size: 10px; - color: #565f89; - letter-spacing: 0.5px; + color: var(--owlry-text-secondary); } diff --git a/src/app.rs b/src/app.rs index 55e9b21..862fae4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,9 +2,10 @@ use crate::cli::CliArgs; use crate::config::Config; use crate::filter::ProviderFilter; use crate::providers::ProviderManager; +use crate::theme; use crate::ui::MainWindow; use gtk4::prelude::*; -use gtk4::{gio, Application}; +use gtk4::{gio, Application, CssProvider}; use gtk4_layer_shell::{Edge, Layer, LayerShell}; use log::debug; use std::cell::RefCell; @@ -63,34 +64,77 @@ impl OwlryApp { // Position from top window.set_margin(Edge::Top, 200); - // Load CSS styling - Self::load_css(); + // Load CSS styling with config for theming + Self::load_css(&config.borrow()); window.present(); } - fn load_css() { - let provider = gtk4::CssProvider::new(); - - // Try to load from config directory first, then fall back to embedded - let config_css = dirs::config_dir().map(|p| p.join("owlry").join("style.css")); - - if let Some(css_path) = config_css { - if css_path.exists() { - provider.load_from_path(&css_path); - debug!("Loaded CSS from {:?}", css_path); - } else { - provider.load_from_string(include_str!("../resources/style.css")); - debug!("Loaded embedded CSS"); - } - } else { - provider.load_from_string(include_str!("../resources/style.css")); - } + fn load_css(config: &Config) { + let display = gtk4::gdk::Display::default().expect("Could not get default display"); + // 1. Load base structural CSS (always applied) + let base_provider = CssProvider::new(); + base_provider.load_from_string(include_str!("../resources/base.css")); gtk4::style_context_add_provider_for_display( - >k4::gdk::Display::default().expect("Could not get default display"), - &provider, + &display, + &base_provider, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, ); + debug!("Loaded base structural CSS"); + + // 2. Load theme if specified + if let Some(ref theme_name) = config.appearance.theme { + let theme_provider = CssProvider::new(); + match theme_name.as_str() { + "owl" => { + theme_provider.load_from_string(include_str!("../resources/owl-theme.css")); + debug!("Loaded built-in owl theme"); + } + _ => { + // Check for custom theme in ~/.config/owlry/themes/{name}.css + if let Some(theme_path) = dirs::config_dir() + .map(|p| p.join("owlry").join("themes").join(format!("{}.css", theme_name))) + { + if theme_path.exists() { + theme_provider.load_from_path(&theme_path); + debug!("Loaded custom theme from {:?}", theme_path); + } else { + debug!("Theme '{}' not found at {:?}", theme_name, theme_path); + } + } + } + } + gtk4::style_context_add_provider_for_display( + &display, + &theme_provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION + 1, + ); + } + + // 3. Load user's custom stylesheet if exists + if let Some(custom_path) = dirs::config_dir().map(|p| p.join("owlry").join("style.css")) { + if custom_path.exists() { + let custom_provider = CssProvider::new(); + custom_provider.load_from_path(&custom_path); + gtk4::style_context_add_provider_for_display( + &display, + &custom_provider, + gtk4::STYLE_PROVIDER_PRIORITY_USER, + ); + debug!("Loaded custom CSS from {:?}", custom_path); + } + } + + // 4. Inject config variables (highest priority for overrides) + let vars_css = theme::generate_variables_css(&config.appearance); + let vars_provider = CssProvider::new(); + vars_provider.load_from_string(&vars_css); + gtk4::style_context_add_provider_for_display( + &display, + &vars_provider, + gtk4::STYLE_PROVIDER_PRIORITY_USER + 1, + ); + debug!("Injected config CSS variables"); } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 8de891c..18bcdbd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,12 +17,36 @@ pub struct GeneralConfig { pub terminal_command: String, } +/// User-customizable theme colors +/// All fields are optional - unset values inherit from GTK theme +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ThemeColors { + pub background: Option, + pub background_secondary: Option, + pub border: Option, + pub text: Option, + pub text_secondary: Option, + pub accent: Option, + pub accent_bright: Option, + // Provider badge colors + pub badge_app: Option, + pub badge_cmd: Option, + pub badge_dmenu: Option, + pub badge_uuctl: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppearanceConfig { pub width: i32, pub height: i32, pub font_size: u32, pub border_radius: u32, + /// Theme name: None = GTK default, "owl" = built-in owl theme + #[serde(default)] + pub theme: Option, + /// Individual color overrides + #[serde(default)] + pub colors: ThemeColors, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -109,6 +133,8 @@ impl Default for Config { height: 400, font_size: 14, border_radius: 12, + theme: None, + colors: ThemeColors::default(), }, providers: ProvidersConfig { applications: true, diff --git a/src/main.rs b/src/main.rs index b273cbe..f1b1ce8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod config; mod filter; mod providers; +mod theme; mod ui; use app::OwlryApp; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..b99c040 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,50 @@ +use crate::config::AppearanceConfig; + +/// Generate CSS with :root variables from config settings +pub fn generate_variables_css(config: &AppearanceConfig) -> String { + let mut css = String::from(":root {\n"); + + // Always inject layout config values + css.push_str(&format!(" --owlry-font-size: {}px;\n", config.font_size)); + css.push_str(&format!(" --owlry-border-radius: {}px;\n", config.border_radius)); + + // Only inject colors if user specified them + if let Some(ref bg) = config.colors.background { + css.push_str(&format!(" --owlry-bg: {};\n", bg)); + } + if let Some(ref bg_secondary) = config.colors.background_secondary { + css.push_str(&format!(" --owlry-bg-secondary: {};\n", bg_secondary)); + } + if let Some(ref border) = config.colors.border { + css.push_str(&format!(" --owlry-border: {};\n", border)); + } + if let Some(ref text) = config.colors.text { + css.push_str(&format!(" --owlry-text: {};\n", text)); + } + if let Some(ref text_secondary) = config.colors.text_secondary { + css.push_str(&format!(" --owlry-text-secondary: {};\n", text_secondary)); + } + if let Some(ref accent) = config.colors.accent { + css.push_str(&format!(" --owlry-accent: {};\n", accent)); + } + if let Some(ref accent_bright) = config.colors.accent_bright { + css.push_str(&format!(" --owlry-accent-bright: {};\n", accent_bright)); + } + + // Provider badge colors + if let Some(ref badge_app) = config.colors.badge_app { + css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app)); + } + if let Some(ref badge_cmd) = config.colors.badge_cmd { + css.push_str(&format!(" --owlry-badge-cmd: {};\n", badge_cmd)); + } + if let Some(ref badge_dmenu) = config.colors.badge_dmenu { + css.push_str(&format!(" --owlry-badge-dmenu: {};\n", badge_dmenu)); + } + if let Some(ref badge_uuctl) = config.colors.badge_uuctl { + css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl)); + } + + css.push_str("}\n"); + css +}