microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-shell-and-cli-windows-job

Branches

Tags

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

Clone

HTTPS

Download ZIP

dotnet/autoShell/Handlers/ThemeActionHandler.cs

389lines · modecode

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