use serde::{Deserialize, Serialize}; use std::process::Command; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SystemTheme { pub theme: String, // "light", "dark", or "unknown" } pub struct ThemeDetector; impl ThemeDetector { pub fn new() -> Self { Self } /// Detect the system theme preference pub fn detect_system_theme(&self) -> SystemTheme { #[cfg(target_os = "linux")] return linux::detect_system_theme(); #[cfg(target_os = "macos")] return macos::detect_system_theme(); #[cfg(target_os = "windows")] return windows::detect_system_theme(); #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] return SystemTheme { theme: "unknown".to_string(), }; } } #[cfg(target_os = "linux")] mod linux { use super::*; pub fn detect_system_theme() -> SystemTheme { // Try multiple methods in order of preference // 1. Try GNOME/GTK settings via gsettings if let Ok(theme) = detect_gnome_theme() { return SystemTheme { theme }; } // 2. Try KDE Plasma settings via kreadconfig5/kreadconfig6 if let Ok(theme) = detect_kde_theme() { return SystemTheme { theme }; } // 3. Try XFCE settings via xfconf-query if let Ok(theme) = detect_xfce_theme() { return SystemTheme { theme }; } // 4. Try looking at current GTK theme name if let Ok(theme) = detect_gtk_theme() { return SystemTheme { theme }; } // 5. Try dconf directly (fallback for GNOME-based systems) if let Ok(theme) = detect_dconf_theme() { return SystemTheme { theme }; } // 6. Try environment variables if let Ok(theme) = detect_env_theme() { return SystemTheme { theme }; } // 7. Try freedesktop portal if let Ok(theme) = detect_portal_theme() { return SystemTheme { theme }; } // 8. Try looking at system color scheme files if let Ok(theme) = detect_system_files_theme() { return SystemTheme { theme }; } // Fallback to unknown SystemTheme { theme: "unknown".to_string(), } } fn detect_gnome_theme() -> Result> { // Check if gsettings is available if !is_command_available("gsettings") { return Err("gsettings not available".into()); } // Try GNOME color scheme first (modern way) if let Ok(output) = Command::new("gsettings") .args(["get", "org.gnome.desktop.interface", "color-scheme"]) .output() { if output.status.success() { let scheme = String::from_utf8_lossy(&output.stdout).trim().to_string(); match scheme.as_str() { "'prefer-dark'" => return Ok("dark".to_string()), "'prefer-light'" => return Ok("light".to_string()), _ => {} } } } // Fallback to GTK theme name detection if let Ok(output) = Command::new("gsettings") .args(["get", "org.gnome.desktop.interface", "gtk-theme"]) .output() { if output.status.success() { let theme_name = String::from_utf8_lossy(&output.stdout) .trim() .trim_matches('\'') .to_lowercase(); if theme_name.contains("dark") || theme_name.contains("night") { return Ok("dark".to_string()); } else if theme_name.contains("light") || theme_name.contains("adwaita") { return Ok("light".to_string()); } } } Err("Could not detect GNOME theme".into()) } fn detect_kde_theme() -> Result> { // Try KDE Plasma 6 first if is_command_available("kreadconfig6") { if let Ok(output) = Command::new("kreadconfig6") .args([ "--file", "kdeglobals", "--group", "KDE", "--key", "LookAndFeelPackage", ]) .output() { if output.status.success() { let theme = String::from_utf8_lossy(&output.stdout) .trim() .to_lowercase(); if theme.contains("dark") || theme.contains("breezedark") { return Ok("dark".to_string()); } else if theme.contains("light") || theme.contains("breeze") { return Ok("light".to_string()); } } } // Try color scheme as well if let Ok(output) = Command::new("kreadconfig6") .args([ "--file", "kdeglobals", "--group", "General", "--key", "ColorScheme", ]) .output() { if output.status.success() { let scheme = String::from_utf8_lossy(&output.stdout) .trim() .to_lowercase(); if scheme.contains("dark") || scheme.contains("breezedark") { return Ok("dark".to_string()); } else if scheme.contains("light") || scheme.contains("breeze") { return Ok("light".to_string()); } } } } // Try KDE Plasma 5 as fallback if is_command_available("kreadconfig5") { if let Ok(output) = Command::new("kreadconfig5") .args([ "--file", "kdeglobals", "--group", "KDE", "--key", "LookAndFeelPackage", ]) .output() { if output.status.success() { let theme = String::from_utf8_lossy(&output.stdout) .trim() .to_lowercase(); if theme.contains("dark") || theme.contains("breezedark") { return Ok("dark".to_string()); } else if theme.contains("light") || theme.contains("breeze") { return Ok("light".to_string()); } } } } Err("Could not detect KDE theme".into()) } fn detect_xfce_theme() -> Result> { if !is_command_available("xfconf-query") { return Err("xfconf-query not available".into()); } // Check XFCE theme if let Ok(output) = Command::new("xfconf-query") .args(["-c", "xsettings", "-p", "/Net/ThemeName"]) .output() { if output.status.success() { let theme = String::from_utf8_lossy(&output.stdout) .trim() .to_lowercase(); if theme.contains("dark") || theme.contains("night") { return Ok("dark".to_string()); } else if theme.contains("light") { return Ok("light".to_string()); } } } // Check XFCE window manager theme as backup if let Ok(output) = Command::new("xfconf-query") .args(["-c", "xfwm4", "-p", "/general/theme"]) .output() { if output.status.success() { let theme = String::from_utf8_lossy(&output.stdout) .trim() .to_lowercase(); if theme.contains("dark") || theme.contains("night") { return Ok("dark".to_string()); } else if theme.contains("light") { return Ok("light".to_string()); } } } Err("Could not detect XFCE theme".into()) } fn detect_gtk_theme() -> Result> { // Try to read GTK3 settings file if let Ok(home) = std::env::var("HOME") { let gtk3_settings = std::path::Path::new(&home).join(".config/gtk-3.0/settings.ini"); if gtk3_settings.exists() { if let Ok(content) = std::fs::read_to_string(gtk3_settings) { for line in content.lines() { if line.starts_with("gtk-theme-name=") { let theme_name = line.split('=').nth(1).unwrap_or("").trim().to_lowercase(); if theme_name.contains("dark") || theme_name.contains("night") { return Ok("dark".to_string()); } else if theme_name.contains("light") || theme_name.contains("adwaita") { return Ok("light".to_string()); } } } } } // Try GTK4 settings let gtk4_settings = std::path::Path::new(&home).join(".config/gtk-4.0/settings.ini"); if gtk4_settings.exists() { if let Ok(content) = std::fs::read_to_string(gtk4_settings) { for line in content.lines() { if line.starts_with("gtk-theme-name=") { let theme_name = line.split('=').nth(1).unwrap_or("").trim().to_lowercase(); if theme_name.contains("dark") || theme_name.contains("night") { return Ok("dark".to_string()); } else if theme_name.contains("light") || theme_name.contains("adwaita") { return Ok("light".to_string()); } } } } } } Err("Could not detect GTK theme".into()) } fn detect_dconf_theme() -> Result> { if !is_command_available("dconf") { return Err("dconf not available".into()); } // Try reading color scheme directly from dconf if let Ok(output) = Command::new("dconf") .args(["read", "/org/gnome/desktop/interface/color-scheme"]) .output() { if output.status.success() { let scheme = String::from_utf8_lossy(&output.stdout).trim().to_string(); match scheme.as_str() { "'prefer-dark'" => return Ok("dark".to_string()), "'prefer-light'" => return Ok("light".to_string()), _ => {} } } } // Try reading GTK theme from dconf if let Ok(output) = Command::new("dconf") .args(["read", "/org/gnome/desktop/interface/gtk-theme"]) .output() { if output.status.success() { let theme_name = String::from_utf8_lossy(&output.stdout) .trim() .trim_matches('\'') .to_lowercase(); if theme_name.contains("dark") || theme_name.contains("night") { return Ok("dark".to_string()); } else if theme_name.contains("light") || theme_name.contains("adwaita") { return Ok("light".to_string()); } } } Err("Could not detect dconf theme".into()) } fn detect_env_theme() -> Result> { // Check common environment variables if let Ok(theme) = std::env::var("GTK_THEME") { let theme_lower = theme.to_lowercase(); if theme_lower.contains("dark") || theme_lower.contains("night") { return Ok("dark".to_string()); } else if theme_lower.contains("light") { return Ok("light".to_string()); } } if let Ok(theme) = std::env::var("QT_STYLE_OVERRIDE") { let theme_lower = theme.to_lowercase(); if theme_lower.contains("dark") || theme_lower.contains("night") { return Ok("dark".to_string()); } else if theme_lower.contains("light") { return Ok("light".to_string()); } } Err("Could not detect theme from environment".into()) } fn detect_portal_theme() -> Result> { if !is_command_available("busctl") { return Err("busctl not available".into()); } // Try to query the color scheme via org.freedesktop.portal.Settings if let Ok(output) = Command::new("busctl") .args([ "--user", "call", "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", "Read", "ss", "org.freedesktop.appearance", "color-scheme", ]) .output() { if output.status.success() { let response = String::from_utf8_lossy(&output.stdout); // Parse DBus response - look for preference values if response.contains(" 1 ") { return Ok("dark".to_string()); } else if response.contains(" 2 ") { return Ok("light".to_string()); } } } Err("Could not detect portal theme".into()) } fn detect_system_files_theme() -> Result> { // Check if we're in a dark terminal (heuristic) if let Ok(term) = std::env::var("TERM") { let term_lower = term.to_lowercase(); if term_lower.contains("dark") || term_lower.contains("night") { return Ok("dark".to_string()); } } // Check if we can determine from desktop session if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") { let desktop_lower = desktop.to_lowercase(); // Some desktops default to dark if desktop_lower.contains("i3") || desktop_lower.contains("sway") { // Window managers often use dark themes by default return Ok("dark".to_string()); } } Err("Could not detect theme from system files".into()) } fn is_command_available(command: &str) -> bool { Command::new("which") .arg(command) .output() .map(|output| output.status.success()) .unwrap_or(false) } } #[cfg(target_os = "macos")] mod macos { use super::*; pub fn detect_system_theme() -> SystemTheme { // macOS theme detection using osascript if let Ok(output) = Command::new("osascript") .args([ "-e", "tell application \"System Events\" to tell appearance preferences to get dark mode", ]) .output() { if output.status.success() { let result = String::from_utf8_lossy(&output.stdout).to_string(); let result = result.trim(); match result { "true" => { return SystemTheme { theme: "dark".to_string(), } } "false" => { return SystemTheme { theme: "light".to_string(), } } _ => {} } } } // Fallback method using defaults if let Ok(output) = Command::new("defaults") .args(["read", "-g", "AppleInterfaceStyle"]) .output() { if output.status.success() { let style = String::from_utf8_lossy(&output.stdout).to_string(); let style = style.trim(); if style.to_lowercase() == "dark" { return SystemTheme { theme: "dark".to_string(), }; } } } // Default to light if we can't determine SystemTheme { theme: "light".to_string(), } } } #[cfg(target_os = "windows")] mod windows { use super::*; pub fn detect_system_theme() -> SystemTheme { // Windows theme detection via registry // This is a simplified implementation - you might want to use winreg crate for better registry access if let Ok(output) = Command::new("reg") .args([ "query", "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "/v", "AppsUseLightTheme", ]) .output() { if output.status.success() { let result = String::from_utf8_lossy(&output.stdout); if result.contains("0x0") { return SystemTheme { theme: "dark".to_string(), }; } else if result.contains("0x1") { return SystemTheme { theme: "light".to_string(), }; } } } // Default to light if we can't determine SystemTheme { theme: "light".to_string(), } } } // Command to expose this functionality to the frontend #[tauri::command] pub fn get_system_theme() -> SystemTheme { let detector = ThemeDetector::new(); detector.detect_system_theme() } #[cfg(test)] mod tests { use super::*; #[test] fn test_theme_detector_creation() { let detector = ThemeDetector::new(); let theme = detector.detect_system_theme(); // Should return a valid theme string assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown")); } #[test] fn test_get_system_theme_command() { let theme = get_system_theme(); assert!(matches!(theme.theme.as_str(), "light" | "dark" | "unknown")); } }