// 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;
}
}