microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5f9a4e6c4b89b03483eeb26d1ac4a60ed282ddd4

Branches

Tags

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

Clone

HTTPS

Download ZIP

dotnet/autoShell/AutoShell.cs

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