// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using autoShell.Handlers.Generated; using autoShell.Services; using Microsoft.Win32; using static autoShell.Services.Interop.SpiConstants; namespace autoShell.Handlers; /// /// Handles theme-related commands: ApplyTheme, ListThemes, SetThemeMode, and SetWallpaper. /// Contains all Windows theme management logic including discovery, application, /// and light/dark mode toggling. /// internal class ThemeActionHandler : ActionHandlerBase { private readonly IRegistryService _registry; private readonly IProcessService _process; private readonly ISystemParametersService _systemParams; private string _previousTheme; private Dictionary _themeDictionary; private Dictionary _themeDisplayNameDictionary; public ThemeActionHandler(IRegistryService registry, IProcessService process, ISystemParametersService systemParams) { _registry = registry; _process = process; _systemParams = systemParams; LoadThemes(); AddAction("ApplyTheme", HandleApplyTheme); AddAction("ListThemes", HandleListThemes); AddAction("SetThemeMode", HandleSetThemeModeCommand); AddAction("SetWallpaper", HandleSetWallpaper); } private ActionResult HandleApplyTheme(ApplyThemeParams p) { string themeName = p.FilePath; bool success = ApplyTheme(themeName); return success ? ActionResult.Ok($"Applied theme '{themeName}'") : ActionResult.Fail($"Failed to apply theme '{themeName}'"); } private ActionResult HandleListThemes(JsonElement parameters) { var themes = GetInstalledThemes(); return ActionResult.Ok("Listed themes", JsonSerializer.SerializeToElement(themes)); } private ActionResult HandleSetThemeModeCommand(SetThemeModeParams p) { string mode = p.Mode; HandleSetThemeMode(mode); return ActionResult.Ok($"Theme mode set to '{mode}'"); } private ActionResult HandleSetWallpaper(SetWallpaperParams p) { string filePath = p.FilePath; _systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, filePath, SPIF_UPDATEINIFILE_SENDCHANGE); return ActionResult.Ok($"Wallpaper set to '{filePath}'"); } #region Theme Management /// /// Applies a Windows theme by name. /// public bool ApplyTheme(string themeName) { try { string previous = GetCurrentTheme(); bool success; if (themeName.Equals("previous", StringComparison.OrdinalIgnoreCase)) { success = RevertToPreviousTheme(); } else { string themePath = FindThemePath(themeName); if (string.IsNullOrEmpty(themePath)) { return false; } _process.StartShellExecute(themePath); success = true; } if (success) { _previousTheme = previous; } return success; } catch { return false; } } /// /// Gets the current Windows theme name. /// public string GetCurrentTheme() { try { const string ThemesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes"; string currentThemePath = _registry.GetValue(ThemesPath, "CurrentTheme") as string; if (!string.IsNullOrEmpty(currentThemePath)) { return Path.GetFileNameWithoutExtension(currentThemePath); } } catch { // Ignore errors reading registry } return null; } /// /// Returns a list of all installed Windows themes. /// public List GetInstalledThemes() { HashSet themes = []; themes.UnionWith(_themeDictionary.Keys); themes.UnionWith(_themeDisplayNameDictionary.Keys); return [.. themes]; } /// /// Gets the name of the previous theme. /// public string GetPreviousTheme() { return _previousTheme; } /// /// Reverts to the previous Windows theme. /// public bool RevertToPreviousTheme() { if (string.IsNullOrEmpty(_previousTheme)) { return false; } string themePath = FindThemePath(_previousTheme); if (string.IsNullOrEmpty(themePath)) { return false; } try { _process.StartShellExecute(themePath); return true; } catch { return false; } } #endregion #region Light/Dark Mode /// /// Sets the Windows light or dark mode by modifying registry keys. /// [System.Runtime.Versioning.SupportedOSPlatform("windows")] public bool SetLightDarkMode(bool useLightMode) { try { const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; int value = useLightMode ? 1 : 0; _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); // Broadcast settings change notification to update UI _registry.BroadcastSettingChange("ImmersiveColorSet"); return true; } catch { return false; } } /// /// Toggles between light and dark mode. /// [System.Runtime.Versioning.SupportedOSPlatform("windows")] public bool ToggleLightDarkMode() { bool? currentMode = GetCurrentLightMode(); return currentMode.HasValue && SetLightDarkMode(!currentMode.Value); } #endregion /// /// Handles SetThemeMode command. /// Value can be "light", "dark", "toggle", or a boolean. /// private void HandleSetThemeMode(string value) { if (string.IsNullOrEmpty(value)) { return; } if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) { ToggleLightDarkMode(); } else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) { SetLightDarkMode(true); } else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) { SetLightDarkMode(false); } else if (bool.TryParse(value, out bool useLightMode)) { SetLightDarkMode(useLightMode); } } /// /// Finds the full path to a theme file by name or display name. /// private string FindThemePath(string themeName) { // First check by file name if (_themeDictionary.TryGetValue(themeName, out string themePath)) { return themePath; } // Then check by display name if (_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) { if (_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) { return themePathFromDisplay; } } return null; } /// /// Gets the current light/dark mode setting from the registry. /// [System.Runtime.Versioning.SupportedOSPlatform("windows")] private bool? GetCurrentLightMode() { try { const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; // AppsUseLightTheme: 0 = dark, 1 = light object value = _registry.GetValue(PersonalizePath, "AppsUseLightTheme"); return value is int intValue ? intValue == 1 : null; } catch { return null; } } /// /// Parses the display name from a .theme file. /// private string GetThemeDisplayName(string themeFilePath) { try { foreach (string line in File.ReadLines(themeFilePath)) { if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) { string displayName = line["DisplayName=".Length..].Trim(); // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) if (displayName.StartsWith('@')) { displayName = ResolveLocalizedString(displayName); } return displayName; } } } catch { // Ignore errors reading theme file } return null; } private void LoadThemes() { _themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); _themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); string[] themePaths = [ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") ]; foreach (string themesFolder in themePaths) { if (Directory.Exists(themesFolder)) { foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) { string themeName = Path.GetFileNameWithoutExtension(themeFile); if (_themeDictionary.TryAdd(themeName, themeFile)) { // Parse display name from theme file string displayName = GetThemeDisplayName(themeFile); if (!string.IsNullOrEmpty(displayName)) { _themeDisplayNameDictionary.TryAdd(displayName, themeName); } } } } } _previousTheme = GetCurrentTheme(); } /// /// Resolves a localized string resource reference. /// private string ResolveLocalizedString(string localizedString) { try { // Remove the @ prefix string resourcePath = localizedString[1..]; // Expand environment variables int commaIndex = resourcePath.LastIndexOf(','); if (commaIndex > 0) { string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); string resourceIdStr = resourcePath[(commaIndex + 1)..]; if (int.TryParse(resourceIdStr, out int resourceId)) { string resolved = _systemParams.LoadStringResource(dllPath, resourceId); if (resolved != null) { return resolved; } } } } catch { // Ignore errors resolving localized string } return localizedString; } }