microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/collate-todos-into-todo-md

Branches

Tags

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

Clone

HTTPS

Download ZIP

dotnet/autoShell/Handlers/ThemeCommandHandler.cs

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