microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
desktop-agent-improvements

Branches

Tags

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

Clone

HTTPS

Download ZIP

dotnet/autoShell/AutoShell.cs

1747lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System;
5using System.Collections;
6using System.Collections.Generic;
7using System.Diagnostics;
8using System.IO;
9using System.Linq;
10using System.Reflection;
11using System.Runtime.InteropServices;
12using System.Text;
13using System.Threading.Tasks;
14using System.Windows.Controls;
15using Microsoft.VisualBasic;
16using Microsoft.WindowsAPICodePack.Shell;
17using Newtonsoft.Json;
18using Newtonsoft.Json.Linq;
19using static autoShell.AutoShell;
20
21
22namespace autoShell;
23
24internal partial class AutoShell
25{
26 // create a map of friendly names to executable paths
27 static Hashtable s_friendlyNameToPath = [];
28 static Hashtable s_friendlyNameToId = [];
29 static double s_savedVolumePct = 0.0;
30
31 static IServiceProvider10 s_shell;
32 static IVirtualDesktopManager s_virtualDesktopManager;
33 static IVirtualDesktopManagerInternal s_virtualDesktopManagerInternal;
34 static IVirtualDesktopManagerInternal_BUGBUG s_virtualDesktopManagerInternal_BUGBUG;
35 static IApplicationViewCollection s_applicationViewCollection;
36 static IVirtualDesktopPinnedApps s_virtualDesktopPinnedApps;
37
38
39 /// <summary>
40 /// Constructor used to get system wide information required for specific commands.
41 /// </summary>
42 static AutoShell()
43 {
44 // get current user name
45 string userName = Environment.UserName;
46 SortedList<string, string> sortedList = new SortedList<string, string>
47 {
48 { "chrome", "chrome.exe" },
49 { "power point", "C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE" },
50 { "powerpoint", "C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE" },
51 { "word", "C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE" },
52 { "winword", "C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE" },
53 { "excel", "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE" },
54 { "outlook", "C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE" },
55 { "visual studio", "devenv.exe" },
56 { "visual studio code", "C:\\Users\\" + userName + "\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe" },
57 { "edge", "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" },
58 { "microsoft edge", "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" },
59 { "notepad", "C:\\Windows\\System32\\notepad.exe" },
60 { "paint", "mspaint.exe" },
61 { "calculator", "calc.exe" },
62 { "file explorer", "C:\\Windows\\explorer.exe" },
63 { "control panel", "C:\\Windows\\System32\\control.exe" },
64 { "task manager", "C:\\Windows\\System32\\Taskmgr.exe" },
65 { "cmd", "C:\\Windows\\System32\\cmd.exe" },
66 { "powershell", "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
67 { "snipping tool", "C:\\Windows\\System32\\SnippingTool.exe" },
68 { "magnifier", "C:\\Windows\\System32\\Magnify.exe" },
69 { "paint 3d", "C:\\Program Files\\WindowsApps\\Microsoft.MSPaint_10.1807.18022.0_x64__8wekyb3d8bbwe\\"},
70 { "m365 copilot", "C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe" },
71 { "copilot", "C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe" },
72 { "spotify", "C:\\Program Files\\WindowsApps\\SpotifyAB.SpotifyMusic_1.279.427.0_x64__zpdnekdrzrea0\\spotify.exe" },
73 };
74
75 // add the entries to the hashtable
76 foreach (var kvp in sortedList)
77 {
78 s_friendlyNameToPath.Add(kvp.Key, kvp.Value);
79 }
80
81 var installedApps = GetAllInstalledAppsIds();
82 foreach (var kvp in installedApps)
83 {
84 s_friendlyNameToId.Add(kvp.Key, kvp.Value);
85 }
86
87 // Load the installed themes
88 LoadThemes();
89
90 // Desktop management
91 s_shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ImmersiveShell));
92 s_virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)s_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID);
93 s_virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)s_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID);
94 s_virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_VirtualDesktopManager));
95 s_applicationViewCollection = (IApplicationViewCollection)s_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID);
96 s_virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)s_shell.QueryService(CLSID_VirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID);
97 }
98
99 /// <summary>
100 /// Program entry point
101 /// </summary>
102 /// <param name="args">Any command line arguments</param>
103 static void Main(string[] args)
104 {
105 string rawCmdLine = Marshal.PtrToStringUni(GetCommandLineW());
106
107 // if there are command line args let's execute those one at a time and then exit
108 // user can specify a single JSON object command or an array of them on the command line
109 if (args.Length > 0)
110 {
111 string exe = $"\"{Environment.ProcessPath}\"";
112 string cmdLine = rawCmdLine.Replace(exe, "");
113
114 if (cmdLine.StartsWith(exe, StringComparison.OrdinalIgnoreCase))
115 {
116 cmdLine = cmdLine[exe.Length..];
117 }
118 else if (cmdLine.StartsWith(Path.GetFileName(Environment.ProcessPath), StringComparison.OrdinalIgnoreCase))
119 {
120 cmdLine = cmdLine[Path.GetFileName(Environment.ProcessPath).Length..];
121 }
122 else if (cmdLine.StartsWith(Path.GetFileNameWithoutExtension(Environment.ProcessPath), StringComparison.OrdinalIgnoreCase))
123 {
124 cmdLine = cmdLine[Path.GetFileNameWithoutExtension(Environment.ProcessPath).Length..];
125 }
126
127 try
128 {
129 JArray commands = JArray.Parse(cmdLine);
130 foreach (JObject jo in commands.Children<JObject>())
131 {
132 execLine(jo);
133 }
134 }
135 catch (JsonReaderException)
136 {
137 execLine(JObject.Parse(cmdLine));
138 }
139
140 // exit
141 return;
142 }
143
144 // run in interactive mode, keep accepting commands until we get the shutdown command
145 bool quit = false;
146 while (!quit)
147 {
148 try
149 {
150 // read a line from the console
151 string line = Console.ReadLine();
152
153 // if stdin is closed (e.g., piped input finished), exit
154 if (line == null)
155 {
156 break;
157 }
158
159 // parse the line as a json object with one or more command keys (with values as parameters)
160 JObject root = JObject.Parse(line);
161
162 // execute the line
163 quit = execLine(root);
164 }
165 catch (Exception ex)
166 {
167 LogError(ex);
168 }
169 }
170 }
171
172 internal static void LogError(Exception ex)
173 {
174 Debug.WriteLine(ex);
175 ConsoleColor previousColor = Console.ForegroundColor;
176 Console.ForegroundColor = ConsoleColor.Red;
177 Console.WriteLine("Error: " + ex.Message);
178 Console.ForegroundColor = previousColor;
179 }
180
181 internal static void LogWarning(string message)
182 {
183 Debug.WriteLine(message);
184 ConsoleColor previousColor = Console.ForegroundColor;
185 Console.ForegroundColor = ConsoleColor.Yellow;
186 Console.WriteLine("Warning: " + message);
187 Console.ForegroundColor = previousColor;
188 }
189
190 static SortedList<string, string> GetAllInstalledAppsIds()
191 {
192 // GUID taken from https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
193 var FOLDERID_AppsFolder = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}");
194 ShellObject appsFolder = (ShellObject)KnownFolderHelper.FromKnownFolderId(FOLDERID_AppsFolder);
195 var appIds = new SortedList<string, string>();
196
197 foreach (var app in (IKnownFolder)appsFolder)
198 {
199 string appName = app.Name.ToLowerInvariant();
200 if (appIds.ContainsKey(appName))
201 {
202 Debug.WriteLine("Key has multiple values: " + appName);
203 }
204 else
205 {
206 // The ParsingName property is the AppUserModelID
207 appIds.Add(appName, app.ParsingName);
208 }
209 }
210
211 return appIds;
212 }
213
214 static void SetMasterVolume(int pct)
215 {
216 // Using Windows Core Audio API via COM interop
217 try
218 {
219 var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
220 deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device);
221 var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID;
222 device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj);
223 var audioEndpointVolume = (IAudioEndpointVolume)obj;
224 audioEndpointVolume.GetMasterVolumeLevelScalar(out float currentVolume);
225 s_savedVolumePct = currentVolume * 100.0;
226 audioEndpointVolume.SetMasterVolumeLevelScalar(pct / 100.0f, Guid.Empty);
227 }
228 catch (Exception ex)
229 {
230 Debug.WriteLine("Failed to set volume: " + ex.Message);
231 }
232 }
233
234 static void RestoreMasterVolume()
235 {
236 // Using Windows Core Audio API via COM interop
237 try
238 {
239 var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
240 deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device);
241 var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID;
242 device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj);
243 var audioEndpointVolume = (IAudioEndpointVolume)obj;
244 audioEndpointVolume.SetMasterVolumeLevelScalar((float)(s_savedVolumePct / 100.0), Guid.Empty);
245 }
246 catch (Exception ex)
247 {
248 Debug.WriteLine("Failed to restore volume: " + ex.Message);
249 }
250 }
251
252 static void SetMasterMute(bool mute)
253 {
254 // Using Windows Core Audio API via COM interop
255 try
256 {
257 var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
258 deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device);
259 var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID;
260 device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj);
261 var audioEndpointVolume = (IAudioEndpointVolume)obj;
262 audioEndpointVolume.GetMute(out bool currentMute);
263 Debug.WriteLine("Current Mute:" + currentMute);
264 audioEndpointVolume.SetMute(mute, Guid.Empty);
265 }
266 catch (Exception ex)
267 {
268 Debug.WriteLine("Failed to set mute: " + ex.Message);
269 }
270 }
271
272 static string ResolveProcessNameFromFriendlyName(string friendlyName)
273 {
274 string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()];
275 if (path != null)
276 {
277 return Path.GetFileNameWithoutExtension(path);
278 }
279 else
280 {
281 return friendlyName;
282 }
283 }
284
285 static IntPtr FindProcessWindowHandle(string processName)
286 {
287 processName = ResolveProcessNameFromFriendlyName(processName);
288 Process[] processes = Process.GetProcessesByName(processName);
289 // loop through the processes that match the name; raise the first one that has a main window
290 foreach (Process p in processes)
291 {
292 if (p.MainWindowHandle != IntPtr.Zero)
293 {
294 return p.MainWindowHandle;
295 }
296 }
297
298 // Try to find by window title if we haven't found it and bring it forward
299 return FindWindowByTitle(processName).hWnd;
300 }
301
302 // given part of a process name, raise the window of that process to the top level
303 static void RaiseWindow(string processName)
304 {
305 processName = ResolveProcessNameFromFriendlyName(processName);
306 Process[] processes = Process.GetProcessesByName(processName);
307 // loop through the processes that match the name; raise the first one that has a main window
308 foreach (Process p in processes)
309 {
310 if (p.MainWindowHandle != IntPtr.Zero)
311 {
312 SetForegroundWindow(p.MainWindowHandle);
313 Interaction.AppActivate(p.Id);
314 return;
315 }
316 }
317
318 // this means all the applications processes are running in the background. This happens for edge and chrome browsers.
319 string path = (string)s_friendlyNameToPath[processName];
320 if (path != null)
321 {
322 Process.Start(path);
323 }
324 else
325 {
326 // Try to find by window title if we haven't found it and bring it forward
327 (nint hWnd1, int pid) = FindWindowByTitle(processName);
328
329 if (hWnd1 != nint.Zero)
330 {
331 SetForegroundWindow(hWnd1);
332 Interaction.AppActivate(pid);
333 }
334 }
335 }
336
337 static void MaximizeWindow(string processName)
338 {
339 processName = ResolveProcessNameFromFriendlyName(processName);
340 Process[] processes = Process.GetProcessesByName(processName);
341 // loop through the processes that match the name; raise the first one that has a main window
342 foreach (Process p in processes)
343 {
344 if (p.MainWindowHandle != IntPtr.Zero)
345 {
346 uint WM_SYSCOMMAND = 0x112;
347 uint SC_MAXIMIZE = 0xf030;
348 SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero);
349 SetForegroundWindow(p.MainWindowHandle);
350 Interaction.AppActivate(p.Id);
351 return;
352 }
353 }
354
355 // if we haven't found what we are looking for let's enumerate the top level windows and try that way
356 (nint hWnd, int pid) = FindWindowByTitle(processName);
357 if (hWnd != nint.Zero)
358 {
359 uint WM_SYSCOMMAND = 0x112;
360 uint SC_MAXIMIZE = 0xf030;
361 SendMessage(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero);
362 SetForegroundWindow(hWnd);
363 Interaction.AppActivate(pid);
364 }
365 }
366
367 static void MinimizeWindow(string processName)
368 {
369 processName = ResolveProcessNameFromFriendlyName(processName);
370 Process[] processes = Process.GetProcessesByName(processName);
371 // loop through the processes that match the name; raise the first one that has a main window
372 foreach (Process p in processes)
373 {
374 if (p.MainWindowHandle != IntPtr.Zero)
375 {
376 uint WM_SYSCOMMAND = 0x112;
377 uint SC_MINIMIZE = 0xF020;
378 SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero);
379 break;
380 }
381 }
382
383 // if we haven't found what we are looking for let's enumerate the top level windows and try that way
384 (nint hWnd, int pid) = FindWindowByTitle(processName);
385 if (hWnd != nint.Zero)
386 {
387 uint WM_SYSCOMMAND = 0x112;
388 uint SC_MINIMIZE = 0xF020;
389 SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero);
390 SetForegroundWindow(hWnd);
391 Interaction.AppActivate(pid);
392 }
393 }
394
395 static void TileWindowPair(string processName1, string processName2)
396 {
397 // find both processes
398 // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe
399 processName1 = ResolveProcessNameFromFriendlyName(processName1);
400 Process[] processes1 = Process.GetProcessesByName(processName1);
401 IntPtr hWnd1 = IntPtr.Zero;
402 IntPtr hWnd2 = IntPtr.Zero;
403 int pid1 = -1;
404 int pid2 = -1;
405
406 foreach (Process p in processes1)
407 {
408 if (p.MainWindowHandle != IntPtr.Zero)
409 {
410 hWnd1 = p.MainWindowHandle;
411 pid1 = p.Id;
412 break;
413 }
414 }
415
416 // If no process found by name, search by window title
417 if (hWnd1 == IntPtr.Zero)
418 {
419 (hWnd1, pid1) = FindWindowByTitle(processName1);
420 }
421
422 processName2 = ResolveProcessNameFromFriendlyName(processName2);
423 Process[] processes2 = Process.GetProcessesByName(processName2);
424 foreach (Process p in processes2)
425 {
426 if (p.MainWindowHandle != IntPtr.Zero)
427 {
428 hWnd2 = p.MainWindowHandle;
429 pid2 = p.Id;
430 break;
431 }
432 }
433
434 // If no process found by name, search by window title
435 if (hWnd2 == IntPtr.Zero)
436 {
437 (hWnd2, pid2) = FindWindowByTitle(processName2);
438 }
439
440 if (hWnd1 != IntPtr.Zero && hWnd2 != IntPtr.Zero)
441 {
442 // TODO: handle multiple monitors
443 // get the screen size
444 IntPtr desktopHandle = GetDesktopWindow();
445 RECT desktopRect = new RECT();
446 GetWindowRect(desktopHandle, ref desktopRect);
447 // get the dimensions of the taskbar
448 // find the taskbar window
449 IntPtr taskbarHandle = IntPtr.Zero;
450 IntPtr hWnd = IntPtr.Zero;
451 while ((hWnd = FindWindowEx(IntPtr.Zero, hWnd, "Shell_TrayWnd", null)) != IntPtr.Zero)
452 {
453 // find the taskbar window's child
454 taskbarHandle = FindWindowEx(hWnd, IntPtr.Zero, "ReBarWindow32", null);
455 if (taskbarHandle != IntPtr.Zero)
456 {
457 break;
458 }
459 }
460 if (hWnd == IntPtr.Zero)
461 {
462 Debug.WriteLine("Taskbar not found");
463 return;
464 }
465 else
466 {
467 RECT taskbarRect = new RECT();
468 GetWindowRect(hWnd, ref taskbarRect);
469 Debug.WriteLine("Taskbar Rect: " + taskbarRect.Left + ", " + taskbarRect.Top + ", " + taskbarRect.Right + ", " + taskbarRect.Bottom);
470 // TODO: handle left, top, right and nonexistant taskbars
471 // subtract the taskbar height from the screen height
472 desktopRect.Bottom -= (int)((taskbarRect.Bottom - taskbarRect.Top) / 2);
473 }
474 // set the window positions using the shellRect and making sure the windows are visible
475 int halfwidth = (desktopRect.Right - desktopRect.Left) / 2;
476 IntPtr HWND_TOP = IntPtr.Zero;
477 uint showWindow = 0x40;
478 SetWindowPos(hWnd1, HWND_TOP, desktopRect.Left, desktopRect.Top, halfwidth, desktopRect.Bottom, showWindow);
479 SetForegroundWindow(hWnd1);
480 Interaction.AppActivate(pid1);
481 SetWindowPos(hWnd2, HWND_TOP, desktopRect.Left + halfwidth, desktopRect.Top, halfwidth, desktopRect.Bottom, showWindow);
482 SetForegroundWindow(hWnd2);
483 Interaction.AppActivate(pid2);
484 }
485 }
486
487 /// <summary>
488 /// Finds a top-level window by searching for a partial match in the window title.
489 /// </summary>
490 /// <param name="titleSearch">The text to search for in window titles (case-insensitive).</param>
491 /// <returns>A tuple containing the window handle and process ID, or (IntPtr.Zero, -1) if not found.</returns>
492 static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch)
493 {
494 IntPtr foundHandle = IntPtr.Zero;
495 int foundPid = -1;
496 StringBuilder windowTitle = new StringBuilder(256);
497
498 EnumWindows((hWnd, lParam) =>
499 {
500 // Only consider visible windows
501 if (!IsWindowVisible(hWnd))
502 {
503 return true; // Continue enumeration
504 }
505
506 // Get window title
507 int length = GetWindowText(hWnd, windowTitle, windowTitle.Capacity);
508 if (length > 0)
509 {
510 string title = windowTitle.ToString();
511 // Case-insensitive partial match
512 if (title.Contains(titleSearch, StringComparison.OrdinalIgnoreCase))
513 {
514 foundHandle = hWnd;
515 GetWindowThreadProcessId(hWnd, out uint pid);
516 foundPid = (int)pid;
517 return false; // Stop enumeration
518 }
519 }
520 return true; // Continue enumeration
521 }, IntPtr.Zero);
522
523 return (foundHandle, foundPid);
524 }
525
526 // given a friendly name, check if it's running and if not, start it; if it's running raise it to the top level
527 static void OpenApplication(string friendlyName)
528 {
529 // check to see if the application is running
530 Process[] processes = Process.GetProcessesByName(friendlyName);
531 if (processes.Length == 0)
532 {
533 // if not, start it
534 Debug.WriteLine("Starting " + friendlyName);
535 string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()];
536 if (path != null)
537 {
538 try
539 {
540 Process.Start(path);
541 }
542 catch (System.ComponentModel.Win32Exception)
543 {
544 // alternate start method
545 Process.Start(friendlyName);
546 }
547 }
548 else
549 {
550 string appModelUserID = (string)s_friendlyNameToId[friendlyName.ToLowerInvariant()];
551 if (appModelUserID != null)
552 {
553 try
554 {
555 Process.Start("explorer.exe", @" shell:appsFolder\" + appModelUserID);
556 }
557 catch { }
558 }
559 }
560 }
561 else
562 {
563 // if so, raise it to the top level
564 Debug.WriteLine("Raising " + friendlyName);
565 RaiseWindow(friendlyName);
566 }
567 }
568
569 // close application
570 static void CloseApplication(string friendlyName)
571 {
572 // check to see if the application is running
573 string processName = ResolveProcessNameFromFriendlyName(friendlyName);
574 Process[] processes = Process.GetProcessesByName(processName);
575 if (processes.Length != 0)
576 {
577 // if so, close it
578 Debug.WriteLine("Closing " + friendlyName);
579 foreach (Process p in processes)
580 {
581 if (p.MainWindowHandle != IntPtr.Zero)
582 {
583 p.CloseMainWindow();
584 }
585 }
586 }
587 }
588
589 private static void SetDesktopWallpaper(string imagePath)
590 {
591 SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, imagePath, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
592 }
593
594 /// <summary>
595 /// Creates virtual desktops from a JSON array of desktop names.
596 /// </summary>
597 /// <param name="jsonValue">JSON array containing desktop names, e.g., ["Work", "Personal", "Gaming"]</param>
598 static void CreateDesktop(string jsonValue)
599 {
600 try
601 {
602 // Parse the JSON array of desktop names
603 JArray desktopNames = JArray.Parse(jsonValue);
604
605 if (desktopNames == null || desktopNames.Count == 0)
606 {
607 desktopNames = ["desktop X"];
608 }
609
610 if (s_virtualDesktopManagerInternal == null)
611 {
612 Debug.WriteLine($"Failed to get Virtual Desktop Manager Internal");
613 return;
614 }
615
616 foreach (JToken desktopNameToken in desktopNames)
617 {
618 string desktopName = desktopNameToken.ToString();
619
620
621 try
622 {
623 // Create a new virtual desktop
624 IVirtualDesktop newDesktop = s_virtualDesktopManagerInternal.CreateDesktop();
625
626 if (newDesktop != null)
627 {
628 // Set the desktop name (Windows 10 build 20231+ / Windows 11)
629 try
630 {
631 // TODO: debug & get working
632 // Works in .NET framework but not .NET
633 //s_virtualDesktopManagerInternal_BUGBUG.SetDesktopName(newDesktop, desktopName);
634 //Debug.WriteLine($"Created virtual desktop: {desktopName}");
635 }
636 catch (Exception ex2)
637 {
638 // Older Windows version - name setting not supported
639 Debug.WriteLine($"Created virtual desktop (naming not supported on this Windows version): {ex2.Message}");
640 }
641 }
642 }
643 catch (Exception ex)
644 {
645 Debug.WriteLine($"Failed to create desktop '{desktopName}': {ex.Message}");
646 }
647 }
648 }
649 catch (JsonException ex)
650 {
651 Debug.WriteLine($"Failed to parse desktop names JSON: {ex.Message}");
652 }
653 catch (Exception ex)
654 {
655 Debug.WriteLine($"Error creating desktops: {ex.Message}");
656 }
657 }
658
659 static void SwitchDesktop(string desktopIdentifier)
660 {
661 if (!int.TryParse(desktopIdentifier, out int index))
662 {
663 // Try to find the desktop by name
664 s_virtualDesktopManagerInternal.SwitchDesktop(FindDesktopByName(desktopIdentifier));
665 }
666 else
667 {
668 SwitchDesktop(index);
669 }
670 }
671
672 static void SwitchDesktop(int index)
673 {
674 s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops);
675 desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od);
676
677 // BUGBUG: different windows versions use different COM interfaces
678 // Different Windows versions use different COM interfaces for desktop switching
679 // Windows 11 22H2 (build 22621) and later use the updated interface
680 if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22621))
681 {
682 // Use the BUGBUG interface for Windows 11 22H2+
683 s_virtualDesktopManagerInternal_BUGBUG.SwitchDesktopWithAnimation((IVirtualDesktop)od);
684 }
685 else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
686 {
687 // Windows 11 21H2 (build 22000)
688 s_virtualDesktopManagerInternal.SwitchDesktopWithAnimation((IVirtualDesktop)od);
689 }
690 else
691 {
692 // Windows 10 - use the original interface
693 s_virtualDesktopManagerInternal.SwitchDesktopAndMoveForegroundView((IVirtualDesktop)od);
694 }
695
696 Marshal.ReleaseComObject(desktops);
697 }
698
699 static void BumpDesktopIndex(int bump)
700 {
701 IVirtualDesktop desktop = s_virtualDesktopManagerInternal.GetCurrentDesktop();
702 int index = GetDesktopIndex(desktop);
703 int count = s_virtualDesktopManagerInternal.GetCount();
704
705 if (index == -1)
706 {
707 Debug.WriteLine("Undable to get the index of the current desktop");
708 return;
709 }
710
711 index += bump;
712
713 if (index > count)
714 {
715 index = 0;
716 }
717 else if (index < 0)
718 {
719 index = count - 1;
720 }
721
722 SwitchDesktop(index);
723 }
724
725 static IVirtualDesktop FindDesktopByName(string name)
726 {
727 int count = s_virtualDesktopManagerInternal.GetCount();
728
729 s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops);
730 for (int i = 0; i < count; i++)
731 {
732 desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od);
733
734 if (string.Equals(((IVirtualDesktop)od).GetName(), name, StringComparison.OrdinalIgnoreCase))
735 {
736 Marshal.ReleaseComObject(desktops);
737 return (IVirtualDesktop)od;
738 }
739 }
740
741 Marshal.ReleaseComObject(desktops);
742
743 return null;
744 }
745
746 static int GetDesktopIndex(IVirtualDesktop desktop)
747 {
748 int index = -1;
749 int count = s_virtualDesktopManagerInternal.GetCount();
750
751 s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops);
752 for (int i = 0; i < count; i++)
753 {
754 desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od);
755
756 if (desktop.GetId() == ((IVirtualDesktop)od).GetId())
757 {
758 Marshal.ReleaseComObject(desktops);
759 return i;
760 }
761 }
762
763 Marshal.ReleaseComObject(desktops);
764
765 return -1;
766 }
767
768 /// <summary>
769 ///
770 /// </summary>
771 /// <param name="value"></param>
772 /// <remarks>Currently not working correction, returns ACCESS_DENIED // TODO: investigate</remarks>
773 static void MoveWindowToDesktop(JToken value)
774 {
775 string process = value.SelectToken("process").ToString();
776 string desktop = value.SelectToken("desktop").ToString();
777 if (string.IsNullOrEmpty(process))
778 {
779 Debug.WriteLine("No process name supplied");
780 return;
781 }
782
783 if (string.IsNullOrEmpty(desktop))
784 {
785 Debug.WriteLine("No desktop id supplied");
786 return;
787 }
788
789 IntPtr hWnd = FindProcessWindowHandle(process);
790
791 if (int.TryParse(desktop, out int desktopIndex))
792 {
793 s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops);
794 if (desktopIndex < 1 || desktopIndex > s_virtualDesktopManagerInternal.GetCount())
795 {
796 Debug.WriteLine("Desktop index out of range");
797 Marshal.ReleaseComObject(desktops);
798 return;
799 }
800 desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od);
801 Guid g = ((IVirtualDesktop)od).GetId();
802 s_virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g);
803 Marshal.ReleaseComObject(desktops);
804 return;
805 }
806
807 IVirtualDesktop ivd = FindDesktopByName(desktop);
808 if (ivd is not null)
809 {
810 Guid desktopGuid = ivd.GetId();
811 s_virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid);
812 }
813 }
814
815 static void PinWindow(string processName)
816 {
817 IntPtr hWnd = FindProcessWindowHandle(processName);
818
819 if (hWnd != IntPtr.Zero)
820 {
821 s_applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view);
822
823 if (view is not null)
824 {
825 s_virtualDesktopPinnedApps.PinView((IApplicationView)view);
826 }
827 }
828 else
829 {
830 Console.WriteLine($"The window handle for '{processName}' could not be found");
831 }
832 }
833
834 static IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal()
835 {
836 try
837 {
838 IServiceProvider shellServiceProvider = (IServiceProvider)Activator.CreateInstance(
839 Type.GetTypeFromCLSID(CLSID_ImmersiveShell));
840
841 shellServiceProvider.QueryService(
842 CLSID_VirtualDesktopManagerInternal,
843 typeof(IVirtualDesktopManagerInternal).GUID,
844 out object objVirtualDesktopManagerInternal);
845
846 return (IVirtualDesktopManagerInternal)objVirtualDesktopManagerInternal;
847 }
848 catch
849 {
850 return null;
851 }
852 }
853
854 static bool execLine(JObject root)
855 {
856 var quit = false;
857 foreach (var kvp in root)
858 {
859 string key = kvp.Key;
860 string value = kvp.Value.ToString();
861 switch (key)
862 {
863 case "launchProgram":
864 OpenApplication(value);
865 break;
866 case "closeProgram":
867 CloseApplication(value);
868 break;
869 case "maximize":
870 MaximizeWindow(value);
871 break;
872 case "minimize":
873 MinimizeWindow(value);
874 break;
875 case "switchTo":
876 RaiseWindow(value);
877 break;
878 case "quit":
879 quit = true;
880 break;
881 case "tile":
882 string[] apps = value.Split(',');
883 if (apps.Length == 2)
884 {
885 TileWindowPair(apps[0], apps[1]);
886 }
887 break;
888 case "volume":
889 int pct = 0;
890 if (int.TryParse(value, out pct))
891 {
892 SetMasterVolume(pct);
893 }
894 break;
895 case "restoreVolume":
896 RestoreMasterVolume();
897 break;
898 case "mute":
899 bool mute = false;
900 if (bool.TryParse(value, out mute))
901 {
902 SetMasterMute(mute);
903 }
904 break;
905 case "listAppNames":
906 var installedApps = GetAllInstalledAppsIds();
907 Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys));
908 break;
909 case "setWallpaper":
910 SetDesktopWallpaper(value);
911 break;
912 case "applyTheme":
913 bool result = ApplyTheme(value);
914 break;
915 case "listThemes":
916 var themes = GetInstalledThemes();
917 Console.WriteLine(JsonConvert.SerializeObject(themes));
918 break;
919 case "setThemeMode":
920 // value can be "light", "dark", "toggle", or boolean
921 if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase))
922 {
923 ToggleLightDarkMode();
924 }
925 else
926 {
927 bool useLightMode;
928 if (bool.TryParse(value, out useLightMode))
929 {
930 SetLightDarkMode(useLightMode);
931 }
932 else if (value.Equals("light", StringComparison.OrdinalIgnoreCase))
933 {
934 SetLightDarkMode(true);
935 }
936 else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase))
937 {
938 SetLightDarkMode(false);
939 }
940 }
941 break;
942 case "createDesktop":
943 CreateDesktop(value);
944 break;
945 case "switchDesktop":
946 SwitchDesktop(value);
947 break;
948 case "nextDesktop":
949 BumpDesktopIndex(1);
950 break;
951 case "previousDesktop":
952 BumpDesktopIndex(-1);
953 break;
954 case "moveWindowToDesktop":
955 MoveWindowToDesktop(kvp.Value);
956 break;
957 case "pinWindow":
958 PinWindow(value);
959 break;
960 case "toggleNotifications":
961 ShellExecute(IntPtr.Zero, "open", "ms-actioncenter:", null, null, 1);
962 break;
963 case "debug":
964 Debugger.Launch();
965 break;
966 case "toggleAirplaneMode":
967 SetAirplaneMode(bool.Parse(value));
968 break;
969 case "listWifiNetworks":
970 ListWifiNetworks();
971 break;
972 case "connectWifi":
973 JObject netInfo = JObject.Parse(value);
974 string ssid = netInfo.Value<string>("ssid");
975 string password = netInfo["password"] is not null ? netInfo.Value<string>("password") : "";
976 ConnectToWifi(ssid, password);
977 break;
978 case "disconnectWifi":
979 DisconnectFromWifi();
980 break;
981 case "setTextSize":
982 if (int.TryParse(value, out int textSizePct))
983 {
984 SetTextSize(textSizePct);
985 }
986 break;
987 case "setScreenResolution":
988 SetDisplayResolution(kvp.Value);
989 break;
990 case "listResolutions":
991 ListDisplayResolutions();
992 break;
993
994 // ===== Settings Actions (50 new handlers) =====
995
996 // Network Settings
997 case "BluetoothToggle":
998 HandleBluetoothToggle(value);
999 break;
1000 case "enableWifi":
1001 HandleEnableWifi(value);
1002 break;
1003 case "enableMeteredConnections":
1004 HandleEnableMeteredConnections(value);
1005 break;
1006
1007 // Display Settings
1008 case "AdjustScreenBrightness":
1009 HandleAdjustScreenBrightness(value);
1010 break;
1011 case "EnableBlueLightFilterSchedule":
1012 HandleEnableBlueLightFilterSchedule(value);
1013 break;
1014 case "adjustColorTemperature":
1015 HandleAdjustColorTemperature(value);
1016 break;
1017 case "DisplayScaling":
1018 HandleDisplayScaling(value);
1019 break;
1020 case "AdjustScreenOrientation":
1021 HandleAdjustScreenOrientation(value);
1022 break;
1023 case "DisplayResolutionAndAspectRatio":
1024 HandleDisplayResolutionAndAspectRatio(value);
1025 break;
1026 case "RotationLock":
1027 HandleRotationLock(value);
1028 break;
1029
1030 // Personalization Settings
1031 case "SystemThemeMode":
1032 HandleSystemThemeMode(value);
1033 break;
1034 case "EnableTransparency":
1035 HandleEnableTransparency(value);
1036 break;
1037 case "ApplyColorToTitleBar":
1038 HandleApplyColorToTitleBar(value);
1039 break;
1040 case "HighContrastTheme":
1041 HandleHighContrastTheme(value);
1042 break;
1043
1044 // Taskbar Settings
1045 case "AutoHideTaskbar":
1046 HandleAutoHideTaskbar(value);
1047 break;
1048 case "TaskbarAlignment":
1049 HandleTaskbarAlignment(value);
1050 break;
1051 case "TaskViewVisibility":
1052 HandleTaskViewVisibility(value);
1053 break;
1054 case "ToggleWidgetsButtonVisibility":
1055 HandleToggleWidgetsButtonVisibility(value);
1056 break;
1057 case "ShowBadgesOnTaskbar":
1058 HandleShowBadgesOnTaskbar(value);
1059 break;
1060 case "DisplayTaskbarOnAllMonitors":
1061 HandleDisplayTaskbarOnAllMonitors(value);
1062 break;
1063 case "DisplaySecondsInSystrayClock":
1064 HandleDisplaySecondsInSystrayClock(value);
1065 break;
1066
1067 // Mouse Settings
1068 case "MouseCursorSpeed":
1069 HandleMouseCursorSpeed(value);
1070 break;
1071 case "MouseWheelScrollLines":
1072 HandleMouseWheelScrollLines(value);
1073 break;
1074 case "setPrimaryMouseButton":
1075 HandleSetPrimaryMouseButton(value);
1076 break;
1077 case "EnhancePointerPrecision":
1078 HandleEnhancePointerPrecision(value);
1079 break;
1080 case "AdjustMousePointerSize":
1081 HandleAdjustMousePointerSize(value);
1082 break;
1083 case "mousePointerCustomization":
1084 HandleMousePointerCustomization(value);
1085 break;
1086
1087 // Touchpad Settings
1088 case "EnableTouchPad":
1089 HandleEnableTouchPad(value);
1090 break;
1091 case "TouchpadCursorSpeed":
1092 HandleTouchpadCursorSpeed(value);
1093 break;
1094
1095 // Privacy Settings
1096 case "ManageMicrophoneAccess":
1097 HandleManageMicrophoneAccess(value);
1098 break;
1099 case "ManageCameraAccess":
1100 HandleManageCameraAccess(value);
1101 break;
1102 case "ManageLocationAccess":
1103 HandleManageLocationAccess(value);
1104 break;
1105
1106 // Power Settings
1107 case "BatterySaverActivationLevel":
1108 HandleBatterySaverActivationLevel(value);
1109 break;
1110 case "setPowerModePluggedIn":
1111 HandleSetPowerModePluggedIn(value);
1112 break;
1113 case "SetPowerModeOnBattery":
1114 HandleSetPowerModeOnBattery(value);
1115 break;
1116
1117 // Gaming Settings
1118 case "enableGameMode":
1119 HandleEnableGameMode(value);
1120 break;
1121
1122 // Accessibility Settings
1123 case "EnableNarratorAction":
1124 HandleEnableNarratorAction(value);
1125 break;
1126 case "EnableMagnifier":
1127 HandleEnableMagnifier(value);
1128 break;
1129 case "enableStickyKeys":
1130 HandleEnableStickyKeysAction(value);
1131 break;
1132 case "EnableFilterKeysAction":
1133 HandleEnableFilterKeysAction(value);
1134 break;
1135 case "MonoAudioToggle":
1136 HandleMonoAudioToggle(value);
1137 break;
1138
1139 // File Explorer Settings
1140 case "ShowFileExtensions":
1141 HandleShowFileExtensions(value);
1142 break;
1143 case "ShowHiddenAndSystemFiles":
1144 HandleShowHiddenAndSystemFiles(value);
1145 break;
1146
1147 // Time & Region Settings
1148 case "AutomaticTimeSettingAction":
1149 HandleAutomaticTimeSettingAction(value);
1150 break;
1151 case "AutomaticDSTAdjustment":
1152 HandleAutomaticDSTAdjustment(value);
1153 break;
1154
1155 // Focus Assist Settings
1156 case "EnableQuietHours":
1157 HandleEnableQuietHours(value);
1158 break;
1159
1160 // Multi-Monitor Settings
1161 case "RememberWindowLocations":
1162 HandleRememberWindowLocationsAction(value);
1163 break;
1164 case "MinimizeWindowsOnMonitorDisconnectAction":
1165 HandleMinimizeWindowsOnMonitorDisconnectAction(value);
1166 break;
1167
1168 default:
1169 Debug.WriteLine("Unknown command: " + key);
1170 break;
1171 }
1172 }
1173 return quit;
1174 }
1175
1176 /// <summary>
1177 /// Sets the airplane mode state using the Radio Management API.
1178 /// </summary>
1179 /// <param name="enable">True to enable airplane mode, false to disable.</param>
1180 static void SetAirplaneMode(bool enable)
1181 {
1182 IRadioManager radioManager = null;
1183 try
1184 {
1185 // Create the Radio Management API COM object
1186 Type radioManagerType = Type.GetTypeFromCLSID(CLSID_RadioManagementAPI);
1187 if (radioManagerType == null)
1188 {
1189 Debug.WriteLine("Failed to get Radio Management API type");
1190 return;
1191 }
1192
1193 object obj = Activator.CreateInstance(radioManagerType);
1194 radioManager = (IRadioManager)obj;
1195
1196 if (radioManager == null)
1197 {
1198 Debug.WriteLine("Failed to create Radio Manager instance");
1199 return;
1200 }
1201
1202 // Get current state (for logging)
1203 int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _);
1204 if (hr < 0)
1205 {
1206 Debug.WriteLine($"Failed to get system radio state: HRESULT 0x{hr:X8}");
1207 return;
1208 }
1209
1210 // currentState: 0 = airplane mode ON (radios off), 1 = airplane mode OFF (radios on)
1211 bool airplaneModeCurrentlyOn = currentState == 0;
1212 Debug.WriteLine($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}");
1213
1214 // Set the new state
1215 // bEnabled: 0 = turn airplane mode ON (disable radios), 1 = turn airplane mode OFF (enable radios)
1216 int newState = enable ? 0 : 1;
1217 hr = radioManager.SetSystemRadioState(newState);
1218 if (hr < 0)
1219 {
1220 Debug.WriteLine($"Failed to set system radio state: HRESULT 0x{hr:X8}");
1221 return;
1222 }
1223
1224 Debug.WriteLine($"Airplane mode set to: {(enable ? "on" : "off")}");
1225 }
1226 catch (COMException ex)
1227 {
1228 Debug.WriteLine($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})");
1229 }
1230 catch (Exception ex)
1231 {
1232 Debug.WriteLine($"Failed to set airplane mode: {ex.Message}");
1233 }
1234 finally
1235 {
1236 if (radioManager != null)
1237 {
1238 Marshal.ReleaseComObject(radioManager);
1239 }
1240 }
1241 }
1242
1243 /// <summary>
1244 /// Lists all WiFi networks currently in range.
1245 /// </summary>
1246 static void ListWifiNetworks()
1247 {
1248 IntPtr clientHandle = IntPtr.Zero;
1249 IntPtr wlanInterfaceList = IntPtr.Zero;
1250 IntPtr networkList = IntPtr.Zero;
1251
1252 try
1253 {
1254 // Open WLAN handle
1255 int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle);
1256 if (result != 0)
1257 {
1258 Debug.WriteLine($"Failed to open WLAN handle: {result}");
1259 return;
1260 }
1261
1262 // Enumerate wireless interfaces
1263 result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList);
1264 if (result != 0)
1265 {
1266 Debug.WriteLine($"Failed to enumerate WLAN interfaces: {result}");
1267 return;
1268 }
1269
1270 WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure<WLAN_INTERFACE_INFO_LIST>(wlanInterfaceList);
1271
1272 if (interfaceList.dwNumberOfItems == 0)
1273 {
1274 Console.WriteLine("[]");
1275 return;
1276 }
1277
1278 var allNetworks = new List<object>();
1279
1280 for (int i = 0; i < interfaceList.dwNumberOfItems; i++)
1281 {
1282 WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i];
1283
1284 // Scan for networks (trigger a refresh)
1285 WlanScan(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
1286
1287 // Small delay to allow scan to complete
1288 System.Threading.Thread.Sleep(100);
1289
1290 // Get available networks
1291 result = WlanGetAvailableNetworkList(clientHandle, ref interfaceInfo.InterfaceGuid, 0, IntPtr.Zero, out networkList);
1292 if (result != 0)
1293 {
1294 Debug.WriteLine($"Failed to get network list: {result}");
1295 continue;
1296 }
1297
1298 WLAN_AVAILABLE_NETWORK_LIST availableNetworkList = Marshal.PtrToStructure<WLAN_AVAILABLE_NETWORK_LIST>(networkList);
1299
1300 IntPtr networkPtr = networkList + 8; // Skip dwNumberOfItems and dwIndex
1301
1302 for (int j = 0; j < availableNetworkList.dwNumberOfItems; j++)
1303 {
1304 WLAN_AVAILABLE_NETWORK network = Marshal.PtrToStructure<WLAN_AVAILABLE_NETWORK>(networkPtr);
1305
1306 string ssid = Encoding.ASCII.GetString(network.dot11Ssid.SSID, 0, (int)network.dot11Ssid.SSIDLength);
1307
1308 if (!string.IsNullOrEmpty(ssid))
1309 {
1310 allNetworks.Add(new
1311 {
1312 SSID = ssid,
1313 SignalQuality = network.wlanSignalQuality,
1314 Secured = network.bSecurityEnabled,
1315 Connected = (network.dwFlags & 1) != 0 // WLAN_AVAILABLE_NETWORK_CONNECTED
1316 });
1317 }
1318
1319 networkPtr += Marshal.SizeOf<WLAN_AVAILABLE_NETWORK>();
1320 }
1321
1322 if (networkList != IntPtr.Zero)
1323 {
1324 WlanFreeMemory(networkList);
1325 networkList = IntPtr.Zero;
1326 }
1327 }
1328
1329 // Remove duplicates and sort by signal strength
1330 var uniqueNetworks = allNetworks
1331 .GroupBy(n => ((dynamic)n).SSID)
1332 .Select(g => g.OrderByDescending(n => ((dynamic)n).SignalQuality).First())
1333 .OrderByDescending(n => ((dynamic)n).SignalQuality)
1334 .ToList();
1335
1336 Console.WriteLine(JsonConvert.SerializeObject(uniqueNetworks));
1337 }
1338 catch (Exception ex)
1339 {
1340 Debug.WriteLine($"Error listing WiFi networks: {ex.Message}");
1341 Console.WriteLine("[]");
1342 }
1343 finally
1344 {
1345 if (networkList != IntPtr.Zero)
1346 WlanFreeMemory(networkList);
1347 if (wlanInterfaceList != IntPtr.Zero)
1348 WlanFreeMemory(wlanInterfaceList);
1349 if (clientHandle != IntPtr.Zero)
1350 WlanCloseHandle(clientHandle, IntPtr.Zero);
1351 }
1352 }
1353
1354 /// <summary>
1355 /// Connects to a WiFi network by name (SSID). If the network requires a password and one is provided,
1356 /// it will create a temporary profile. For networks with existing profiles, it connects using the profile.
1357 /// </summary>
1358 /// <param name="ssid">The SSID of the network to connect to.</param>
1359 /// <param name="password">Optional password for secured networks.</param>
1360 static void ConnectToWifi(string ssid, string password = null)
1361 {
1362 IntPtr clientHandle = IntPtr.Zero;
1363 IntPtr wlanInterfaceList = IntPtr.Zero;
1364
1365 try
1366 {
1367 // Open WLAN handle
1368 int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle);
1369 if (result != 0)
1370 {
1371 LogWarning($"Failed to open WLAN handle: {result}");
1372 return;
1373 }
1374
1375 // Enumerate wireless interfaces
1376 result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList);
1377 if (result != 0)
1378 {
1379 LogWarning($"Failed to enumerate WLAN interfaces: {result}");
1380 return;
1381 }
1382
1383 WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure<WLAN_INTERFACE_INFO_LIST>(wlanInterfaceList);
1384
1385 if (interfaceList.dwNumberOfItems == 0)
1386 {
1387 LogWarning("No wireless interfaces found.");
1388 return;
1389 }
1390
1391 // Use the first available wireless interface
1392 WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[0];
1393
1394 // If password is provided, create a profile and connect
1395 if (!string.IsNullOrEmpty(password))
1396 {
1397 string profileXml = GenerateWifiProfileXml(ssid, password);
1398
1399 result = WlanSetProfile(clientHandle, ref interfaceInfo.InterfaceGuid, 0, profileXml, null, true, IntPtr.Zero, out uint reasonCode);
1400 if (result != 0)
1401 {
1402 LogWarning($"Failed to set WiFi profile: {result}, reason: {reasonCode}");
1403 return;
1404 }
1405 }
1406
1407 // Set up connection parameters
1408 WLAN_CONNECTION_PARAMETERS connectionParams = new WLAN_CONNECTION_PARAMETERS
1409 {
1410 wlanConnectionMode = WLAN_CONNECTION_MODE.wlan_connection_mode_profile,
1411 strProfile = ssid,
1412 pDot11Ssid = IntPtr.Zero,
1413 pDesiredBssidList = IntPtr.Zero,
1414 dot11BssType = DOT11_BSS_TYPE.dot11_BSS_type_any,
1415 dwFlags = 0
1416 };
1417
1418 result = WlanConnect(clientHandle, ref interfaceInfo.InterfaceGuid, ref connectionParams, IntPtr.Zero);
1419 if (result != 0)
1420 {
1421 LogWarning($"Failed to connect to WiFi network '{ssid}': {result}");
1422 return;
1423 }
1424
1425 Debug.WriteLine($"Successfully initiated connection to WiFi network: {ssid}");
1426 Console.WriteLine($"Connecting to WiFi network: {ssid}");
1427 }
1428 catch (Exception ex)
1429 {
1430 LogError(ex);
1431 }
1432 finally
1433 {
1434 if (wlanInterfaceList != IntPtr.Zero)
1435 WlanFreeMemory(wlanInterfaceList);
1436 if (clientHandle != IntPtr.Zero)
1437 WlanCloseHandle(clientHandle, IntPtr.Zero);
1438 }
1439 }
1440
1441 /// <summary>
1442 /// Generates a WiFi profile XML for WPA2-Personal (PSK) networks.
1443 /// </summary>
1444 static string GenerateWifiProfileXml(string ssid, string password)
1445 {
1446 // Convert SSID to hex
1447 string ssidHex = BitConverter.ToString(Encoding.UTF8.GetBytes(ssid)).Replace("-", "");
1448
1449 return $@"<?xml version=""1.0""?>
1450<WLANProfile xmlns=""http://www.microsoft.com/networking/WLAN/profile/v1"">
1451 <name>{ssid}</name>
1452 <SSIDConfig>
1453 <SSID>
1454 <hex>{ssidHex}</hex>
1455 <name>{ssid}</name>
1456 </SSID>
1457 </SSIDConfig>
1458 <connectionType>ESS</connectionType>
1459 <connectionMode>auto</connectionMode>
1460 <MSM>
1461 <security>
1462 <authEncryption>
1463 <authentication>WPA2PSK</authentication>
1464 <encryption>AES</encryption>
1465 <useOneX>false</useOneX>
1466 </authEncryption>
1467 <sharedKey>
1468 <keyType>passPhrase</keyType>
1469 <protected>false</protected>
1470 <keyMaterial>{password}</keyMaterial>
1471 </sharedKey>
1472 </security>
1473 </MSM>
1474</WLANProfile>";
1475 }
1476
1477 /// <summary>
1478 /// Disconnects from the currently connected WiFi network.
1479 /// </summary>
1480 /// <summary>
1481 /// Sets the system text scaling factor (percentage).
1482 /// </summary>
1483 /// <param name="percentage">The text scaling percentage (100-225).</param>
1484 static void SetTextSize(int percentage)
1485 {
1486 try
1487 {
1488 if (percentage == -1)
1489 {
1490 percentage = new Random().Next(100, 225 + 1);
1491 }
1492
1493 // Clamp the percentage to valid range
1494 if (percentage < 100)
1495 {
1496 percentage = 100;
1497 }
1498 else if (percentage > 225)
1499 {
1500 percentage = 225;
1501 }
1502
1503 // Open the Settings app to the ease of access page
1504 Process.Start(new ProcessStartInfo
1505 {
1506 FileName = "ms-settings:easeofaccess",
1507 UseShellExecute = true
1508 });
1509
1510 // Use UI Automation to navigate and set the text size
1511 UIAutomation.SetTextSizeViaUIAutomation(percentage);
1512 }
1513 catch (Exception ex)
1514 {
1515 LogError(ex);
1516 }
1517 }
1518
1519 /// <summary>
1520 /// Lists all available display resolutions for the primary monitor.
1521 /// </summary>
1522 static void ListDisplayResolutions()
1523 {
1524 try
1525 {
1526 var resolutions = new List<object>();
1527 DEVMODE devMode = new DEVMODE();
1528 devMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE));
1529
1530 int modeNum = 0;
1531 while (EnumDisplaySettings(null, modeNum, ref devMode))
1532 {
1533 resolutions.Add(new
1534 {
1535 Width = devMode.dmPelsWidth,
1536 Height = devMode.dmPelsHeight,
1537 BitsPerPixel = devMode.dmBitsPerPel,
1538 RefreshRate = devMode.dmDisplayFrequency
1539 });
1540 modeNum++;
1541 }
1542
1543 // Remove duplicates and sort by resolution
1544 var uniqueResolutions = resolutions
1545 .GroupBy(r => new { ((dynamic)r).Width, ((dynamic)r).Height, ((dynamic)r).RefreshRate })
1546 .Select(g => g.First())
1547 .OrderByDescending(r => ((dynamic)r).Width)
1548 .ThenByDescending(r => ((dynamic)r).Height)
1549 .ThenByDescending(r => ((dynamic)r).RefreshRate)
1550 .ToList();
1551
1552 Console.WriteLine(JsonConvert.SerializeObject(uniqueResolutions));
1553 }
1554 catch (Exception ex)
1555 {
1556 LogError(ex);
1557 }
1558 }
1559
1560 /// <summary>
1561 /// Sets the display resolution.
1562 /// </summary>
1563 /// <param name="value">JSON object with "width" and "height" properties, or a string like "1920x1080".</param>
1564 static void SetDisplayResolution(JToken value)
1565 {
1566 try
1567 {
1568 uint width;
1569 uint height;
1570 uint? refreshRate = null;
1571
1572 // Parse the input - can be JSON object or string like "1920x1080"
1573 if (value.Type == JTokenType.Object)
1574 {
1575 width = value.Value<uint>("width");
1576 height = value.Value<uint>("height");
1577 if (value["refreshRate"] != null)
1578 {
1579 refreshRate = value.Value<uint>("refreshRate");
1580 }
1581 }
1582 else
1583 {
1584 string resString = value.ToString();
1585 string[] parts = resString.ToLowerInvariant().Split('x', '@');
1586 if (parts.Length < 2)
1587 {
1588 LogWarning("Invalid resolution format. Use 'WIDTHxHEIGHT' or 'WIDTHxHEIGHT@REFRESH' (e.g., '1920x1080' or '1920x1080@60')");
1589 return;
1590 }
1591
1592 if (!uint.TryParse(parts[0].Trim(), out width) || !uint.TryParse(parts[1].Trim(), out height))
1593 {
1594 LogWarning("Invalid resolution values. Width and height must be positive integers.");
1595 return;
1596 }
1597
1598 if (parts.Length >= 3 && uint.TryParse(parts[2].Trim(), out uint parsedRefresh))
1599 {
1600 refreshRate = parsedRefresh;
1601 }
1602 }
1603
1604 // Get the current display settings
1605 DEVMODE currentMode = new DEVMODE();
1606 currentMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE));
1607
1608 if (!EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref currentMode))
1609 {
1610 LogWarning("Failed to get current display settings.");
1611 return;
1612 }
1613
1614 // Find a matching display mode
1615 DEVMODE newMode = new DEVMODE();
1616 newMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE));
1617
1618 int modeNum = 0;
1619 bool found = false;
1620 DEVMODE bestMatch = new DEVMODE();
1621
1622 while (EnumDisplaySettings(null, modeNum, ref newMode))
1623 {
1624 if (newMode.dmPelsWidth == width && newMode.dmPelsHeight == height)
1625 {
1626 if (refreshRate.HasValue)
1627 {
1628 if (newMode.dmDisplayFrequency == refreshRate.Value)
1629 {
1630 bestMatch = newMode;
1631 found = true;
1632 break;
1633 }
1634 }
1635 else
1636 {
1637 // Prefer higher refresh rate if not specified
1638 if (!found || newMode.dmDisplayFrequency > bestMatch.dmDisplayFrequency)
1639 {
1640 bestMatch = newMode;
1641 found = true;
1642 }
1643 }
1644 }
1645 modeNum++;
1646 }
1647
1648 if (!found)
1649 {
1650 LogWarning($"Resolution {width}x{height}" + (refreshRate.HasValue ? $"@{refreshRate}Hz" : "") + " is not supported.");
1651 return;
1652 }
1653
1654 // Set the required fields
1655 bestMatch.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY;
1656
1657 // TODO: better handle return value from change mode
1658 // Test if the mode change will work
1659 int testResult = ChangeDisplaySettings(ref bestMatch, CDS_TEST);
1660 if (testResult != DISP_CHANGE_SUCCESSFUL && testResult != -2)
1661 {
1662 LogWarning($"Display mode test failed with code: {testResult}");
1663 return;
1664 }
1665
1666 // Apply the change
1667 int result = ChangeDisplaySettings(ref bestMatch, CDS_UPDATEREGISTRY);
1668 switch (result)
1669 {
1670 case DISP_CHANGE_SUCCESSFUL:
1671 Console.WriteLine($"Resolution changed to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight}@{bestMatch.dmDisplayFrequency}Hz");
1672 break;
1673 case DISP_CHANGE_RESTART:
1674 Console.WriteLine($"Resolution will change to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight} after restart.");
1675 break;
1676 default:
1677 LogWarning($"Failed to change resolution. Error code: {result}");
1678 break;
1679 }
1680 }
1681 catch (Exception ex)
1682 {
1683 LogError(ex);
1684 }
1685 }
1686
1687 static void DisconnectFromWifi()
1688 {
1689 IntPtr clientHandle = IntPtr.Zero;
1690 IntPtr wlanInterfaceList = IntPtr.Zero;
1691
1692 try
1693 {
1694 // Open WLAN handle
1695 int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle);
1696 if (result != 0)
1697 {
1698 LogWarning($"Failed to open WLAN handle: {result}");
1699 return;
1700 }
1701
1702 // Enumerate wireless interfaces
1703 result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList);
1704 if (result != 0)
1705 {
1706 LogWarning($"Failed to enumerate WLAN interfaces: {result}");
1707 return;
1708 }
1709
1710 WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure<WLAN_INTERFACE_INFO_LIST>(wlanInterfaceList);
1711
1712 if (interfaceList.dwNumberOfItems == 0)
1713 {
1714 LogWarning("No wireless interfaces found.");
1715 return;
1716 }
1717
1718 // Disconnect from all wireless interfaces
1719 for (int i = 0; i < interfaceList.dwNumberOfItems; i++)
1720 {
1721 WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i];
1722
1723 result = WlanDisconnect(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero);
1724 if (result != 0)
1725 {
1726 LogWarning($"Failed to disconnect from WiFi on interface {i}: {result}");
1727 }
1728 else
1729 {
1730 Debug.WriteLine($"Successfully disconnected from WiFi on interface: {interfaceInfo.strInterfaceDescription}");
1731 Console.WriteLine("Disconnected from WiFi");
1732 }
1733 }
1734 }
1735 catch (Exception ex)
1736 {
1737 LogError(ex);
1738 }
1739 finally
1740 {
1741 if (wlanInterfaceList != IntPtr.Zero)
1742 WlanFreeMemory(wlanInterfaceList);
1743 if (clientHandle != IntPtr.Zero)
1744 WlanCloseHandle(clientHandle, IntPtr.Zero);
1745 }
1746 }
1747}
1748