microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
55ef4624bfdb8b9c4f1b33c48e656f822c067e9b

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

dotnet/autoShell/Handlers/ThemeActionHandler.cs

420lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System;
5using System.Collections.Generic;
6using System.IO;
7using System.Runtime.InteropServices;
8using System.Text.Json;
9using autoShell.Handlers.Generated;
10using autoShell.Services;
11using autoShell.Services.Interop;
12using Microsoft.Win32;
13
14namespace autoShell.Handlers;
15
16/// <summary>
17/// Handles theme-related commands: ApplyTheme, ListThemes, SetThemeMode, and SetWallpaper.
18/// Contains all Windows theme management logic including discovery, application,
19/// and light/dark mode toggling.
20/// </summary>
21internal partial class ThemeActionHandler : ActionHandlerBase
22{
23 #region P/Invoke
24
25 private const int SPI_SETDESKWALLPAPER = 0x0014;
26 private const int SPIF_UPDATEINIFILE_SENDCHANGE = 3;
27 private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002;
28
29 [LibraryImport(NativeDlls.Kernel32, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
30 private static partial IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
31
32 [LibraryImport(NativeDlls.Kernel32, SetLastError = true)]
33 [return: MarshalAs(UnmanagedType.Bool)]
34 private static partial bool FreeLibrary(IntPtr hModule);
35
36 [LibraryImport(NativeDlls.User32, StringMarshalling = StringMarshalling.Utf16)]
37 private static partial int LoadString(IntPtr hInstance, uint uID, [Out] char[] lpBuffer, int nBufferMax);
38
39 #endregion P/Invoke
40
41 private readonly IRegistryService _registry;
42 private readonly IProcessService _process;
43 private readonly ISystemParametersService _systemParams;
44
45 private string _previousTheme;
46 private Dictionary<string, string> _themeDictionary;
47 private Dictionary<string, string> _themeDisplayNameDictionary;
48
49 public ThemeActionHandler(IRegistryService registry, IProcessService process, ISystemParametersService systemParams)
50 {
51 _registry = registry;
52 _process = process;
53 _systemParams = systemParams;
54
55 LoadThemes();
56
57 AddAction<ApplyThemeParams>("ApplyTheme", HandleApplyTheme);
58 AddAction("ListThemes", HandleListThemes);
59 AddAction<SetThemeModeParams>("SetThemeMode", HandleSetThemeModeCommand);
60 AddAction<SetWallpaperParams>("SetWallpaper", HandleSetWallpaper);
61 }
62
63 private ActionResult HandleApplyTheme(ApplyThemeParams p)
64 {
65 string themeName = p.FilePath;
66 bool success = ApplyTheme(themeName);
67 return success
68 ? ActionResult.Ok($"Applied theme '{themeName}'")
69 : ActionResult.Fail($"Failed to apply theme '{themeName}'");
70 }
71
72 private ActionResult HandleListThemes(JsonElement parameters)
73 {
74 var themes = GetInstalledThemes();
75 return ActionResult.Ok("Listed themes", JsonSerializer.SerializeToElement(themes));
76 }
77
78 private ActionResult HandleSetThemeModeCommand(SetThemeModeParams p)
79 {
80 string mode = p.Mode;
81 HandleSetThemeMode(mode);
82 return ActionResult.Ok($"Theme mode set to '{mode}'");
83 }
84
85 private ActionResult HandleSetWallpaper(SetWallpaperParams p)
86 {
87 string filePath = p.FilePath;
88 _systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, filePath, SPIF_UPDATEINIFILE_SENDCHANGE);
89 return ActionResult.Ok($"Wallpaper set to '{filePath}'");
90 }
91
92 #region Theme Management
93
94 /// <summary>
95 /// Applies a Windows theme by name.
96 /// </summary>
97 public bool ApplyTheme(string themeName)
98 {
99 try
100 {
101 string previous = GetCurrentTheme();
102 bool success;
103
104 if (themeName.Equals("previous", StringComparison.OrdinalIgnoreCase))
105 {
106 success = RevertToPreviousTheme();
107 }
108 else
109 {
110 string themePath = FindThemePath(themeName);
111 if (string.IsNullOrEmpty(themePath))
112 {
113 return false;
114 }
115
116 _process.StartShellExecute(themePath);
117 success = true;
118 }
119
120 if (success)
121 {
122 _previousTheme = previous;
123 }
124
125 return success;
126 }
127 catch
128 {
129 return false;
130 }
131 }
132
133 /// <summary>
134 /// Gets the current Windows theme name.
135 /// </summary>
136 public string GetCurrentTheme()
137 {
138 try
139 {
140 const string ThemesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes";
141 string currentThemePath = _registry.GetValue(ThemesPath, "CurrentTheme") as string;
142 if (!string.IsNullOrEmpty(currentThemePath))
143 {
144 return Path.GetFileNameWithoutExtension(currentThemePath);
145 }
146 }
147 catch
148 {
149 // Ignore errors reading registry
150 }
151 return null;
152 }
153
154 /// <summary>
155 /// Returns a list of all installed Windows themes.
156 /// </summary>
157 public List<string> GetInstalledThemes()
158 {
159 HashSet<string> themes = [];
160
161 themes.UnionWith(_themeDictionary.Keys);
162 themes.UnionWith(_themeDisplayNameDictionary.Keys);
163
164 return [.. themes];
165 }
166
167 /// <summary>
168 /// Gets the name of the previous theme.
169 /// </summary>
170 public string GetPreviousTheme()
171 {
172 return _previousTheme;
173 }
174
175 /// <summary>
176 /// Reverts to the previous Windows theme.
177 /// </summary>
178 public bool RevertToPreviousTheme()
179 {
180 if (string.IsNullOrEmpty(_previousTheme))
181 {
182 return false;
183 }
184
185 string themePath = FindThemePath(_previousTheme);
186 if (string.IsNullOrEmpty(themePath))
187 {
188 return false;
189 }
190
191 try
192 {
193 _process.StartShellExecute(themePath);
194 return true;
195 }
196 catch
197 {
198 return false;
199 }
200 }
201
202 #endregion
203
204 #region Light/Dark Mode
205
206 /// <summary>
207 /// Sets the Windows light or dark mode by modifying registry keys.
208 /// </summary>
209 [System.Runtime.Versioning.SupportedOSPlatform("windows")]
210 public bool SetLightDarkMode(bool useLightMode)
211 {
212 try
213 {
214 const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
215 int value = useLightMode ? 1 : 0;
216
217 _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord);
218 _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord);
219
220 // Broadcast settings change notification to update UI
221 _registry.BroadcastSettingChange("ImmersiveColorSet");
222
223 return true;
224 }
225 catch
226 {
227 return false;
228 }
229 }
230
231 /// <summary>
232 /// Toggles between light and dark mode.
233 /// </summary>
234 [System.Runtime.Versioning.SupportedOSPlatform("windows")]
235 public bool ToggleLightDarkMode()
236 {
237 bool? currentMode = GetCurrentLightMode();
238 return currentMode.HasValue && SetLightDarkMode(!currentMode.Value);
239 }
240
241 #endregion
242
243 /// <summary>
244 /// Handles SetThemeMode command.
245 /// Value can be "light", "dark", "toggle", or a boolean.
246 /// </summary>
247 private void HandleSetThemeMode(string value)
248 {
249 if (string.IsNullOrEmpty(value))
250 {
251 return;
252 }
253
254 if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase))
255 {
256 ToggleLightDarkMode();
257 }
258 else if (value.Equals("light", StringComparison.OrdinalIgnoreCase))
259 {
260 SetLightDarkMode(true);
261 }
262 else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase))
263 {
264 SetLightDarkMode(false);
265 }
266 else if (bool.TryParse(value, out bool useLightMode))
267 {
268 SetLightDarkMode(useLightMode);
269 }
270 }
271
272 /// <summary>
273 /// Finds the full path to a theme file by name or display name.
274 /// </summary>
275 private string FindThemePath(string themeName)
276 {
277 // First check by file name
278 if (_themeDictionary.TryGetValue(themeName, out string themePath))
279 {
280 return themePath;
281 }
282
283 // Then check by display name
284 if (_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay))
285 {
286 if (_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay))
287 {
288 return themePathFromDisplay;
289 }
290 }
291
292 return null;
293 }
294
295 /// <summary>
296 /// Gets the current light/dark mode setting from the registry.
297 /// </summary>
298 [System.Runtime.Versioning.SupportedOSPlatform("windows")]
299 private bool? GetCurrentLightMode()
300 {
301 try
302 {
303 const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
304 // AppsUseLightTheme: 0 = dark, 1 = light
305 object value = _registry.GetValue(PersonalizePath, "AppsUseLightTheme");
306 return value is int intValue ? intValue == 1 : null;
307 }
308 catch
309 {
310 return null;
311 }
312 }
313
314 /// <summary>
315 /// Parses the display name from a .theme file.
316 /// </summary>
317 private static string GetThemeDisplayName(string themeFilePath)
318 {
319 try
320 {
321 foreach (string line in File.ReadLines(themeFilePath))
322 {
323 if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase))
324 {
325 string displayName = line["DisplayName=".Length..].Trim();
326 // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013)
327 if (displayName.StartsWith('@'))
328 {
329 displayName = ResolveLocalizedString(displayName);
330 }
331 return displayName;
332 }
333 }
334 }
335 catch
336 {
337 // Ignore errors reading theme file
338 }
339 return null;
340 }
341
342 private void LoadThemes()
343 {
344 _themeDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
345 _themeDisplayNameDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
346
347 string[] themePaths =
348 [
349 Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"),
350 Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"),
351 Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes")
352 ];
353
354 foreach (string themesFolder in themePaths)
355 {
356 if (Directory.Exists(themesFolder))
357 {
358 foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme"))
359 {
360 string themeName = Path.GetFileNameWithoutExtension(themeFile);
361 if (_themeDictionary.TryAdd(themeName, themeFile))
362 {
363 // Parse display name from theme file
364 string displayName = GetThemeDisplayName(themeFile);
365 if (!string.IsNullOrEmpty(displayName))
366 {
367 _themeDisplayNameDictionary.TryAdd(displayName, themeName);
368 }
369 }
370 }
371 }
372 }
373
374 _previousTheme = GetCurrentTheme();
375 }
376
377 /// <summary>
378 /// Resolves a localized string resource reference.
379 /// </summary>
380 private static string ResolveLocalizedString(string localizedString)
381 {
382 try
383 {
384 // Remove the @ prefix
385 string resourcePath = localizedString[1..];
386 // Expand environment variables
387 int commaIndex = resourcePath.LastIndexOf(',');
388 if (commaIndex > 0)
389 {
390 string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]);
391 string resourceIdStr = resourcePath[(commaIndex + 1)..];
392 if (int.TryParse(resourceIdStr, out int resourceId))
393 {
394 char[] buffer = new char[256];
395 IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE);
396 if (hModule != IntPtr.Zero)
397 {
398 try
399 {
400 int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Length);
401 if (result > 0)
402 {
403 return new string(buffer, 0, result);
404 }
405 }
406 finally
407 {
408 FreeLibrary(hModule);
409 }
410 }
411 }
412 }
413 }
414 catch
415 {
416 // Ignore errors resolving localized string
417 }
418 return localizedString;
419 }
420}
421